├── 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(``);
14 |
15 | // set dimension section
16 | $(`${this.containerSelector} .toolpanel#draw-panel .content`).append(`
17 |
18 |
26 |
27 | Brush Type
28 |
29 | Pencil
30 | Circle
31 | Spray
32 |
33 |
34 |
35 | Brush Color
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 | 
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(``)
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(``);
9 |
10 | // set dimension section
11 | (() => {
12 | $(`${this.containerSelector} .toolpanel#background-panel .content`).append(`
13 |
14 |
Canvas Size
15 |
23 |
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 | Orientation
70 |
71 | Linear
72 | Radial
73 |
74 |
75 |
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 '+(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(``);
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(`${item.icon} `);
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 | Zoom
104 |
105 | ${[0.05, 0.1, 0.25, 0.5, 0.75, 1, 1.5, 2, 2.5, 3].map((item =>
106 | `${item*100}% `
107 | ))}
108 |
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(`${item.icon} `);
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(``);
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 | Font Family
100 |
101 |
102 | Open Sans
103 | Oswald
104 | Playfair Display
105 | Cormorant Garamond
106 | Impact
107 | Lucida Console
108 | Comic Sans
109 | Dancing Script
110 | Indie Flower
111 | Amatic SC
112 | Permanent Marker
113 |
114 |
115 |
116 |
117 |
124 |
131 |
138 |
139 |
140 |
141 |
142 | Text Alignment
143 |
144 | Left
145 | Center
146 | Right
147 | Justify
148 |
149 |
150 |
151 |
152 |
153 | Text Color
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 |
244 |
Style ${BorderStyleList.map(item => `${item.label} `)}
245 |
Corner Type Square Round
246 |
Color
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 | Orientation
310 |
311 | Linear
312 | Radial
313 |
314 |
315 |
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 += `${item.icon} `
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 |
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 | })()
--------------------------------------------------------------------------------