├── screenshots └── editor.jpg ├── lib ├── saveInBrowser.js ├── caret-right.svg ├── caret-left.svg ├── tip.js ├── zoom.js ├── drawingLine.js ├── drawingText.js ├── freeDrawSettings.js ├── upload.js ├── copyPaste.js ├── canvas.js ├── canvasSettings.js ├── drawingPath.js ├── style.css ├── core.js ├── utils.js ├── shapes.js ├── toolbar.js └── selectionSettings.js ├── LICENSE ├── vendor ├── grapick.min.css ├── undo-redo-stack.js └── grapick.min.js ├── index.html ├── readme.md └── script.js /screenshots/editor.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeHole7/fabricjs-image-editor-origin/HEAD/screenshots/editor.jpg -------------------------------------------------------------------------------- /lib/saveInBrowser.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Define utils to save/load canvas status with local storage 3 | */ 4 | window.saveInBrowser = { 5 | save: (name, value) => { 6 | // if item is an object, stringify 7 | if (value instanceof Object) { 8 | value = JSON.stringify(value); 9 | } 10 | 11 | localStorage.setItem(name, value); 12 | }, 13 | load: (name) => { 14 | let value = localStorage.getItem(name); 15 | value = JSON.parse(value); 16 | 17 | return value; 18 | }, 19 | remove: (name) => { 20 | localStorage.removeItem(name); 21 | } 22 | } -------------------------------------------------------------------------------- /lib/caret-right.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /lib/caret-left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 CodeHole7 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 | -------------------------------------------------------------------------------- /lib/tip.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Define actions to manage tip section 3 | */ 4 | (function () { 5 | 'use strict'; 6 | 7 | function tipPanel() { 8 | const defaultTips = [ 9 | 'Tip: use arrows to move a selected object by 1 pixel!', 10 | 'Tip: Shift + Click to select and modify multiple objects!', 11 | 'Tip: hold Shift when rotating an object for 15° angle jumps!', 12 | 'Tip: hold Shift when drawing a line for 15° angle jumps!', 13 | 'Tip: Ctrl +/-, Ctrl + wheel to zoom in and zoom out!', 14 | ] 15 | const _self = this; 16 | $(`${this.containerSelector} .canvas-holder .content`).append(` 17 |
${defaultTips[parseInt(Math.random() * defaultTips.length)]}
`) 18 | this.hideTip = function () { 19 | $(`${_self.containerSelector} .canvas-holder .content #tip-container`).hide(); 20 | } 21 | 22 | this.showTip = function () { 23 | $(`${_self.containerSelector} .canvas-holder .content #tip-container`).show(); 24 | } 25 | 26 | this.updateTip = function (str) { 27 | typeof str === 'string' && $(`${_self.containerSelector} .canvas-holder .content #tip-container`).html(str); 28 | } 29 | } 30 | 31 | window.ImageEditor.prototype.initializeTipSection = tipPanel; 32 | })(); -------------------------------------------------------------------------------- /vendor/grapick.min.css: -------------------------------------------------------------------------------- 1 | .grp-wrapper { 2 | background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==") 3 | } 4 | 5 | .grp-preview { 6 | position: absolute; 7 | top: 0; 8 | left: 0; 9 | width: 100%; 10 | height: 100%; 11 | cursor: crosshair 12 | } 13 | 14 | .grp-handler { 15 | width: 4px; 16 | margin-left: -2px; 17 | user-select: none; 18 | -webkit-user-select: none; 19 | -moz-user-select: none; 20 | height: 100% 21 | } 22 | 23 | .grp-handler-close { 24 | color: rgba(0, 0, 0, 0.4); 25 | border-radius: 100%; 26 | box-shadow: 0 2px 10px rgba(0, 0, 0, 0.25); 27 | background-color: #fff; 28 | text-align: center; 29 | width: 15px; 30 | height: 15px; 31 | margin-left: -5px; 32 | line-height: 10px; 33 | font-size: 21px; 34 | cursor: pointer 35 | } 36 | 37 | .grp-handler-close-c { 38 | position: absolute; 39 | top: -17px 40 | } 41 | 42 | .grp-handler-drag { 43 | background-color: rgba(0, 0, 0, 0.5); 44 | cursor: col-resize; 45 | width: 100%; 46 | height: 100% 47 | } 48 | 49 | .grp-handler-selected .grp-handler-drag { 50 | background-color: rgba(255, 255, 255, 0.5) 51 | } 52 | 53 | .grp-handler-cp-c { 54 | display: none 55 | } 56 | 57 | .grp-handler-selected .grp-handler-cp-c { 58 | display: block 59 | } 60 | 61 | .grp-handler-cp-wrap { 62 | width: 15px; 63 | height: 15px; 64 | margin-left: -8px; 65 | border: 3px solid #fff; 66 | box-shadow: 0 2px 10px rgba(0, 0, 0, 0.25); 67 | overflow: hidden; 68 | border-radius: 100%; 69 | cursor: pointer 70 | } 71 | 72 | .grp-handler-cp-wrap input[type=color] { 73 | opacity: 0; 74 | cursor: pointer 75 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 |
47 | 48 | 49 | -------------------------------------------------------------------------------- /lib/zoom.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Define action to zoom in/out by mouse+key events 3 | */ 4 | // keyboard shortcuts and zoom calculations 5 | const minZoom = 0.05 6 | const maxZoom = 3 7 | 8 | // zoom with key 9 | const zoomWithKeys = (e, canvas, applyZoom) => { 10 | const key = e.which || e.keyCode 11 | 12 | // ctr -: zoom out 13 | if (key === 189 && e.ctrlKey) { 14 | e.preventDefault() 15 | if (canvas.getZoom() === minZoom) return 16 | 17 | let updatedZoom = parseInt(canvas.getZoom() * 100) 18 | 19 | // 25% jumps 20 | if ((updatedZoom % 25) !== 0) { 21 | while ((updatedZoom % 25) !== 0) { 22 | updatedZoom = updatedZoom - 1 23 | } 24 | } else { 25 | updatedZoom = updatedZoom - 25 26 | } 27 | 28 | updatedZoom = updatedZoom / 100 29 | updatedZoom = (updatedZoom <= 0) ? minZoom : updatedZoom 30 | 31 | applyZoom(updatedZoom) 32 | } 33 | 34 | 35 | // ctr +: zoom in 36 | if (key === 187 && e.ctrlKey) { 37 | e.preventDefault() 38 | if (canvas.getZoom() === maxZoom) return 39 | 40 | let updatedZoom = parseInt(canvas.getZoom() * 100) 41 | 42 | // 25% jumps 43 | if ((updatedZoom % 25) !== 0) { 44 | while ((updatedZoom % 25) !== 0) { 45 | updatedZoom = updatedZoom + 1 46 | } 47 | } else { 48 | updatedZoom = updatedZoom + 25 49 | } 50 | 51 | updatedZoom = updatedZoom / 100 52 | updatedZoom = (updatedZoom > maxZoom) ? maxZoom : updatedZoom 53 | 54 | applyZoom(updatedZoom) 55 | } 56 | 57 | 58 | // ctr 0: reset 59 | if ((key === 96 || key === 48 || key === 192) && e.ctrlKey) { 60 | e.preventDefault() 61 | applyZoom(1) 62 | } 63 | } 64 | 65 | // zoom with mouse 66 | const zoomWithMouse = (e, canvas, applyZoom) => { 67 | if (!e.ctrlKey) return 68 | e.preventDefault() 69 | 70 | let updatedZoom = canvas.getZoom().toFixed(2) 71 | let zoomAmount = (e.deltaY > 0) ? -5 : 5 72 | updatedZoom = ((updatedZoom * 100) + zoomAmount) / 100 73 | if (updatedZoom < minZoom || updatedZoom > maxZoom) return 74 | 75 | applyZoom(updatedZoom) 76 | } -------------------------------------------------------------------------------- /lib/drawingLine.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Define action to draw line by mouse actions 3 | */ 4 | (function () { 5 | var lineDrawing = function (fabricCanvas) { 6 | let isDrawingLine = false, 7 | lineToDraw, pointer, pointerPoints 8 | 9 | fabricCanvas.on('mouse:down', (o) => { 10 | if (!fabricCanvas.isDrawingLineMode) return 11 | 12 | isDrawingLine = true 13 | pointer = fabricCanvas.getPointer(o.e) 14 | pointerPoints = [pointer.x, pointer.y, pointer.x, pointer.y] 15 | 16 | lineToDraw = new fabric.Line(pointerPoints, { 17 | strokeWidth: 2, 18 | stroke: '#000000' 19 | }); 20 | lineToDraw.selectable = false 21 | lineToDraw.evented = false 22 | lineToDraw.strokeUniform = true 23 | fabricCanvas.add(lineToDraw) 24 | }); 25 | 26 | fabricCanvas.on('mouse:move', (o) => { 27 | if (!isDrawingLine) return 28 | 29 | pointer = fabricCanvas.getPointer(o.e) 30 | 31 | if (o.e.shiftKey) { 32 | // calc angle 33 | let startX = pointerPoints[0] 34 | let startY = pointerPoints[1] 35 | let x2 = pointer.x - startX 36 | let y2 = pointer.y - startY 37 | let r = Math.sqrt(x2 * x2 + y2 * y2) 38 | let angle = (Math.atan2(y2, x2) / Math.PI * 180) 39 | 40 | angle = parseInt(((angle + 7.5) % 360) / 15) * 15 41 | 42 | let cosx = r * Math.cos(angle * Math.PI / 180) 43 | let sinx = r * Math.sin(angle * Math.PI / 180) 44 | 45 | lineToDraw.set({ 46 | x2: cosx + startX, 47 | y2: sinx + startY 48 | }) 49 | 50 | } else { 51 | lineToDraw.set({ 52 | x2: pointer.x, 53 | y2: pointer.y 54 | }) 55 | } 56 | 57 | fabricCanvas.renderAll() 58 | 59 | }); 60 | 61 | fabricCanvas.on('mouse:up', () => { 62 | if (!isDrawingLine) return 63 | 64 | lineToDraw.setCoords() 65 | isDrawingLine = false 66 | fabricCanvas.trigger('object:modified') 67 | }); 68 | } 69 | 70 | window.ImageEditor.prototype.initializeLineDrawing = lineDrawing; 71 | })() -------------------------------------------------------------------------------- /lib/drawingText.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Define action to draw text 3 | */ 4 | (function () { 5 | const textBoxDrawing = function (fabricCanvas) { 6 | 7 | let isDrawingText = false, 8 | textboxRect, origX, origY, pointer; 9 | 10 | 11 | fabricCanvas.on('mouse:down', (o) => { 12 | if (!fabricCanvas.isDrawingTextMode) return; 13 | 14 | isDrawingText = true; 15 | pointer = fabricCanvas.getPointer(o.e); 16 | origX = pointer.x; 17 | origY = pointer.y; 18 | textboxRect = new fabric.Rect({ 19 | left: origX, 20 | top: origY, 21 | width: pointer.x - origX, 22 | height: pointer.y - origY, 23 | strokeWidth: 1, 24 | stroke: '#C00000', 25 | fill: 'rgba(192, 0, 0, 0.2)', 26 | transparentCorners: false 27 | }); 28 | fabricCanvas.add(textboxRect); 29 | }); 30 | 31 | 32 | fabricCanvas.on('mouse:move', (o) => { 33 | if (!isDrawingText) return; 34 | 35 | pointer = fabricCanvas.getPointer(o.e); 36 | 37 | if (origX > pointer.x) { 38 | textboxRect.set({ 39 | left: Math.abs(pointer.x) 40 | }); 41 | } 42 | 43 | if (origY > pointer.y) { 44 | textboxRect.set({ 45 | top: Math.abs(pointer.y) 46 | }); 47 | } 48 | 49 | textboxRect.set({ 50 | width: Math.abs(origX - pointer.x) 51 | }); 52 | textboxRect.set({ 53 | height: Math.abs(origY - pointer.y) 54 | }); 55 | 56 | fabricCanvas.renderAll(); 57 | }); 58 | 59 | 60 | fabricCanvas.on('mouse:up', () => { 61 | if (!isDrawingText) return; 62 | 63 | isDrawingText = false; 64 | 65 | // get final rect coords and replace it with textbox 66 | let textbox = new fabric.Textbox('Your text goes here...', { 67 | left: textboxRect.left, 68 | top: textboxRect.top, 69 | width: textboxRect.width < 80 ? 80 : textboxRect.width, 70 | fontSize: 18, 71 | fontFamily: "'Open Sans', sans-serif" 72 | }); 73 | fabricCanvas.remove(textboxRect); 74 | fabricCanvas.add(textbox).setActiveObject(textbox) 75 | textbox.setControlsVisibility({ 76 | 'mb': false 77 | }); 78 | fabricCanvas.trigger('object:modified') 79 | }); 80 | 81 | } 82 | 83 | window.ImageEditor.prototype.initializeTextBoxDrawing = textBoxDrawing; 84 | })(); -------------------------------------------------------------------------------- /vendor/undo-redo-stack.js: -------------------------------------------------------------------------------- 1 | (function (global, factory) { 2 | typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : 3 | typeof define === 'function' && define.amd ? define(factory) : 4 | (global.UndoRedoStack = factory()); 5 | }(this, (function () { 6 | 'use strict'; 7 | 8 | const push$1 = state => 9 | val => state.push(val); 10 | 11 | const pop = state => () => 12 | state.pop(); 13 | 14 | const isEmpty = state => () => 15 | state.length < 1; 16 | 17 | const clear$1 = state => () => 18 | state.splice(0); 19 | 20 | const current = state => () => 21 | state[state.length - 1]; 22 | 23 | const getValues$1 = state => () => [...state]; 24 | 25 | function main$1(state) { 26 | return { 27 | push: push$1(state), 28 | pop: pop(state), 29 | isEmpty: isEmpty(state), 30 | clear: clear$1(state), 31 | current: current(state), 32 | getValues: getValues$1(state) 33 | } 34 | } 35 | 36 | function stack(state = false) { 37 | return main$1(state || []) 38 | } 39 | 40 | /** 41 | * An undo/redo history manager implemented as two stacks 42 | */ 43 | var push = function push(undoStack) { 44 | return function (val) { 45 | return undoStack.push(val); 46 | }; 47 | }; 48 | 49 | var undo = function undo(undoStack, redoStack) { 50 | return function () { 51 | if (!undoStack.isEmpty()) { 52 | redoStack.push(undoStack.pop()); 53 | } 54 | }; 55 | }; 56 | 57 | var redo = function redo(undoStack, redoStack) { 58 | return function () { 59 | if (!redoStack.isEmpty()) { 60 | undoStack.push(redoStack.pop()); 61 | } 62 | }; 63 | }; 64 | 65 | var clear = function clear(undoStack, redoStack) { 66 | return function () { 67 | undoStack.clear(); 68 | redoStack.clear(); 69 | }; 70 | }; 71 | 72 | var latest = function latest(undoStack) { 73 | return function () { 74 | return undoStack.current(); 75 | }; 76 | }; 77 | 78 | var getValues = function getValues(undoStack, redoStack) { 79 | return function () { 80 | return { 81 | undo: undoStack.getValues(), 82 | redo: redoStack.getValues() 83 | }; 84 | }; 85 | }; 86 | 87 | function main(undoStack, redoStack) { 88 | return { 89 | push: push(undoStack), 90 | undo: undo(undoStack, redoStack), 91 | redo: redo(undoStack, redoStack), 92 | clear: clear(undoStack, redoStack), 93 | latest: latest(undoStack), 94 | getValues: getValues(undoStack, redoStack) 95 | }; 96 | } 97 | 98 | function undoRedo() { 99 | var undoStack = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false; 100 | var redoStack = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; 101 | 102 | return undoStack && redoStack ? main(undoStack, redoStack) : main(stack(), stack()); 103 | } 104 | 105 | return undoRedo; 106 | 107 | }))); -------------------------------------------------------------------------------- /lib/freeDrawSettings.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Define action to pen draw by mouse action 3 | */ 4 | (function () { 5 | 'use strict'; 6 | 7 | var freeDrawSettings = function () { 8 | let width = 1; 9 | let style = 'pencil'; 10 | let color = 'black'; 11 | 12 | const _self = this; 13 | $(`${this.containerSelector} .main-panel`).append(`

Free Draw

`); 14 | 15 | // set dimension section 16 | $(`${this.containerSelector} .toolpanel#draw-panel .content`).append(` 17 |
18 |
19 | 20 |
21 | 22 | 23 | 24 |
25 |
26 |
27 | 28 | 33 |
34 |
35 | 36 | 37 |
38 |
39 | `); 40 | 41 | let updateBrush = () => { 42 | try { 43 | switch (style) { 44 | case 'circle': 45 | _self.canvas.freeDrawingBrush = new fabric.CircleBrush(_self.canvas) 46 | break 47 | 48 | case 'spray': 49 | _self.canvas.freeDrawingBrush = new fabric.SprayBrush(_self.canvas) 50 | break 51 | 52 | default: 53 | _self.canvas.freeDrawingBrush = new fabric.PencilBrush(_self.canvas) 54 | break 55 | } 56 | 57 | _self.canvas.freeDrawingBrush.width = width; 58 | _self.canvas.freeDrawingBrush.color = color; 59 | 60 | } catch (_) {} 61 | } 62 | 63 | $(`${this.containerSelector} .toolpanel#draw-panel .content #input-brush-width`).change(function () { 64 | try { 65 | width = parseInt($(this).val()); 66 | updateBrush(); 67 | } catch (_) {} 68 | }) 69 | 70 | $(`${this.containerSelector} .toolpanel#draw-panel .content #input-brush-type`).change(function () { 71 | style = $(this).val(); 72 | updateBrush(); 73 | }) 74 | 75 | $(`${this.containerSelector} .toolpanel#draw-panel .content #color-picker`).spectrum({ 76 | type: "color", 77 | showInput: "true", 78 | showInitial: "true", 79 | allowEmpty: "false", 80 | }); 81 | 82 | $(`${this.containerSelector} .toolpanel#draw-panel .content #color-picker`).change(function () { 83 | try { 84 | color = $(this).val(); 85 | updateBrush(); 86 | } catch (_) {} 87 | }) 88 | } 89 | 90 | window.ImageEditor.prototype.initializeFreeDrawSettings = freeDrawSettings; 91 | })(); -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Fabric.JS Image Editor 2 | This image editor allows users to draw default shapes, pen-drawing, line, curve + straight path, text, png/jpg/svg images on browser. 3 | 4 | [Demo!](https://fabricjs-image-editor-f62330.netlify.app) 5 | ![Positioning Example](screenshots/editor.jpg) 6 | 7 | ## Dependency 8 | * jQuery v3.5.1 9 | * jQuery spectrum-colorpicker2 10 | * Fabric.js v3.6.3 11 | 12 | ## Initialize 13 | ```javascript 14 | // define toolbar buttons to show 15 | // if this value is undefined or its length is 0, default toolbar buttons will be shown 16 | const buttons = [ 17 | 'select', 18 | 'shapes', 19 | // 'draw', 20 | // 'line', 21 | // 'path', 22 | // 'textbox', 23 | // 'upload', 24 | // 'background', 25 | 'undo', 26 | 'redo', 27 | 'save', 28 | 'download', 29 | 'clear' 30 | ]; 31 | 32 | // define custom shapes 33 | // if this value is undefined or its length is 0, default shapes will be used 34 | const shapes = [ 35 | ``, 36 | ``, 37 | `` 38 | ]; 39 | 40 | var imgEditor = new ImageEditor('#image-editor-container', buttons, shapes); 41 | ``` 42 | 43 | ## Save/Load Editor status 44 | 45 | ```javascript 46 | let status = imgEditor.getCanvasJSON(); 47 | imgEditor.setCanvasStatus(status); 48 | ``` 49 | -------------------------------------------------------------------------------- /lib/upload.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Define action to upload, drag & drop images into canvas 3 | */ 4 | (function () { 5 | var upload = function (canvas) { 6 | const _self = this; 7 | this.openDragDropPanel = function () { 8 | console.log('open drag drop panel') 9 | $('body').append(`
10 |
11 |
12 |
Drag & drop files
or click to browse.
JPG, PNG or SVG only!
13 |
14 |
15 |
`) 16 | $('.custom-modal-container').click(function () { 17 | $(this).remove() 18 | }) 19 | 20 | $('.drag-drop-input').click(function () { 21 | console.log('click drag drop') 22 | $(`${_self.containerSelector} #btn-image-upload`).click(); 23 | }) 24 | 25 | $(".drag-drop-input").on("dragover", function (event) { 26 | event.preventDefault(); 27 | event.stopPropagation(); 28 | $(this).addClass('dragging'); 29 | }); 30 | 31 | $(".drag-drop-input").on("dragleave", function (event) { 32 | event.preventDefault(); 33 | event.stopPropagation(); 34 | $(this).removeClass('dragging'); 35 | }); 36 | 37 | $(".drag-drop-input").on("drop", function (event) { 38 | event.preventDefault(); 39 | event.stopPropagation(); 40 | $(this).removeClass('dragging'); 41 | if (event.originalEvent.dataTransfer) { 42 | if (event.originalEvent.dataTransfer.files.length) { 43 | let files = event.originalEvent.dataTransfer.files 44 | processFiles(files); 45 | $('.custom-modal-container').remove(); 46 | } 47 | } 48 | }); 49 | } 50 | 51 | const processFiles = (files) => { 52 | if (files.length === 0) return; 53 | const allowedTypes = ['image/jpeg', 'image/png', 'image/svg+xml'] 54 | 55 | for (let file of files) { 56 | // check type 57 | if (!allowedTypes.includes(file.type)) continue 58 | 59 | let reader = new FileReader() 60 | 61 | // handle svg 62 | if (file.type === 'image/svg+xml') { 63 | reader.onload = (f) => { 64 | fabric.loadSVGFromString(f.target.result, (objects, options) => { 65 | let obj = fabric.util.groupSVGElements(objects, options) 66 | obj.set({ 67 | left: 0, 68 | top: 0 69 | }).setCoords() 70 | canvas.add(obj) 71 | 72 | canvas.renderAll() 73 | canvas.trigger('object:modified') 74 | }) 75 | } 76 | reader.readAsText(file) 77 | continue 78 | } 79 | 80 | // handle image, read file, add to canvas 81 | reader.onload = (f) => { 82 | fabric.Image.fromURL(f.target.result, (img) => { 83 | img.set({ 84 | left: 0, 85 | top: 0 86 | }) 87 | img.scaleToHeight(300) 88 | img.scaleToWidth(300) 89 | canvas.add(img) 90 | 91 | canvas.renderAll() 92 | canvas.trigger('object:modified') 93 | }) 94 | } 95 | 96 | reader.readAsDataURL(file) 97 | } 98 | } 99 | 100 | this.containerEl.append(``); 101 | document.querySelector(`${this.containerSelector} #btn-image-upload`).addEventListener('change', function (e) { 102 | if (e.target.files.length === 0) return; 103 | processFiles(e.target.files) 104 | }) 105 | } 106 | 107 | window.ImageEditor.prototype.initializeUpload = upload; 108 | })() -------------------------------------------------------------------------------- /lib/copyPaste.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Define copy/paste actions on fabric js canvas 3 | */ 4 | (function () { 5 | 'use strict'; 6 | const copyPaste = (canvas) => { 7 | 8 | // copy 9 | document.addEventListener('copy', (e) => { 10 | if (!canvas.getActiveObject()) return 11 | 12 | // copy image as dataUrl 13 | if (canvas.getActiveObject().type === 'image') { 14 | e.preventDefault() 15 | 16 | e.clipboardData.setData('text/plain', canvas.getActiveObject().toDataURL()) 17 | } 18 | 19 | 20 | // if selection is not an image, copy as JSON 21 | if (canvas.getActiveObject().type !== 'image') { 22 | e.preventDefault() 23 | canvas.getActiveObject().clone((cloned) => { 24 | e.clipboardData.setData('text/plain', JSON.stringify(cloned.toJSON())) 25 | }) 26 | } 27 | }) 28 | 29 | // JSON string validator 30 | const isJSONObjectString = (s) => { 31 | try { 32 | const o = JSON.parse(s); 33 | return !!o && (typeof o === 'object') && !Array.isArray(o) 34 | } catch { 35 | return false 36 | } 37 | } 38 | 39 | // base64 validator 40 | const isBase64String = (str) => { 41 | try { 42 | str = str.split('base64,').pop() 43 | window.atob(str) 44 | return true 45 | } catch (e) { 46 | return false 47 | } 48 | } 49 | 50 | // paste 51 | document.addEventListener('paste', (e) => { 52 | let pasteTextData = e.clipboardData.getData('text') 53 | 54 | // check if base64 image 55 | if (pasteTextData && isBase64String(pasteTextData)) { 56 | fabric.Image.fromURL(pasteTextData, (img) => { 57 | img.set({ 58 | left: 0, 59 | top: 0 60 | }) 61 | img.scaleToHeight(100) 62 | img.scaleToWidth(100) 63 | canvas.add(img) 64 | canvas.setActiveObject(img) 65 | canvas.trigger('object:modified') 66 | }) 67 | 68 | return 69 | } 70 | 71 | // check if there's an image in clipboard items 72 | if (e.clipboardData.items.length > 0) { 73 | for (let i = 0; i < e.clipboardData.items.length; i++) { 74 | if (e.clipboardData.items[i].type.indexOf('image') === 0) { 75 | let blob = e.clipboardData.items[i].getAsFile() 76 | if (blob !== null) { 77 | let reader = new FileReader() 78 | reader.onload = (f) => { 79 | fabric.Image.fromURL(f.target.result, (img) => { 80 | img.set({ 81 | left: 0, 82 | top: 0 83 | }) 84 | img.scaleToHeight(100) 85 | img.scaleToWidth(100) 86 | canvas.add(img) 87 | canvas.setActiveObject(img) 88 | canvas.trigger('object:modified') 89 | }) 90 | } 91 | reader.readAsDataURL(blob) 92 | } 93 | } 94 | } 95 | } 96 | 97 | // check if JSON and type is valid 98 | let validTypes = ['rect', 'circle', 'line', 'path', 'polygon', 'polyline', 'textbox', 'group'] 99 | if (isJSONObjectString(pasteTextData)) { 100 | let obj = JSON.parse(pasteTextData) 101 | if (!validTypes.includes(obj.type)) return 102 | 103 | // insert and select 104 | fabric.util.enlivenObjects([obj], function (objects) { 105 | objects.forEach(function (o) { 106 | o.set({ 107 | left: 0, 108 | top: 0 109 | }) 110 | canvas.add(o) 111 | o.setCoords() 112 | canvas.setActiveObject(o) 113 | }) 114 | canvas.renderAll() 115 | canvas.trigger('object:modified') 116 | }) 117 | } 118 | }) 119 | } 120 | 121 | window.ImageEditor.prototype.initializeCopyPaste = copyPaste; 122 | })() -------------------------------------------------------------------------------- /script.js: -------------------------------------------------------------------------------- 1 | try { 2 | // define toolbar buttons to show 3 | // if this value is undefined or its length is 0, default toolbar buttons will be shown 4 | const buttons = [ 5 | 'select', 6 | 'shapes', 7 | 'draw', 8 | 'line', 9 | 'path', 10 | 'textbox', 11 | 'upload', 12 | 'background', 13 | 'undo', 14 | 'redo', 15 | 'save', 16 | 'download', 17 | 'clear' 18 | ]; 19 | 20 | // define custom shapes 21 | // if this value is undefined or its length is 0, default shapes will be used 22 | const shapes = [ 23 | ``, 24 | ``, 25 | `` 26 | ]; 27 | 28 | var imgEditor = new ImageEditor('#image-editor-container', buttons, []); 29 | console.log('initialize image editor'); 30 | 31 | // let status = imgEditor.getCanvasJSON(); 32 | // imgEditor.setCanvasStatus(status); 33 | 34 | } catch (_) { 35 | const browserWarning = document.createElement('div') 36 | browserWarning.innerHTML = '

Your browser is out of date!
Please update to a modern browser, for example:Chrome!

'; 37 | 38 | browserWarning.setAttribute( 39 | 'style', 40 | 'position: fixed; z-index: 1000; width: 100%; height: 100%; top: 0; left: 0; background-color: #f9f9f9; text-align: center; color: #555;' 41 | ) 42 | 43 | // check for flex and grid support 44 | let divGrid = document.createElement('div') 45 | divGrid.style['display'] = 'grid' 46 | let supportsGrid = divGrid.style['display'] === 'grid' 47 | 48 | let divFlex = document.createElement('div') 49 | divFlex.style['display'] = 'flex' 50 | let supportsFlex = divFlex.style['display'] === 'flex' 51 | 52 | if (!supportsGrid || !supportsFlex) { 53 | document.body.appendChild(browserWarning) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /lib/canvas.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Canvas section management of image editor 3 | */ 4 | (function () { 5 | 'use strict'; 6 | var canvas = function () { 7 | try { 8 | $(`${this.containerSelector} .main-panel`).append(`
`); 9 | const fabricCanvas = new fabric.Canvas('c').setDimensions({ 10 | width: 800, 11 | height: 600 12 | }) 13 | 14 | fabricCanvas.originalW = fabricCanvas.width; 15 | fabricCanvas.originalH = fabricCanvas.height; 16 | 17 | // set up selection style 18 | fabric.Object.prototype.transparentCorners = false; 19 | fabric.Object.prototype.cornerStyle = 'circle'; 20 | fabric.Object.prototype.borderColor = '#C00000'; 21 | fabric.Object.prototype.cornerColor = '#C00000'; 22 | fabric.Object.prototype.cornerStrokeColor = '#FFF'; 23 | fabric.Object.prototype.padding = 0; 24 | 25 | // retrieve active selection to react state 26 | fabricCanvas.on('selection:created', (e) => this.setActiveSelection(e.target)) 27 | fabricCanvas.on('selection:updated', (e) => this.setActiveSelection(e.target)) 28 | fabricCanvas.on('selection:cleared', (e) => this.setActiveSelection(null)) 29 | 30 | // snap to an angle on rotate if shift key is down 31 | fabricCanvas.on('object:rotating', (e) => { 32 | if (e.e.shiftKey) { 33 | e.target.snapAngle = 15; 34 | } else { 35 | e.target.snapAngle = false; 36 | } 37 | }) 38 | 39 | fabricCanvas.on('object:modified', () => { 40 | console.log('trigger: modified') 41 | let currentState = this.canvas.toJSON(); 42 | this.history.push(JSON.stringify(currentState)); 43 | }) 44 | 45 | const savedCanvas = saveInBrowser.load('canvasEditor'); 46 | if (savedCanvas) { 47 | fabricCanvas.loadFromJSON(savedCanvas, fabricCanvas.renderAll.bind(fabricCanvas)); 48 | } 49 | 50 | // move objects with arrow keys 51 | (() => document.addEventListener('keydown', (e) => { 52 | const key = e.which || e.keyCode; 53 | let activeObject; 54 | 55 | if (document.querySelectorAll('textarea:focus, input:focus').length > 0) return; 56 | 57 | if (key === 37 || key === 38 || key === 39 || key === 40) { 58 | e.preventDefault(); 59 | activeObject = fabricCanvas.getActiveObject(); 60 | if (!activeObject) { 61 | return; 62 | } 63 | } 64 | 65 | if (key === 37) { 66 | activeObject.left -= 1; 67 | } else if (key === 39) { 68 | activeObject.left += 1; 69 | } else if (key === 38) { 70 | activeObject.top -= 1; 71 | } else if (key === 40) { 72 | activeObject.top += 1; 73 | } 74 | 75 | if (key === 37 || key === 38 || key === 39 || key === 40) { 76 | activeObject.setCoords(); 77 | fabricCanvas.renderAll(); 78 | fabricCanvas.trigger('object:modified'); 79 | } 80 | }))(); 81 | 82 | // delete object on del key 83 | (() => { 84 | document.addEventListener('keydown', (e) => { 85 | const key = e.which || e.keyCode; 86 | if ( 87 | key === 46 && 88 | document.querySelectorAll('textarea:focus, input:focus').length === 0 89 | ) { 90 | 91 | fabricCanvas.getActiveObjects().forEach(obj => { 92 | fabricCanvas.remove(obj); 93 | }); 94 | 95 | fabricCanvas.discardActiveObject().requestRenderAll(); 96 | fabricCanvas.trigger('object:modified') 97 | } 98 | }) 99 | })(); 100 | 101 | setTimeout(() => { 102 | let currentState = fabricCanvas.toJSON(); 103 | this.history.push(JSON.stringify(currentState)); 104 | }, 1000); 105 | 106 | return fabricCanvas; 107 | } catch (_) { 108 | console.error("can't create canvas instance"); 109 | return null; 110 | } 111 | } 112 | 113 | window.ImageEditor.prototype.initializeCanvas = canvas; 114 | })(); -------------------------------------------------------------------------------- /lib/canvasSettings.js: -------------------------------------------------------------------------------- 1 | /** 2 | * initialize canvas setting panel 3 | */ 4 | (function () { 5 | 'use strict'; 6 | var canvasSettings = function () { 7 | const _self = this; 8 | $(`${this.containerSelector} .main-panel`).append(`

Canvas Settings

`); 9 | 10 | // set dimension section 11 | (() => { 12 | $(`${this.containerSelector} .toolpanel#background-panel .content`).append(` 13 |
14 |

Canvas Size

15 |
16 | 17 |
18 | 19 | 20 | 21 |
22 |
23 |
24 | 25 |
26 | 27 | 28 | 29 |
30 |
31 |
32 | `); 33 | 34 | var setDimension = () => { 35 | try { 36 | let width = $(`${this.containerSelector} .toolpanel#background-panel .content #input-width`).val(); 37 | let height = $(`${this.containerSelector} .toolpanel#background-panel .content #input-height`).val(); 38 | _self.canvas.setWidth(width) 39 | _self.canvas.originalW = width 40 | _self.canvas.setHeight(height) 41 | _self.canvas.originalH = height 42 | _self.canvas.renderAll() 43 | _self.canvas.trigger('object:modified') 44 | } catch (_) {} 45 | } 46 | 47 | $(`${this.containerSelector} .toolpanel#background-panel .content #input-width`).change(setDimension) 48 | $(`${this.containerSelector} .toolpanel#background-panel .content #input-height`).change(setDimension) 49 | })(); 50 | // end set dimension section 51 | 52 | // background color 53 | (() => { 54 | $(`${this.containerSelector} .toolpanel#background-panel .content`).append(` 55 |
56 |
57 |
58 |
Color Fill
59 |
Gradient Fill
60 |
61 |
62 |
63 |
64 |
65 |
66 | 67 |
68 |
69 | 70 | 74 |
75 |
76 | 77 |
78 | 79 | 80 | 81 |
82 |
83 |
84 |
85 |
86 |
87 | `) 88 | 89 | $(`${this.containerSelector} .toolpanel#background-panel .content .tab-label`).click(function () { 90 | $(`${_self.containerSelector} .toolpanel#background-panel .content .tab-label`).removeClass('active'); 91 | $(this).addClass('active'); 92 | let target = $(this).data('value'); 93 | $(this).closest('.tab-container').find('.tab-content').hide(); 94 | $(this).closest('.tab-container').find(`.tab-content[data-value=${target}]`).show(); 95 | 96 | if (target === 'color-fill') { 97 | let color = $(`${_self.containerSelector} .toolpanel#background-panel .content #color-picker`).val(); 98 | try { 99 | _self.canvas.backgroundColor = color; 100 | _self.canvas.renderAll(); 101 | } catch (_) { 102 | console.log("can't update background color") 103 | } 104 | } else { 105 | updateGradientFill(); 106 | } 107 | }) 108 | 109 | $(`${this.containerSelector} .toolpanel#background-panel .content .tab-label[data-value=color-fill]`).click(); 110 | 111 | $(`${this.containerSelector} .toolpanel#background-panel .content #color-picker`).spectrum({ 112 | flat: true, 113 | showPalette: false, 114 | showButtons: false, 115 | type: "color", 116 | showInput: "true", 117 | allowEmpty: "false", 118 | move: function (color) { 119 | let hex = 'transparent'; 120 | color && (hex = color.toRgbString()); // #ff0000 121 | _self.canvas.backgroundColor = hex; 122 | _self.canvas.renderAll(); 123 | } 124 | }); 125 | 126 | const gp = new Grapick({ 127 | el: `${this.containerSelector} .toolpanel#background-panel .content #gradient-picker`, 128 | colorEl: '' // I'll use this for the custom color picker 129 | }); 130 | 131 | gp.setColorPicker(handler => { 132 | const el = handler.getEl().querySelector('#colorpicker'); 133 | $(el).spectrum({ 134 | showPalette: false, 135 | showButtons: false, 136 | type: "color", 137 | showInput: "true", 138 | allowEmpty: "false", 139 | color: handler.getColor(), 140 | showAlpha: true, 141 | change(color) { 142 | handler.setColor(color.toRgbString()); 143 | }, 144 | move(color) { 145 | handler.setColor(color.toRgbString(), 0); 146 | } 147 | }); 148 | }); 149 | 150 | gp.addHandler(0, 'red'); 151 | gp.addHandler(100, 'blue'); 152 | 153 | const updateGradientFill = () => { 154 | let stops = gp.getHandlers(); 155 | let orientation = $(`${this.containerSelector} .toolpanel#background-panel .content .gradient-orientation-container #select-orientation`).val(); 156 | let angle = parseInt($(`${this.containerSelector} .toolpanel#background-panel .content .gradient-orientation-container #input-angle`).val()); 157 | 158 | let gradient = generateFabricGradientFromColorStops(stops, _self.canvas.width, _self.canvas.height, orientation, angle); 159 | _self.canvas.setBackgroundColor(gradient) 160 | _self.canvas.renderAll() 161 | } 162 | 163 | // Do stuff on change of the gradient 164 | gp.on('change', complete => { 165 | updateGradientFill(); 166 | }) 167 | 168 | $(`${this.containerSelector} .toolpanel#background-panel .content .gradient-orientation-container #select-orientation`).change(function () { 169 | let type = $(this).val(); 170 | if (type === 'radial') { 171 | $(this).closest('.gradient-orientation-container').find('#angle-input-container').hide(); 172 | } else { 173 | $(this).closest('.gradient-orientation-container').find('#angle-input-container').show(); 174 | } 175 | updateGradientFill(); 176 | }) 177 | 178 | $(`${this.containerSelector} .toolpanel#background-panel .content .gradient-orientation-container #input-angle`).change(function () { 179 | updateGradientFill(); 180 | }) 181 | })(); 182 | } 183 | 184 | window.ImageEditor.prototype.initializeCanvasSettingPanel = canvasSettings; 185 | })() -------------------------------------------------------------------------------- /lib/drawingPath.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Define action to draw path by mouse action 3 | */ 4 | (function () { 5 | const inRange = (radius, cursorX, cursorY, targetX, targetY) => { 6 | if ( 7 | Math.abs(cursorX - targetX) <= radius && 8 | Math.abs(cursorY - targetY) <= radius 9 | ) { 10 | return true 11 | } 12 | 13 | return false 14 | } 15 | 16 | const pathDrawing = (fabricCanvas) => { 17 | 18 | let isDrawingPath = false, 19 | pathToDraw, 20 | pointer, 21 | updatedPath, 22 | isMouseDown = false, 23 | isDrawingCurve = false, 24 | rememberX, rememberY 25 | 26 | 27 | fabricCanvas.on('mouse:down', (o) => { 28 | if (!fabricCanvas.isDrawingPathMode) return 29 | 30 | isMouseDown = true 31 | isDrawingPath = true 32 | pointer = fabricCanvas.getPointer(o.e) 33 | 34 | 35 | // if first point, no extras, just place the point 36 | if (!pathToDraw) { 37 | pathToDraw = new fabric.Path(`M${pointer.x} ${pointer.y} L${pointer.x} ${pointer.y}`, { 38 | strokeWidth: 2, 39 | stroke: '#000000', 40 | fill: false 41 | }) 42 | pathToDraw.selectable = false 43 | pathToDraw.evented = false 44 | pathToDraw.strokeUniform = true 45 | fabricCanvas.add(pathToDraw) 46 | 47 | return 48 | } 49 | 50 | // not the first point, add a new line 51 | if (pathToDraw) { 52 | pathToDraw.path.push(['L', pointer.x, pointer.y]) 53 | 54 | // recalc path dimensions 55 | let dims = pathToDraw._calcDimensions() 56 | pathToDraw.set({ 57 | width: dims.width, 58 | height: dims.height, 59 | left: dims.left, 60 | top: dims.top, 61 | pathOffset: { 62 | x: dims.width / 2 + dims.left, 63 | y: dims.height / 2 + dims.top 64 | }, 65 | dirty: true 66 | }) 67 | pathToDraw.setCoords() 68 | fabricCanvas.renderAll() 69 | 70 | return 71 | } 72 | }); 73 | 74 | 75 | 76 | fabricCanvas.on('mouse:move', (o) => { 77 | 78 | if (!fabricCanvas.isDrawingPathMode) return 79 | 80 | if (!isDrawingPath) return 81 | 82 | // update the last path command as we move the mouse 83 | pointer = fabricCanvas.getPointer(o.e) 84 | 85 | if (!isDrawingCurve) { 86 | updatedPath = ['L', pointer.x, pointer.y] 87 | } 88 | 89 | pathToDraw.path.pop() 90 | 91 | 92 | // shift key is down, jump angles 93 | if (o.e.shiftKey && !isDrawingCurve) { 94 | // last fix, placed point 95 | let lastPoint = [...pathToDraw.path].pop() 96 | let startX = lastPoint[1] 97 | let startY = lastPoint[2] 98 | 99 | let x2 = pointer.x - startX 100 | let y2 = pointer.y - startY 101 | let r = Math.sqrt(x2 * x2 + y2 * y2) 102 | let angle = (Math.atan2(y2, x2) / Math.PI * 180) 103 | 104 | angle = parseInt(((angle + 7.5) % 360) / 15) * 15 105 | 106 | let cosx = r * Math.cos(angle * Math.PI / 180) 107 | let sinx = r * Math.sin(angle * Math.PI / 180) 108 | 109 | updatedPath[1] = cosx + startX 110 | updatedPath[2] = sinx + startY 111 | } 112 | 113 | 114 | // detect and snap to closest line if within range 115 | if (pathToDraw.path.length > 1 && !isDrawingCurve) { 116 | // foreach all points, except last 117 | let snapPoints = [...pathToDraw.path] 118 | snapPoints.pop() 119 | for (let p of snapPoints) { 120 | // line 121 | if ((p[0] === 'L' || p[0] === 'M') && inRange(10, pointer.x, pointer.y, p[1], p[2])) { 122 | updatedPath[1] = p[1] 123 | updatedPath[2] = p[2] 124 | break 125 | } 126 | 127 | // curve 128 | if (p[0] === 'Q' && inRange(10, pointer.x, pointer.y, p[3], p[4])) { 129 | updatedPath[1] = p[3] 130 | updatedPath[2] = p[4] 131 | break 132 | } 133 | 134 | } 135 | } 136 | 137 | // curve creating 138 | if (isMouseDown) { 139 | 140 | if (!isDrawingCurve && pathToDraw.path.length > 1) { 141 | 142 | isDrawingCurve = true 143 | 144 | // get last path position and remove last path so we can update it 145 | let lastPath = pathToDraw.path.pop() 146 | 147 | if (lastPath[0] === 'Q') { 148 | updatedPath = ['Q', lastPath[3], lastPath[4], lastPath[3], lastPath[4]] 149 | rememberX = lastPath[3] 150 | rememberY = lastPath[4] 151 | } else { 152 | updatedPath = ['Q', lastPath[1], lastPath[2], lastPath[1], lastPath[2]] 153 | rememberX = lastPath[1] 154 | rememberY = lastPath[2] 155 | } 156 | 157 | } else if (isDrawingCurve) { 158 | 159 | // detect mouse move and calc Q position 160 | let mouseMoveX = pointer.x - updatedPath[3] 161 | let mouseMoveY = pointer.y - updatedPath[4] 162 | 163 | updatedPath = [ 164 | 'Q', 165 | rememberX - mouseMoveX, 166 | rememberY - mouseMoveY, 167 | rememberX, 168 | rememberY 169 | ] 170 | 171 | } 172 | 173 | } 174 | 175 | // add new path 176 | pathToDraw.path.push(updatedPath) 177 | 178 | // recalc path dimensions 179 | let dims = pathToDraw._calcDimensions(); 180 | pathToDraw.set({ 181 | width: dims.width, 182 | height: dims.height, 183 | left: dims.left, 184 | top: dims.top, 185 | pathOffset: { 186 | x: dims.width / 2 + dims.left, 187 | y: dims.height / 2 + dims.top 188 | }, 189 | dirty: true 190 | }) 191 | fabricCanvas.renderAll() 192 | 193 | }) 194 | 195 | fabricCanvas.on('mouse:up', (o) => { 196 | if (!fabricCanvas.isDrawingPathMode) { 197 | isMouseDown = false 198 | isDrawingCurve = false 199 | return 200 | } 201 | 202 | isMouseDown = false 203 | 204 | if (isDrawingCurve) { 205 | // place current curve by starting a new line 206 | pointer = fabricCanvas.getPointer(o.e) 207 | pathToDraw.path.push(['L', pointer.x, pointer.y]) 208 | 209 | // recalc path dimensions 210 | let dims = pathToDraw._calcDimensions() 211 | pathToDraw.set({ 212 | width: dims.width, 213 | height: dims.height, 214 | left: dims.left, 215 | top: dims.top, 216 | pathOffset: { 217 | x: dims.width / 2 + dims.left, 218 | y: dims.height / 2 + dims.top 219 | }, 220 | dirty: true 221 | }) 222 | pathToDraw.setCoords() 223 | fabricCanvas.renderAll() 224 | } 225 | 226 | isDrawingCurve = false 227 | 228 | }) 229 | 230 | // cancel drawing, remove last line 231 | const cancelDrawing = () => { 232 | // remove last line 233 | pathToDraw.path.pop() 234 | 235 | if (pathToDraw.path.length > 1) { 236 | 237 | let dims = pathToDraw._calcDimensions(); 238 | pathToDraw.set({ 239 | width: dims.width, 240 | height: dims.height, 241 | left: dims.left, 242 | top: dims.top, 243 | pathOffset: { 244 | x: dims.width / 2 + dims.left, 245 | y: dims.height / 2 + dims.top 246 | }, 247 | dirty: true 248 | }) 249 | 250 | } else { 251 | // if there is no line, just the starting point then remove 252 | fabricCanvas.remove(pathToDraw); 253 | } 254 | 255 | fabricCanvas.renderAll() 256 | fabricCanvas.trigger('object:modified') 257 | 258 | pathToDraw = null 259 | isDrawingPath = false 260 | } 261 | 262 | // cancel drawing on esc key or outside click 263 | document.addEventListener('keydown', (e) => { 264 | if (!isDrawingPath) return 265 | 266 | const key = e.which || e.keyCode; 267 | if (key === 27) cancelDrawing() 268 | }) 269 | 270 | document.addEventListener('mousedown', (e) => { 271 | if (!isDrawingPath) return 272 | 273 | if (!document.querySelector('.canvas-container').contains(e.target)) { 274 | cancelDrawing() 275 | } 276 | }) 277 | 278 | } 279 | 280 | window.ImageEditor.prototype.initializePathDrawing = pathDrawing; 281 | })() -------------------------------------------------------------------------------- /lib/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: "Open Sans", sans-serif; 4 | } 5 | 6 | .grp-handler-cp-c { 7 | margin-left: -20px; 8 | } 9 | 10 | .default-container { 11 | width: 100%; 12 | height: 100%; 13 | /* display: flex; */ 14 | } 15 | 16 | .toolbar { 17 | line-height: 0; 18 | background-color: #f2f2f2; 19 | box-shadow: 0 0 3px 0 rgba(50, 50, 50, .25); 20 | display: flex; 21 | justify-content: space-between; 22 | align-items: center; 23 | } 24 | 25 | .toolbar button { 26 | width: 64px; 27 | height: 54px; 28 | opacity: .55; 29 | clear: both; 30 | border: 0; 31 | border-radius: unset; 32 | outline: none; 33 | } 34 | 35 | .toolbar button.active, 36 | .toolbar button:hover { 37 | opacity: 1; 38 | border-left: 1px solid #ccc; 39 | border-right: 1px solid #ccc; 40 | box-shadow: inset 5px 0 10px 0 rgba(50, 50, 50, .1); 41 | } 42 | 43 | .toolbar button img, 44 | .toolbar button svg { 45 | width: 22px; 46 | height: 22px; 47 | } 48 | 49 | .main-panel { 50 | height: calc(100% - 54px); 51 | display: flex; 52 | position: relative; 53 | } 54 | 55 | .floating-zoom-level-container { 56 | position: absolute; 57 | z-index: 10000; 58 | background: white; 59 | padding: 10px 20px; 60 | bottom: 0; 61 | right: 0; 62 | border: 1px solid #ccc; 63 | } 64 | 65 | .canvas-holder { 66 | margin: auto; 67 | width: 100%; 68 | height: 100%; 69 | overflow: auto; 70 | display: flex; 71 | align-items: center; 72 | justify-content: center; 73 | background: #eaeaea; 74 | } 75 | 76 | .canvas-container { 77 | background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAHUlEQVQ4jWNgYGAQIYAJglEDhoUBg9+FowbQ2gAARjwKARjtnN8AAAAASUVORK5CYII="); 78 | background-size: 30px 30px; 79 | border: 1px solid #ccc; 80 | margin: auto; 81 | } 82 | 83 | .toolpanel { 84 | background-color: #f9f9f9; 85 | width: 300px; 86 | top: 0; 87 | left: 0; 88 | height: 100%; 89 | border: 1px solid #ddd; 90 | transition: all .4s; 91 | box-sizing: border-box; 92 | text-align: left; 93 | font-size: 13px; 94 | color: #777; 95 | display: none; 96 | position: absolute; 97 | z-index: 9999; 98 | } 99 | 100 | .toolpanel.closed { 101 | left: -300px; 102 | } 103 | 104 | .toolpanel.visible { 105 | display: initial; 106 | } 107 | 108 | .toolpanel .content { 109 | padding: 20px; 110 | position: relative; 111 | height: -webkit-fill-available; 112 | height: -ms-fill-available; 113 | height: fill-available; 114 | height: -moz-fill-available; 115 | } 116 | 117 | .toolpanel .title { 118 | font-size: 14px; 119 | font-weight: 700; 120 | margin: 0; 121 | padding-bottom: 10px; 122 | width: 100%; 123 | border-bottom: 1px solid #ddd; 124 | color: #333; 125 | text-transform: uppercase; 126 | } 127 | 128 | .toolpanel .content .hide-show-handler { 129 | position: absolute; 130 | top: calc(50% - 40px); 131 | right: -42px; 132 | width: 40px; 133 | height: 80px; 134 | background: #f9f9f9; 135 | border: 1px solid #ddd; 136 | border-top-right-radius: 3px; 137 | border-bottom-right-radius: 3px; 138 | cursor: pointer; 139 | 140 | background-image: url('/lib/caret-left.svg'); 141 | background-size: 10px; 142 | background-repeat: no-repeat; 143 | background-position: center center; 144 | } 145 | 146 | .toolpanel.closed .content .hide-show-handler { 147 | background-image: url('/lib/caret-right.svg'); 148 | } 149 | 150 | .spectrum.with-add-on { 151 | width: 40px; 152 | } 153 | 154 | #shapes-panel .button { 155 | cursor: pointer; 156 | line-height: 0; 157 | overflow: hidden; 158 | padding: 0; 159 | width: 32px; 160 | height: 32px; 161 | display: inline-block; 162 | margin: 9px; 163 | } 164 | 165 | #background-panel .canvas-size-setting input { 166 | width: 60px; 167 | background-color: #fff; 168 | border-radius: 6px; 169 | border: 2px solid #e4e4e4; 170 | padding: 4px 10px; 171 | line-height: 18px; 172 | font-size: 13px; 173 | } 174 | 175 | #select-panel .text-section .style button, 176 | #select-panel .alignment-section button, 177 | #select-panel .object-options button { 178 | padding: 0; 179 | width: 32px; 180 | height: 32px; 181 | background-color: #fff; 182 | border: 1px solid #ddd; 183 | text-align: center; 184 | outline: none; 185 | } 186 | 187 | #select-panel button svg { 188 | opacity: .7; 189 | width: 18px; 190 | height: 18px; 191 | vertical-align: middle; 192 | } 193 | 194 | #select-panel .text-section .style, 195 | #select-panel .text-section .family, 196 | #select-panel .text-section .sizes, 197 | #select-panel .text-section .align, 198 | #select-panel .text-section .color { 199 | margin-bottom: 20px; 200 | } 201 | 202 | #select-panel .text-section .sizes input { 203 | width: 50px; 204 | } 205 | 206 | .toolpanel#select-panel .text-section, 207 | .toolpanel#select-panel .effect-section { 208 | display: none; 209 | } 210 | 211 | .toolpanel#select-panel.type-group .border-section { 212 | display: none; 213 | } 214 | 215 | .toolpanel#select-panel.type-group .fill-section { 216 | display: none; 217 | } 218 | 219 | .toolpanel#select-panel.type-textbox .text-section { 220 | display: block; 221 | } 222 | 223 | .toolpanel#select-panel.type-textbox .fill-section { 224 | display: none; 225 | } 226 | 227 | .toolpanel#select-panel.type-image .effect-section { 228 | display: block; 229 | } 230 | 231 | .toolpanel#select-panel.type-image .fill-section { 232 | display: none; 233 | } 234 | 235 | .custom-modal-container { 236 | position: absolute; 237 | width: 100%; 238 | height: 100%; 239 | top: 0; 240 | left: 0; 241 | background: #3333; 242 | display: flex; 243 | justify-content: center; 244 | align-items: center; 245 | } 246 | 247 | .custom-modal-content { 248 | background: white; 249 | width: max-content; 250 | padding: 20px; 251 | } 252 | 253 | .custom-modal-content .button-download { 254 | border: 1px solid #ccc; 255 | padding: 10px; 256 | cursor: pointer; 257 | margin: 5px; 258 | border-radius: 3px; 259 | } 260 | 261 | .custom-modal-content .button-download:hover { 262 | background: #ccc; 263 | transition: 0.3s; 264 | } 265 | 266 | .toolpanel .input-container { 267 | display: flex; 268 | align-items: center; 269 | padding-top: 5px; 270 | padding-bottom: 5px; 271 | } 272 | 273 | .toolpanel .input-container label { 274 | width: 50%; 275 | } 276 | 277 | .toolpanel .input-container select { 278 | width: 50%; 279 | height: 29px; 280 | border: 1px solid #ccc; 281 | border-radius: 5px; 282 | outline: none; 283 | } 284 | 285 | .toolpanel .input-container .sp-replacer { 286 | width: 50%; 287 | } 288 | 289 | .toolpanel .input-container .custom-number-input { 290 | background: #ebebeb; 291 | display: flex; 292 | align-items: center; 293 | padding: 1px; 294 | height: 30px; 295 | background-color: #e4e4e4; 296 | border-radius: 6px; 297 | text-align: center; 298 | } 299 | 300 | .toolpanel .input-container .custom-number-input button { 301 | width: 36px !important; 302 | height: 30px !important; 303 | background-color: #fff; 304 | background-clip: padding-box; 305 | border-radius: 6px; 306 | color: #333; 307 | border: 1px solid transparent; 308 | font-size: 16px; 309 | cursor: pointer; 310 | outline: none; 311 | } 312 | 313 | .toolpanel .input-container .custom-number-input input { 314 | height: 30px !important; 315 | width: 60px !important; 316 | background: transparent !important; 317 | border: none; 318 | outline: none; 319 | text-align: center; 320 | } 321 | 322 | .toolpanel .input-container .custom-number-input input::-webkit-outer-spin-button, 323 | .toolpanel .input-container .custom-number-input input::-webkit-inner-spin-button { 324 | -webkit-appearance: none; 325 | margin: 0; 326 | } 327 | 328 | /* Firefox */ 329 | .toolpanel .input-container .custom-number-input input[type=number] { 330 | -moz-appearance: textfield; 331 | } 332 | 333 | .tab-container .tabs { 334 | padding-top: 20px; 335 | padding-bottom: 20px; 336 | display: flex; 337 | justify-content: space-between; 338 | } 339 | 340 | .tab-container .tabs .tab-label { 341 | font-size: 16px; 342 | cursor: pointer; 343 | } 344 | 345 | .tab-container .tabs .tab-label.active { 346 | color: black 347 | } 348 | 349 | .gradient-orientation-container { 350 | padding-top: 40px; 351 | } 352 | 353 | .drag-drop-input { 354 | background-color: #fff; 355 | width: 100%; 356 | box-sizing: border-box; 357 | border: 2px dashed #ccc; 358 | border-radius: 6px; 359 | text-align: center; 360 | padding: 120px; 361 | } 362 | 363 | .drag-drop-input.dragging { 364 | border-color: #4368a9; 365 | } 366 | 367 | #tip-container { 368 | padding: 10px; 369 | text-align: center; 370 | touch-action: none; 371 | cursor: default; 372 | color: #888; 373 | } -------------------------------------------------------------------------------- /lib/core.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The Core of Image Editor 3 | */ 4 | (function () { 5 | 'use strict'; 6 | 7 | /** 8 | * Image Editor class 9 | * @param {String} containerSelector jquery selector for image editor container 10 | * @param {Array} buttons define toolbar buttons 11 | * @param {Array} shapes define shapes 12 | */ 13 | var ImageEditor = function (containerSelector, buttons, shapes) { 14 | this.containerSelector = containerSelector; 15 | this.containerEl = $(containerSelector); 16 | 17 | this.buttons = buttons; 18 | this.shapes = shapes; 19 | 20 | this.containerEl.addClass('default-container'); 21 | 22 | this.canvas = null; 23 | this.activeTool = null; 24 | this.activeSelection = null; 25 | 26 | /** 27 | * Get current state of canvas as object 28 | * @returns {Object} 29 | */ 30 | this.getCanvasJSON = () => { 31 | return this.canvas.toJSON(); 32 | } 33 | 34 | /** 35 | * Set canvas status by object 36 | * @param {Object} current the object of fabric canvas status 37 | */ 38 | this.setCanvasJSON = (current) => { 39 | current && this.canvas.loadFromJSON(JSON.parse(current), this.canvas.renderAll.bind(this.canvas)) 40 | } 41 | 42 | /** 43 | * Event handler to set active tool 44 | * @param {String} id tool id 45 | */ 46 | this.setActiveTool = (id) => { 47 | this.activeTool = id; 48 | $(`${containerSelector} .toolpanel`).removeClass('visible'); 49 | if (id !== 'select' || (id == 'select' && this.activeSelection)) { 50 | $(`${containerSelector} .toolpanel#${id}-panel`).addClass('visible'); 51 | if (id === 'select') { 52 | console.log('selection') 53 | $(`${containerSelector} .toolpanel#${id}-panel`).attr('class', `toolpanel visible type-${this.activeSelection.type}`) 54 | } 55 | } 56 | 57 | if (id !== 'select') { 58 | this.canvas.discardActiveObject(); 59 | this.canvas.renderAll(); 60 | this.activeSelection = null; 61 | } 62 | 63 | this.canvas.isDrawingLineMode = false; 64 | this.canvas.isDrawingPathMode = false; 65 | this.canvas.isDrawingMode = false; 66 | this.canvas.isDrawingTextMode = false; 67 | 68 | this.canvas.defaultCursor = 'default'; 69 | this.canvas.selection = true; 70 | this.canvas.forEachObject(o => { 71 | o.selectable = true; 72 | o.evented = true; 73 | }) 74 | 75 | switch (id) { 76 | case 'draw': 77 | this.canvas.isDrawingMode = true; 78 | break; 79 | case 'line': 80 | this.canvas.isDrawingLineMode = true 81 | this.canvas.defaultCursor = 'crosshair' 82 | this.canvas.selection = false 83 | this.canvas.forEachObject(o => { 84 | o.selectable = false 85 | o.evented = false 86 | }); 87 | break; 88 | case 'path': 89 | this.canvas.isDrawingPathMode = true 90 | this.canvas.defaultCursor = 'crosshair' 91 | this.canvas.selection = false 92 | this.canvas.forEachObject(o => { 93 | o.selectable = false 94 | o.evented = false 95 | }); 96 | this.updateTip('Tip: click to place points, press and pull for curves! Click outside or press Esc to cancel!'); 97 | break; 98 | case 'textbox': 99 | this.canvas.isDrawingTextMode = true 100 | this.canvas.defaultCursor = 'crosshair' 101 | this.canvas.selection = false 102 | this.canvas.forEachObject(o => { 103 | o.selectable = false 104 | o.evented = false 105 | }); 106 | break; 107 | case 'upload': 108 | this.openDragDropPanel(); 109 | break; 110 | default: 111 | this.updateTip('Tip: hold Shift when drawing a line for 15° angle jumps!'); 112 | break; 113 | } 114 | } 115 | 116 | /** 117 | * Event handler when perform undo 118 | */ 119 | this.undo = () => { 120 | console.log('undo') 121 | try { 122 | let undoList = this.history.getValues().undo; 123 | if (undoList.length) { 124 | let current = undoList[undoList.length - 1]; 125 | this.history.undo(); 126 | current && this.canvas.loadFromJSON(JSON.parse(current), this.canvas.renderAll.bind(this.canvas)) 127 | } 128 | } catch (_) { 129 | console.error("undo failed") 130 | } 131 | } 132 | 133 | /** 134 | * Event handler when perform redo 135 | */ 136 | this.redo = () => { 137 | console.log('redo') 138 | try { 139 | let redoList = this.history.getValues().redo; 140 | if (redoList.length) { 141 | let current = redoList[redoList.length - 1]; 142 | this.history.redo(); 143 | current && this.canvas.loadFromJSON(JSON.parse(current), this.canvas.renderAll.bind(this.canvas)) 144 | } 145 | } catch (_) { 146 | console.error("redo failed") 147 | } 148 | } 149 | 150 | /** 151 | * Event handler when select objects on fabric canvas 152 | * @param {Object} activeSelection fabric js object 153 | */ 154 | this.setActiveSelection = (activeSelection) => { 155 | this.activeSelection = activeSelection; 156 | this.setActiveTool('select'); 157 | } 158 | 159 | /** 160 | * Initialize undo/redo stack 161 | */ 162 | this.configUndoRedoStack = () => { 163 | this.history = window.UndoRedoStack(); 164 | const ctrZY = (e) => { 165 | const key = e.which || e.keyCode; 166 | 167 | if (e.ctrlKey && document.querySelectorAll('textarea:focus, input:focus').length === 0) { 168 | if (key === 90) this.undo() 169 | if (key === 89) this.redo() 170 | } 171 | } 172 | document.addEventListener('keydown', ctrZY) 173 | } 174 | 175 | /** 176 | * Initialize zoom events 177 | */ 178 | this.initializeZoomEvents = () => { 179 | this.applyZoom = (zoom) => { 180 | this.canvas.setZoom(zoom) 181 | this.canvas.setWidth(this.canvas.originalW * this.canvas.getZoom()) 182 | this.canvas.setHeight(this.canvas.originalH * this.canvas.getZoom()) 183 | } 184 | 185 | // zoom out/in/reset (ctr + -/+/0) 186 | const keyZoom = (e) => zoomWithKeys(e, this.canvas, this.applyZoom) 187 | document.addEventListener('keydown', keyZoom) 188 | 189 | // zoom out/in with mouse 190 | const mouseZoom = (e) => zoomWithMouse(e, this.canvas, this.applyZoom) 191 | document.addEventListener('wheel', mouseZoom, { 192 | passive: false 193 | }) 194 | } 195 | 196 | /** 197 | * Initialize image editor 198 | */ 199 | this.init = () => { 200 | this.configUndoRedoStack(); 201 | 202 | this.initializeToolbar(); 203 | this.initializeMainPanel(); 204 | 205 | this.initializeShapes(); 206 | 207 | this.initializeFreeDrawSettings(); 208 | this.initializeCanvasSettingPanel(); 209 | this.initializeSelectionSettings(); 210 | 211 | this.canvas = this.initializeCanvas(); 212 | 213 | this.initializeLineDrawing(this.canvas); 214 | this.initializePathDrawing(this.canvas); 215 | this.initializeTextBoxDrawing(this.canvas); 216 | this.initializeUpload(this.canvas); 217 | this.initializeCopyPaste(this.canvas); 218 | this.initializeTipSection(); 219 | 220 | this.initializeZoomEvents(); 221 | 222 | this.extendHideShowToolPanel(); 223 | this.extendNumberInput(); 224 | } 225 | 226 | /** 227 | * Initialize main panel 228 | */ 229 | this.initializeMainPanel = () => { 230 | $(`${containerSelector}`).append('
'); 231 | } 232 | 233 | /** 234 | * Add features to hide/show tool panel 235 | */ 236 | this.extendHideShowToolPanel = () => { 237 | $(`${this.containerSelector} .toolpanel .content`).each(function () { 238 | $(this).append(`
`) 239 | }) 240 | 241 | $(`${this.containerSelector} .toolpanel .content .hide-show-handler`).click(function () { 242 | let panel = $(this).closest('.toolpanel'); 243 | panel.toggleClass('closed'); 244 | }) 245 | } 246 | 247 | /** 248 | * Extend custom number input with increase/decrease button 249 | */ 250 | this.extendNumberInput = () => { 251 | $(`${containerSelector} .decrease`).click(function () { 252 | let input = $(this).closest('.custom-number-input').find('input[type=number]') 253 | let step = input.attr('step'); 254 | if (!step) step = 1; 255 | else { 256 | step = parseFloat(step); 257 | } 258 | let val = parseFloat(input.val()); 259 | input.val((val - step).toFixed(step.countDecimals())); 260 | input.change(); 261 | }) 262 | $(`${containerSelector} .increase`).click(function () { 263 | let input = $(this).closest('.custom-number-input').find('input[type=number]') 264 | let step = input.attr('step'); 265 | if (!step) step = 1; 266 | else { 267 | step = parseFloat(step); 268 | } 269 | let val = parseFloat(input.val()); 270 | input.val((val + step).toFixed(step.countDecimals())); 271 | input.change(); 272 | }) 273 | } 274 | 275 | this.init(); 276 | } 277 | 278 | window.ImageEditor = ImageEditor; 279 | })(); -------------------------------------------------------------------------------- /vendor/grapick.min.js: -------------------------------------------------------------------------------- 1 | /*! grapick - 0.1.7 */ 2 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.Grapick=t():e.Grapick=t()}(this,function(){return function(e){function t(i){if(n[i])return n[i].exports;var r=n[i]={i:i,l:!1,exports:{}};return e[i].call(r.exports,r,r.exports,t),r.l=!0,r.exports}var n={};return t.m=e,t.c=n,t.d=function(e,n,i){t.o(e,n)||Object.defineProperty(e,n,{configurable:!1,enumerable:!0,get:i})},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="",t(t.s=1)}([function(e,t,n){"use strict";function i(e,t,n){t=t.split(/\s+/);for(var i=0;i0&&void 0!==arguments[0]?arguments[0]:{};o(this,t);var n=a(this,(t.__proto__||Object.getPrototypeOf(t)).call(this));e=Object.assign({},e);var i={pfx:"grp",el:".grp",colorEl:"",min:0,max:100,direction:"90deg",type:"linear",height:"30px",width:"100%"};for(var r in i)r in e||(e[r]=i[r]);var l=e.el;if(!((l="string"==typeof l?document.querySelector(l):l)instanceof HTMLElement))throw"Element not found, given "+l;return n.el=l,n.handlers=[],n.options=e,n.on("handler:color:change",function(e,t){return n.change(t)}),n.on("handler:position:change",function(e,t){return n.change(t)}),n.on("handler:remove",function(e){return n.change(1)}),n.on("handler:add",function(e){return n.change(1)}),n.render(),n}return l(t,e),s(t,[{key:"setColorPicker",value:function(e){this.colorPicker=e}},{key:"getValue",value:function(e,t){var n=this.getColorValue(),i=e||this.getType(),r=t||this.getDirection();return n?i+"-gradient("+r+", "+n+")":""}},{key:"getSafeValue",value:function(e,t){var n=this.previewEl,i=this.getValue(e,t);if(!this.sandEl&&(this.sandEl=document.createElement("div")),!n||!i)return"";for(var o=this.sandEl.style,a=[i].concat(r(this.getPrefixedValues(e,t))),l=void 0,s=0;s0&&void 0!==arguments[0]?arguments[0]:"",n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},i=this.type,r=this.direction,o=t.indexOf("(")+1,a=t.lastIndexOf(")"),l=t.substring(o,a),s=l.split(/,(?![^(]*\)) /);if(this.clear(n),!l)return void this.updatePreview();s.length>2&&(r=s.shift());var c=void 0;["repeating-linear","repeating-radial","linear","radial"].forEach(function(e){t.indexOf(g(e))>-1&&!c&&(c=1,i=e)}),this.setDirection(r,n),this.setType(i,n),s.forEach(function(t){var i=t.split(" "),r=parseFloat(i.pop()),o=i.join("");e.addHandler(r,o,0,n)}),this.updatePreview()}},{key:"getColorValue",value:function(){var e=this.handlers;return e.sort(v),e=1==e.length?[e[0],e[0]]:e,e.map(function(e){return e.getValue()}).join(", ")}},{key:"getPrefixedValues",value:function(e,t){var n=this.getValue(e,t);return["-moz-","-webkit-","-o-","-ms-"].map(function(e){return""+e+n})}},{key:"change",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:1,t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};this.updatePreview(),!t.silent&&this.emit("change",e)}},{key:"setDirection",value:function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};this.options.direction=e,this.change(1,t)}},{key:"getDirection",value:function(){return this.options.direction}},{key:"setType",value:function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};this.options.type=e,this.change(1,t)}},{key:"getType",value:function(){return this.options.type}},{key:"addHandler",value:function(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:1,i=arguments.length>3&&void 0!==arguments[3]?arguments[3]:{},r=new d.default(this,e,t,n);return!i.silent&&this.emit("handler:add",r),r}},{key:"getHandler",value:function(e){return this.handlers[e]}},{key:"getHandlers",value:function(){return this.handlers}},{key:"clear",value:function(){for(var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=this.handlers,n=t.length-1;n>=0;n--)t[n].remove(e)}},{key:"getSelected",value:function(){for(var e=this.getHandlers(),t=0;ti||o\n
\n \n ';var l=t.querySelector("."+o),s=t.querySelector("."+a),c=l.style;c.position="relative",this.wrapperEl=l,this.previewEl=s,i&&(c.height=i),r&&(c.width=r),this.initEvents(),this.updatePreview()}}}]),t}(u.default);t.default=p},function(e,t){function n(){}n.prototype={on:function(e,t,n){var i=this.e||(this.e={});return(i[e]||(i[e]=[])).push({fn:t,ctx:n}),this},once:function(e,t,n){function i(){r.off(e,i),t.apply(n,arguments)}var r=this;return i._=t,this.on(e,i,n)},emit:function(e){var t=[].slice.call(arguments,1),n=((this.e||(this.e={}))[e]||[]).slice(),i=0,r=n.length;for(i;i1&&void 0!==arguments[1]?arguments[1]:0,r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"black",o=arguments.length>3&&void 0!==arguments[3]?arguments[3]:1;i(this,e),t.getHandlers().push(this),this.gp=t,this.position=n,this.color=r,this.selected=0,this.render(),o&&this.select()}return r(e,[{key:"toJSON",value:function(){return{position:this.position,selected:this.selected,color:this.color}}},{key:"setColor",value:function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:1;this.color=e,this.emit("handler:color:change",this,t)}},{key:"setPosition",value:function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:1,n=this.getEl();this.position=e,n&&(n.style.left=e+"%"),this.emit("handler:position:change",this,t)}},{key:"getColor",value:function(){return this.color}},{key:"getPosition",value:function(){return this.position}},{key:"isSelected",value:function(){return!!this.selected}},{key:"getValue",value:function(){return this.getColor()+" "+this.getPosition()+"%"}},{key:"select",value:function(){var e=this.getEl();this.gp.getHandlers().forEach(function(e){return e.deselect()}),this.selected=1;var t=this.getSelectedCls();e&&(e.className+=" "+t),this.emit("handler:select",this)}},{key:"deselect",value:function(){var e=this.getEl();this.selected=0;var t=this.getSelectedCls();e&&(e.className=e.className.replace(t,"").trim()),this.emit("handler:deselect",this)}},{key:"getSelectedCls",value:function(){return this.gp.options.pfx+"-handler-selected"}},{key:"remove",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=this.getEl(),n=this.gp.getHandlers(),i=n.splice(n.indexOf(this),1)[0];return t&&t.parentNode.removeChild(t),!e.silent&&this.emit("handler:remove",i),i}},{key:"getEl",value:function(){return this.el}},{key:"initEvents",value:function(){var e=this,t=this.getEl(),n=this.gp.previewEl,i=this.gp.options,r=i.min,a=i.max,l=t.querySelector("[data-toggle=handler-close]"),s=t.querySelector("[data-toggle=handler-color-c]"),c=t.querySelector("[data-toggle=handler-color-wrap]"),u=t.querySelector("[data-toggle=handler-color]"),h=t.querySelector("[data-toggle=handler-drag]");if(s&&(0,o.on)(s,"click",function(e){return e.stopPropagation()}),l&&(0,o.on)(l,"click",function(t){t.stopPropagation(),e.remove()}),u&&(0,o.on)(u,"change",function(t){var n=t.target,i=n.value;e.setColor(i),c&&(c.style.backgroundColor=i)}),h){var d=0,f=0,v=0,g={},p={},y={},m=function(t){v=1,y.x=t.clientX-p.x,y.y=t.clientY-p.y,d=100*y.x,d/=g.w,d=f+d,d=da?a:d,e.setPosition(d,0),e.emit("handler:drag",e,d),0===t.which&&k(t)},k=function t(n){v&&(v=0,e.setPosition(d),(0,o.off)(document,"touchmove mousemove",m),(0,o.off)(document,"touchend mouseup",t),e.emit("handler:drag:end",e,d))},b=function(t){0===t.button&&(e.select(),f=e.position,g.w=n.clientWidth,g.h=n.clientHeight,p.x=t.clientX,p.y=t.clientY,(0,o.on)(document,"touchmove mousemove",m),(0,o.on)(document,"touchend mouseup",k),e.emit("handler:drag:start",e))};(0,o.on)(h,"touchstart mousedown",b),(0,o.on)(h,"click",function(e){return e.stopPropagation()})}}},{key:"emit",value:function(){var e;(e=this.gp).emit.apply(e,arguments)}},{key:"render",value:function(){var e=this.gp,t=e.options,n=e.previewEl,i=e.colorPicker,r=t.pfx,o=t.colorEl,a=this.getColor();if(n){var l=document.createElement("div"),s=l.style,c=r+"-handler";return l.className=c,l.innerHTML='\n
\n
\n
\n
\n
\n '+(o||'\n
\n \n
')+"\n
\n ",s.position="absolute",s.top=0,s.left=this.position+"%",n.appendChild(l),this.el=l,this.initEvents(),i&&i(this),l}}}]),e}();t.default=a}])}); -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Define util functions 3 | */ 4 | 5 | /** 6 | * Get fabric js gradient from colorstops, orientation and angle 7 | * @param {Array} handlers array of color stops 8 | * @param {Number} width gradient width 9 | * @param {Number} height gradient height 10 | * @param {String} orientation orientation type linear/radial 11 | * @param {Number} angle the angle of linear gradient 12 | */ 13 | const generateFabricGradientFromColorStops = (handlers, width, height, orientation, angle) => { 14 | const gradAngleToCoords = (angle) => { 15 | let anglePI = (-parseInt(angle, 10)) * (Math.PI / 180) 16 | let angleCoords = { 17 | 'x1': (Math.round(50 + Math.sin(anglePI) * 50)) / 100, 18 | 'y1': (Math.round(50 + Math.cos(anglePI) * 50)) / 100, 19 | 'x2': (Math.round(50 + Math.sin(anglePI + Math.PI) * 50)) / 100, 20 | 'y2': (Math.round(50 + Math.cos(anglePI + Math.PI) * 50)) / 100, 21 | } 22 | 23 | return angleCoords 24 | } 25 | 26 | let bgGradient = {}; 27 | let colorStops = []; 28 | 29 | for (var i in handlers) { 30 | colorStops.push({ 31 | id: i, 32 | color: handlers[i].color, 33 | offset: handlers[i].position / 100, 34 | }) 35 | } 36 | 37 | if (orientation === 'linear') { 38 | let angleCoords = gradAngleToCoords(angle) 39 | bgGradient = new fabric.Gradient({ 40 | type: 'linear', 41 | coords: { 42 | x1: angleCoords.x1 * width, 43 | y1: angleCoords.y1 * height, 44 | x2: angleCoords.x2 * width, 45 | y2: angleCoords.y2 * height 46 | }, 47 | colorStops, 48 | }) 49 | } else if (orientation === 'radial') { 50 | bgGradient = new fabric.Gradient({ 51 | type: 'radial', 52 | coords: { 53 | x1: width / 2, 54 | y1: height / 2, 55 | r1: 0, 56 | x2: width / 2, 57 | y2: height / 2, 58 | r2: width / 2 59 | }, 60 | colorStops: colorStops 61 | }); 62 | } 63 | 64 | return bgGradient 65 | } 66 | 67 | const getRealBBox = async (obj) => { 68 | 69 | let tempCanv, ctx, w, h; 70 | 71 | // we need to use a temp canvas to get imagedata 72 | const getImageData = (dataUrl) => { 73 | if (tempCanv == null) { 74 | tempCanv = document.createElement('canvas'); 75 | tempCanv.style.border = '1px solid blue'; 76 | tempCanv.style.position = 'absolute'; 77 | tempCanv.style.top = '-100%'; 78 | tempCanv.style.visibility = 'hidden'; 79 | ctx = tempCanv.getContext('2d'); 80 | document.body.appendChild(tempCanv); 81 | } 82 | 83 | return new Promise(function (resolve, reject) { 84 | if (dataUrl == null) return reject(); 85 | 86 | var image = new Image(); 87 | image.addEventListener('load', () => { 88 | w = image.width; 89 | h = image.height; 90 | tempCanv.width = w; 91 | tempCanv.height = h; 92 | ctx.drawImage(image, 0, 0, w, h); 93 | var imageData = ctx.getImageData(0, 0, w, h).data.buffer; 94 | resolve(imageData, false); 95 | }); 96 | image.src = dataUrl; 97 | }); 98 | } 99 | 100 | 101 | // analyze pixels 1-by-1 102 | const scanPixels = (imageData) => { 103 | var data = new Uint32Array(imageData), 104 | x, y, y1, y2, x1 = w, 105 | x2 = 0; 106 | 107 | // y1 108 | for (y = 0; y < h; y++) { 109 | for (x = 0; x < w; x++) { 110 | if (data[y * w + x] & 0xff000000) { 111 | y1 = y; 112 | y = h; 113 | break; 114 | } 115 | } 116 | } 117 | 118 | // y2 119 | for (y = h - 1; y > y1; y--) { 120 | for (x = 0; x < w; x++) { 121 | if (data[y * w + x] & 0xff000000) { 122 | y2 = y; 123 | y = 0; 124 | break; 125 | } 126 | } 127 | } 128 | 129 | // x1 130 | for (y = y1; y < y2; y++) { 131 | for (x = 0; x < w; x++) { 132 | if (x < x1 && data[y * w + x] & 0xff000000) { 133 | x1 = x; 134 | break; 135 | } 136 | } 137 | } 138 | 139 | // x2 140 | for (y = y1; y < y2; y++) { 141 | for (x = w - 1; x > x1; x--) { 142 | if (x > x2 && data[y * w + x] & 0xff000000) { 143 | x2 = x; 144 | break; 145 | } 146 | } 147 | } 148 | 149 | return { 150 | x1: x1, 151 | x2: x2, 152 | y1: y1, 153 | y2: y2, 154 | width: x2 - x1, 155 | height: y2 - y1 156 | } 157 | } 158 | 159 | let data = await getImageData(obj.toDataURL()); 160 | 161 | return scanPixels(data); 162 | 163 | } 164 | 165 | /** 166 | * Align objects on canvas according to the pos 167 | * @param {Object} canvas fabric js canvas 168 | * @param {Array} activeSelection the array of fabric js objects 169 | * @param {String} pos the position to align left/center-h/right/top/center-v/bottom 170 | */ 171 | const alignObject = (canvas, activeSelection, pos) => { 172 | switch (pos) { 173 | case 'left': 174 | 175 | (async () => { 176 | let bound = activeSelection.getBoundingRect() 177 | let realBound = await getRealBBox(activeSelection) 178 | activeSelection.set('left', (activeSelection.left - bound.left - realBound.x1)) 179 | activeSelection.setCoords() 180 | canvas.renderAll() 181 | canvas.trigger('object:modified') 182 | })() 183 | 184 | break 185 | 186 | case 'center-h': 187 | 188 | (async () => { 189 | let bound = activeSelection.getBoundingRect() 190 | let realBound = await getRealBBox(activeSelection) 191 | activeSelection.set( 192 | 'left', 193 | (activeSelection.left - bound.left - realBound.x1) + (canvas.width / 2) - (realBound.width / 2) 194 | ) 195 | activeSelection.setCoords() 196 | canvas.renderAll() 197 | canvas.trigger('object:modified') 198 | })() 199 | 200 | break 201 | 202 | case 'right': 203 | 204 | (async () => { 205 | let bound = activeSelection.getBoundingRect() 206 | let realBound = await getRealBBox(activeSelection) 207 | activeSelection.set('left', (activeSelection.left - bound.left - realBound.x1) + canvas.width - realBound.width) 208 | activeSelection.setCoords() 209 | canvas.renderAll() 210 | canvas.trigger('object:modified') 211 | })() 212 | 213 | break 214 | 215 | case 'top': 216 | 217 | (async () => { 218 | let bound = activeSelection.getBoundingRect() 219 | let realBound = await getRealBBox(activeSelection) 220 | activeSelection.set('top', (activeSelection.top - bound.top - realBound.y1)) 221 | activeSelection.setCoords() 222 | canvas.renderAll() 223 | canvas.trigger('object:modified') 224 | })() 225 | 226 | break 227 | 228 | case 'center-v': 229 | 230 | (async () => { 231 | let bound = activeSelection.getBoundingRect() 232 | let realBound = await getRealBBox(activeSelection) 233 | activeSelection.set( 234 | 'top', 235 | (activeSelection.top - bound.top - realBound.y1) + (canvas.height / 2) - (realBound.height / 2) 236 | ) 237 | activeSelection.setCoords() 238 | canvas.renderAll() 239 | canvas.trigger('object:modified') 240 | })() 241 | 242 | break 243 | 244 | case 'bottom': 245 | 246 | (async () => { 247 | let bound = activeSelection.getBoundingRect() 248 | let realBound = await getRealBBox(activeSelection) 249 | activeSelection.set( 250 | 'top', 251 | (activeSelection.top - bound.top - realBound.y1) + (canvas.height - realBound.height) 252 | ) 253 | activeSelection.setCoords() 254 | canvas.renderAll() 255 | canvas.trigger('object:modified') 256 | })() 257 | 258 | break 259 | 260 | default: 261 | break 262 | } 263 | } 264 | 265 | /** 266 | * Get the filters of current image selection 267 | * @param {Object} activeSelection fabric js object 268 | */ 269 | const getCurrentEffect = (activeSelection) => { 270 | let updatedEffects = { 271 | opacity: 100, 272 | blur: 0, 273 | brightness: 50, 274 | saturation: 50, 275 | gamma: { 276 | r: 45, 277 | g: 45, 278 | b: 45 279 | } 280 | } 281 | 282 | updatedEffects.opacity = activeSelection.opacity * 100 283 | 284 | let hasBlur = activeSelection.filters.find(x => x.blur) 285 | if (hasBlur) { 286 | updatedEffects.blur = hasBlur.blur * 100 287 | } 288 | 289 | let hasBrightness = activeSelection.filters.find(x => x.brightness) 290 | if (hasBrightness) { 291 | updatedEffects.brightness = ((hasBrightness.brightness + 1) / 2) * 100 292 | } 293 | 294 | let hasSaturation = activeSelection.filters.find(x => x.saturation) 295 | if (hasSaturation) { 296 | updatedEffects.saturation = ((hasSaturation.saturation + 1) / 2) * 100 297 | } 298 | 299 | let hasGamma = activeSelection.filters.find(x => x.gamma) 300 | if (hasGamma) { 301 | updatedEffects.gamma.r = Math.round(hasGamma.gamma[0] / 0.022) 302 | updatedEffects.gamma.g = Math.round(hasGamma.gamma[1] / 0.022) 303 | updatedEffects.gamma.b = Math.round(hasGamma.gamma[2] / 0.022) 304 | } 305 | 306 | return updatedEffects; 307 | } 308 | 309 | const getUpdatedFilter = (effects, effect, value) => { 310 | let updatedEffects = { 311 | ...effects 312 | } 313 | switch (effect) { 314 | case 'gamma.r': 315 | updatedEffects.gamma.r = value 316 | break 317 | case 'gamma.g': 318 | updatedEffects.gamma.g = value 319 | break 320 | case 'gamma.b': 321 | updatedEffects.gamma.b = value 322 | break 323 | 324 | default: 325 | updatedEffects[effect] = value 326 | break 327 | } 328 | 329 | effects = updatedEffects; 330 | 331 | // rebuild filter array, calc values for fabric 332 | // blur 0-1 (def val 0), brightness, saturation -1-1 (def val: 0), gamma 0-2.2 (def val: 1) 333 | let updatedFilters = [] 334 | 335 | if (effects.blur > 0) { 336 | updatedFilters.push(new fabric.Image.filters.Blur({ 337 | blur: effects.blur / 100 338 | })); 339 | } 340 | 341 | if (effects.brightness !== 50) { 342 | updatedFilters.push(new fabric.Image.filters.Brightness({ 343 | brightness: ((effects.brightness / 100) * 2) - 1 344 | })); 345 | } 346 | 347 | if (effects.saturation !== 50) { 348 | updatedFilters.push(new fabric.Image.filters.Saturation({ 349 | saturation: ((effects.saturation / 100) * 2) - 1 350 | })); 351 | } 352 | 353 | if ( 354 | effects.gamma.r !== 45 || 355 | effects.gamma.g !== 45 || 356 | effects.gamma.b !== 45 357 | ) { 358 | updatedFilters.push(new fabric.Image.filters.Gamma({ 359 | gamma: [ 360 | Math.round((effects.gamma.r * 0.022) * 10) / 10, 361 | Math.round((effects.gamma.g * 0.022) * 10) / 10, 362 | Math.round((effects.gamma.b * 0.022) * 10) / 10 363 | ] 364 | })); 365 | } 366 | 367 | return updatedFilters; 368 | } 369 | 370 | const getActiveFontStyle = (activeSelection, styleName) => { 371 | if (activeSelection.getSelectionStyles && activeSelection.isEditing) { 372 | let styles = activeSelection.getSelectionStyles() 373 | if (styles.find(o => o[styleName] === '')) { 374 | return '' 375 | } 376 | 377 | return styles[0][styleName] 378 | } 379 | 380 | return activeSelection[styleName] || '' 381 | } 382 | 383 | 384 | const setActiveFontStyle = (activeSelection, styleName, value) => { 385 | if (activeSelection.setSelectionStyles && activeSelection.isEditing) { 386 | let style = {} 387 | style[styleName] = value; 388 | activeSelection.setSelectionStyles(style) 389 | activeSelection.setCoords() 390 | } else { 391 | activeSelection.set(styleName, value) 392 | } 393 | } 394 | 395 | const downloadImage = (data, extension = 'png', mimeType = 'image/png') => { 396 | const imageData = data.toString().replace(/^data:image\/(png|jpeg|jpg);base64,/, ''); 397 | const byteCharacters = atob(imageData); 398 | const byteNumbers = new Array(byteCharacters.length); 399 | for (let i = 0; i < byteCharacters.length; i += 1) { 400 | byteNumbers[i] = byteCharacters.charCodeAt(i); 401 | } 402 | const byteArray = new Uint8Array(byteNumbers); 403 | const file = new Blob([byteArray], { 404 | type: mimeType + ';base64' 405 | }); 406 | const fileURL = window.URL.createObjectURL(file); 407 | 408 | // IE doesn't allow using a blob object directly as link href 409 | // instead it is necessary to use msSaveOrOpenBlob 410 | if (window.navigator && window.navigator.msSaveOrOpenBlob) { 411 | window.navigator.msSaveOrOpenBlob(file); 412 | return; 413 | } 414 | const link = document.createElement('a'); 415 | link.href = fileURL; 416 | link.download = 'image.' + extension; 417 | link.dispatchEvent(new MouseEvent('click')); 418 | setTimeout(() => { 419 | // for Firefox it is necessary to delay revoking the ObjectURL 420 | window.URL.revokeObjectURL(fileURL); 421 | }, 60); 422 | } 423 | 424 | 425 | const downloadSVG = (SVGmarkup) => { 426 | const url = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(SVGmarkup); 427 | 428 | const link = document.createElement('a'); 429 | link.href = url; 430 | link.download = 'image.svg'; 431 | link.dispatchEvent(new MouseEvent('click')); 432 | setTimeout(() => { 433 | // for Firefox it is necessary to delay revoking the ObjectURL 434 | window.URL.revokeObjectURL(url); 435 | }, 60); 436 | } -------------------------------------------------------------------------------- /lib/shapes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Define action to add shape to canvas 3 | */ 4 | (function () { 5 | 'use strict'; 6 | const defaultShapes = [ 7 | ``, 8 | ``, 9 | ``, 10 | ``, 11 | ``, 12 | ``, 13 | ``, 14 | ``, 15 | ``, 16 | ``, 17 | ``, 18 | ``, 19 | ``, 20 | ``, 21 | ``, 22 | ``, 23 | ``, 24 | ``, 25 | ``, 26 | ``, 27 | ``, 28 | ``, 29 | ``, 30 | ``, 31 | ``, 32 | ``, 33 | ``, 34 | ``, 35 | ``, 36 | ``, 37 | ``, 38 | ``, 39 | ``, 40 | ``, 41 | `` 42 | ] 43 | 44 | var shapes = function () { 45 | const _self = this; 46 | 47 | let ShapeList = defaultShapes; 48 | if (Array.isArray(this.shapes) && this.shapes.length) ShapeList = this.shapes; 49 | $(`${this.containerSelector} .main-panel`).append(`

Shapes

`); 50 | 51 | ShapeList.forEach(svg => { 52 | $(`${this.containerSelector} .toolpanel#shapes-panel .content`).append(`
${svg}
`) 53 | }) 54 | 55 | $(`${this.containerSelector} .toolpanel#shapes-panel .content .button`).click(function () { 56 | let svg = $(this).html(); 57 | 58 | try { 59 | fabric.loadSVGFromString( 60 | svg, 61 | (objects, options) => { 62 | var obj = fabric.util.groupSVGElements(objects, options) 63 | obj.strokeUniform = true 64 | obj.strokeLineJoin = 'miter' 65 | obj.scaleToWidth(100) 66 | obj.scaleToHeight(100) 67 | obj.set({ 68 | left: 0, 69 | top: 0 70 | }) 71 | _self.canvas.add(obj).renderAll() 72 | _self.canvas.trigger('object:modified') 73 | } 74 | ) 75 | } catch (_) { 76 | console.error("can't add shape"); 77 | } 78 | }) 79 | } 80 | 81 | window.ImageEditor.prototype.initializeShapes = shapes; 82 | })(); -------------------------------------------------------------------------------- /lib/toolbar.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Initialize toolbar 3 | */ 4 | (function () { 5 | 'use strict'; 6 | var defaultButtons = [{ 7 | name: 'select', 8 | title: 'Select/move object (V)', 9 | icon: `` 10 | }, { 11 | name: 'shapes', 12 | title: 'Shapes', 13 | icon: `` 14 | }, { 15 | name: 'draw', 16 | title: 'Free draw', 17 | icon: `` 18 | }, { 19 | name: 'line', 20 | title: 'Line', 21 | icon: `` 22 | }, { 23 | name: 'path', 24 | title: 'Connectable lines & curves', 25 | icon: '' 26 | }, { 27 | name: 'textbox', 28 | title: 'Text box', 29 | icon: `` 30 | }, { 31 | name: 'upload', 32 | title: 'Upload image', 33 | icon: `` 34 | }, { 35 | name: 'background', 36 | title: 'Canvas option', 37 | icon: `` 38 | }] 39 | 40 | const defaultExtendedButtons = [{ 41 | name: 'undo', 42 | title: 'Undo', 43 | icon: `` 44 | }, { 45 | name: 'redo', 46 | title: 'Redo', 47 | icon: `` 48 | }, { 49 | name: 'save', 50 | title: 'Save', 51 | icon: `` 52 | }, { 53 | name: 'download', 54 | title: 'Download', 55 | icon: `` 56 | }, { 57 | name: 'clear', 58 | title: 'Clear', 59 | icon: `` 60 | }] 61 | 62 | var toolbar = function () { 63 | const _self = this; 64 | let buttons = []; 65 | let extendedButtons = []; 66 | if (Array.isArray(this.buttons) && this.buttons.length) { 67 | defaultButtons.forEach(item => { 68 | if (this.buttons.includes(item.name)) buttons.push(item); 69 | }); 70 | defaultExtendedButtons.forEach(item => { 71 | if (this.buttons.includes(item.name)) extendedButtons.push(item); 72 | }) 73 | } else { 74 | buttons = defaultButtons; 75 | extendedButtons = defaultExtendedButtons; 76 | } 77 | 78 | try { 79 | this.containerEl.append(`
`); 80 | 81 | // main buttons 82 | (() => { 83 | buttons.forEach(item => { 84 | $(`${this.containerSelector} #toolbar .main-buttons`).append(``); 85 | }) 86 | 87 | $(`${this.containerSelector} #toolbar .main-buttons button`).click(function () { 88 | let id = $(this).attr('id'); 89 | 90 | $(`${_self.containerSelector} #toolbar button`).removeClass('active'); 91 | $(`${_self.containerSelector} #toolbar button#${id}`).addClass('active'); 92 | _self.setActiveTool(id); 93 | }) 94 | })(); 95 | 96 | // zoom 97 | (() => { 98 | let currentZoomLevel = 1; 99 | $(`${this.containerSelector}`).append( 100 | `
` 101 | ) 102 | $(`${this.containerSelector} .floating-zoom-level-container`).append(` 103 | 104 | 109 | `); 110 | $(`${this.containerSelector} .floating-zoom-level-container #input-zoom-level`).change(function () { 111 | let zoom = parseFloat($(this).val()); 112 | typeof _self.applyZoom === 'function' && _self.applyZoom(zoom) 113 | }) 114 | })(); 115 | // extended buttons 116 | (() => { 117 | extendedButtons.forEach(item => { 118 | $(`${this.containerSelector} #toolbar .extended-buttons`).append(``); 119 | }) 120 | 121 | $(`${this.containerSelector} #toolbar .extended-buttons button`).click(function () { 122 | let id = $(this).attr('id'); 123 | if (id === 'save') { 124 | if (window.confirm('The current canvas will be saved in your local! Are you sure?')) { 125 | saveInBrowser.save('canvasEditor', _self.canvas.toJSON()); 126 | } 127 | } else if (id === 'clear') { 128 | if (window.confirm('This will clear the canvas! Are you sure?')) { 129 | _self.canvas.clear(), saveInBrowser.remove('canvasEditor'); 130 | } 131 | } else if (id === 'download') { 132 | $('body').append(`
133 |
134 |
Download as SVG
135 |
Download as PNG
136 |
Download as JPG
137 |
138 |
`) 139 | 140 | $(".custom-modal-container").click(function () { 141 | $(this).remove(); 142 | }) 143 | 144 | $(".custom-modal-container .button-download").click(function (e) { 145 | let type = $(this).attr('id'); 146 | if (type === 'svg') downloadSVG(_self.canvas.toSVG()); 147 | else if (type === 'png') downloadImage(_self.canvas.toDataURL()) 148 | else if (type === 'jpg') downloadImage(_self.canvas.toDataURL({ 149 | format: 'jpeg' 150 | }), 'jpg', 'image/jpeg'); 151 | }) 152 | 153 | } else if (id === 'undo') _self.undo(); 154 | else if (id === 'redo') _self.redo(); 155 | }) 156 | })() 157 | } catch (_) { 158 | console.error("can't create toolbar"); 159 | } 160 | } 161 | 162 | window.ImageEditor.prototype.initializeToolbar = toolbar; 163 | })(); -------------------------------------------------------------------------------- /lib/selectionSettings.js: -------------------------------------------------------------------------------- 1 | /** 2 | * initialize selection setting panel 3 | */ 4 | (function () { 5 | 'use strict'; 6 | const BorderStyleList = [{ 7 | value: { 8 | strokeDashArray: [], 9 | strokeLineCap: 'butt' 10 | }, 11 | label: "Stroke" 12 | }, { 13 | value: { 14 | strokeDashArray: [1, 10], 15 | strokeLineCap: 'butt' 16 | }, 17 | label: 'Dash-1' 18 | }, { 19 | value: { 20 | strokeDashArray: [1, 10], 21 | strokeLineCap: 'round' 22 | }, 23 | label: 'Dash-2' 24 | }, { 25 | value: { 26 | strokeDashArray: [15, 15], 27 | strokeLineCap: 'square' 28 | }, 29 | label: 'Dash-3' 30 | }, { 31 | value: { 32 | strokeDashArray: [15, 15], 33 | strokeLineCap: 'round' 34 | }, 35 | label: 'Dash-4' 36 | }, { 37 | value: { 38 | strokeDashArray: [25, 25], 39 | strokeLineCap: 'square' 40 | }, 41 | label: 'Dash-5', 42 | }, { 43 | value: { 44 | strokeDashArray: [25, 25], 45 | strokeLineCap: 'round' 46 | }, 47 | label: 'Dash-6', 48 | }, { 49 | value: { 50 | strokeDashArray: [1, 8, 16, 8, 1, 20], 51 | strokeLineCap: 'square' 52 | }, 53 | label: 'Dash-7', 54 | }, { 55 | value: { 56 | strokeDashArray: [1, 8, 16, 8, 1, 20], 57 | strokeLineCap: 'round' 58 | }, 59 | label: 'Dash-8', 60 | }] 61 | const AlignmentButtonList = [{ 62 | pos: 'left', 63 | icon: `` 64 | }, { 65 | pos: 'center-h', 66 | icon: ``, 67 | }, { 68 | pos: 'right', 69 | icon: ``, 70 | }, { 71 | pos: 'top', 72 | icon: ``, 73 | }, { 74 | pos: 'center-v', 75 | icon: `` 76 | }, { 77 | pos: 'bottom', 78 | icon: `` 79 | }] 80 | var selectionSettings = function () { 81 | const _self = this; 82 | $(`${this.containerSelector} .main-panel`).append(`

Selection Settings

`); 83 | 84 | // font section 85 | (() => { 86 | $(`${this.containerSelector} .toolpanel#select-panel .content`).append(` 87 |
88 |

Font Style

89 |
90 | 91 | 92 | 93 | 94 | 95 | 96 |
97 |
98 |
99 | 100 | 114 |
115 |
116 |
117 |
118 |
119 | 120 | 121 | 122 |
123 |
124 |
125 |
126 | 127 | 128 | 129 |
130 |
131 |
132 |
133 | 134 | 135 | 136 |
137 |
138 |

139 |
140 |
141 |
142 | 143 | 149 |
150 |
151 |
152 |
153 | 154 | 155 |
156 |
157 |
158 |
159 | `); 160 | $(`${this.containerSelector} .toolpanel#select-panel .style button`).click(function () { 161 | let type = $(this).attr('id'); 162 | switch (type) { 163 | case 'bold': 164 | setActiveFontStyle(_self.activeSelection, 'fontWeight', getActiveFontStyle(_self.activeSelection, 'fontWeight') === 'bold' ? '' : 'bold') 165 | break; 166 | case 'italic': 167 | setActiveFontStyle(_self.activeSelection, 'fontStyle', getActiveFontStyle(_self.activeSelection, 'fontStyle') === 'italic' ? '' : 'italic') 168 | break; 169 | case 'underline': 170 | setActiveFontStyle(_self.activeSelection, 'underline', !getActiveFontStyle(_self.activeSelection, 'underline')) 171 | break; 172 | case 'linethrough': 173 | setActiveFontStyle(_self.activeSelection, 'linethrough', !getActiveFontStyle(_self.activeSelection, 'linethrough')) 174 | break; 175 | case 'subscript': 176 | if (getActiveFontStyle(_self.activeSelection, 'deltaY') > 0) { 177 | setActiveFontStyle(_self.activeSelection, 'fontSize', undefined) 178 | setActiveFontStyle(_self.activeSelection, 'deltaY', undefined) 179 | } else { 180 | _self.activeSelection.setSubscript() 181 | _self.canvas.renderAll() 182 | } 183 | break; 184 | case 'superscript': 185 | if (getActiveFontStyle(_self.activeSelection, 'deltaY') < 0) { 186 | setActiveFontStyle(_self.activeSelection, 'fontSize', undefined) 187 | setActiveFontStyle(_self.activeSelection, 'deltaY', undefined) 188 | } else { 189 | _self.activeSelection.setSuperscript() 190 | _self.canvas.renderAll() 191 | } 192 | break; 193 | default: 194 | break; 195 | } 196 | _self.canvas.renderAll(), _self.canvas.trigger('object:modified'); 197 | }) 198 | 199 | $(`${this.containerSelector} .toolpanel#select-panel .family #font-family`).change(function () { 200 | let family = $(this).val(); 201 | setActiveFontStyle(_self.activeSelection, 'fontFamily', family) 202 | _self.canvas.renderAll(), _self.canvas.trigger('object:modified'); 203 | }) 204 | 205 | $(`${this.containerSelector} .toolpanel#select-panel .sizes input`).change(function () { 206 | let value = parseFloat($(this).val()); 207 | let type = $(this).attr('id'); 208 | setActiveFontStyle(_self.activeSelection, type, value); 209 | _self.canvas.renderAll(), _self.canvas.trigger('object:modified'); 210 | }) 211 | 212 | $(`${this.containerSelector} .toolpanel#select-panel .align #text-align`).change(function () { 213 | let mode = $(this).val(); 214 | setActiveFontStyle(_self.activeSelection, 'textAlign', mode); 215 | _self.canvas.renderAll(), _self.canvas.trigger('object:modified'); 216 | }) 217 | 218 | $(`${this.containerSelector} .toolpanel#select-panel .color #color-picker`).spectrum({ 219 | type: "color", 220 | showInput: "true", 221 | allowEmpty: "false" 222 | }); 223 | 224 | $(`${this.containerSelector} .toolpanel#select-panel .color #color-picker`).change(function () { 225 | let color = $(this).val(); 226 | setActiveFontStyle(_self.activeSelection, 'fill', color) 227 | _self.canvas.renderAll(), _self.canvas.trigger('object:modified'); 228 | }) 229 | })(); 230 | // end font section 231 | 232 | // border section 233 | (() => { 234 | $(`${this.containerSelector} .toolpanel#select-panel .content`).append(` 235 |
236 |

Border

237 |
238 |
239 | 240 | 241 | 242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 | `); 250 | 251 | $(`${this.containerSelector} .toolpanel#select-panel .border-section #color-picker`).spectrum({ 252 | showButtons: false, 253 | type: "color", 254 | showInput: "true", 255 | allowEmpty: "false", 256 | move: function (color) { 257 | let hex = 'transparent'; 258 | color && (hex = color.toRgbString()); // #ff0000 259 | _self.canvas.getActiveObjects().forEach(obj => obj.set('stroke', hex)) 260 | _self.canvas.renderAll(), _self.canvas.trigger('object:modified') 261 | } 262 | }); 263 | 264 | $(`${this.containerSelector} .toolpanel#select-panel .border-section #input-border-width`).change(function () { 265 | let width = parseInt($(this).val()); 266 | _self.canvas.getActiveObjects().forEach(obj => obj.set({ 267 | strokeUniform: true, 268 | strokeWidth: width 269 | })) 270 | _self.canvas.renderAll(), _self.canvas.trigger('object:modified') 271 | }) 272 | 273 | $(`${this.containerSelector} .toolpanel#select-panel .border-section #input-border-style`).change(function () { 274 | try { 275 | let style = JSON.parse($(this).val()); 276 | _self.canvas.getActiveObjects().forEach(obj => obj.set({ 277 | strokeUniform: true, 278 | strokeDashArray: style.strokeDashArray, 279 | strokeLineCap: style.strokeLineCap 280 | })) 281 | _self.canvas.renderAll(), _self.canvas.trigger('object:modified') 282 | } catch (_) {} 283 | }) 284 | 285 | $(`${this.containerSelector} .toolpanel#select-panel .border-section #input-corner-type`).change(function () { 286 | let corner = $(this).val(); 287 | _self.canvas.getActiveObjects().forEach(obj => obj.set('strokeLineJoin', corner)) 288 | _self.canvas.renderAll(), _self.canvas.trigger('object:modified') 289 | }) 290 | })(); 291 | // end border section 292 | 293 | // fill color section 294 | (() => { 295 | $(`${this.containerSelector} .toolpanel#select-panel .content`).append(` 296 |
297 |
298 |
299 |
Color Fill
300 |
Gradient Fill
301 |
302 |
303 |
304 |
305 |
306 |
307 |
308 |
309 | 310 | 314 |
315 |
316 | 317 |
318 | 319 | 320 | 321 |
322 |
323 |
324 |
325 |
326 |
327 | `); 328 | 329 | $(`${this.containerSelector} .toolpanel#select-panel .content .tab-label`).click(function () { 330 | $(`${_self.containerSelector} .toolpanel#select-panel .content .tab-label`).removeClass('active'); 331 | $(this).addClass('active'); 332 | let target = $(this).data('value'); 333 | $(this).closest('.tab-container').find('.tab-content').hide(); 334 | $(this).closest('.tab-container').find(`.tab-content[data-value=${target}]`).show(); 335 | if (target === 'color-fill') { 336 | let color = $(`${_self.containerSelector} .toolpanel#select-panel .fill-section #color-picker`).val(); 337 | try { 338 | _self.canvas.getActiveObjects().forEach(obj => obj.set('fill', color)) 339 | _self.canvas.renderAll(), _self.canvas.trigger('object:modified') 340 | } catch (_) { 341 | console.log("can't update background color") 342 | } 343 | } else { 344 | updateGradientFill(); 345 | } 346 | }) 347 | 348 | $(`${_self.containerSelector} .toolpanel#select-panel .content .tab-label[data-value=color-fill]`).click(); 349 | 350 | $(`${this.containerSelector} .toolpanel#select-panel .fill-section #color-picker`).spectrum({ 351 | flat: true, 352 | showPalette: false, 353 | showButtons: false, 354 | type: "color", 355 | showInput: "true", 356 | allowEmpty: "false", 357 | move: function (color) { 358 | let hex = 'transparent'; 359 | color && (hex = color.toRgbString()); // #ff0000 360 | _self.canvas.getActiveObjects().forEach(obj => obj.set('fill', hex)) 361 | _self.canvas.renderAll(), _self.canvas.trigger('object:modified') 362 | } 363 | }); 364 | 365 | const gp = new Grapick({ 366 | el: `${this.containerSelector} .toolpanel#select-panel .fill-section #gradient-picker`, 367 | colorEl: '' 368 | }); 369 | 370 | gp.setColorPicker(handler => { 371 | const el = handler.getEl().querySelector('#colorpicker'); 372 | $(el).spectrum({ 373 | showPalette: false, 374 | showButtons: false, 375 | type: "color", 376 | color: handler.getColor(), 377 | showAlpha: true, 378 | change(color) { 379 | handler.setColor(color.toRgbString()); 380 | }, 381 | move(color) { 382 | handler.setColor(color.toRgbString(), 0); 383 | } 384 | }); 385 | }); 386 | gp.addHandler(0, 'red'); 387 | gp.addHandler(100, 'blue'); 388 | 389 | const updateGradientFill = () => { 390 | let stops = gp.getHandlers(); 391 | let orientation = $(`${this.containerSelector} .toolpanel#select-panel .content .gradient-orientation-container #select-orientation`).val(); 392 | let angle = parseInt($(`${this.containerSelector} .toolpanel#select-panel .content .gradient-orientation-container #input-angle`).val()); 393 | 394 | let gradient = generateFabricGradientFromColorStops(stops, _self.activeSelection.width, _self.activeSelection.height, orientation, angle); 395 | _self.activeSelection.set('fill', gradient); 396 | _self.canvas.renderAll() 397 | } 398 | 399 | gp.on('change', complete => { 400 | updateGradientFill(); 401 | }) 402 | 403 | $(`${this.containerSelector} .toolpanel#select-panel .content .gradient-orientation-container #select-orientation`).change(function () { 404 | let type = $(this).val(); 405 | console.log('orientation', type) 406 | if (type === 'radial') { 407 | $(this).closest('.gradient-orientation-container').find('#angle-input-container').hide(); 408 | } else { 409 | $(this).closest('.gradient-orientation-container').find('#angle-input-container').show(); 410 | } 411 | updateGradientFill(); 412 | }) 413 | 414 | $(`${this.containerSelector} .toolpanel#select-panel .content .gradient-orientation-container #input-angle`).change(function () { 415 | updateGradientFill(); 416 | }) 417 | 418 | })(); 419 | // end fill color section 420 | 421 | // alignment section 422 | (() => { 423 | let buttons = ``; 424 | AlignmentButtonList.forEach(item => { 425 | buttons += `` 426 | }) 427 | $(`${this.containerSelector} .toolpanel#select-panel .content`).append(` 428 |
429 |

Alignment

430 | ${buttons} 431 |
432 |
433 | `); 434 | 435 | $(`${this.containerSelector} .toolpanel#select-panel .alignment-section button`).click(function () { 436 | let pos = $(this).data('pos'); 437 | alignObject(_self.canvas, _self.activeSelection, pos); 438 | }) 439 | })(); 440 | // end alignment section 441 | 442 | // object options section 443 | (() => { 444 | $(`${this.containerSelector} .toolpanel#select-panel .content`).append(` 445 |
446 |

Object Options

447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 |
456 |
457 | `); 458 | 459 | $(`${this.containerSelector} .toolpanel#select-panel .object-options #flip-h`).click(() => { 460 | this.activeSelection.set('flipX', !this.activeSelection.flipX); 461 | this.canvas.renderAll(), this.canvas.trigger('object:modified'); 462 | }) 463 | $(`${this.containerSelector} .toolpanel#select-panel .object-options #flip-v`).click(() => { 464 | this.activeSelection.set('flipY', !this.activeSelection.flipY); 465 | this.canvas.renderAll(), this.canvas.trigger('object:modified'); 466 | }) 467 | $(`${this.containerSelector} .toolpanel#select-panel .object-options #bring-fwd`).click(() => { 468 | this.canvas.bringForward(this.activeSelection) 469 | this.canvas.renderAll(), this.canvas.trigger('object:modified'); 470 | }) 471 | $(`${this.containerSelector} .toolpanel#select-panel .object-options #bring-back`).click(() => { 472 | this.canvas.sendBackwards(this.activeSelection) 473 | this.canvas.renderAll(), this.canvas.trigger('object:modified'); 474 | }) 475 | $(`${this.containerSelector} .toolpanel#select-panel .object-options #duplicate`).click(() => { 476 | let clonedObjects = [] 477 | let activeObjects = this.canvas.getActiveObjects() 478 | activeObjects.forEach(obj => { 479 | obj.clone(clone => { 480 | this.canvas.add(clone.set({ 481 | strokeUniform: true, 482 | left: obj.aCoords.tl.x + 20, 483 | top: obj.aCoords.tl.y + 20 484 | })); 485 | 486 | if (activeObjects.length === 1) { 487 | this.canvas.setActiveObject(clone) 488 | } 489 | clonedObjects.push(clone) 490 | }) 491 | }) 492 | 493 | if (clonedObjects.length > 1) { 494 | let sel = new fabric.ActiveSelection(clonedObjects, { 495 | canvas: this.canvas, 496 | }); 497 | this.canvas.setActiveObject(sel) 498 | } 499 | 500 | this.canvas.requestRenderAll(), this.canvas.trigger('object:modified') 501 | }) 502 | $(`${this.containerSelector} .toolpanel#select-panel .object-options #delete`).click(() => { 503 | this.canvas.getActiveObjects().forEach(obj => this.canvas.remove(obj)) 504 | this.canvas.discardActiveObject().requestRenderAll(), this.canvas.trigger('object:modified'); 505 | }) 506 | $(`${this.containerSelector} .toolpanel#select-panel .object-options #group`).click(() => { 507 | if (this.activeSelection.type !== 'activeSelection') return; 508 | this.canvas.getActiveObject().toGroup() 509 | this.canvas.requestRenderAll(), this.canvas.trigger('object:modified') 510 | }) 511 | $(`${this.containerSelector} .toolpanel#select-panel .object-options #ungroup`).click(() => { 512 | if (this.activeSelection.type !== 'group') return; 513 | this.canvas.getActiveObject().toActiveSelection() 514 | this.canvas.requestRenderAll(), this.canvas.trigger('object:modified'); 515 | }) 516 | })(); 517 | // end object options section 518 | 519 | // effect section 520 | (() => { 521 | $(`${this.containerSelector} .toolpanel#select-panel .content`).append(` 522 |
523 |

Effect

524 |
525 |
526 |
527 |
528 |
Gamma
529 |
530 |
531 |
532 |
533 |
534 | `); 535 | 536 | $(`${this.containerSelector} .toolpanel#select-panel .effect-section #opacity`).change(function () { 537 | let opacity = parseFloat($(this).val()); 538 | _self.activeSelection.set('opacity', opacity) 539 | _self.canvas.renderAll(), _self.canvas.trigger('object:modified') 540 | }) 541 | 542 | $(`${this.containerSelector} .toolpanel#select-panel .effect-section .effect`).change(function () { 543 | let effect = $(this).attr('id'); 544 | let value = parseFloat($(this).val()); 545 | let currentEffect = getCurrentEffect(_self.activeSelection); 546 | _self.activeSelection.filters = getUpdatedFilter(currentEffect, effect, value); 547 | _self.activeSelection.applyFilters(); 548 | _self.canvas.renderAll(), _self.canvas.trigger('object:modified') 549 | }) 550 | })(); 551 | // end effect section 552 | } 553 | 554 | window.ImageEditor.prototype.initializeSelectionSettings = selectionSettings; 555 | })() --------------------------------------------------------------------------------