橡皮擦
This commit is contained in:
parent
b4e7867922
commit
89ef820366
|
@ -30,7 +30,7 @@
|
|||
"electron-log": "^5.1.7",
|
||||
"electron-updater": "^6.1.7",
|
||||
"element-plus": "^2.7.6",
|
||||
"fabric-with-erasing": "^1.0.1",
|
||||
"fabric": "^5.3.0",
|
||||
"js-cookie": "^3.0.5",
|
||||
"jsencrypt": "^3.3.2",
|
||||
"jsondiffpatch": "0.6.0",
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
<svg style="fill: currentColor;color: #ccc;" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1023 366.1L662.3 5.2 585 82.6l-34.4-34.4L69.7 538l34.4 34.4-51.5 34.4c-68.7 68.7-68.7 171.9 0 240.6l120.2 120.3c68.7 68.7 171.8 68.7 240.5 0l42.9-43 34.4 34.4 489.5-489.8-34.4-25.8 77.3-77.4zM662.3 65.4l300.6 300.8-51.5 51.6-300.6-309.5 51.5-42.9zM404.7 924.7c-60.1 60.2-154.6 60.2-214.7 0l-94.5-94.5c-60.1-60.2-60.1-154.7 0-214.8l34.4-17.2L430.5 899l-25.8 25.7z" />
|
||||
</svg>
|
Before Width: | Height: | Size: 502 B |
|
@ -1,10 +1,776 @@
|
|||
/**
|
||||
* @description 封装fabric js
|
||||
*/
|
||||
// import { fabric } from 'fabric'
|
||||
import { fabric } from 'fabric-with-erasing'
|
||||
import { fabric } from 'fabric'
|
||||
// import { fabric } from 'fabric-with-erasing'
|
||||
// import fabric from './fabric'
|
||||
// import * as fabric from 'fabric'
|
||||
import { diff, unpatch, patch } from 'jsondiffpatch'
|
||||
|
||||
function baseBrush() {
|
||||
/** ERASER_START */
|
||||
|
||||
/**
|
||||
* add `eraser` to enlivened props
|
||||
*/
|
||||
fabric.Object.ENLIVEN_PROPS.push('eraser');
|
||||
|
||||
var __drawClipPath = fabric.Object.prototype._drawClipPath;
|
||||
var _needsItsOwnCache = fabric.Object.prototype.needsItsOwnCache;
|
||||
var _toObject = fabric.Object.prototype.toObject;
|
||||
var _getSvgCommons = fabric.Object.prototype.getSvgCommons;
|
||||
var __createBaseClipPathSVGMarkup = fabric.Object.prototype._createBaseClipPathSVGMarkup;
|
||||
var __createBaseSVGMarkup = fabric.Object.prototype._createBaseSVGMarkup;
|
||||
|
||||
fabric.Object.prototype.cacheProperties.push('eraser');
|
||||
fabric.Object.prototype.stateProperties.push('eraser');
|
||||
|
||||
/**
|
||||
* @fires erasing:end
|
||||
*/
|
||||
fabric.util.object.extend(fabric.Object.prototype, {
|
||||
/**
|
||||
* Indicates whether this object can be erased by {@link fabric.EraserBrush}
|
||||
* The `deep` option introduces fine grained control over a group's `erasable` property.
|
||||
* When set to `deep` the eraser will erase nested objects if they are erasable, leaving the group and the other objects untouched.
|
||||
* When set to `true` the eraser will erase the entire group. Once the group changes the eraser is propagated to its children for proper functionality.
|
||||
* When set to `false` the eraser will leave all objects including the group untouched.
|
||||
* @tutorial {@link http://fabricjs.com/erasing#erasable_property}
|
||||
* @type boolean | 'deep'
|
||||
* @default true
|
||||
*/
|
||||
erasable: true,
|
||||
|
||||
/**
|
||||
* @tutorial {@link http://fabricjs.com/erasing#eraser}
|
||||
* @type fabric.Eraser
|
||||
*/
|
||||
eraser: undefined,
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @returns Boolean
|
||||
*/
|
||||
needsItsOwnCache: function () {
|
||||
return _needsItsOwnCache.call(this) || !!this.eraser;
|
||||
},
|
||||
|
||||
/**
|
||||
* draw eraser above clip path
|
||||
* @override
|
||||
* @private
|
||||
* @param {CanvasRenderingContext2D} ctx
|
||||
* @param {fabric.Object} clipPath
|
||||
*/
|
||||
_drawClipPath: function (ctx, clipPath) {
|
||||
__drawClipPath.call(this, ctx, clipPath);
|
||||
if (this.eraser) {
|
||||
// update eraser size to match instance
|
||||
var size = this._getNonTransformedDimensions();
|
||||
this.eraser.isType('eraser') && this.eraser.set({
|
||||
width: size.x,
|
||||
height: size.y
|
||||
});
|
||||
__drawClipPath.call(this, ctx, this.eraser);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns an object representation of an instance
|
||||
* @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
|
||||
* @return {Object} Object representation of an instance
|
||||
*/
|
||||
toObject: function (propertiesToInclude) {
|
||||
var object = _toObject.call(this, ['erasable'].concat(propertiesToInclude));
|
||||
if (this.eraser && !this.eraser.excludeFromExport) {
|
||||
object.eraser = this.eraser.toObject(propertiesToInclude);
|
||||
}
|
||||
return object;
|
||||
},
|
||||
|
||||
/* _TO_SVG_START_ */
|
||||
/**
|
||||
* Returns id attribute for svg output
|
||||
* @override
|
||||
* @return {String}
|
||||
*/
|
||||
getSvgCommons: function () {
|
||||
return _getSvgCommons.call(this) + (this.eraser ? 'mask="url(#' + this.eraser.clipPathId + ')" ' : '');
|
||||
},
|
||||
|
||||
/**
|
||||
* create svg markup for eraser
|
||||
* use <mask> to achieve erasing for svg, credit: https://travishorn.com/removing-parts-of-shapes-in-svg-b539a89e5649
|
||||
* must be called before object markup creation as it relies on the `clipPathId` property of the mask
|
||||
* @param {Function} [reviver]
|
||||
* @returns
|
||||
*/
|
||||
_createEraserSVGMarkup: function (reviver) {
|
||||
if (this.eraser) {
|
||||
this.eraser.clipPathId = 'MASK_' + fabric.Object.__uid++;
|
||||
return [
|
||||
'<mask id="', this.eraser.clipPathId, '" >',
|
||||
this.eraser.toSVG(reviver),
|
||||
'</mask>', '\n'
|
||||
].join('');
|
||||
}
|
||||
return '';
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_createBaseClipPathSVGMarkup: function (objectMarkup, options) {
|
||||
return [
|
||||
this._createEraserSVGMarkup(options && options.reviver),
|
||||
__createBaseClipPathSVGMarkup.call(this, objectMarkup, options)
|
||||
].join('');
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_createBaseSVGMarkup: function (objectMarkup, options) {
|
||||
return [
|
||||
this._createEraserSVGMarkup(options && options.reviver),
|
||||
__createBaseSVGMarkup.call(this, objectMarkup, options)
|
||||
].join('');
|
||||
}
|
||||
/* _TO_SVG_END_ */
|
||||
});
|
||||
|
||||
var __restoreObjectsState = fabric.Group.prototype._restoreObjectsState;
|
||||
fabric.util.object.extend(fabric.Group.prototype, {
|
||||
/**
|
||||
* @private
|
||||
* @param {fabric.Path} path
|
||||
*/
|
||||
_addEraserPathToObjects: function (path) {
|
||||
this._objects.forEach(function (object) {
|
||||
fabric.EraserBrush.prototype._addPathToObjectEraser.call(
|
||||
fabric.EraserBrush.prototype,
|
||||
object,
|
||||
path
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Applies the group's eraser to its objects
|
||||
* @tutorial {@link http://fabricjs.com/erasing#erasable_property}
|
||||
*/
|
||||
applyEraserToObjects: function () {
|
||||
var _this = this, eraser = this.eraser;
|
||||
if (eraser) {
|
||||
delete this.eraser;
|
||||
var transform = _this.calcTransformMatrix();
|
||||
eraser.clone(function (eraser) {
|
||||
var clipPath = _this.clipPath;
|
||||
eraser.getObjects('path')
|
||||
.forEach(function (path) {
|
||||
// first we transform the path from the group's coordinate system to the canvas'
|
||||
var originalTransform = fabric.util.multiplyTransformMatrices(
|
||||
transform,
|
||||
path.calcTransformMatrix()
|
||||
);
|
||||
fabric.util.applyTransformToObject(path, originalTransform);
|
||||
if (clipPath) {
|
||||
clipPath.clone(function (_clipPath) {
|
||||
var eraserPath = fabric.EraserBrush.prototype.applyClipPathToPath.call(
|
||||
fabric.EraserBrush.prototype,
|
||||
path,
|
||||
_clipPath,
|
||||
transform
|
||||
);
|
||||
_this._addEraserPathToObjects(eraserPath);
|
||||
}, ['absolutePositioned', 'inverted']);
|
||||
}
|
||||
else {
|
||||
_this._addEraserPathToObjects(path);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Propagate the group's eraser to its objects, crucial for proper functionality of the eraser within the group and nested objects.
|
||||
* @private
|
||||
*/
|
||||
_restoreObjectsState: function () {
|
||||
this.erasable === true && this.applyEraserToObjects();
|
||||
return __restoreObjectsState.call(this);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* An object's Eraser
|
||||
* @private
|
||||
* @class fabric.Eraser
|
||||
* @extends fabric.Group
|
||||
* @memberof fabric
|
||||
*/
|
||||
fabric.Eraser = fabric.util.createClass(fabric.Group, {
|
||||
/**
|
||||
* @readonly
|
||||
* @static
|
||||
*/
|
||||
type: 'eraser',
|
||||
|
||||
/**
|
||||
* @default
|
||||
*/
|
||||
originX: 'center',
|
||||
|
||||
/**
|
||||
* @default
|
||||
*/
|
||||
originY: 'center',
|
||||
|
||||
drawObject: function (ctx) {
|
||||
ctx.save();
|
||||
ctx.fillStyle = 'black';
|
||||
ctx.fillRect(-this.width / 2, -this.height / 2, this.width, this.height);
|
||||
ctx.restore();
|
||||
this.callSuper('drawObject', ctx);
|
||||
},
|
||||
|
||||
/**
|
||||
* eraser should retain size
|
||||
* dimensions should not change when paths are added or removed
|
||||
* handled by {@link fabric.Object#_drawClipPath}
|
||||
* @override
|
||||
* @private
|
||||
*/
|
||||
_getBounds: function () {
|
||||
// noop
|
||||
},
|
||||
|
||||
/* _TO_SVG_START_ */
|
||||
/**
|
||||
* Returns svg representation of an instance
|
||||
* use <mask> to achieve erasing for svg, credit: https://travishorn.com/removing-parts-of-shapes-in-svg-b539a89e5649
|
||||
* for masking we need to add a white rect before all paths
|
||||
*
|
||||
* @param {Function} [reviver] Method for further parsing of svg representation.
|
||||
* @return {String} svg representation of an instance
|
||||
*/
|
||||
_toSVG: function (reviver) {
|
||||
var svgString = ['<g ', 'COMMON_PARTS', ' >\n'];
|
||||
var x = -this.width / 2, y = -this.height / 2;
|
||||
var rectSvg = [
|
||||
'<rect ', 'fill="white" ',
|
||||
'x="', x, '" y="', y,
|
||||
'" width="', this.width, '" height="', this.height,
|
||||
'" />\n'
|
||||
].join('');
|
||||
svgString.push('\t\t', rectSvg);
|
||||
for (var i = 0, len = this._objects.length; i < len; i++) {
|
||||
svgString.push('\t\t', this._objects[i].toSVG(reviver));
|
||||
}
|
||||
svgString.push('</g>\n');
|
||||
return svgString;
|
||||
},
|
||||
/* _TO_SVG_END_ */
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns {@link fabric.Eraser} instance from an object representation
|
||||
* @static
|
||||
* @memberOf fabric.Eraser
|
||||
* @param {Object} object Object to create an Eraser from
|
||||
* @param {Function} [callback] Callback to invoke when an eraser instance is created
|
||||
*/
|
||||
fabric.Eraser.fromObject = function (object, callback) {
|
||||
var objects = object.objects;
|
||||
fabric.util.enlivenObjects(objects, function (enlivenedObjects) {
|
||||
var options = fabric.util.object.clone(object, true);
|
||||
delete options.objects;
|
||||
fabric.util.enlivenObjectEnlivables(object, options, function () {
|
||||
callback && callback(new fabric.Eraser(enlivenedObjects, options, true));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
var __renderOverlay = fabric.Canvas.prototype._renderOverlay;
|
||||
/**
|
||||
* @fires erasing:start
|
||||
* @fires erasing:end
|
||||
*/
|
||||
fabric.util.object.extend(fabric.Canvas.prototype, {
|
||||
/**
|
||||
* Used by {@link #renderAll}
|
||||
* @returns boolean
|
||||
*/
|
||||
isErasing: function () {
|
||||
return (
|
||||
this.isDrawingMode &&
|
||||
this.freeDrawingBrush &&
|
||||
this.freeDrawingBrush.type === 'eraser' &&
|
||||
this.freeDrawingBrush._isErasing
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* While erasing the brush clips out the erasing path from canvas
|
||||
* so we need to render it on top of canvas every render
|
||||
* @param {CanvasRenderingContext2D} ctx
|
||||
*/
|
||||
_renderOverlay: function (ctx) {
|
||||
__renderOverlay.call(this, ctx);
|
||||
if (this.isErasing() && !this.freeDrawingBrush.inverted) {
|
||||
this.freeDrawingBrush._render();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* EraserBrush class
|
||||
* Supports selective erasing meaning that only erasable objects are affected by the eraser brush.
|
||||
* Supports **inverted** erasing meaning that the brush can "undo" erasing.
|
||||
*
|
||||
* In order to support selective erasing, the brush clips the entire canvas
|
||||
* and then draws all non-erasable objects over the erased path using a pattern brush so to speak (masking).
|
||||
* If brush is **inverted** there is no need to clip canvas. The brush draws all erasable objects without their eraser.
|
||||
* This achieves the desired effect of seeming to erase or unerase only erasable objects.
|
||||
* After erasing is done the created path is added to all intersected objects' `eraser` property.
|
||||
*
|
||||
* In order to update the EraserBrush call `preparePattern`.
|
||||
* It may come in handy when canvas changes during erasing (i.e animations) and you want the eraser to reflect the changes.
|
||||
*
|
||||
* @tutorial {@link http://fabricjs.com/erasing}
|
||||
* @class fabric.EraserBrush
|
||||
* @extends fabric.PencilBrush
|
||||
* @memberof fabric
|
||||
*/
|
||||
fabric.EraserBrush = fabric.util.createClass(
|
||||
fabric.PencilBrush,
|
||||
/** @lends fabric.EraserBrush.prototype */ {
|
||||
type: 'eraser',
|
||||
|
||||
/**
|
||||
* When set to `true` the brush will create a visual effect of undoing erasing
|
||||
*/
|
||||
inverted: false,
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_isErasing: false,
|
||||
|
||||
/**
|
||||
*
|
||||
* @private
|
||||
* @param {fabric.Object} object
|
||||
* @returns boolean
|
||||
*/
|
||||
_isErasable: function (object) {
|
||||
return object.erasable !== false;
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
* This is designed to support erasing a collection with both erasable and non-erasable objects.
|
||||
* Iterates over collections to allow nested selective erasing.
|
||||
* Prepares the pattern brush that will draw on the top context to achieve the desired visual effect.
|
||||
* If brush is **NOT** inverted render all non-erasable objects.
|
||||
* If brush is inverted render all erasable objects that have been erased with their clip path inverted.
|
||||
* This will render the erased parts as if they were not erased.
|
||||
*
|
||||
* @param {fabric.Collection} collection
|
||||
* @param {CanvasRenderingContext2D} ctx
|
||||
* @param {{ visibility: fabric.Object[], eraser: fabric.Object[], collection: fabric.Object[] }} restorationContext
|
||||
*/
|
||||
_prepareCollectionTraversal: function (collection, ctx, restorationContext) {
|
||||
collection.forEachObject(function (obj) {
|
||||
if (obj.forEachObject && obj.erasable === 'deep') {
|
||||
// traverse
|
||||
this._prepareCollectionTraversal(obj, ctx, restorationContext);
|
||||
}
|
||||
else if (!this.inverted && obj.erasable && obj.visible) {
|
||||
// render only non-erasable objects
|
||||
obj.visible = false;
|
||||
collection.dirty = true;
|
||||
restorationContext.visibility.push(obj);
|
||||
restorationContext.collection.push(collection);
|
||||
}
|
||||
else if (this.inverted && obj.visible) {
|
||||
// render only erasable objects that were erased
|
||||
if (obj.erasable && obj.eraser) {
|
||||
obj.eraser.inverted = true;
|
||||
obj.dirty = true;
|
||||
collection.dirty = true;
|
||||
restorationContext.eraser.push(obj);
|
||||
restorationContext.collection.push(collection);
|
||||
}
|
||||
else {
|
||||
obj.visible = false;
|
||||
collection.dirty = true;
|
||||
restorationContext.visibility.push(obj);
|
||||
restorationContext.collection.push(collection);
|
||||
}
|
||||
}
|
||||
}, this);
|
||||
},
|
||||
|
||||
/**
|
||||
* Prepare the pattern for the erasing brush
|
||||
* This pattern will be drawn on the top context, achieving a visual effect of erasing only erasable objects
|
||||
* @todo decide how overlay color should behave when `inverted === true`, currently draws over it which is undesirable
|
||||
* @private
|
||||
*/
|
||||
preparePattern: function () {
|
||||
if (!this._patternCanvas) {
|
||||
this._patternCanvas = fabric.util.createCanvasElement();
|
||||
}
|
||||
var canvas = this._patternCanvas;
|
||||
canvas.width = this.canvas.width;
|
||||
canvas.height = this.canvas.height;
|
||||
var patternCtx = canvas.getContext('2d');
|
||||
if (this.canvas._isRetinaScaling()) {
|
||||
var retinaScaling = this.canvas.getRetinaScaling();
|
||||
this.canvas.__initRetinaScaling(retinaScaling, canvas, patternCtx);
|
||||
}
|
||||
var backgroundImage = this.canvas.backgroundImage,
|
||||
bgErasable = backgroundImage && this._isErasable(backgroundImage),
|
||||
overlayImage = this.canvas.overlayImage,
|
||||
overlayErasable = overlayImage && this._isErasable(overlayImage);
|
||||
if (!this.inverted && ((backgroundImage && !bgErasable) || !!this.canvas.backgroundColor)) {
|
||||
if (bgErasable) { this.canvas.backgroundImage = undefined; }
|
||||
this.canvas._renderBackground(patternCtx);
|
||||
if (bgErasable) { this.canvas.backgroundImage = backgroundImage; }
|
||||
}
|
||||
else if (this.inverted && (backgroundImage && bgErasable)) {
|
||||
var color = this.canvas.backgroundColor;
|
||||
this.canvas.backgroundColor = undefined;
|
||||
this.canvas._renderBackground(patternCtx);
|
||||
this.canvas.backgroundColor = color;
|
||||
}
|
||||
patternCtx.save();
|
||||
patternCtx.transform.apply(patternCtx, this.canvas.viewportTransform);
|
||||
var restorationContext = { visibility: [], eraser: [], collection: [] };
|
||||
this._prepareCollectionTraversal(this.canvas, patternCtx, restorationContext);
|
||||
this.canvas._renderObjects(patternCtx, this.canvas._objects);
|
||||
restorationContext.visibility.forEach(function (obj) { obj.visible = true; });
|
||||
restorationContext.eraser.forEach(function (obj) {
|
||||
obj.eraser.inverted = false;
|
||||
obj.dirty = true;
|
||||
});
|
||||
restorationContext.collection.forEach(function (obj) { obj.dirty = true; });
|
||||
patternCtx.restore();
|
||||
if (!this.inverted && ((overlayImage && !overlayErasable) || !!this.canvas.overlayColor)) {
|
||||
if (overlayErasable) { this.canvas.overlayImage = undefined; }
|
||||
__renderOverlay.call(this.canvas, patternCtx);
|
||||
if (overlayErasable) { this.canvas.overlayImage = overlayImage; }
|
||||
}
|
||||
else if (this.inverted && (overlayImage && overlayErasable)) {
|
||||
var color = this.canvas.overlayColor;
|
||||
this.canvas.overlayColor = undefined;
|
||||
__renderOverlay.call(this.canvas, patternCtx);
|
||||
this.canvas.overlayColor = color;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets brush styles
|
||||
* @private
|
||||
* @param {CanvasRenderingContext2D} ctx
|
||||
*/
|
||||
_setBrushStyles: function (ctx) {
|
||||
this.callSuper('_setBrushStyles', ctx);
|
||||
ctx.strokeStyle = 'black';
|
||||
},
|
||||
|
||||
/**
|
||||
* **Customiztion**
|
||||
*
|
||||
* if you need the eraser to update on each render (i.e animating during erasing) override this method by **adding** the following (performance may suffer):
|
||||
* @example
|
||||
* ```
|
||||
* if(ctx === this.canvas.contextTop) {
|
||||
* this.preparePattern();
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @override fabric.BaseBrush#_saveAndTransform
|
||||
* @param {CanvasRenderingContext2D} ctx
|
||||
*/
|
||||
_saveAndTransform: function (ctx) {
|
||||
this.callSuper('_saveAndTransform', ctx);
|
||||
this._setBrushStyles(ctx);
|
||||
ctx.globalCompositeOperation = ctx === this.canvas.getContext() ? 'destination-out' : 'source-over';
|
||||
},
|
||||
|
||||
/**
|
||||
* We indicate {@link fabric.PencilBrush} to repaint itself if necessary
|
||||
* @returns
|
||||
*/
|
||||
needsFullRender: function () {
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {fabric.Point} pointer
|
||||
* @param {fabric.IEvent} options
|
||||
* @returns
|
||||
*/
|
||||
onMouseDown: function (pointer, options) {
|
||||
if (!this.canvas._isMainEvent(options.e)) {
|
||||
return;
|
||||
}
|
||||
this._prepareForDrawing(pointer);
|
||||
// capture coordinates immediately
|
||||
// this allows to draw dots (when movement never occurs)
|
||||
this._captureDrawingPath(pointer);
|
||||
|
||||
// prepare for erasing
|
||||
this.preparePattern();
|
||||
this._isErasing = true;
|
||||
this.canvas.fire('erasing:start');
|
||||
this._render();
|
||||
},
|
||||
|
||||
/**
|
||||
* Rendering Logic:
|
||||
* 1. Use brush to clip canvas by rendering it on top of canvas (unnecessary if `inverted === true`)
|
||||
* 2. Render brush with canvas pattern on top context
|
||||
*
|
||||
*/
|
||||
_render: function () {
|
||||
var ctx;
|
||||
if (!this.inverted) {
|
||||
// clip canvas
|
||||
ctx = this.canvas.getContext();
|
||||
this.callSuper('_render', ctx);
|
||||
}
|
||||
// render brush and mask it with image of non erasables
|
||||
ctx = this.canvas.contextTop;
|
||||
this.canvas.clearContext(ctx);
|
||||
this.callSuper('_render', ctx);
|
||||
ctx.save();
|
||||
var t = this.canvas.getRetinaScaling(), s = 1 / t;
|
||||
ctx.scale(s, s);
|
||||
ctx.globalCompositeOperation = 'source-in';
|
||||
ctx.drawImage(this._patternCanvas, 0, 0);
|
||||
ctx.restore();
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates fabric.Path object
|
||||
* @override
|
||||
* @private
|
||||
* @param {(string|number)[][]} pathData Path data
|
||||
* @return {fabric.Path} Path to add on canvas
|
||||
* @returns
|
||||
*/
|
||||
createPath: function (pathData) {
|
||||
var path = this.callSuper('createPath', pathData);
|
||||
path.globalCompositeOperation = this.inverted ? 'source-over' : 'destination-out';
|
||||
path.stroke = this.inverted ? 'white' : 'black';
|
||||
return path;
|
||||
},
|
||||
|
||||
/**
|
||||
* Utility to apply a clip path to a path.
|
||||
* Used to preserve clipping on eraser paths in nested objects.
|
||||
* Called when a group has a clip path that should be applied to the path before applying erasing on the group's objects.
|
||||
* @param {fabric.Path} path The eraser path in canvas coordinate plane
|
||||
* @param {fabric.Object} clipPath The clipPath to apply to the path
|
||||
* @param {number[]} clipPathContainerTransformMatrix The transform matrix of the object that the clip path belongs to
|
||||
* @returns {fabric.Path} path with clip path
|
||||
*/
|
||||
applyClipPathToPath: function (path, clipPath, clipPathContainerTransformMatrix) {
|
||||
var pathInvTransform = fabric.util.invertTransform(path.calcTransformMatrix()),
|
||||
clipPathTransform = clipPath.calcTransformMatrix(),
|
||||
transform = clipPath.absolutePositioned ?
|
||||
pathInvTransform :
|
||||
fabric.util.multiplyTransformMatrices(
|
||||
pathInvTransform,
|
||||
clipPathContainerTransformMatrix
|
||||
);
|
||||
// when passing down a clip path it becomes relative to the parent
|
||||
// so we transform it acoordingly and set `absolutePositioned` to false
|
||||
clipPath.absolutePositioned = false;
|
||||
fabric.util.applyTransformToObject(
|
||||
clipPath,
|
||||
fabric.util.multiplyTransformMatrices(
|
||||
transform,
|
||||
clipPathTransform
|
||||
)
|
||||
);
|
||||
// We need to clip `path` with both `clipPath` and it's own clip path if existing (`path.clipPath`)
|
||||
// so in turn `path` erases an object only where it overlaps with all it's clip paths, regardless of how many there are.
|
||||
// this is done because both clip paths may have nested clip paths of their own (this method walks down a collection => this may reccur),
|
||||
// so we can't assign one to the other's clip path property.
|
||||
path.clipPath = path.clipPath ? fabric.util.mergeClipPaths(clipPath, path.clipPath) : clipPath;
|
||||
return path;
|
||||
},
|
||||
|
||||
/**
|
||||
* Utility to apply a clip path to a path.
|
||||
* Used to preserve clipping on eraser paths in nested objects.
|
||||
* Called when a group has a clip path that should be applied to the path before applying erasing on the group's objects.
|
||||
* @param {fabric.Path} path The eraser path
|
||||
* @param {fabric.Object} object The clipPath to apply to path belongs to object
|
||||
* @param {Function} callback Callback to be invoked with the cloned path after applying the clip path
|
||||
*/
|
||||
clonePathWithClipPath: function (path, object, callback) {
|
||||
var objTransform = object.calcTransformMatrix();
|
||||
var clipPath = object.clipPath;
|
||||
var _this = this;
|
||||
path.clone(function (_path) {
|
||||
clipPath.clone(function (_clipPath) {
|
||||
callback(_this.applyClipPathToPath(_path, _clipPath, objTransform));
|
||||
}, ['absolutePositioned', 'inverted']);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds path to object's eraser, walks down object's descendants if necessary
|
||||
*
|
||||
* @fires erasing:end on object
|
||||
* @param {fabric.Object} obj
|
||||
* @param {fabric.Path} path
|
||||
*/
|
||||
_addPathToObjectEraser: function (obj, path) {
|
||||
var _this = this;
|
||||
// object is collection, i.e group
|
||||
if (obj.forEachObject && obj.erasable === 'deep') {
|
||||
var targets = obj._objects.filter(function (_obj) {
|
||||
return _obj.erasable;
|
||||
});
|
||||
if (targets.length > 0 && obj.clipPath) {
|
||||
this.clonePathWithClipPath(path, obj, function (_path) {
|
||||
targets.forEach(function (_obj) {
|
||||
_this._addPathToObjectEraser(_obj, _path);
|
||||
});
|
||||
});
|
||||
}
|
||||
else if (targets.length > 0) {
|
||||
targets.forEach(function (_obj) {
|
||||
_this._addPathToObjectEraser(_obj, path);
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
// prepare eraser
|
||||
var eraser = obj.eraser;
|
||||
if (!eraser) {
|
||||
eraser = new fabric.Eraser();
|
||||
obj.eraser = eraser;
|
||||
}
|
||||
// clone and add path
|
||||
path.clone(function (path) {
|
||||
// http://fabricjs.com/using-transformations
|
||||
var desiredTransform = fabric.util.multiplyTransformMatrices(
|
||||
fabric.util.invertTransform(
|
||||
obj.calcTransformMatrix()
|
||||
),
|
||||
path.calcTransformMatrix()
|
||||
);
|
||||
fabric.util.applyTransformToObject(path, desiredTransform);
|
||||
eraser.addWithUpdate(path);
|
||||
obj.set('dirty', true);
|
||||
obj.fire('erasing:end', {
|
||||
path: path
|
||||
});
|
||||
if (obj.group && Array.isArray(_this.__subTargets)) {
|
||||
_this.__subTargets.push(obj);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Add the eraser path to canvas drawables' clip paths
|
||||
*
|
||||
* @param {fabric.Canvas} source
|
||||
* @param {fabric.Canvas} path
|
||||
* @returns {Object} canvas drawables that were erased by the path
|
||||
*/
|
||||
applyEraserToCanvas: function (path) {
|
||||
var canvas = this.canvas;
|
||||
var drawables = {};
|
||||
[
|
||||
'backgroundImage',
|
||||
'overlayImage',
|
||||
].forEach(function (prop) {
|
||||
var drawable = canvas[prop];
|
||||
if (drawable && drawable.erasable) {
|
||||
this._addPathToObjectEraser(drawable, path);
|
||||
drawables[prop] = drawable;
|
||||
}
|
||||
}, this);
|
||||
return drawables;
|
||||
},
|
||||
|
||||
/**
|
||||
* On mouseup after drawing the path on contextTop canvas
|
||||
* we use the points captured to create an new fabric path object
|
||||
* and add it to every intersected erasable object.
|
||||
*/
|
||||
_finalizeAndAddPath: function () {
|
||||
var ctx = this.canvas.contextTop, canvas = this.canvas;
|
||||
ctx.closePath();
|
||||
if (this.decimate) {
|
||||
this._points = this.decimatePoints(this._points, this.decimate);
|
||||
}
|
||||
|
||||
// clear
|
||||
canvas.clearContext(canvas.contextTop);
|
||||
this._isErasing = false;
|
||||
|
||||
var pathData = this._points && this._points.length > 1 ?
|
||||
this.convertPointsToSVGPath(this._points) :
|
||||
null;
|
||||
if (!pathData || this._isEmptySVGPath(pathData)) {
|
||||
canvas.fire('erasing:end');
|
||||
// do not create 0 width/height paths, as they are
|
||||
// rendered inconsistently across browsers
|
||||
// Firefox 4, for example, renders a dot,
|
||||
// whereas Chrome 10 renders nothing
|
||||
canvas.requestRenderAll();
|
||||
return;
|
||||
}
|
||||
|
||||
var path = this.createPath(pathData);
|
||||
// needed for `intersectsWithObject`
|
||||
path.setCoords();
|
||||
// commense event sequence
|
||||
canvas.fire('before:path:created', { path: path });
|
||||
|
||||
// finalize erasing
|
||||
var drawables = this.applyEraserToCanvas(path);
|
||||
var _this = this;
|
||||
this.__subTargets = [];
|
||||
var targets = [];
|
||||
canvas.forEachObject(function (obj) {
|
||||
if (obj.erasable && obj.intersectsWithObject(path, true, true)) {
|
||||
_this._addPathToObjectEraser(obj, path);
|
||||
targets.push(obj);
|
||||
}
|
||||
});
|
||||
// fire erasing:end
|
||||
canvas.fire('erasing:end', {
|
||||
path: path,
|
||||
targets: targets,
|
||||
subTargets: this.__subTargets,
|
||||
drawables: drawables
|
||||
});
|
||||
delete this.__subTargets;
|
||||
|
||||
canvas.requestRenderAll();
|
||||
this._resetShadow();
|
||||
|
||||
// fire event 'path' created
|
||||
canvas.fire('path:created', { path: path });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/** ERASER_END */
|
||||
}
|
||||
baseBrush() // 加载橡皮擦到组件上
|
||||
// 当前使用到的常量|类型(枚举) ============================
|
||||
export class TYPES {
|
||||
static ActionMode = {
|
||||
|
@ -378,10 +1144,21 @@ export const FreeStyle = {
|
|||
const canvas = fabricVue?.canvas
|
||||
const drawConfig = fabricVue?.drawConfig
|
||||
const eraserBrush = new fabric.EraserBrush(canvas)
|
||||
const width = Utils.getWidth(drawConfig.eraserWidth, fabricVue)
|
||||
canvas.isDrawingMode = true
|
||||
canvas.freeDrawingBrush = eraserBrush
|
||||
canvas.freeDrawingBrush.width = Utils.getWidth(drawConfig.eraserWidth, fabricVue)
|
||||
canvas.freeDrawingBrush.width = width
|
||||
canvas.freeDrawingBrush.color = '#FFF'
|
||||
// FabricVue.canvas.freeDrawingCursor = `url(/imgs/erase.svg) 10 10,crosshair`
|
||||
canvas.freeDrawingCursor = FreeStyle.reaserSvg(width)
|
||||
},
|
||||
reaserSvg: (width, color = '#ccc') => { // 橡皮擦-鼠标样式(自定义)
|
||||
const svg = `
|
||||
<svg style="fill: currentColor;color: ${color};" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1023 366.1L662.3 5.2 585 82.6l-34.4-34.4L69.7 538l34.4 34.4-51.5 34.4c-68.7 68.7-68.7 171.9 0 240.6l120.2 120.3c68.7 68.7 171.8 68.7 240.5 0l42.9-43 34.4 34.4 489.5-489.8-34.4-25.8 77.3-77.4zM662.3 65.4l300.6 300.8-51.5 51.6-300.6-309.5 51.5-42.9zM404.7 924.7c-60.1 60.2-154.6 60.2-214.7 0l-94.5-94.5c-60.1-60.2-60.1-154.7 0-214.8l34.4-17.2L430.5 899l-25.8 25.7z" />
|
||||
</svg>`
|
||||
const svgUrl = `data:image/svg+xml;base64,${btoa(svg)}`
|
||||
return `url(${svgUrl}) ${width/2} ${width}, crosshair`
|
||||
}
|
||||
}
|
||||
// 事件类
|
||||
|
@ -600,8 +1377,8 @@ export class CanvasEvent {
|
|||
}
|
||||
// 移除事件
|
||||
removeEvent() {
|
||||
this.windowEvent.removeWindowEvent()
|
||||
this.touchEvent.removeTouchEvent()
|
||||
this.windowEvent?.removeWindowEvent()
|
||||
this.touchEvent?.removeTouchEvent()
|
||||
}
|
||||
}
|
||||
// 历史类
|
||||
|
@ -860,7 +1637,7 @@ export class fabricVue {
|
|||
*/
|
||||
deleteObject() {
|
||||
// Disable deletion in text input state
|
||||
if (this.textElement.isTextEditing) {
|
||||
if (this.textElement?.isTextEditing) {
|
||||
return
|
||||
}
|
||||
if (this.canvas) {
|
||||
|
@ -1010,5 +1787,4 @@ export class fabricVue {
|
|||
}
|
||||
export const FabricVue = new fabricVue()
|
||||
export default FabricVue
|
||||
export const Fabric = fabric
|
||||
|
||||
|
|
|
@ -5,19 +5,21 @@
|
|||
<script setup>
|
||||
// 功能说明:画板
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { FabricVue } from '@/plugins/fabric'
|
||||
import {FabricVue, TYPES} from '@/plugins/fabric'
|
||||
import { useBoardStore, useDrawStore } from '@/store/modules/draw'
|
||||
const canvasRef = ref(null)
|
||||
const props = defineProps({
|
||||
modelValue: String
|
||||
})
|
||||
|
||||
onMounted(async() => {
|
||||
if (canvasRef.value) {
|
||||
useBoardStore().backgroundColor = 'transparent'
|
||||
useDrawStore().drawColors = ['red']
|
||||
// useBoardStore().backgroundColor = 'transparent'
|
||||
// useDrawStore().drawColors = ['red']
|
||||
FabricVue.drawConfig.drawColors = ['red']
|
||||
FabricVue.boardConfig.backgroundColor = 'transparent'
|
||||
const option = { freeDrawingCursor: 'default' }
|
||||
await FabricVue.initCanvas(canvasRef.value, option)
|
||||
// FabricVue.canvas.backgroundColor = 'transparent'
|
||||
// FabricVue.canvas.setWidth(500)
|
||||
// FabricVue.canvas.setHeight(500)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div class="warp-all">
|
||||
<board-vue></board-vue>
|
||||
<board-vue v-model="tabActive"></board-vue>
|
||||
<!-- 底部工具栏 -->
|
||||
<el-row id="test" class="tool-bottom-all" @mouseenter="mouseChange(0)" @mouseleave="mouseChange(1)">
|
||||
<el-col :span="3" class="flex justify-center items-center">
|
||||
|
@ -47,6 +47,7 @@ const tabChange = (val) => { // 切换tab-change
|
|||
case 'brush':
|
||||
break
|
||||
case 'eraser':
|
||||
|
||||
break
|
||||
case 'interact':
|
||||
break
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
<canvas ref="canvasRef" />
|
||||
<button @click="eraseTo">橡皮擦
|
||||
<i class="iconfont icon-xiangpica"></i>
|
||||
<img src="/imgs/erase.svg" alt="" srcset="">
|
||||
</button>
|
||||
<button @click="close">销毁</button>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
@ -11,63 +11,38 @@
|
|||
import { ref, onMounted } from 'vue'
|
||||
// import { FabricVue } from '@/plugins/fabric'
|
||||
// import { useBoardStore } from '@/store/modules/draw'
|
||||
import {Fabric,FabricVue, TYPES} from '@/plugins/fabric'
|
||||
import {FabricVue, TYPES} from '@/plugins/fabric'
|
||||
let canvasRef = ref(null)
|
||||
let canvas = null
|
||||
|
||||
onMounted(async() => {
|
||||
console.log(canvasRef, FabricVue)
|
||||
// console.log(canvasRef, FabricVue)
|
||||
// canvasRef.value = 123
|
||||
if (canvasRef.value) {
|
||||
// useBoardStore().backgroundColor = 'transparent'
|
||||
const option = { freeDrawingCursor: 'default' }
|
||||
// await FabricVue.initCanvas(canvasRef.value, option)
|
||||
await FabricVue.initCanvas(canvasRef.value, option)
|
||||
// FabricVue.canvas.setWidth(500)
|
||||
// FabricVue.canvas.setHeight(500)
|
||||
FabricVue.drawConfig.drawColors = ['red']
|
||||
await FabricVue.initCanvas(canvasRef.value, option)
|
||||
|
||||
}
|
||||
// if (canvasRef.value) {
|
||||
// canvas = new fabric.Canvas(canvasRef.value,{
|
||||
// isDrawingMode: true,
|
||||
// freeDrawingCursor: 'default',
|
||||
// backgroundColor: 'transparent',
|
||||
// width: window.innerWidth,
|
||||
// height: window.innerHeight
|
||||
// })
|
||||
// canvas.isDrawingMode = true
|
||||
// }
|
||||
})
|
||||
const eraseTo = () => { // 橡皮擦
|
||||
FabricVue.handleMode(TYPES.ActionMode.ERASE)
|
||||
const brush = FabricVue.canvas.freeDrawingBrush
|
||||
console.log('brush', brush)
|
||||
// FabricVue.canvas.freeDrawingCursor = `url(/imgs/erase.svg),crosshair `
|
||||
let tempRect
|
||||
FabricVue.canvas.on('mouse:down', (e) => {
|
||||
})
|
||||
FabricVue.canvas.on('mouse:move', (e) => {
|
||||
|
||||
if (tempRect) {
|
||||
tempRect.set({
|
||||
left: e.pointer.x - brush.width/2,
|
||||
top: e.pointer.y - brush.width/2,
|
||||
})
|
||||
FabricVue.canvas.renderAll()
|
||||
} else {
|
||||
tempRect = new Fabric.Rect({
|
||||
left: e.pointer.x - brush.width/2,
|
||||
top: e.pointer.y - brush.width/2,
|
||||
width: brush.width,
|
||||
height: brush.width,
|
||||
fill: 'transparent',
|
||||
stroke: 'red',
|
||||
strokeWidth: 1,
|
||||
hasControls: false,
|
||||
hasBorders: false,
|
||||
selectable: false,
|
||||
evented: false
|
||||
})
|
||||
FabricVue.canvas.add(tempRect)
|
||||
// FabricVue.removeCanvas()
|
||||
// FabricVue.canvas.dispose()
|
||||
// canvas.dispose()
|
||||
}
|
||||
})
|
||||
FabricVue.canvas.on('mouse:up', (e) => {
|
||||
if (tempRect) {
|
||||
FabricVue.canvas.remove(tempRect);
|
||||
tempRect = null;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const close = () => { FabricVue.removeCanvas() }
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
Loading…
Reference in New Issue