├── samples ├── assets │ └── images │ │ ├── car-engine.jpg │ │ └── motorcycle-engine.jpg ├── translation │ └── index.html ├── element │ └── index.html ├── visualize │ └── index.html └── main │ └── index.html ├── README.md ├── LICENSE └── src ├── draw-maker.element.js ├── assets └── translations │ ├── draw-maker.es-PY.js │ └── draw-maker.pt-BR.js └── draw-maker.js /samples/assets/images/car-engine.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mpecenin/draw-maker/HEAD/samples/assets/images/car-engine.jpg -------------------------------------------------------------------------------- /samples/assets/images/motorcycle-engine.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mpecenin/draw-maker/HEAD/samples/assets/images/motorcycle-engine.jpg -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # draw-maker 2 | Fabric.js visual editor to draw over images 3 | 4 | ## Demos/Samples 5 | 1. [main](samples/main/index.html) [(preview)](https://mpecenin.github.io/draw-maker/samples/main/index.html) 6 | 2. [visualize](samples/visualize/index.html) [(preview)](https://mpecenin.github.io/draw-maker/samples/visualize/index.html) 7 | 3. [element](samples/element/index.html) [(preview)](https://mpecenin.github.io/draw-maker/samples/element/index.html) 8 | 4. [translation](samples/translation/index.html) [(preview)](https://mpecenin.github.io/draw-maker/samples/translation/index.html) 9 | -------------------------------------------------------------------------------- /samples/translation/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | DrawMaker 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /samples/element/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | DrawMaker 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |

19 | 20 | 21 | 22 |

23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Marcelo Pecenin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/draw-maker.element.js: -------------------------------------------------------------------------------- 1 | (function (owner) { 2 | "use strict"; 3 | 4 | let ns = owner.drawmaker || (owner.drawmaker = {}); 5 | 6 | let DrawMakerElement = class extends HTMLElement { 7 | 8 | constructor() { 9 | super(); 10 | } 11 | 12 | connectedCallback() { 13 | let options = this._loadOptions(); 14 | new ns.DrawMaker(this, options); 15 | } 16 | 17 | _loadOptions() { 18 | let options = {}; 19 | let value = this.getAttribute("options"); 20 | if (value && value.trim() !== "") { 21 | options = JSON.parse(value); 22 | } 23 | value = this.getAttribute("width"); 24 | if (value && value.trim() !== "") { 25 | options.width = value; 26 | } 27 | value = this.getAttribute("height"); 28 | if (value && value.trim() !== "") { 29 | options.height = value; 30 | } 31 | value = this.getAttribute("backgroundImage"); 32 | if (value && value.trim() !== "") { 33 | options.backgroundImage = value; 34 | } 35 | return options; 36 | } 37 | 38 | } 39 | 40 | customElements.define("dm-drawmaker", DrawMakerElement); 41 | 42 | })(this); -------------------------------------------------------------------------------- /src/assets/translations/draw-maker.es-PY.js: -------------------------------------------------------------------------------- 1 | (function (owner) { 2 | "use strict"; 3 | 4 | let ns = owner.drawmaker || (owner.drawmaker = {}); 5 | 6 | ns.textTranslation = { 7 | "Draw Rectangle": "Dibujar Rectángulo", 8 | "Draw Ellipse": "Dibujar Elipse", 9 | "Draw Triangle": "Dibujar Triángulo", 10 | "Draw Line": "Dibujar Linea", 11 | "Draw Arrow": "Dibujar Flecha", 12 | "Draw Double Arrow": "Dibujar Doble Flecha", 13 | "Text Box": "Caja de Texto", 14 | "Free Drawing": "Dibujo Libre", 15 | "Remove": "Eliminar", 16 | "Duplicate": "Duplicar", 17 | "Zoom In": "Acercar Zoom", 18 | "Zoom Out": "Alejar Zoom", 19 | "Fullscreen": "Pantalla Completa", 20 | "Settings": "Configuraciones", 21 | "Fill/Text Color": "Color de Relleno/Texto", 22 | "Line/Border Color": "Color de Línea/Borde", 23 | "Line/Border Width": "Grosor de Línea/Borde", 24 | "Line/Border Style": "Estilo de Línea/Borde", 25 | "Object Opacity": "Opacidad del Objeto", 26 | "Bring Forwards": "Traer al Frente", 27 | "Send Backwards": "Enviar al Revés", 28 | "Bring to Front": "Traer al Cima", 29 | "Send to Back": "Enviar al Fondo", 30 | "Without Color / Transparent": "Sin Color / Transparente", 31 | }; 32 | 33 | })(this); -------------------------------------------------------------------------------- /src/assets/translations/draw-maker.pt-BR.js: -------------------------------------------------------------------------------- 1 | (function (owner) { 2 | "use strict"; 3 | 4 | let ns = owner.drawmaker || (owner.drawmaker = {}); 5 | 6 | ns.textTranslation = { 7 | "Draw Rectangle": "Desenha Retângulo", 8 | "Draw Ellipse": "Desenha Elipse", 9 | "Draw Triangle": "Desenha Triângulo", 10 | "Draw Line": "Desenha Linha", 11 | "Draw Arrow": "Desenha Seta", 12 | "Draw Double Arrow": "Desenha Seta Dupla", 13 | "Text Box": "Caixa de Texto", 14 | "Free Drawing": "Desenho Livre", 15 | "Remove": "Remover", 16 | "Duplicate": "Duplicar", 17 | "Zoom In": "Ampliar Zoom", 18 | "Zoom Out": "Reduzir Zoom", 19 | "Fullscreen": "Tela Cheia", 20 | "Settings": "Configurações", 21 | "Fill/Text Color": "Cor de Preenchimento/Texto", 22 | "Line/Border Color": "Cor da Linha/Borda", 23 | "Line/Border Width": "Espessura da Linha/Borda", 24 | "Line/Border Style": "Estilo da Linha/Borda", 25 | "Object Opacity": "Opacidade do Objeto", 26 | "Bring Forwards": "Trazer para Frente", 27 | "Send Backwards": "Enviar para Trás", 28 | "Bring to Front": "Trazer para o Topo", 29 | "Send to Back": "Enviar para o Fundo", 30 | "Without Color / Transparent": "Sem Cor / Transparente", 31 | }; 32 | 33 | })(this); -------------------------------------------------------------------------------- /samples/visualize/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | DrawMaker 8 | 9 | 10 | 11 | 12 | 13 | 65 | 66 | 67 | 68 |
69 |

70 | 71 | 72 | 88 | 89 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /samples/main/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | DrawMaker 8 | 9 | 10 | 11 | 12 | 48 | 49 | 107 | 108 | 109 | 110 |
111 |

112 | 113 | 114 | 115 | 116 | 117 | 118 |

119 |
120 |

121 |
122 |

123 |
124 |

125 |
126 |

127 |
128 | 129 | 130 | -------------------------------------------------------------------------------- /src/draw-maker.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * DrawMaker by Marcelo Pecenin - https://github.com/mpecenin/draw-maker 3 | * License - https://mit-license.org (MIT License) 4 | */ 5 | (function (owner) { 6 | "use strict"; 7 | 8 | let ns = owner.drawmaker || (owner.drawmaker = {}); 9 | 10 | ns.DrawMaker = class { 11 | 12 | constructor(container, options) { 13 | this._controller = null; // Private access (#controller). 14 | this.canvas = null; // Direct access to Fabric Canvas API. 15 | 16 | if (!container instanceof HTMLElement) { 17 | throw new TypeError("Parameter container must be an instance of a host HTML element."); 18 | } 19 | 20 | let root = container.attachShadow({ mode: "open" }); 21 | this._controller = new DrawMakerController(root, options); 22 | this.canvas = this._controller.canvas; 23 | container.drawmaker = this; 24 | } 25 | 26 | clear() { 27 | this._controller.clear(); 28 | } 29 | 30 | isEmpty() { 31 | return this._controller.isEmpty(); 32 | } 33 | 34 | loadJSON(json) { 35 | this._controller.loadJSON(json); 36 | } 37 | 38 | toJSON() { 39 | return this._controller.toJSON(); 40 | } 41 | 42 | toSVG() { 43 | return this._controller.toSVG(); 44 | } 45 | 46 | reset(options) { 47 | return this._controller.reset(options); 48 | } 49 | 50 | } 51 | 52 | let tt = function (key) { 53 | let value = ns.textTranslation && ns.textTranslation[key]; 54 | return value || key; 55 | } 56 | 57 | let DrawMakerController = class { 58 | 59 | constructor(root, options) { 60 | this._DEFAULT_COLOR = "#ff0000"; 61 | this._DEFAULT_STROKE_WIDTH = 2; 62 | this._MIN_ZOOM = 1; 63 | this._ZOOM_SHIFT_FACTOR = 0.75; 64 | this._MAX_ZOOM = this._MIN_ZOOM + (this._ZOOM_SHIFT_FACTOR * 5); 65 | 66 | this._root = root; 67 | this._selected = null; 68 | this._making = null; 69 | 70 | this._loadOptions(options); 71 | this._loadTemplate(); 72 | } 73 | 74 | _loadOptions(options) { 75 | options = options || {}; 76 | 77 | this.width = (options.width > 0) ? options.width : 400; 78 | this.height = (options.height > 0) ? options.height : 400; 79 | this.backgroundImage = options.backgroundImage || null; 80 | 81 | this.fill = String(options.fill).startsWith("#") ? options.fill.toLowerCase() : null; 82 | this.stroke = String(options.stroke).startsWith("#") ? options.stroke.toLowerCase() : this._DEFAULT_COLOR; 83 | this.strokeWidth = (options.strokeWidth >= 0) ? options.strokeWidth : this._DEFAULT_STROKE_WIDTH; 84 | this.strokeDashArray = (Array.isArray(options.strokeDashArray) && options.strokeDashArray.length > 0) ? options.strokeDashArray : null; 85 | this.opacity = (options.opacity > 0) ? options.opacity : 1; 86 | } 87 | 88 | _loadTemplate() { 89 | this._attachTemplate(` 90 | 422 | `); 423 | } 424 | 425 | _colorTemplate() { 426 | let range = ["00", "90", "ff"]; 427 | let template = ``; 428 | range.forEach(g => { 429 | range.forEach(b => { 430 | range.forEach(r => { 431 | template += ``; 432 | }); 433 | template += `
`; 434 | }); 435 | }); 436 | return template; 437 | } 438 | 439 | _attachTemplate(textHtml) { 440 | this._root.innerHTML = textHtml; 441 | let templateEl = this._root.getElementById("dm-template"); 442 | this._root.appendChild(templateEl.content); 443 | let canvasEl = this._root.getElementById("dm-canvas"); 444 | 445 | this.canvas = new fabric.Canvas(canvasEl, { 446 | width: this.width, 447 | height: this.height, 448 | perPixelTargetFind: true, 449 | targetFindTolerance: 15, 450 | preserveObjectStacking: true, 451 | svgViewportTransformation: false, 452 | }); 453 | 454 | this._setBackgroundImage(); 455 | 456 | // --------------------------------------------------------------------------------------- 457 | 458 | this.canvas.on("mouse:down", options => { 459 | if (this._selected && !this._making) { 460 | this._making = this._selected; 461 | this._making.start(options); 462 | this._dispatchEvent("dm:maker:start", { makerType: this._making.type }); 463 | } 464 | }); 465 | this.canvas.on("mouse:move", options => { 466 | if (this._making) { 467 | this._making.update(options); 468 | } 469 | }); 470 | this.canvas.on("mouse:up", options => { 471 | if (this._making) { 472 | let makerType = this._making.type; 473 | this._making.stop(options); 474 | this._making = null; 475 | this._dispatchEvent("dm:maker:stop", { makerType: makerType }); 476 | } 477 | }); 478 | 479 | // --------------------------------------------------------------------------------------- 480 | 481 | this.canvas.on("selection:created", options => { 482 | let objs = this.getSelectedObjects(); 483 | if (objs.length > 0) { 484 | objs.forEach(obj => { 485 | obj.borderScaleFactor = 3; 486 | }); 487 | this._attachAttribute(".dm-drawmaker", "data-dm-object-select", true); 488 | } 489 | }); 490 | this.canvas.on("selection:cleared", options => { 491 | this._attachAttribute(".dm-drawmaker", "data-dm-object-select", false); 492 | }); 493 | 494 | this._attachListener(".dm-drawmaker", "dm:zoom:change", (listener, event) => { 495 | let limit = (this.canvas.getZoom() >= this._MAX_ZOOM) ? "max" : (this.canvas.getZoom() <= this._MIN_ZOOM) ? "min" : ""; 496 | this._attachAttribute(".dm-drawmaker", "data-dm-zoom-limit", limit); 497 | }); 498 | 499 | // --------------------------------------------------------------------------------------- 500 | 501 | this._attachListener(".dm-menu [data-dm-maker]", this._eventType("mousedown", "touchstart", "pointerdown"), (listener, event) => { 502 | if (listener.classList.contains("dm-select")) { 503 | return this._onMakerDeselect(); 504 | } else { 505 | return this._onMakerSelect(listener.getAttribute("data-dm-maker")); 506 | } 507 | }); 508 | 509 | this._attachListener(".dm-menu [data-dm-maker].dm-stop-deselect", "dm:maker:stop", (listener, event) => { 510 | if (listener.getAttribute("data-dm-maker") === event.detail.makerType) { 511 | return this._onMakerDeselect(); 512 | } 513 | }); 514 | 515 | this._attachListener(".dm-menu [data-dm-maker]", "dm:maker:select", (listener, event) => { 516 | if (listener.getAttribute("data-dm-maker") === event.detail.makerType) { 517 | listener.classList.add("dm-select"); 518 | } 519 | }); 520 | this._attachListener(".dm-menu [data-dm-maker]", "dm:maker:deselect", (listener, event) => { 521 | if (listener.getAttribute("data-dm-maker") === event.detail.makerType) { 522 | listener.classList.remove("dm-select"); 523 | } 524 | }); 525 | 526 | this._attachListener(".dm-menu [data-dm-operation='remove']", this._eventType("mousedown", "touchstart", "pointerdown"), (listener, event) => { 527 | let objs = this.getSelectedObjects(); 528 | this.canvas.discardActiveObject(); 529 | this.canvas.remove(...objs); 530 | }); 531 | 532 | this._attachListener(".dm-menu [data-dm-operation='duplicate']", this._eventType("mousedown", "touchstart", "pointerdown"), (listener, event) => { 533 | this._onDuplicate(); 534 | }); 535 | 536 | this._attachListener(".dm-menu [data-dm-operation='zoomIn']", this._eventType("mousedown", "touchstart", "pointerdown"), (listener, event) => { 537 | return this._onZoomIn(); 538 | }); 539 | this._attachListener(".dm-menu [data-dm-operation='zoomOut']", this._eventType("mousedown", "touchstart", "pointerdown"), (listener, event) => { 540 | return this._onZoomOut(); 541 | }); 542 | 543 | this._attachListener(".dm-menu [data-dm-operation='fullscreen']", this._eventType("mousedown", "touchstart", "pointerdown"), (listener, event) => { 544 | if (listener.classList.contains("dm-select")) { 545 | this._detachClass(".dm-drawmaker", "dm-fullscreen"); 546 | listener.classList.remove("dm-select"); 547 | } else { 548 | this._attachClass(".dm-drawmaker", "dm-fullscreen"); 549 | listener.classList.add("dm-select"); 550 | } 551 | }); 552 | 553 | this._attachListener(".dm-menu [data-dm-operation='bringForwards']", this._eventType("mousedown", "touchstart", "pointerdown"), (listener, event) => { 554 | this.getSelectedObjects().forEach(obj => { 555 | this.canvas.bringForward(obj); 556 | }); 557 | }); 558 | this._attachListener(".dm-menu [data-dm-operation='sendBackwards']", this._eventType("mousedown", "touchstart", "pointerdown"), (listener, event) => { 559 | this.getSelectedObjects().forEach(obj => { 560 | this.canvas.sendBackwards(obj); 561 | }); 562 | }); 563 | 564 | this._attachListener(".dm-menu [data-dm-operation='bringToFront']", this._eventType("mousedown", "touchstart", "pointerdown"), (listener, event) => { 565 | this.getSelectedObjects().forEach(obj => { 566 | this.canvas.bringToFront(obj); 567 | }); 568 | }); 569 | this._attachListener(".dm-menu [data-dm-operation='sendToBack']", this._eventType("mousedown", "touchstart", "pointerdown"), (listener, event) => { 570 | this.getSelectedObjects().forEach(obj => { 571 | this.canvas.sendToBack(obj); 572 | }); 573 | }); 574 | 575 | // --------------------------------------------------------------------------------------- 576 | 577 | this._attachListener(".dm-menu [data-dm-property]", this._eventType("mousedown", "touchstart", "pointerdown"), (listener, event) => { 578 | let property = listener.getAttribute("data-dm-property"); 579 | let currValue = this._onGetProperty(property); 580 | let modal = ".dm-modal[data-dm-property='" + property + "']"; 581 | this._attachClass(modal, "dm-modal-display"); 582 | this._attachAttribute(modal + " button", "data-dm-property", property); 583 | this._detachClass(modal + " button", "dm-select"); 584 | this._attachClass(modal + " button[value='" + currValue + "']", "dm-select"); 585 | }); 586 | 587 | this._attachListener(".dm-modal[data-dm-property] button", this._eventType("mousedown", "touchstart", "pointerdown"), (listener, event) => { 588 | let property = listener.getAttribute("data-dm-property"); 589 | let selectValue = listener.value; 590 | let modal = ".dm-modal[data-dm-property='" + property + "']"; 591 | if (selectValue === "__hide__") { 592 | this._detachClass(modal, "dm-modal-display"); 593 | } else { 594 | this._onSetProperty(property, selectValue); 595 | this._detachClass(modal + " button", "dm-select"); 596 | this._attachClass(modal + " button[value='" + selectValue + "']", "dm-select"); 597 | } 598 | }); 599 | 600 | } 601 | 602 | _setBackgroundImage() { 603 | if (!this.backgroundImage) 604 | return; 605 | let uri = this.backgroundImage.toString(); 606 | let options = { excludeFromExport: true, scaleX: 1, scaleY: 1 }; 607 | this.canvas.setBackgroundImage(uri, () => { 608 | if (!this.canvas.backgroundImage) 609 | return; 610 | let image = this.canvas.backgroundImage; 611 | image.scaleToWidth(this.canvas.getWidth(), true); 612 | if (image.getScaledHeight() > this.canvas.getHeight()) { 613 | image.scaleToHeight(this.canvas.getHeight(), true); 614 | } 615 | this.canvas.requestRenderAll(); 616 | }, options); 617 | } 618 | 619 | _eventType(mouseType, touchType, pointerType) { 620 | if ((fabric.window && "onpointerdown" in fabric.window) || (fabric.document && "onpointerdown" in fabric.document)) 621 | return pointerType; 622 | if ((fabric.window && "ontouchstart" in fabric.window) || (fabric.document && "ontouchstart" in fabric.document)) 623 | return touchType; 624 | return mouseType; 625 | } 626 | 627 | _attachListener(selector, eventType, callback) { 628 | let isCustomEvent = eventType.includes(":"); 629 | this._root.querySelectorAll(selector).forEach(el => { 630 | if (isCustomEvent) 631 | this._root.addEventListener(eventType, callback.bind(this, el)); 632 | else 633 | el.addEventListener(eventType, callback.bind(this, el)); 634 | }); 635 | } 636 | 637 | _dispatchEvent(eventType, detail) { 638 | let customInit = { detail: detail || {} }; 639 | let customEvent = new CustomEvent(eventType, customInit); 640 | return this._root.dispatchEvent(customEvent); 641 | } 642 | 643 | _attachAttribute(selector, attribute, value) { 644 | this._root.querySelectorAll(selector).forEach(el => { 645 | el.setAttribute(attribute, value); 646 | }); 647 | } 648 | 649 | _detachAttribute(selector, attribute) { 650 | this._root.querySelectorAll(selector).forEach(el => { 651 | el.removeAttribute(attribute); 652 | }); 653 | } 654 | 655 | _attachClass(selector, styleClass) { 656 | this._root.querySelectorAll(selector).forEach(el => { 657 | el.classList.add(styleClass); 658 | }); 659 | } 660 | 661 | _detachClass(selector, styleClass) { 662 | this._root.querySelectorAll(selector).forEach(el => { 663 | el.classList.remove(styleClass); 664 | }); 665 | } 666 | 667 | getSelectedObjects() { 668 | let objs = this.canvas.getActiveObjects(); 669 | objs = objs.filter(obj => { 670 | return obj.drawmaker && obj.drawmaker.type; 671 | }); 672 | return objs; 673 | } 674 | 675 | getSelectableObjects() { 676 | let objs = this.canvas.getObjects(); 677 | objs = objs.filter(obj => { 678 | return obj.drawmaker && obj.drawmaker.type && obj.selectable; 679 | }); 680 | return objs; 681 | } 682 | 683 | _dummyObjectSelection() { 684 | // Some individual object selection/visibility only works after a grouping selection. 685 | let objs = this.getSelectableObjects(); 686 | this.canvas.discardActiveObject(); 687 | this.canvas.setActiveObject(new fabric.ActiveSelection(objs, { canvas: this.canvas })); 688 | this.canvas.discardActiveObject(); 689 | } 690 | 691 | enableObjectSelection() { 692 | // After enable, eventually not all objects are selectable with the mouse. 693 | this._dummyObjectSelection(); 694 | this.canvas.discardActiveObject(); 695 | this.getSelectableObjects().forEach(obj => { 696 | obj.set("evented", true); 697 | }); 698 | this.canvas.set("selection", true); 699 | this.canvas.requestRenderAll(); 700 | } 701 | 702 | disableObjectSelection() { 703 | this.canvas.discardActiveObject(); 704 | this.getSelectableObjects().forEach(obj => { 705 | obj.set("evented", false); 706 | }); 707 | this.canvas.set("selection", false); 708 | this.canvas.requestRenderAll(); 709 | } 710 | 711 | _onMakerSelect(makerType) { 712 | if (this._making) { 713 | return false; 714 | } 715 | if (this._selected && !this._onMakerDeselect()) { 716 | return false; 717 | } 718 | this._selected = new ns[makerType](this); 719 | this._selected.select(); 720 | this.canvas.defaultCursor = "crosshair"; 721 | this.disableObjectSelection(); 722 | this._dispatchEvent("dm:maker:select", { makerType: makerType }); 723 | return true; 724 | } 725 | 726 | _onMakerDeselect() { 727 | if (this._making) { 728 | return false; 729 | } 730 | if (this._selected) { 731 | let makerType = this._selected.type; 732 | this._selected.deselect(); 733 | this._selected = null; 734 | this.canvas.defaultCursor = "default"; 735 | this.enableObjectSelection(); 736 | this._dispatchEvent("dm:maker:deselect", { makerType: makerType }); 737 | } 738 | return true; 739 | } 740 | 741 | _resetZoom() { 742 | this.canvas.discardActiveObject(); 743 | this.canvas.setViewportTransform([1, 0, 0, 1, 0, 0]); 744 | this.canvas.setDimensions({ width: this.width, height: this.height }); 745 | this._dispatchEvent("dm:zoom:change", { zoomValue: this.canvas.getZoom() }); 746 | // After zooming, eventually not all objects are visible. 747 | this._dummyObjectSelection(); 748 | } 749 | 750 | _onZoomIn() { 751 | if (this.canvas.getZoom() === this._MAX_ZOOM) { 752 | return false; 753 | } 754 | this.canvas.discardActiveObject(); 755 | let zoom = Math.min(this.canvas.getZoom() + this._ZOOM_SHIFT_FACTOR, this._MAX_ZOOM); 756 | this.canvas.setZoom(zoom); 757 | this.canvas.setDimensions({ width: (this.width * zoom), height: (this.height * zoom) }); 758 | this._dispatchEvent("dm:zoom:change", { zoomValue: this.canvas.getZoom() }); 759 | // After zooming, eventually not all objects are visible. 760 | this._dummyObjectSelection(); 761 | return true; 762 | } 763 | 764 | _onZoomOut() { 765 | if (this.canvas.getZoom() === this._MIN_ZOOM) { 766 | return false; 767 | } 768 | let zoom = Math.max(this.canvas.getZoom() - this._ZOOM_SHIFT_FACTOR, this._MIN_ZOOM); 769 | if (zoom <= this._MIN_ZOOM) { 770 | this._resetZoom(); 771 | } else { 772 | this.canvas.discardActiveObject(); 773 | this.canvas.setZoom(zoom); 774 | this.canvas.setDimensions({ width: (this.width * zoom), height: (this.height * zoom) }); 775 | this._dispatchEvent("dm:zoom:change", { zoomValue: this.canvas.getZoom() }); 776 | // After zooming, eventually not all objects are visible. 777 | this._dummyObjectSelection(); 778 | } 779 | return true; 780 | } 781 | 782 | _onDuplicate() { 783 | let active = this.canvas.getActiveObject(); 784 | if (!active) 785 | return; 786 | active.clone(cloned => { 787 | this.canvas.discardActiveObject(); 788 | cloned.set({ 789 | left: cloned.left + 10, 790 | top: cloned.top + 10, 791 | evented: true 792 | }); 793 | if (cloned.type === "activeSelection") { 794 | cloned.canvas = this.canvas; 795 | cloned.forEachObject(obj => { 796 | this.canvas.add(obj); 797 | }); 798 | cloned.setCoords(); 799 | } else { 800 | this.canvas.add(cloned); 801 | } 802 | this.canvas.setActiveObject(cloned); 803 | this.canvas.requestRenderAll(); 804 | }, ["drawmaker"]); 805 | } 806 | 807 | _onGetProperty(property) { 808 | let currValue = this[property]; 809 | let maker, objValue, first = true; 810 | this.getSelectedObjects().forEach(obj => { 811 | if (!(obj.drawmaker && obj.drawmaker.type)) 812 | return; 813 | maker = new ns[obj.drawmaker.type](this); 814 | objValue = maker.get(obj, property); 815 | if (first && objValue !== undefined) { 816 | currValue = objValue; 817 | first = false; 818 | } 819 | if (!first && objValue !== undefined && objValue !== currValue) { 820 | currValue = undefined; 821 | } 822 | }); 823 | return JSON.stringify(currValue); 824 | } 825 | 826 | _onSetProperty(property, value) { 827 | let selectValue = JSON.parse(value); 828 | let objs = this.getSelectedObjects(); 829 | if (objs.length > 0) { 830 | // With active selection, sometimes group objects are shifted. 831 | this.canvas.discardActiveObject(); 832 | objs.forEach(obj => { 833 | if (!(obj.drawmaker && obj.drawmaker.type)) 834 | return; 835 | let maker = new ns[obj.drawmaker.type](this); 836 | maker.set(obj, property, selectValue); 837 | }); 838 | if (objs.length > 1) { 839 | this.canvas.setActiveObject(new fabric.ActiveSelection(objs, { canvas: this.canvas })); 840 | } else { 841 | this.canvas.setActiveObject(objs[0]); 842 | } 843 | } else { 844 | this[property] = selectValue; 845 | } 846 | } 847 | 848 | clear() { 849 | this.canvas.clear(); 850 | this._resetZoom(); 851 | this._setBackgroundImage(); 852 | } 853 | 854 | isEmpty() { 855 | return this.canvas.getObjects().length <= 0; 856 | } 857 | 858 | loadJSON(json) { 859 | this.clear(); 860 | this.canvas.loadFromJSON(json, () => { 861 | this._setBackgroundImage(); 862 | this.canvas.requestRenderAll(); 863 | }); 864 | } 865 | 866 | toJSON() { 867 | this._resetZoom(); 868 | return JSON.stringify(this.canvas.toJSON(["drawmaker"])); 869 | } 870 | 871 | toSVG() { 872 | this._resetZoom(); 873 | return this.canvas.toSVG({ suppressPreamble: true }); 874 | } 875 | 876 | reset(options) { 877 | this._loadOptions(options); 878 | this.clear(); 879 | } 880 | 881 | } 882 | 883 | ns.BaseMaker = class { 884 | 885 | constructor(dm) { 886 | this.type = null; 887 | this.props = []; 888 | this.dm = dm; 889 | } 890 | 891 | start(options) { 892 | // default 893 | } 894 | 895 | update(options) { 896 | // default 897 | } 898 | 899 | stop(options) { 900 | // default 901 | } 902 | 903 | select() { 904 | // default 905 | } 906 | 907 | deselect() { 908 | // default 909 | } 910 | 911 | get(obj, property) { 912 | if (!this.props.includes(property)) 913 | return; 914 | return obj.get(property); 915 | } 916 | 917 | set(obj, property, value) { 918 | if (!this.props.includes(property)) 919 | return; 920 | obj.set(property, value); 921 | // Update object bounding box, as needed. 922 | obj.setCoords(); 923 | this.dm.canvas.requestRenderAll(); 924 | } 925 | 926 | roundFloat(floatNumber) { 927 | return Math.round((floatNumber + Number.EPSILON) * 100.0) / 100.0; 928 | } 929 | 930 | isVisibleColor(color) { 931 | return (!color || color === "") ? false : true; 932 | } 933 | 934 | isVisibleStrokeWidth(width) { 935 | return (!width || width < 1) ? false : true; 936 | } 937 | 938 | assertColor(color, assert) { 939 | if (assert === undefined) { 940 | return this.isVisibleColor(color) ? color : this.dm._DEFAULT_COLOR; 941 | } else { 942 | return (assert === true) ? this.assertColor(color) : color; 943 | } 944 | } 945 | 946 | assertStrokeWidth(width, assert) { 947 | if (assert === undefined) { 948 | return this.isVisibleStrokeWidth(width) ? width : this.dm._DEFAULT_STROKE_WIDTH; 949 | } else { 950 | return (assert === true) ? this.assertStrokeWidth(width) : width; 951 | } 952 | } 953 | 954 | } 955 | 956 | ns.RectMaker = class extends ns.BaseMaker { 957 | 958 | constructor(dm) { 959 | super(dm); 960 | this.type = "RectMaker"; 961 | this.props = ["fill", "stroke", "strokeWidth", "strokeDashArray", "opacity"]; 962 | this._startX = 0; 963 | this._startY = 0; 964 | this._shape = null; 965 | this._attached = false; 966 | } 967 | 968 | _position(options) { 969 | let pointer = this.dm.canvas.getPointer(options.e); 970 | if (!this._startX || !this._startY) { 971 | this._startX = pointer.x; 972 | this._startY = pointer.y; 973 | } 974 | let pos = {}; 975 | pos.left = (pointer.x < this._startX) ? pointer.x : this._startX; 976 | pos.top = (pointer.y < this._startY) ? pointer.y : this._startY; 977 | pos.width = this.roundFloat(Math.abs(pointer.x - this._startX)); 978 | pos.height = this.roundFloat(Math.abs(pointer.y - this._startY)); 979 | return pos; 980 | } 981 | 982 | start(options) { 983 | this._startX = this._startY = null; 984 | let pos = this._position(options); 985 | this._shape = new fabric.Rect({ 986 | drawmaker: { type: this.type }, 987 | left: pos.left, 988 | top: pos.top, 989 | width: pos.width, 990 | height: pos.height, 991 | fill: this.dm.fill, 992 | stroke: this.assertColor(this.dm.stroke, !this.isVisibleColor(this.dm.fill)), 993 | strokeWidth: this.assertStrokeWidth(this.dm.strokeWidth, !this.isVisibleColor(this.dm.fill)), 994 | strokeDashArray: this.dm.strokeDashArray, 995 | opacity: this.dm.opacity, 996 | evented: false, 997 | }); 998 | this._attached = false; 999 | } 1000 | 1001 | update(options) { 1002 | let pos = this._position(options); 1003 | this._shape.set("left", pos.left); 1004 | this._shape.set("top", pos.top); 1005 | this._shape.set("width", pos.width); 1006 | this._shape.set("height", pos.height); 1007 | if (!this._attached) { 1008 | this.dm.canvas.add(this._shape); 1009 | this._attached = true; 1010 | } 1011 | this._shape.setCoords(); 1012 | this.dm.canvas.requestRenderAll(); 1013 | } 1014 | 1015 | } 1016 | 1017 | ns.EllipseMaker = class extends ns.RectMaker { 1018 | 1019 | constructor(dm) { 1020 | super(dm); 1021 | this.type = "EllipseMaker"; 1022 | this.props = ["fill", "stroke", "strokeWidth", "strokeDashArray", "opacity"]; 1023 | } 1024 | 1025 | start(options) { 1026 | this._startX = this._startY = null; 1027 | let pos = this._position(options); 1028 | this._shape = new fabric.Ellipse({ 1029 | drawmaker: { type: this.type }, 1030 | left: pos.left, 1031 | top: pos.top, 1032 | rx: this.roundFloat(pos.width / 2.0), 1033 | ry: this.roundFloat(pos.height / 2.0), 1034 | fill: this.dm.fill, 1035 | stroke: this.assertColor(this.dm.stroke, !this.isVisibleColor(this.dm.fill)), 1036 | strokeWidth: this.assertStrokeWidth(this.dm.strokeWidth, !this.isVisibleColor(this.dm.fill)), 1037 | strokeDashArray: this.dm.strokeDashArray, 1038 | opacity: this.dm.opacity, 1039 | evented: false, 1040 | }); 1041 | this._attached = false; 1042 | } 1043 | 1044 | update(options) { 1045 | let pos = this._position(options); 1046 | this._shape.set("left", pos.left); 1047 | this._shape.set("top", pos.top); 1048 | this._shape.set("rx", this.roundFloat(pos.width / 2.0)); 1049 | this._shape.set("ry", this.roundFloat(pos.height / 2.0)); 1050 | if (!this._attached) { 1051 | this.dm.canvas.add(this._shape); 1052 | this._attached = true; 1053 | } 1054 | this._shape.setCoords(); 1055 | this.dm.canvas.requestRenderAll(); 1056 | } 1057 | 1058 | } 1059 | 1060 | ns.TriangleMaker = class extends ns.RectMaker { 1061 | 1062 | constructor(dm) { 1063 | super(dm); 1064 | this.type = "TriangleMaker"; 1065 | this.props = ["fill", "stroke", "strokeWidth", "strokeDashArray", "opacity"]; 1066 | } 1067 | 1068 | start(options) { 1069 | this._startX = this._startY = null; 1070 | let pos = this._position(options); 1071 | this._shape = new fabric.Triangle({ 1072 | drawmaker: { type: this.type }, 1073 | left: pos.left, 1074 | top: pos.top, 1075 | width: pos.width, 1076 | height: pos.height, 1077 | fill: this.dm.fill, 1078 | stroke: this.assertColor(this.dm.stroke, !this.isVisibleColor(this.dm.fill)), 1079 | strokeWidth: this.assertStrokeWidth(this.dm.strokeWidth, !this.isVisibleColor(this.dm.fill)), 1080 | strokeDashArray: this.dm.strokeDashArray, 1081 | opacity: this.dm.opacity, 1082 | evented: false, 1083 | }); 1084 | this._attached = false; 1085 | } 1086 | 1087 | } 1088 | 1089 | ns.LineMaker = class extends ns.BaseMaker { 1090 | 1091 | constructor(dm) { 1092 | super(dm); 1093 | this.type = "LineMaker"; 1094 | this.props = ["stroke", "strokeWidth", "strokeDashArray", "opacity"]; 1095 | this._startX = 0; 1096 | this._startY = 0; 1097 | this._shape = null; 1098 | this._attached = false; 1099 | } 1100 | 1101 | _position(options) { 1102 | let pointer = this.dm.canvas.getPointer(options.e); 1103 | if (!this._startX || !this._startY) { 1104 | this._startX = pointer.x; 1105 | this._startY = pointer.y; 1106 | } 1107 | let pos = {}; 1108 | pos.x1 = this._startX; 1109 | pos.y1 = this._startY; 1110 | pos.x2 = pointer.x; 1111 | pos.y2 = pointer.y; 1112 | return pos; 1113 | } 1114 | 1115 | start(options) { 1116 | this._startX = this._startY = null; 1117 | let pos = this._position(options); 1118 | this._shape = new fabric.Line([pos.x1, pos.y1, pos.x2, pos.y2], { 1119 | drawmaker: { type: this.type }, 1120 | originX: "center", 1121 | originY: "center", 1122 | fill: null, 1123 | stroke: this.assertColor(this.dm.stroke), 1124 | strokeWidth: this.assertStrokeWidth(this.dm.strokeWidth), 1125 | strokeDashArray: this.dm.strokeDashArray, 1126 | opacity: this.dm.opacity, 1127 | evented: false, 1128 | }); 1129 | this._attached = false; 1130 | } 1131 | 1132 | update(options) { 1133 | let pos = this._position(options); 1134 | this._shape.set("x2", pos.x2); 1135 | this._shape.set("y2", pos.y2); 1136 | if (!this._attached) { 1137 | this.dm.canvas.add(this._shape); 1138 | this._attached = true; 1139 | } 1140 | this._shape.setCoords(); 1141 | this.dm.canvas.requestRenderAll(); 1142 | } 1143 | 1144 | } 1145 | 1146 | ns.ArrowMaker = class extends ns.LineMaker { 1147 | 1148 | constructor(dm) { 1149 | super(dm); 1150 | this.type = "ArrowMaker"; 1151 | this.props = ["stroke", "strokeWidth", "strokeDashArray", "opacity"]; 1152 | } 1153 | 1154 | _headSize(strokeWidth) { 1155 | let factor = 6; 1156 | let size = Math.round(factor + (strokeWidth * (factor / 2.0))); 1157 | return (strokeWidth > 0) ? size : 0; 1158 | }; 1159 | 1160 | _arrowAngle(x1, y1, x2, y2) { 1161 | return this.roundFloat((Math.atan2((y2 - y1), (x2 - x1)) * (180 / Math.PI)) + 90); 1162 | }; 1163 | 1164 | _position(options) { 1165 | let pos = super._position(options); 1166 | pos.angle = this._arrowAngle(pos.x1, pos.y1, pos.x2, pos.y2); 1167 | return pos; 1168 | } 1169 | 1170 | start(options) { 1171 | this._startX = this._startY = null; 1172 | let pos = this._position(options); 1173 | let stroke = this.assertColor(this.dm.stroke); 1174 | let strokeWidth = this.assertStrokeWidth(this.dm.strokeWidth); 1175 | let lineShape = new fabric.Line([pos.x1, pos.y1, pos.x2, pos.y2], { 1176 | originX: "center", 1177 | originY: "center", 1178 | fill: null, 1179 | stroke: stroke, 1180 | strokeWidth: strokeWidth, 1181 | strokeDashArray: this.dm.strokeDashArray, 1182 | opacity: this.dm.opacity, 1183 | selectable: false, 1184 | evented: false, 1185 | }); 1186 | let headSize = this._headSize(strokeWidth); 1187 | let triangleShape = new fabric.Triangle({ 1188 | originX: "center", 1189 | originY: "center", 1190 | left: pos.x2, 1191 | top: pos.y2, 1192 | width: headSize, 1193 | height: headSize, 1194 | angle: pos.angle, 1195 | fill: stroke, 1196 | stroke: null, 1197 | strokeWidth: 0, 1198 | strokeDashArray: null, 1199 | opacity: this.dm.opacity, 1200 | selectable: false, 1201 | evented: false, 1202 | }); 1203 | this._shape = [lineShape, triangleShape]; 1204 | this._attached = false; 1205 | } 1206 | 1207 | update(options) { 1208 | let pos = this._position(options); 1209 | let lineShape = this._shape[0]; 1210 | lineShape.set("x2", pos.x2); 1211 | lineShape.set("y2", pos.y2); 1212 | let triangleShape = this._shape[1]; 1213 | triangleShape.set("left", pos.x2); 1214 | triangleShape.set("top", pos.y2); 1215 | triangleShape.set("angle", pos.angle); 1216 | if (!this._attached) { 1217 | this.dm.canvas.add(...this._shape); 1218 | this._attached = true; 1219 | } 1220 | lineShape.setCoords(); 1221 | triangleShape.setCoords(); 1222 | this.dm.canvas.requestRenderAll(); 1223 | } 1224 | 1225 | stop(options) { 1226 | if (this._attached) { 1227 | this.dm.canvas.remove(...this._shape); 1228 | let group = new fabric.Group(this._shape, { 1229 | drawmaker: { type: this.type }, 1230 | evented: false, 1231 | }); 1232 | this.dm.canvas.add(group); 1233 | } 1234 | this.dm.canvas.requestRenderAll(); 1235 | } 1236 | 1237 | get(obj, property) { 1238 | if (!this.props.includes(property)) 1239 | return; 1240 | let item = obj.getObjects("line")[0]; 1241 | return item.get(property); 1242 | } 1243 | 1244 | set(obj, property, value) { 1245 | if (!this.props.includes(property)) 1246 | return; 1247 | if (property === "stroke") { 1248 | obj.getObjects("triangle").forEach(item => { 1249 | item.set("fill", value); 1250 | }); 1251 | obj.getObjects("line").forEach(item => { 1252 | item.set(property, value); 1253 | }); 1254 | } 1255 | else if (property === "strokeWidth") { 1256 | let headSize = this._headSize(value); 1257 | obj.getObjects("triangle").forEach(item => { 1258 | item.set("width", headSize); 1259 | item.set("height", headSize); 1260 | }); 1261 | obj.getObjects("line").forEach(item => { 1262 | item.set(property, value); 1263 | }); 1264 | } 1265 | else if (property === "strokeDashArray") { 1266 | obj.getObjects("line").forEach(item => { 1267 | item.set(property, value); 1268 | }); 1269 | } 1270 | else { 1271 | obj.getObjects().forEach(item => { 1272 | item.set(property, value); 1273 | }); 1274 | } 1275 | // Update the group and objects bounding box, as needed. 1276 | obj.addWithUpdate(); 1277 | this.dm.canvas.requestRenderAll(); 1278 | } 1279 | 1280 | } 1281 | 1282 | ns.DoubleArrowMaker = class extends ns.ArrowMaker { 1283 | 1284 | constructor(dm) { 1285 | super(dm); 1286 | this.type = "DoubleArrowMaker"; 1287 | this.props = ["stroke", "strokeWidth", "strokeDashArray", "opacity"]; 1288 | } 1289 | 1290 | start(options) { 1291 | this._startX = this._startY = null; 1292 | let pos = this._position(options); 1293 | let stroke = this.assertColor(this.dm.stroke); 1294 | let strokeWidth = this.assertStrokeWidth(this.dm.strokeWidth); 1295 | let lineShape = new fabric.Line([pos.x1, pos.y1, pos.x2, pos.y2], { 1296 | originX: "center", 1297 | originY: "center", 1298 | fill: null, 1299 | stroke: stroke, 1300 | strokeWidth: strokeWidth, 1301 | strokeDashArray: this.dm.strokeDashArray, 1302 | opacity: this.dm.opacity, 1303 | selectable: false, 1304 | evented: false, 1305 | }); 1306 | let headSize = this._headSize(strokeWidth); 1307 | let fromTriangleShape = new fabric.Triangle({ 1308 | originX: "center", 1309 | originY: "center", 1310 | left: pos.x1, 1311 | top: pos.y1, 1312 | width: headSize, 1313 | height: headSize, 1314 | angle: pos.angle + 180, 1315 | fill: stroke, 1316 | stroke: null, 1317 | strokeWidth: 0, 1318 | strokeDashArray: null, 1319 | opacity: this.dm.opacity, 1320 | selectable: false, 1321 | evented: false, 1322 | }); 1323 | let toTriangleShape = new fabric.Triangle({ 1324 | originX: "center", 1325 | originY: "center", 1326 | left: pos.x2, 1327 | top: pos.y2, 1328 | width: headSize, 1329 | height: headSize, 1330 | angle: pos.angle, 1331 | fill: stroke, 1332 | stroke: null, 1333 | strokeWidth: 0, 1334 | strokeDashArray: null, 1335 | opacity: this.dm.opacity, 1336 | selectable: false, 1337 | evented: false, 1338 | }); 1339 | this._shape = [lineShape, fromTriangleShape, toTriangleShape]; 1340 | this._attached = false; 1341 | } 1342 | 1343 | update(options) { 1344 | let pos = this._position(options); 1345 | let lineShape = this._shape[0]; 1346 | lineShape.set("x2", pos.x2); 1347 | lineShape.set("y2", pos.y2); 1348 | let fromTriangleShape = this._shape[1]; 1349 | fromTriangleShape.set("angle", pos.angle + 180); 1350 | let toTriangleShape = this._shape[2]; 1351 | toTriangleShape.set("left", pos.x2); 1352 | toTriangleShape.set("top", pos.y2); 1353 | toTriangleShape.set("angle", pos.angle); 1354 | if (!this._attached) { 1355 | this.dm.canvas.add(...this._shape); 1356 | this._attached = true; 1357 | } 1358 | lineShape.setCoords(); 1359 | fromTriangleShape.setCoords(); 1360 | toTriangleShape.setCoords(); 1361 | this.dm.canvas.requestRenderAll(); 1362 | } 1363 | 1364 | } 1365 | 1366 | ns.TextboxMaker = class extends ns.BaseMaker { 1367 | 1368 | constructor(dm) { 1369 | super(dm); 1370 | this.type = "TextboxMaker"; 1371 | this.props = ["fill", "opacity"]; 1372 | this._shape = null; 1373 | } 1374 | 1375 | start(options) { 1376 | let pointer = this.dm.canvas.getPointer(options.e); 1377 | this._shape = new fabric.Textbox("Text...", { 1378 | drawmaker: { type: this.type }, 1379 | originX: "center", 1380 | originY: "center", 1381 | left: pointer.x, 1382 | top: pointer.y, 1383 | textAlign: "left", 1384 | lineHeight: 1, 1385 | fontSize: 20, 1386 | fontWeight: "normal", 1387 | fontStyle: "normal", 1388 | fill: this.assertColor(this.dm.fill), 1389 | stroke: null, 1390 | strokeWidth: 0, 1391 | strokeDashArray: null, 1392 | opacity: this.dm.opacity, 1393 | evented: false, 1394 | }); 1395 | this.dm.canvas.add(this._shape); 1396 | this.dm.canvas.requestRenderAll(); 1397 | } 1398 | 1399 | update(options) { 1400 | let pointer = this.dm.canvas.getPointer(options.e); 1401 | this._shape.set("left", pointer.x); 1402 | this._shape.set("top", pointer.y); 1403 | this._shape.setCoords(); 1404 | this.dm.canvas.requestRenderAll(); 1405 | } 1406 | 1407 | } 1408 | 1409 | ns.PencilBrushMaker = class extends ns.BaseMaker { 1410 | 1411 | constructor(dm) { 1412 | super(dm); 1413 | this.type = "PencilBrushMaker"; 1414 | this.props = ["stroke", "strokeWidth", "strokeDashArray", "opacity"]; 1415 | this._shape = null; 1416 | } 1417 | 1418 | _updatePencil() { 1419 | // Pencil to draw, but it generates Paths. 1420 | this._shape.color = this.assertColor(this.dm.stroke); 1421 | this._shape.width = this.assertStrokeWidth(this.dm.strokeWidth); 1422 | this._shape.strokeDashArray = this.dm.strokeDashArray; 1423 | this._shape.opacity = this.dm.opacity; 1424 | } 1425 | 1426 | start(options) { 1427 | this._updatePencil(); 1428 | } 1429 | 1430 | select() { 1431 | this._shape = new fabric.PencilBrush(this.dm.canvas); 1432 | this._shape.createPath = function () { 1433 | let path = Object.getPrototypeOf(this).createPath.apply(this, arguments); 1434 | // Function overriding needed to add to each new pencil brush path. 1435 | path.drawmaker = this.drawmaker; 1436 | // fabric.PencilBrush doesn't have opacity property yet, but fabric.Path has. 1437 | path.opacity = this.opacity; 1438 | return path; 1439 | }; 1440 | this._shape.drawmaker = { type: this.type }; 1441 | this._updatePencil(); 1442 | this.dm.canvas.freeDrawingBrush = this._shape; 1443 | this.dm.canvas.isDrawingMode = true; 1444 | this.dm.canvas.requestRenderAll(); 1445 | } 1446 | 1447 | deselect() { 1448 | this.dm.canvas.isDrawingMode = false; 1449 | this.dm.canvas.requestRenderAll(); 1450 | } 1451 | 1452 | } 1453 | 1454 | })(this); --------------------------------------------------------------------------------