├── .browserslistrc ├── vue.config.js ├── demo.png ├── babel.config.js ├── public ├── favicon.ico └── index.html ├── src ├── assets │ └── logo.png ├── main.js ├── router │ └── index.js ├── App.vue ├── views │ └── Home.vue └── libs │ └── eraser_brush.mixin.js ├── .gitignore ├── .eslintrc.js ├── README.md └── package.json /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | lintOnSave: false 3 | } -------------------------------------------------------------------------------- /demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CC4J/fabric-drawing-board/HEAD/demo.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ["@vue/cli-plugin-babel/preset"], 3 | }; 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CC4J/fabric-drawing-board/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CC4J/fabric-drawing-board/HEAD/src/assets/logo.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import ElementUI from 'element-ui'; 3 | import 'element-ui/lib/theme-chalk/index.css'; 4 | import App from "./App.vue"; 5 | import router from "./router"; 6 | 7 | Vue.use(ElementUI); 8 | 9 | Vue.config.productionTip = false; 10 | 11 | new Vue({ 12 | router, 13 | render: (h) => h(App), 14 | }).$mount("#app"); -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import VueRouter from "vue-router"; 3 | import Home from "../views/Home.vue"; 4 | 5 | Vue.use(VueRouter); 6 | 7 | const routes = [{ 8 | path: "/", 9 | name: "Home", 10 | component: Home, 11 | }]; 12 | 13 | const router = new VueRouter({ 14 | routes, 15 | }); 16 | 17 | export default router; -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | }, 6 | // extends: ["plugin:vue/essential", "eslint:recommended", "@vue/prettier"], 7 | parserOptions: { 8 | parser: "babel-eslint", 9 | }, 10 | rules: { 11 | "no-console": process.env.NODE_ENV === "production" ? "warn" : "off", 12 | "no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off", 13 | }, 14 | }; -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 30 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 9 | # fabric-drawing-borard 10 | 11 | use fabric to achievement drawing board and support free drawing, line, rect,cirlce,eraser,text,pan,scale,undo,redo,clear, save. 12 | 13 | 使用fabric实现画板功能,支持画笔,绘制直线,矩形,圆形,文字,移动缩放画布,撤销重做,清屏与保存功能。 14 | 15 | ![demo](https://github.com/CC4J/fabric-drawing-board/blob/master/demo.png?raw=true) 16 | 17 | ## Project setup 18 | ``` 19 | npm install 20 | ``` 21 | 22 | ### Compiles and hot-reloads for development 23 | ``` 24 | npm run serve 25 | ``` 26 | 27 | ## Usage 28 | 29 | ### npm package 30 | 31 | ``` 32 | npm install --save fabric-drawing-board 33 | 34 | ``` 35 | 36 | ### Repo 37 | 38 | - https://github.com/CC4J/fabric-drawing-board-plugin 39 | 40 | ### npm package demo 41 | 42 | - https://github.com/CC4J/fabric-drawing-board-plugin-demo -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fabric-drawing-board", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "core-js": "^3.6.5", 12 | "element-ui": "^2.15.2", 13 | "fabric": "^4.5.0", 14 | "vue": "^2.6.11", 15 | "vue-color": "^2.8.1", 16 | "vue-router": "^3.2.0", 17 | "vuex": "^3.4.0" 18 | }, 19 | "devDependencies": { 20 | "@vue/cli-plugin-babel": "~4.5.0", 21 | "@vue/cli-plugin-eslint": "~4.5.0", 22 | "@vue/cli-plugin-router": "~4.5.0", 23 | "@vue/cli-plugin-vuex": "~4.5.0", 24 | "@vue/cli-service": "~4.5.0", 25 | "@vue/eslint-config-prettier": "^6.0.0", 26 | "babel-eslint": "^10.1.0", 27 | "eslint": "^6.7.2", 28 | "eslint-plugin-prettier": "^3.3.1", 29 | "eslint-plugin-vue": "^6.2.2", 30 | "less": "^3.0.4", 31 | "less-loader": "^5.0.0", 32 | "prettier": "^2.2.1", 33 | "vue-template-compiler": "^2.6.11" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 114 | 115 | 597 | 641 | -------------------------------------------------------------------------------- /src/libs/eraser_brush.mixin.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | /** ERASER_START */ 3 | var __setBgOverlayColor = fabric.StaticCanvas.prototype.__setBgOverlayColor; 4 | var ___setBgOverlay = fabric.StaticCanvas.prototype.__setBgOverlay; 5 | var __setSVGBgOverlayColor = fabric.StaticCanvas.prototype._setSVGBgOverlayColor; 6 | fabric.util.object.extend(fabric.StaticCanvas.prototype, { 7 | backgroundColor: undefined, 8 | overlayColor: undefined, 9 | /** 10 | * Create Rect that holds the color to support erasing 11 | * patches {@link CommonMethods#_initGradient} 12 | * @private 13 | * @param {'bakground'|'overlay'} property 14 | * @param {(String|fabric.Pattern|fabric.Rect)} color Color or pattern or rect (in case of erasing) 15 | * @param {Function} callback Callback to invoke when color is set 16 | * @param {Object} options 17 | * @return {fabric.Canvas} instance 18 | * @chainable true 19 | */ 20 | __setBgOverlayColor: function(property, color, callback, options) { 21 | if (color && color.isType && color.isType('rect')) { 22 | // color is already an object 23 | this[property] = color; 24 | color.set(options); 25 | callback && callback(this[property]); 26 | } else { 27 | var _this = this; 28 | var cb = function() { 29 | _this[property] = new fabric.Rect(fabric.util.object.extend({ 30 | width: _this.width, 31 | height: _this.height, 32 | fill: _this[property], 33 | }, options)); 34 | callback && callback(_this[property]); 35 | }; 36 | __setBgOverlayColor.call(this, property, color, cb); 37 | // invoke cb in case of gradient 38 | // see {@link CommonMethods#_initGradient} 39 | if (color && color.colorStops && !(color instanceof fabric.Gradient)) { 40 | cb(); 41 | } 42 | } 43 | 44 | return this; 45 | }, 46 | 47 | setBackgroundColor: function(backgroundColor, callback, options) { 48 | return this.__setBgOverlayColor('backgroundColor', backgroundColor, callback, options); 49 | }, 50 | 51 | setOverlayColor: function(overlayColor, callback, options) { 52 | return this.__setBgOverlayColor('overlayColor', overlayColor, callback, options); 53 | }, 54 | 55 | /** 56 | * patch serialization - from json 57 | * background/overlay properties could be objects if parsed by this mixin or could be legacy values 58 | * @private 59 | * @param {String} property Property to set (backgroundImage, overlayImage, backgroundColor, overlayColor) 60 | * @param {(Object|String)} value Value to set 61 | * @param {Object} loaded Set loaded property to true if property is set 62 | * @param {Object} callback Callback function to invoke after property is set 63 | */ 64 | __setBgOverlay: function(property, value, loaded, callback) { 65 | var _this = this; 66 | 67 | if ((property === 'backgroundColor' || property === 'overlayColor') && 68 | (value && typeof value === 'object' && value.type === 'rect')) { 69 | fabric.util.enlivenObjects([value], function(enlivedObject) { 70 | _this[property] = enlivedObject[0]; 71 | loaded[property] = true; 72 | callback && callback(); 73 | }); 74 | } else { 75 | ___setBgOverlay.call(this, property, value, loaded, callback); 76 | } 77 | }, 78 | 79 | /** 80 | * patch serialization - to svg 81 | * background/overlay properties could be objects if parsed by this mixin or could be legacy values 82 | * @private 83 | */ 84 | _setSVGBgOverlayColor: function(markup, property, reviver) { 85 | var filler = this[property + 'Color']; 86 | if (filler && filler.isType && filler.isType('rect')) { 87 | var excludeFromExport = filler.excludeFromExport || (this[property] && this[property].excludeFromExport); 88 | if (filler && !excludeFromExport && filler.toSVG) { 89 | markup.push(filler.toSVG(reviver)); 90 | } 91 | } else { 92 | __setSVGBgOverlayColor.call(this, markup, property, reviver); 93 | } 94 | }, 95 | 96 | /** 97 | * @private 98 | * @param {CanvasRenderingContext2D} ctx Context to render on 99 | * @param {string} property 'background' or 'overlay' 100 | */ 101 | _renderBackgroundOrOverlay: function(ctx, property) { 102 | var fill = this[property + 'Color'], 103 | object = this[property + 'Image'], 104 | v = this.viewportTransform, 105 | needsVpt = this[property + 'Vpt']; 106 | if (!fill && !object) { 107 | return; 108 | } 109 | if (fill || object) { 110 | ctx.save(); 111 | if (needsVpt) { 112 | ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); 113 | } 114 | fill && fill.render(ctx); 115 | object && object.render(ctx); 116 | ctx.restore(); 117 | } 118 | }, 119 | }); 120 | 121 | var _toObject = fabric.Object.prototype.toObject; 122 | var __createBaseSVGMarkup = fabric.Object.prototype._createBaseSVGMarkup; 123 | fabric.util.object.extend(fabric.Object.prototype, { 124 | /** 125 | * Indicates whether this object can be erased by {@link fabric.EraserBrush} 126 | * @type boolean 127 | * @default true 128 | */ 129 | erasable: true, 130 | 131 | /** 132 | * 133 | * @returns {fabric.Group | null} 134 | */ 135 | getEraser: function() { 136 | return this.clipPath && this.clipPath.eraser ? this.clipPath : null; 137 | }, 138 | 139 | /** 140 | * Returns an object representation of an instance 141 | * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output 142 | * @return {Object} Object representation of an instance 143 | */ 144 | toObject: function(additionalProperties) { 145 | return _toObject.call(this, ['erasable'].concat(additionalProperties)); 146 | }, 147 | 148 | /** 149 | * use to achieve erasing for svg 150 | * credit: https://travishorn.com/removing-parts-of-shapes-in-svg-b539a89e5649 151 | * @param {Function} reviver 152 | * @returns {string} markup 153 | */ 154 | eraserToSVG: function(options) { 155 | var eraser = this.getEraser(); 156 | if (eraser) { 157 | var fill = eraser._objects[0].fill; 158 | eraser._objects[0].fill = 'white'; 159 | eraser.clipPathId = 'CLIPPATH_' + fabric.Object.__uid++; 160 | var commons = [ 161 | 'id="' + eraser.clipPathId + '"', 162 | /*options.additionalTransform ? ' transform="' + options.additionalTransform + '" ' : ''*/ 163 | ].join(' '); 164 | var objectMarkup = ['', '', eraser.toSVG(options.reviver), '', '']; 165 | eraser._objects[0].fill = fill; 166 | return objectMarkup.join('\n'); 167 | } 168 | return ''; 169 | }, 170 | 171 | /** 172 | * use to achieve erasing for svg, override 173 | * @param {string[]} objectMarkup 174 | * @param {Object} options 175 | * @returns 176 | */ 177 | _createBaseSVGMarkup: function(objectMarkup, options) { 178 | var eraser = this.getEraser(); 179 | if (eraser) { 180 | var eraserMarkup = this.eraserToSVG(options); 181 | this.clipPath = null; 182 | var markup = __createBaseSVGMarkup.call(this, objectMarkup, options); 183 | this.clipPath = eraser; 184 | return [ 185 | eraserMarkup, 186 | markup.replace('>', 'mask="url(#' + eraser.clipPathId + ')" >') 187 | ].join('\n'); 188 | } else { 189 | return __createBaseSVGMarkup.call(this, objectMarkup, options); 190 | } 191 | } 192 | }); 193 | 194 | var _groupToObject = fabric.Group.prototype.toObject; 195 | fabric.util.object.extend(fabric.Group.prototype, { 196 | /** 197 | * Returns an object representation of an instance 198 | * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output 199 | * @return {Object} Object representation of an instance 200 | */ 201 | toObject: function(additionalProperties) { 202 | return _groupToObject.call(this, ['eraser'].concat(additionalProperties)); 203 | } 204 | }); 205 | 206 | fabric.util.object.extend(fabric.Canvas.prototype, { 207 | /** 208 | * Used by {@link #renderAll} 209 | * @returns boolean 210 | */ 211 | isErasing: function() { 212 | return ( 213 | this.isDrawingMode && 214 | this.freeDrawingBrush && 215 | this.freeDrawingBrush.type === 'eraser' && 216 | this.freeDrawingBrush._isErasing 217 | ); 218 | }, 219 | 220 | /** 221 | * While erasing, the brush is in charge of rendering the canvas 222 | * It uses both layers to achieve diserd erasing effect 223 | * 224 | * @returns fabric.Canvas 225 | */ 226 | renderAll: function() { 227 | if (this.contextTopDirty && !this._groupSelector && !this.isDrawingMode) { 228 | this.clearContext(this.contextTop); 229 | this.contextTopDirty = false; 230 | } 231 | // while erasing the brush is in charge of rendering the canvas so we return 232 | if (this.isErasing()) { 233 | this.freeDrawingBrush._render(); 234 | return; 235 | } 236 | if (this.hasLostContext) { 237 | this.renderTopLayer(this.contextTop); 238 | } 239 | var canvasToDrawOn = this.contextContainer; 240 | this.renderCanvas(canvasToDrawOn, this._chooseObjectsToRender()); 241 | return this; 242 | } 243 | }); 244 | 245 | 246 | /** 247 | * EraserBrush class 248 | * Supports selective erasing meaning that only erasable objects are affected by the eraser brush. 249 | * In order to support selective erasing all non erasable objects are rendered on the main/bottom ctx 250 | * while the entire canvas is rendered on the top ctx. 251 | * Canvas bakground/overlay image/color are handled as well. 252 | * When erasing occurs, the path clips the top ctx and reveals the bottom ctx. 253 | * This achieves the desired effect of seeming to erase only erasable objects. 254 | * After erasing is done the created path is added to all intersected objects' `clipPath` property. 255 | * 256 | * 257 | * @class fabric.EraserBrush 258 | * @extends fabric.PencilBrush 259 | */ 260 | fabric.EraserBrush = fabric.util.createClass( 261 | fabric.PencilBrush, 262 | /** @lends fabric.EraserBrush.prototype */ 263 | { 264 | type: 'eraser', 265 | 266 | /** 267 | * Indicates that the ctx is ready and rendering can begin. 268 | * Used to prevent a race condition caused by {@link fabric.EraserBrush#onMouseMove} firing before {@link fabric.EraserBrush#onMouseDown} has completed 269 | * 270 | * @private 271 | */ 272 | _ready: false, 273 | 274 | /** 275 | * @private 276 | */ 277 | _drawOverlayOnTop: false, 278 | 279 | /** 280 | * @private 281 | */ 282 | _isErasing: false, 283 | 284 | initialize: function(canvas) { 285 | this.callSuper('initialize', canvas); 286 | this._renderBound = this._render.bind(this); 287 | this.render = this.render.bind(this); 288 | }, 289 | 290 | /** 291 | * Used to hide a drawable from the rendering process 292 | * @param {fabric.Object} object 293 | */ 294 | hideObject: function(object) { 295 | if (object) { 296 | object._originalOpacity = object.opacity; 297 | object.set({ opacity: 0 }); 298 | } 299 | }, 300 | 301 | /** 302 | * Restores hiding an object 303 | * {@link fabric.EraserBrush#hideObject} 304 | * @param {fabric.Object} object 305 | */ 306 | restoreObjectVisibility: function(object) { 307 | if (object && object._originalOpacity) { 308 | object.set({ opacity: object._originalOpacity }); 309 | object._originalOpacity = undefined; 310 | } 311 | }, 312 | 313 | /** 314 | * Drawing Logic For background drawables: (`backgroundImage`, `backgroundColor`) 315 | * 1. if erasable = true: 316 | * we need to hide the drawable on the bottom ctx so when the brush is erasing it will clip the top ctx and reveal white space underneath 317 | * 2. if erasable = false: 318 | * we need to draw the drawable only on the bottom ctx so the brush won't affect it 319 | * @param {'bottom' | 'top' | 'overlay'} layer 320 | */ 321 | prepareCanvasBackgroundForLayer: function(layer) { 322 | if (layer === 'overlay') { 323 | return; 324 | } 325 | var canvas = this.canvas; 326 | var image = canvas.get('backgroundImage'); 327 | var color = canvas.get('backgroundColor'); 328 | var erasablesOnLayer = layer === 'top'; 329 | if (image && image.erasable === !erasablesOnLayer) { 330 | this.hideObject(image); 331 | } 332 | if (color && color.erasable === !erasablesOnLayer) { 333 | this.hideObject(color); 334 | } 335 | }, 336 | 337 | /** 338 | * Drawing Logic For overlay drawables (`overlayImage`, `overlayColor`) 339 | * We must draw on top ctx to be on top of visible canvas 340 | * 1. if erasable = true: 341 | * we need to draw the drawable on the top ctx as a normal object 342 | * 2. if erasable = false: 343 | * we need to draw the drawable on top of the brush, 344 | * this means we need to repaint for every stroke 345 | * 346 | * @param {'bottom' | 'top' | 'overlay'} layer 347 | * @returns boolean render overlay above brush 348 | */ 349 | prepareCanvasOverlayForLayer: function(layer) { 350 | var canvas = this.canvas; 351 | var image = canvas.get('overlayImage'); 352 | var color = canvas.get('overlayColor'); 353 | if (layer === 'bottom') { 354 | this.hideObject(image); 355 | this.hideObject(color); 356 | return false; 357 | }; 358 | var erasablesOnLayer = layer === 'top'; 359 | var renderOverlayOnTop = (image && !image.erasable) || (color && !color.erasable); 360 | if (image && image.erasable === !erasablesOnLayer) { 361 | this.hideObject(image); 362 | } 363 | if (color && color.erasable === !erasablesOnLayer) { 364 | this.hideObject(color); 365 | } 366 | return renderOverlayOnTop; 367 | }, 368 | 369 | /** 370 | * @private 371 | */ 372 | restoreCanvasDrawables: function() { 373 | var canvas = this.canvas; 374 | this.restoreObjectVisibility(canvas.get('backgroundImage')); 375 | this.restoreObjectVisibility(canvas.get('backgroundColor')); 376 | this.restoreObjectVisibility(canvas.get('overlayImage')); 377 | this.restoreObjectVisibility(canvas.get('overlayColor')); 378 | }, 379 | 380 | /** 381 | * @private 382 | * This is designed to support erasing a group with both erasable and non-erasable objects. 383 | * Iterates over collections to allow nested selective erasing. 384 | * Used by {@link fabric.EraserBrush#prepareCanvasObjectsForLayer} 385 | * to prepare the bottom layer by hiding erasable nested objects 386 | * 387 | * @param {fabric.Collection} collection 388 | */ 389 | prepareCollectionTraversal: function(collection) { 390 | var _this = this; 391 | collection.forEachObject(function(obj) { 392 | if (obj.forEachObject) { 393 | _this.prepareCollectionTraversal(obj); 394 | } else { 395 | if (obj.erasable) { 396 | _this.hideObject(obj); 397 | } 398 | } 399 | }); 400 | }, 401 | 402 | /** 403 | * @private 404 | * Used by {@link fabric.EraserBrush#prepareCanvasObjectsForLayer} 405 | * to reverse the action of {@link fabric.EraserBrush#prepareCollectionTraversal} 406 | * 407 | * @param {fabric.Collection} collection 408 | */ 409 | restoreCollectionTraversal: function(collection) { 410 | var _this = this; 411 | collection.forEachObject(function(obj) { 412 | if (obj.forEachObject) { 413 | _this.restoreCollectionTraversal(obj); 414 | } else { 415 | _this.restoreObjectVisibility(obj); 416 | } 417 | }); 418 | }, 419 | 420 | /** 421 | * @private 422 | * This is designed to support erasing a group with both erasable and non-erasable objects. 423 | * 424 | * @param {'bottom' | 'top' | 'overlay'} layer 425 | */ 426 | prepareCanvasObjectsForLayer: function(layer) { 427 | if (layer !== 'bottom') { return; } 428 | this.prepareCollectionTraversal(this.canvas); 429 | }, 430 | 431 | /** 432 | * @private 433 | * @param {'bottom' | 'top' | 'overlay'} layer 434 | */ 435 | restoreCanvasObjectsFromLayer: function(layer) { 436 | if (layer !== 'bottom') { return; } 437 | this.restoreCollectionTraversal(this.canvas); 438 | }, 439 | 440 | /** 441 | * @private 442 | * @param {'bottom' | 'top' | 'overlay'} layer 443 | * @returns boolean render overlay above brush 444 | */ 445 | prepareCanvasForLayer: function(layer) { 446 | this.prepareCanvasBackgroundForLayer(layer); 447 | this.prepareCanvasObjectsForLayer(layer); 448 | return this.prepareCanvasOverlayForLayer(layer); 449 | }, 450 | 451 | /** 452 | * @private 453 | * @param {'bottom' | 'top' | 'overlay'} layer 454 | */ 455 | restoreCanvasFromLayer: function(layer) { 456 | this.restoreCanvasDrawables(); 457 | this.restoreCanvasObjectsFromLayer(layer); 458 | }, 459 | 460 | /** 461 | * Render all non-erasable objects on bottom layer with the exception of overlays to avoid being clipped by the brush. 462 | * Groups are rendered for nested selective erasing, non-erasable objects are visible while erasable objects are not. 463 | */ 464 | renderBottomLayer: function() { 465 | var canvas = this.canvas; 466 | this.prepareCanvasForLayer('bottom'); 467 | canvas.renderCanvas( 468 | canvas.getContext(), 469 | canvas.getObjects().filter(function(obj) { 470 | return !obj.erasable || obj.isType('group'); 471 | }) 472 | ); 473 | this.restoreCanvasFromLayer('bottom'); 474 | }, 475 | 476 | /** 477 | * 1. Render all objects on top layer, erasable and non-erasable 478 | * This is important for cases such as overlapping objects, the background object erasable and the foreground object not erasable. 479 | * 2. Render the brush 480 | */ 481 | renderTopLayer: function() { 482 | var canvas = this.canvas; 483 | this._drawOverlayOnTop = this.prepareCanvasForLayer('top'); 484 | canvas.renderCanvas( 485 | canvas.contextTop, 486 | canvas.getObjects() 487 | ); 488 | this.callSuper('_render'); 489 | this.restoreCanvasFromLayer('top'); 490 | }, 491 | 492 | /** 493 | * Render all non-erasable overlays on top of the brush so that they won't get erased 494 | */ 495 | renderOverlay: function() { 496 | this.prepareCanvasForLayer('overlay'); 497 | var canvas = this.canvas; 498 | var ctx = canvas.contextTop; 499 | this._saveAndTransform(ctx); 500 | canvas._renderOverlay(ctx); 501 | ctx.restore(); 502 | this.restoreCanvasFromLayer('overlay'); 503 | }, 504 | 505 | /** 506 | * @extends @class fabric.BaseBrush 507 | * @param {CanvasRenderingContext2D} ctx 508 | */ 509 | _saveAndTransform: function(ctx) { 510 | this.callSuper('_saveAndTransform', ctx); 511 | ctx.globalCompositeOperation = 'destination-out'; 512 | }, 513 | 514 | /** 515 | * We indicate {@link fabric.PencilBrush} to repaint itself if necessary 516 | * @returns 517 | */ 518 | needsFullRender: function() { 519 | return this.callSuper('needsFullRender') || this._drawOverlayOnTop; 520 | }, 521 | 522 | /** 523 | * 524 | * @param {fabric.Point} pointer 525 | * @param {fabric.IEvent} options 526 | * @returns 527 | */ 528 | onMouseDown: function(pointer, options) { 529 | if (!this.canvas._isMainEvent(options.e)) { 530 | return; 531 | } 532 | this._prepareForDrawing(pointer); 533 | // capture coordinates immediately 534 | // this allows to draw dots (when movement never occurs) 535 | this._captureDrawingPath(pointer); 536 | 537 | this._isErasing = true; 538 | this.canvas.fire('erasing:start'); 539 | this._ready = true; 540 | this._render(); 541 | }, 542 | 543 | /** 544 | * Rendering is done in 4 steps: 545 | * 1. Draw all non-erasable objects on bottom ctx with the exception of overlays {@link fabric.EraserBrush#renderBottomLayer} 546 | * 2. Draw all objects on top ctx including erasable drawables {@link fabric.EraserBrush#renderTopLayer} 547 | * 3. Draw eraser {@link fabric.PencilBrush#_render} at {@link fabric.EraserBrush#renderTopLayer} 548 | * 4. Draw non-erasable overlays {@link fabric.EraserBrush#renderOverlay} 549 | * 550 | * @param {fabric.Canvas} canvas 551 | */ 552 | _render: function() { 553 | if (!this._ready) { 554 | return; 555 | } 556 | this.isRendering = 1; 557 | this.renderBottomLayer(); 558 | this.renderTopLayer(); 559 | this.renderOverlay(); 560 | this.isRendering = 0; 561 | }, 562 | 563 | /** 564 | * @public 565 | */ 566 | render: function() { 567 | if (this._isErasing) { 568 | if (this.isRendering) { 569 | this.isRendering = fabric.util.requestAnimFrame(this._renderBound); 570 | } else { 571 | this._render(); 572 | } 573 | return true; 574 | } 575 | return false; 576 | }, 577 | 578 | /** 579 | * Adds path to existing clipPath of object 580 | * 581 | * @param {fabric.Object} obj 582 | * @param {fabric.Path} path 583 | */ 584 | _addPathToObjectEraser: function(obj, path) { 585 | var clipObject; 586 | var _this = this; 587 | // object is collection, i.e group 588 | if (obj.forEachObject) { 589 | obj.forEachObject(function(_obj) { 590 | if (_obj.erasable) { 591 | _this._addPathToObjectEraser(_obj, path); 592 | } 593 | }); 594 | return; 595 | } 596 | if (!obj.getEraser()) { 597 | var size = obj._getNonTransformedDimensions(); 598 | var rect = new fabric.Rect({ 599 | width: size.x, 600 | height: size.y, 601 | clipPath: obj.clipPath, 602 | originX: 'center', 603 | originY: 'center' 604 | }); 605 | clipObject = new fabric.Group([rect], { 606 | eraser: true 607 | }); 608 | } else { 609 | clipObject = obj.clipPath; 610 | } 611 | 612 | path.clone(function(path) { 613 | path.globalCompositeOperation = 'destination-out'; 614 | // http://fabricjs.com/using-transformations 615 | var desiredTransform = fabric.util.multiplyTransformMatrices( 616 | fabric.util.invertTransform( 617 | obj.calcTransformMatrix() 618 | ), 619 | path.calcTransformMatrix() 620 | ); 621 | fabric.util.applyTransformToObject(path, desiredTransform); 622 | clipObject.addWithUpdate(path); 623 | obj.set({ 624 | clipPath: clipObject, 625 | dirty: true 626 | }); 627 | }); 628 | }, 629 | 630 | /** 631 | * Add the eraser path to canvas drawables' clip paths 632 | * 633 | * @param {fabric.Canvas} source 634 | * @param {fabric.Canvas} path 635 | * @returns {Object} canvas drawables that were erased by the path 636 | */ 637 | applyEraserToCanvas: function(path) { 638 | var canvas = this.canvas; 639 | var drawables = {}; 640 | [ 641 | 'backgroundImage', 642 | 'backgroundColor', 643 | 'overlayImage', 644 | 'overlayColor', 645 | ].forEach(function(prop) { 646 | var drawable = canvas[prop]; 647 | if (drawable && drawable.erasable) { 648 | this._addPathToObjectEraser(drawable, path); 649 | drawables[prop] = drawable; 650 | } 651 | }, this); 652 | return drawables; 653 | }, 654 | 655 | /** 656 | * On mouseup after drawing the path on contextTop canvas 657 | * we use the points captured to create an new fabric path object 658 | * and add it to every intersected erasable object. 659 | */ 660 | _finalizeAndAddPath: function() { 661 | var ctx = this.canvas.contextTop, 662 | canvas = this.canvas; 663 | ctx.closePath(); 664 | if (this.decimate) { 665 | this._points = this.decimatePoints(this._points, this.decimate); 666 | } 667 | 668 | // clear 669 | canvas.clearContext(canvas.contextTop); 670 | this._isErasing = false; 671 | 672 | var pathData = this._points && this._points.length > 1 ? 673 | this.convertPointsToSVGPath(this._points).join('') : 674 | 'M 0 0 Q 0 0 0 0 L 0 0'; 675 | if (pathData === 'M 0 0 Q 0 0 0 0 L 0 0') { 676 | canvas.fire('erasing:end'); 677 | // do not create 0 width/height paths, as they are 678 | // rendered inconsistently across browsers 679 | // Firefox 4, for example, renders a dot, 680 | // whereas Chrome 10 renders nothing 681 | canvas.requestRenderAll(); 682 | return; 683 | } 684 | 685 | var path = this.createPath(pathData); 686 | canvas.fire('before:path:created', { path: path }); 687 | 688 | // finalize erasing 689 | var drawables = this.applyEraserToCanvas(path); 690 | var _this = this; 691 | var targets = []; 692 | canvas.forEachObject(function(obj) { 693 | if (obj.erasable && obj.intersectsWithObject(path, true)) { 694 | _this._addPathToObjectEraser(obj, path); 695 | targets.push(obj); 696 | } 697 | }); 698 | 699 | canvas.fire('erasing:end', { path: path, targets: targets, drawables: drawables }); 700 | 701 | canvas.requestRenderAll(); 702 | path.setCoords(); 703 | this._resetShadow(); 704 | 705 | // fire event 'path' created 706 | canvas.fire('path:created', { path: path }); 707 | } 708 | } 709 | ); 710 | 711 | /** ERASER_END */ 712 | })(); --------------------------------------------------------------------------------