diff --git a/src/public/app/widgets/type_widgets/canvas-note-utils/EraserBrush.js b/src/public/app/widgets/type_widgets/canvas-note-utils/EraserBrush.js index 1e966788f..b934c092a 100644 --- a/src/public/app/widgets/type_widgets/canvas-note-utils/EraserBrush.js +++ b/src/public/app/widgets/type_widgets/canvas-note-utils/EraserBrush.js @@ -1,128 +1,150 @@ +import _cloneDeep from './lib/lodash.cloneDeep.js'; + const EraserBrushFactory = (fabric) => { + /** + * ErasedGroup, part of EraserBrush + * + * Made it so that the bound is calculated on the original only + * + * Note: Might not work with versions other than 3.1.0 / 4.0.0 since it uses some + * fabric.js overwriting + * + * Source: https://github.com/fabricjs/fabric.js/issues/1225#issuecomment-499620550 + */ + const ErasedGroup = fabric.util.createClass(fabric.Group, { + original: null, + erasedPath: null, + initialize: function (original, erasedPath, options, isAlreadyGrouped) { + this.original = original; + this.erasedPath = erasedPath; + this.callSuper( + 'initialize', + [this.original, this.erasedPath], + options, + isAlreadyGrouped, + ); + }, + _calcBounds: function (onlyWidthHeight) { + const aX = [], + aY = [], + props = ['tr', 'br', 'bl', 'tl'], + jLen = props.length, + ignoreZoom = true; + + let o = this.original; + o.setCoords(ignoreZoom); + for (let j = 0; j < jLen; j++) { + const prop = props[j]; + aX.push(o.aCoords[prop].x); // when using dev-fabric js, we need aCoords, in minified oCoords + aY.push(o.aCoords[prop].y); // when using dev-fabric js, we need aCoords, in minified oCoords + } + + console.log('_calcBounds', aX, aY, props, jLen, onlyWidthHeight); + + this._getBounds(aX, aY, onlyWidthHeight); + }, + }); + + /** + * EraserBrush, part of EraserBrush + * + * Made it so that the path will be 'merged' with other objects + * into a customized group and has a 'destination-out' composition + * + * Note: Might not work with versions other than 3.1.0 / 4.0.0 since it uses some + * fabric.js overwriting + * + * Source: https://github.com/fabricjs/fabric.js/issues/1225#issuecomment-499620550 + */ + const EraserBrush = fabric.util.createClass(fabric.PencilBrush, { /** - * ErasedGroup, part of EraserBrush - * - * Made it so that the bound is calculated on the original only - * - * Note: Might not work with versions other than 3.1.0 / 4.0.0 since it uses some - * fabric.js overwriting - * - * Source: https://github.com/fabricjs/fabric.js/issues/1225#issuecomment-499620550 + * On mouseup after drawing the path on contextTop canvas + * we use the points captured to create an new fabric path object + * and add it to the fabric canvas. */ - const ErasedGroup = fabric.util.createClass(fabric.Group, { - original: null, - erasedPath: null, - initialize: function (original, erasedPath, options, isAlreadyGrouped) { - this.original = original; - this.erasedPath = erasedPath; - this.callSuper('initialize', [this.original, this.erasedPath], options, isAlreadyGrouped); - }, + _finalizeAndAddPath: async function () { + var ctx = this.canvas.contextTop; + ctx.closePath(); + if (this.decimate) { + this._points = this.decimatePoints(this._points, this.decimate); + } + var pathData = this.convertPointsToSVGPath(this._points).join(''); + if (pathData === 'M 0 0 Q 0 0 0 0 L 0 0') { + // do not create 0 width/height paths, as they are + // rendered inconsistently across browsers + // Firefox 4, for example, renders a dot, + // whereas Chrome 10 renders nothing + this.canvas.requestRenderAll(); + return; + } - _calcBounds: function (onlyWidthHeight) { - const aX = [], - aY = [], - props = ['tr', 'br', 'bl', 'tl'], - jLen = props.length, - ignoreZoom = true; + // use globalCompositeOperation to 'fake' eraser + var path = this.createPath(pathData); + path.globalCompositeOperation = 'destination-out'; + path.selectable = false; + path.evented = false; + path.absolutePositioned = true; - let o = this.original; - o.setCoords(ignoreZoom); - for (let j = 0; j < jLen; j++) { - const prop = props[j]; - aX.push(o.oCoords[prop].x); - aY.push(o.oCoords[prop].y); - } + // grab all the objects that intersects with the path, filter out objects + // that are not desired, such as Text and IText + // otherwise text might get erased (under some circumstances, this might be desired?!) + const objects = this.canvas.getObjects().filter((obj) => { + if (obj instanceof fabric.Textbox) return false; + if (obj instanceof fabric.Text) return false; + if (obj instanceof fabric.IText) return false; + // get all objects, that intersect + // intersectsWithObject(x, absoluteopt=true) <- enables working eraser during zoom + if (!obj.intersectsWithObject(path, true)) return false; + return true; + }); - this._getBounds(aX, aY, onlyWidthHeight); - }, + // async loop to ensure, that first we do the erasing for all objects, and then update canvas + for (const intersectedObject of objects) { + // eraserPath is handled by reference later, so we need copy for every intersectedObject + const eraserPath = _cloneDeep(path); + + // by adding path-object with 'destination-out', it will be 'erased' + const erasedGroup = new ErasedGroup(intersectedObject, eraserPath); + + const erasedGroupDataURL = erasedGroup.toDataURL({ + withoutTransform: true, + }); + // Be aware of async behavior! + const fabricImage = await fabricImageFromURLPromise(erasedGroupDataURL); + // TODO: If complete path was erased, remove canvas object completely! Right now, an empty image is added + console.log(eraserPath, erasedGroup, 'fabricimage', fabricImage); + console.image(erasedGroupDataURL); + fabricImage.set({ + left: erasedGroup.left, + top: erasedGroup.top, + }); + + this.canvas.remove(intersectedObject); + this.canvas.add(fabricImage); + } + + this.canvas.renderAll(); + // removes path of eraser + this.canvas.clearContext(this.canvas.contextTop); + this._resetShadow(); + }, + }); + + /** + * Promisiefied fromUrl: + * http://fabricjs.com/docs/fabric.Image.html#.fromURL + * + * @param {string} url URL to create an image from + * @param {object} imgOptionsopt Options object + */ + const fabricImageFromURLPromise = (url, imgOptionsopt) => { + return new Promise((resolve) => { + fabric.Image.fromURL(url, resolve, imgOptionsopt); }); + }; - /** - * EraserBrush, part of EraserBrush - * - * Made it so that the path will be 'merged' with other objects - * into a customized group and has a 'destination-out' composition - * - * Note: Might not work with versions other than 3.1.0 / 4.0.0 since it uses some - * fabric.js overwriting - * - * Source: https://github.com/fabricjs/fabric.js/issues/1225#issuecomment-499620550 - */ - const EraserBrush = fabric.util.createClass(fabric.PencilBrush, { - - /** - * On mouseup after drawing the path on contextTop canvas - * we use the points captured to create an new fabric path object - * and add it to the fabric canvas. - */ - _finalizeAndAddPath: function () { - var ctx = this.canvas.contextTop; - ctx.closePath(); - if (this.decimate) { - this._points = this.decimatePoints(this._points, this.decimate); - } - var pathData = this.convertPointsToSVGPath(this._points).join(''); - if (pathData === 'M 0 0 Q 0 0 0 0 L 0 0') { - // do not create 0 width/height paths, as they are - // rendered inconsistently across browsers - // Firefox 4, for example, renders a dot, - // whereas Chrome 10 renders nothing - this.canvas.requestRenderAll(); - return; - } - - // use globalCompositeOperation to 'fake' eraser - var path = this.createPath(pathData); - path.globalCompositeOperation = 'destination-out'; - path.selectable = false; - path.evented = false; - path.absolutePositioned = true; - - // grab all the objects that intersects with the path - const objects = this.canvas.getObjects().filter((obj) => { - // if (obj instanceof fabric.Textbox) return false; - // if (obj instanceof fabric.IText) return false; - if (!obj.intersectsWithObject(path)) return false; - return true; - }); - - if (objects.length > 0) { - - // merge those objects into a group - const mergedGroup = new fabric.Group(objects); - - // This will perform the actual 'erasing' - // NOTE: you can do this for each object, instead of doing it with a merged group - // however, there will be a visible lag when there's many objects affected by this - const newPath = new ErasedGroup(mergedGroup, path); - - const left = newPath.left; - const top = newPath.top; - - // convert it into a dataURL, then back to a fabric image - const newData = newPath.toDataURL({ - withoutTransform: true - }); - fabric.Image.fromURL(newData, (fabricImage) => { - fabricImage.set({ - left: left, - top: top, - }); - - // remove the old objects then add the new image - this.canvas.remove(...objects); - this.canvas.add(fabricImage); - }); - } - - this.canvas.clearContext(this.canvas.contextTop); - this.canvas.renderAll(); - this._resetShadow(); - }, - }); - - return {EraserBrush, ErasedGroup}; + return { EraserBrush, ErasedGroup }; }; export default EraserBrushFactory; diff --git a/src/public/app/widgets/type_widgets/canvas-note-utils/EraserBrushPath.js b/src/public/app/widgets/type_widgets/canvas-note-utils/EraserBrushPath.js new file mode 100644 index 000000000..ed71c3e71 --- /dev/null +++ b/src/public/app/widgets/type_widgets/canvas-note-utils/EraserBrushPath.js @@ -0,0 +1,69 @@ +const EraserBrushPathFactory = (fabric) => { + /** + * EraserBrushPath, part of EraserBrushPath + * + * Made it so that the path will be 'merged' with other objects + * into a customized group and has a 'destination-out' composition + * + * Note: Might not work with versions other than 3.1.0 / 4.0.0 since it uses some + * fabric.js overwriting + * + * Source: https://github.com/fabricjs/fabric.js/issues/1225#issuecomment-499620550 + */ + const EraserBrushPath = fabric.util.createClass(fabric.PencilBrush, { + /** + * On mouseup after drawing the path on contextTop canvas + * we use the points captured to create an new fabric path object + * and add it to the fabric canvas. + */ + _finalizeAndAddPath: async function () { + var ctx = this.canvas.contextTop; + ctx.closePath(); + if (this.decimate) { + this._points = this.decimatePoints(this._points, this.decimate); + } + var pathData = this.convertPointsToSVGPath(this._points).join(''); + if (pathData === 'M 0 0 Q 0 0 0 0 L 0 0') { + // do not create 0 width/height paths, as they are + // rendered inconsistently across browsers + // Firefox 4, for example, renders a dot, + // whereas Chrome 10 renders nothing + this.canvas.requestRenderAll(); + return; + } + + // use globalCompositeOperation to 'fake' eraser + var path = this.createPath(pathData); + path.globalCompositeOperation = 'destination-out'; + path.selectable = false; + path.evented = false; + path.absolutePositioned = true; + + // grab all the objects that intersects with the path, filter out objects + // that are not desired, such as Text and IText + // otherwise text might get erased (under some circumstances, this might be desired?!) + const objects = this.canvas.getObjects().filter((obj) => { + if (obj instanceof fabric.Textbox) return false; + if (obj instanceof fabric.Text) return false; + if (obj instanceof fabric.IText) return false; + // intersectsWithObject(x, absoluteopt=true) <- enables working eraser during zoom + if (!obj.intersectsWithObject(path, true)) return false; + return true; + }); + + // async loop to ensure, that first we do the erasing for all objects, and then update canvas + for (const intersectedObject of objects) { + this.canvas.remove(intersectedObject); + } + + this.canvas.renderAll(); + // removes path of eraser + this.canvas.clearContext(this.canvas.contextTop); + this._resetShadow(); + }, + }); + + return { EraserBrushPath }; +}; + +export default EraserBrushPathFactory; diff --git a/src/public/app/widgets/type_widgets/canvas-note-utils/gui.js b/src/public/app/widgets/type_widgets/canvas-note-utils/gui.js new file mode 100644 index 000000000..66a4c0506 --- /dev/null +++ b/src/public/app/widgets/type_widgets/canvas-note-utils/gui.js @@ -0,0 +1,193 @@ +import EraserBrushFactory from './EraserBrush.js'; +import EraserBrushPathFactory from './EraserBrushPath.js'; + +/** + * add listeners to buttons + */ +export const initButtons = (self) => { + const canvas = self.$canvas; + + var saveCanvas = $('#save-canvas'), + refreshCanvas = $('#refresh-canvas'), + zoom100 = $('#zoom-100'), + showSVG = $('#show-svg'), + clearEl = $('#clear-canvas'), + undo = $('#undo'), + redo = $('#redo'); + const deletedItems = []; + + undo.on('click', () => { + // // Source: https://stackoverflow.com/a/28666556 + // var lastItemIndex = canvas.getObjects().length - 1; + // var item = canvas.item(lastItemIndex); + + // deletedItems.push(item); + // // if(item.get('type') === 'path') { + // canvas.remove(item); + // canvas.renderAll(); + // // } + + canvas.undo(); //fabric-history + }); + + redo.on('click', () => { + // const lastItem = deletedItems.pop(); + // if (lastItem) { + // canvas.add(lastItem); + // canvas.renderAll(); + // } + + canvas.redo(); //fabric-history + }); + + clearEl.on('click', () => { + console.log('cE-oC'); + canvas.clear(); + }); + + saveCanvas.on('click', () => { + console.log('sC-oC'); + const canvasContent = canvas.toJSON(); + console.log('Canvas JSON', canvasContent); + const payload = { + width: self.width, + height: self.height, + lastScale: self.lastScale, + canvas: canvasContent, + }; + localStorage.setItem('infiniteCanvas', JSON.stringify(payload)); + }); + + refreshCanvas.on('click', () => { + console.log('rC-oC'); + const infiniteCanvas = JSON.parse(localStorage.getItem('infiniteCanvas') || ""); + console.log('rcoc, inf', infiniteCanvas); + + canvas.loadFromJSON(infiniteCanvas.canvas, () => { + self.width = self.scaledWidth = infiniteCanvas.width; + self.height = self.scaledHeight = infiniteCanvas.height; + self.lastScale = infiniteCanvas.lastScale; + canvas.setWidth(infiniteCanvas.width); + canvas.setHeight(infiniteCanvas.height); + self.$canvasContainer.width(infiniteCanvas.width).height(infiniteCanvas.height); + canvas.renderAll(); + }); + }); + + zoom100.on('click', () => { + console.log('zoom100'); + // TODO extract zoom to into separate function (reuse for zoom 100% button) + // zoom level of canvas + self.resetZoom(); + + canvas.renderAll(); + }); + + showSVG.on('click', () => { + console.log('showSVG'); + const svg = self.$canvas.toSVG(); + const imageSrc = `data:image/svg+xml;utf8,${svg}`; + // $('#svgImage').html(``); + $('#svgImage').html(`${svg}`); + }); + + $('#enlarge-left').on('click', () => { + const enlargeValue = parseInt($('#enlargeValue').val(), 10); + self.$canvas.transformCanvas('left', enlargeValue); + }); + $('#enlarge-top').on('click', () => { + const enlargeValue = parseInt($('#enlargeValue').val(), 10); + self.$canvas.transformCanvas('top', enlargeValue); + }); + $('#enlarge-right').on('click', () => { + const enlargeValue = parseInt($('#enlargeValue').val(), 10); + self.$canvas.transformCanvas('right', enlargeValue); + }); + $('#enlarge-bottom').on('click', () => { + const enlargeValue = parseInt($('#enlargeValue').val(), 10); + self.$canvas.transformCanvas('bottom', enlargeValue); + }); + $('#crop-canvas').on('click', () => { + self.cropCanvas(); + }); + + $('#mode-select').on('click', () => { + self.$canvas.isDrawingMode = false; + self.drawWithTouch = false; + }); + $('#mode-drawWithTouch').on('click', () => { + self.drawWithTouch = true; + }); +}; + +export const initPens = (self) => { + const canvas = self.$canvas; + $('#pen-1').on('click', () => { + canvas.freeDrawingBrush = new fabric['PencilBrush'](canvas); + canvas.freeDrawingBrush.color = 'black'; + canvas.freeDrawingBrush.width = 2; + canvas.isDrawingMode = true; + }); + $('#pen-2').on('click', () => { + canvas.freeDrawingBrush = new fabric['PencilBrush'](canvas); + canvas.freeDrawingBrush.color = 'red'; + canvas.freeDrawingBrush.width = 2; + canvas.isDrawingMode = true; + }); + $('#pen-3').on('click', () => { + canvas.freeDrawingBrush = new fabric['PencilBrush'](canvas); + canvas.freeDrawingBrush.color = 'green'; + canvas.freeDrawingBrush.width = 2; + canvas.isDrawingMode = true; + }); + $('#pen-4').on('click', () => { + canvas.freeDrawingBrush = new fabric['PencilBrush'](canvas); + canvas.freeDrawingBrush.color = 'blue'; + canvas.freeDrawingBrush.width = 2; + canvas.isDrawingMode = true; + }); + $('#marker-1').on('click', () => { + canvas.freeDrawingBrush = new fabric['PencilBrush'](canvas); + canvas.freeDrawingBrush.color = 'rgba(255, 255, 0, 0.5)'; + canvas.freeDrawingBrush.width = 10; + canvas.isDrawingMode = true; + }); + $('#marker-2').on('click', () => { + canvas.freeDrawingBrush = new fabric['PencilBrush'](canvas); + canvas.freeDrawingBrush.color = 'rgba(241,229,170, 0.5)'; + canvas.freeDrawingBrush.width = 10; + canvas.isDrawingMode = true; + }); + $('#marker-3').on('click', () => { + canvas.freeDrawingBrush = new fabric['PencilBrush'](canvas); + canvas.freeDrawingBrush.color = 'rgba(51,204,0, 0.5)'; + canvas.freeDrawingBrush.width = 10; + canvas.isDrawingMode = true; + }); + $('#marker-4').on('click', () => { + canvas.freeDrawingBrush = new fabric['PencilBrush'](canvas); + canvas.freeDrawingBrush.color = 'rgba(75,141,242, 0.5)'; + canvas.freeDrawingBrush.width = 10; + canvas.isDrawingMode = true; + }); + $('#eraser').on('click', () => { + const { EraserBrush } = EraserBrushFactory(fabric); + const eraserBrush = new EraserBrush(canvas); + eraserBrush.width = 10; + eraserBrush.color = 'rgb(236,195,195)'; // erser works with opacity! + canvas.freeDrawingBrush = eraserBrush; + canvas.isDrawingMode = true; + }); + $('#eraser-path').on('click', () => { + const { EraserBrushPath } = EraserBrushPathFactory(fabric); + const eraserBrush = new EraserBrushPath(canvas); + eraserBrush.width = 8; + eraserBrush.color = 'rgba(236,195,220, 20)'; // erser works with opacity! + canvas.freeDrawingBrush = eraserBrush; + canvas.isDrawingMode = true; + }); + $('#text-1').on('click', () => { + self.activatePlaceTextBox = true; + canvas.isDrawingMode = false; + }); +}; diff --git a/src/public/app/widgets/type_widgets/canvas-note-utils/infinite-drawing-canvas.js b/src/public/app/widgets/type_widgets/canvas-note-utils/infinite-drawing-canvas.js new file mode 100644 index 000000000..cccba8200 --- /dev/null +++ b/src/public/app/widgets/type_widgets/canvas-note-utils/infinite-drawing-canvas.js @@ -0,0 +1,503 @@ +import _throttle from './lib/lodash.throttle.js'; +import _debounce from './lib/lodash.debounce.js'; +import sleep from './lib/sleep.js'; +import deleteIcon from './lib/deleteIcon.js'; + +var img = document.createElement('img'); +img.src = deleteIcon; + +/** + * Class of all valid Infinite Canvas States + * + * usage: + * const canvasState = new CanvasState(); + * canvasState.on('selecting', ()=>{}); + * canvasState.activate('selecting'); +Inspiration: https://stackoverflow.com/a/53917410 +https://developer.mozilla.org/en-US/docs/Web/API/EventTarget + */ +class CanvasState extends EventTarget { + constructor(initialState) { + this.states = { + IDLE: 'idle', + INTERACTING: 'interacting', + DRAGGING: 'dragging', + PANNING: 'panning', + SELECTING: 'selecting', + PINCH_ZOOMING: 'pinch_zooming', + SELECTED: 'selected,', + }; + + this.currentState = initialState || this.state.IDLE; + + this.listeners = {}; + } + + activate(state) { + if (this._isValidState(state)) { + this.currentState = state; + this.dispatchEvent(new Event(state)); + } else { + throw new Error(`This is not a valid State: '${state}`); + } + } + + _isValidState(state) { + const statesArray = Object.values(this.states); + return statesArray.find(state); + } + + get() { + return this.currentState; + } + + getStates() { + return this.states; + } +} + +/** + * Infinite Canvas + */ +class InfiniteCanvas { + constructor($canvas, $parent, $canvasContainer) { + this.$canvas = $canvas; + this.$canvasContainer = $canvasContainer; + this.$parent = $parent; + + // Canvas + this.isDragging; + this.selection; + this.lastPosX; + this.lastPosY; + this.startPosX = 0; + this.startPosY = 0; + this.numberOfPanEvents; + this.lastScale = 1; + this.fonts = [ + 'Times New Roman', + 'Arial', + 'Verdana', + 'Calibri', + 'Consolas', + 'Comic Sans MS', + ]; + this.width = this.scaledWidth = 1500; //px + this.height = this.scaledHeight = 1500; //px + this.drawWithTouch = false; + this.activatePlaceTextBox = false; + + // bind methods to this + this.handlePointerEventBefore = this.handlePointerEventBefore.bind(this); + this.resizeCanvas = this.resizeCanvas.bind(this); + this.handlePinch = this.handlePinch.bind(this); + this.handlePinchEnd = this.handlePinchEnd.bind(this); + this.handlePanStart = this.handlePanStart.bind(this); + this.handlePanning = this.handlePanning.bind(this); + this.handlePanEnd = this.handlePanEnd.bind(this); + this.transformCanvas = this.transformCanvas.bind(this); + this.resetZoom = this.resetZoom.bind(this); + this.cropCanvas = this.cropCanvas.bind(this); + this.placeTextBox = this.placeTextBox.bind(this); + } + + overrideFabric() { + const self = this; + + fabric.Object.prototype.controls.deleteControl = new fabric.Control({ + x: 0.5, + y: -0.5, + offsetY: 16, + cursorStyle: 'pointer', + mouseUpHandler: self.deleteObject, + render: self.renderIcon, + cornerSize: 24, + }); + } + + renderIcon(ctx, left, top, styleOverride, fabricObject) { + var size = this.cornerSize; + ctx.save(); + ctx.translate(left, top); + ctx.rotate(fabric.util.degreesToRadians(fabricObject.angle)); + ctx.drawImage(img, -size / 2, -size / 2, size, size); + ctx.restore(); + } + + deleteObject(eventData, target) { + var canvas = target.canvas; + canvas.remove(target); + canvas.requestRenderAll(); + } + + initFabric() { + this.overrideFabric(); + + const canvasElement = this.$canvas.get(0); // fabric.Canvas requires HTMLElement + this.canvasElement = canvasElement; + + const self = this; + const canvas = new fabric.Canvas(canvasElement, { + isDrawingMode: false, + allowTouchScrolling: true, + transparentCorners: false, + }); + this.$canvas = canvas; + // fabric.Object.prototype.transparentCorners = false; + + // Resizing + // FIXME: canvas should only enlarge, maybe we dont even need, since canvas will scroll behind parent! + // const canvasNote = this.$parent.get(0); + // new ResizeObserver(_throttle(this.resizeCanvas, 200)).observe(canvasNote); // this leads to a eraserbrush remaining... + + // Handle different input devices: Touch (Finger), Pen, Mouse + canvas.on('mouse:down:before', this.handlePointerEventBefore); + + this.hammer = new Hammer.Manager(canvas.upperCanvasEl); + var pinch = new Hammer.Pinch(); + var pan = new Hammer.Pan(); + this.hammer.add([pinch, pan]); + + // Zoom (Pinch) + // FIXME: not working + // Problem: Somehow eraser planes from matched do not overlay and then do not erase + this.hammer.on('pinchmove', _throttle(this.handlePinch, 20)); + // the pinchend call must be debounced, since a pinchmove event might + // occur after a couple of ms after the actual pinchend event. With the + // debounce, it is garuanted, that this.lastScale and the scale for the + // next pinch zoom is set correctly + this.hammer.on('pinchend', _debounce(this.handlePinchEnd, 200)); + + // Move Canvas + this.hammer.on('panstart', this.handlePanStart); + this.hammer.on('pan', this.handlePanning); + this.hammer.on('panend', this.handlePanEnd); + + canvas.transformCanvas = this.transformCanvas; + + return self; + } + + /** + * + * @param {string} direction [top, left, right, bottom] + * @param {float} distance distance in px + */ + transformCanvas(direction, distance) { + console.log('transforming', direction, distance); + const canvas = this.$canvas; + this.resetZoom(); + + const items = canvas.getObjects(); + + // Move all items, so that it seems canvas was added on the outside + for (let i = 0; i < items.length; i++) { + const item = canvas.item(i).setCoords(); + console.log('tc, item', item); + if (direction === 'top') { + // move all down + item.top = item.top + distance; + } + if (direction === 'left') { + // move all to the right + item.left = item.left + distance; + } + } + + let newWidth = this.scaledWidth, + newHeight = this.scaledHeight; + + if (direction === 'top' || direction === 'bottom') { + newHeight = this.scaledHeight + distance; + } else if (direction === 'left' || direction === 'right') { + newWidth = this.scaledWidth + distance; + } + this.scaledWidth = this.width = newWidth; + this.scaledHeight = this.height = newHeight; + canvas.setWidth(newWidth); + canvas.setHeight(newHeight); + + this.$canvasContainer.width(newWidth).height(newHeight); + + canvas.renderAll(); + console.log('called tc', direction, distance); + } + + resetZoom() { + const canvas = this.$canvas; + + // zoom level of canvas + canvas.setZoom(1); + // width of + canvas.setWidth(this.width); + canvas.setHeight(this.height); + // reset scale, so that for next pinch we start with "fresh" values + this.scaledWidth = this.width; + this.scaledHeight = this.height; + this.lastScale = 1; + // set div container of canvas + this.$canvasContainer.width(this.width).height(this.height); + } + + handlePointerEventBefore(fabricEvent) { + const canvas = this.$canvas; + const inputType = this.recognizeInput(fabricEvent.e); + console.log('mdb', fabricEvent, fabricEvent.e, 'inputType', inputType); + // place text box independent of touch type + if (this.activatePlaceTextBox) { + if (fabricEvent && fabricEvent.absolutePointer) { + this.placeTextBox(fabricEvent.absolutePointer.x, fabricEvent.absolutePointer.y); + this.activatePlaceTextBox = false; + return; + } + } + + // recognize touch + if (inputType === 'touch') { + if (this.drawWithTouch) { + // drawing + canvas.isDrawingMode = true; + } else { + // panning + console.log('mdb touch'); + canvas.isDrawingMode = false; + canvas.selection = false; + // unselect any possible targets (if you start the pan on an object) + if (fabricEvent.target && canvas) { + // source: https://stackoverflow.com/a/25535052 + canvas.deactivateAll().renderAll(); + } + } + } else if (inputType === 'pen') { + // draw with pen + console.log('mdb pen'); + canvas.isDrawingMode = true; + } else if (inputType === 'mouse') { + // draw with mouse + console.log('mdb mouse, draw'); + } else { + console.log('mdb input type not recognized!'); + throw new Error('input type not recognized!'); + } + } + + placeTextBox(x, y) { + const canvas = this.$canvas; + canvas.add( + new fabric.IText('Tap and Type', { + fontFamily: 'Arial', + // fontWeith: '200', + fontSize: 15, + left: x, + top: y, + }), + ); + canvas.isDrawingMode = false; + } + + handlePinch(e) { + console.log('hp', e); + const canvas = this.$canvas; + console.log('pinch', e, 'pinchingi scale', this.lastScale, e.scale); + // during pinch, we need to focus top left corner. + // otherwise canvas might slip underneath the container and misalign. + let point = null; + point = new fabric.Point(0, 0); + // point = new fabric.Point(e.center.x, e.center.y); + canvas.zoomToPoint(point, this.lastScale * e.scale); + } + + handlePinchEnd(e) { + const canvas = this.$canvas; + + console.log('hpe', e); + this.lastScale = this.lastScale * e.scale; + console.log('pinchend', this.lastScale, e.scale, e); + + // resize canvas, maybe this fixes eraser + this.scaledWidth = this.scaledWidth * e.scale; + this.scaledHeight = this.scaledHeight * e.scale; + canvas.setWidth(this.scaledWidth); + canvas.setHeight(this.scaledHeight); + + this.$canvasContainer.width(this.scaledWidth).height(this.scaledHeight); + + // ("width", `${self.width}px`); + // console.log('zoom100, cc', self.$canvasContainer); + + // reactivate drawing mode after the pinch is over + } + + handlePanStart(e) { + const canvas = this.$canvas; + console.log('panstart', e); + + if ( + e.pointerType === 'touch' && + !this.drawWithTouch // pointertype mouse and canvas state mouse-drag + ) { + canvas.isDrawingMode = false; + canvas.isDragging = true; + canvas.selection = false; + this.selection = false; + + var scrollContainer = $('#parentContainer').get(0); + this.startPosX = scrollContainer.scrollLeft; + this.startPosY = scrollContainer.scrollTop; + } + } + + handlePanning(e) { + const canvas = this.$canvas; + // console.log('panning', e); + + if (e.pointerType === 'touch') { + // console.log('pan', e); + if (canvas.isDragging) { + // scrolltest + const panMultiplier = 1.0; + const dx = this.startPosX - e.deltaX * panMultiplier; + const dy = this.startPosY - e.deltaY * panMultiplier; + var scrollContainer = $('#parentContainer'); + scrollContainer.scrollLeft(dx); + scrollContainer.scrollTop(dy); + canvas.requestRenderAll(); + } + } + } + + async handlePanEnd(e) { + const canvas = this.$canvas; + console.log('panend', e); + + if (e.pointerType === 'touch') { + // take momentum of panning to do it once panning is finished + // let deltaX = e.deltaX; + // let deltaY = e.deltaY; + // for(let v = Math.abs(e.overallVelocity); v>0; v=v-0.1) { + // if (deltaX > 0) { + // deltaX = e.deltaX + e.deltaX * v; + // } else { + // deltaX = e.deltaX - e.deltaX * v; + // } + // deltaY = e.deltaY + e.deltaY * v; + // const newEvent = {...e, overallVelocity: v, deltaX, deltaY}; + // console.log('vel', v, deltaX, deltaY, newEvent); + // this.handlePanning(newEvent); + // await this.sleep(1000); + // } + + // on mouse up we want to recalculate new interaction + // for all objects, so we call setViewportTransform + // canvas.setViewportTransform(canvas.viewportTransform); + canvas.isDragging = false; + canvas.selection = true; + + var scrollContainer = $('#parentContainer').get(0); + this.startPosX = scrollContainer.scrollLeft; + this.startPosY = scrollContainer.scrollTop; + } + } + + /** + * + * @param {FabricPointerEvent} e + */ + recognizeInput(e) { + const TOUCH = 'touch'; + const PEN = 'pen'; + const MOUSE = 'mouse'; + // we need to modify fabric.js in order to get the + // pointerevent and not only the touchevent when using pen + console.log('recognizeInput Touchevent', e); + + if (e.touches) { + if (e.touches.length > 1) { + // most likely pinch, since two fingers, aka touch inputs + console.log('recognizeInput', TOUCH); + return TOUCH; + } + if (e.touches.length === 1) { + // now it may be pen or one finger + const touchEvent = e.touches[0] || {}; + console.log('recognizeInput Touchevent', touchEvent); + if (touchEvent.radiusX === 0.5 && touchEvent.radiusY === 0.5) { + // when we have pointer event, we can distinguish between + // pen (buttons=1) and eraser (buttons=32) <- pointerevent + // at least on chrome; firefox not supported :-( + console.log('recognizeInput', PEN); + return PEN; + } else { + console.log('recognizeInput', TOUCH); + return TOUCH; + } + } + } else { + console.log('recognizeInput', MOUSE); + return MOUSE; + } + } + + // detect parent div size change + resizeCanvas() { + const canvas = this.$canvas; + const width = this.$parent.width(); + const height = this.$parent.height(); + console.log(`setting canvas to ${width} x ${height}px`); + // canvas.setWidth(width); + // canvas.setHeight(height); + canvas.setWidth(1500); + canvas.setHeight(1500); + canvas.renderAll(); + } + + /** + * Crop the canvas to the surrounding box of all elements on the canvas + * + Learnings: we must NOT use fabric.Group, since this messes with items and then + SVG export is scwed. Items coordinates are not set correctly! + fabric.Group(items).aCoords does NOT work. + Therefore we need to get bounding box ourselves + Note: Or maybe we can use group, destroy and readd everything afterwards: + http://fabricjs.com/manage-selection + https://gist.github.com/msievers/6069778#gistcomment-2030151 + https://stackoverflow.com/a/31828460 + */ + async cropCanvas() { + console.log('cropCanvas'); + const canvas = this.$canvas; + + // get all objects + const items = canvas.getObjects(); + // get maximum bounding rectangle of all objects + const bound = { tl: { x: Infinity, y: Infinity }, br: { x: 0, y: 0 } }; + for (let i = 0; i < items.length; i++) { + // focus on tl/br; + const item = items[i]; + const tl = item.aCoords.tl; + const br = item.aCoords.br; + console.log('cC, item', tl, br); + if (tl.x < bound.tl.x) { + bound.tl.x = tl.x; + } + if (tl.y < bound.tl.y) { + bound.tl.y = tl.y; + } + if (br.x > bound.br.x) { + bound.br.x = br.x; + } + if (br.y > bound.br.y) { + bound.br.y = br.y; + } + } + console.log('cC, bounds:', bound); + + // cut area on all sides + this.transformCanvas('left', -bound.tl.x); + this.transformCanvas('top', -bound.tl.y); + this.transformCanvas('right', -(this.width - bound.br.x + bound.tl.x)); + this.transformCanvas('bottom', -(this.height - bound.br.y + bound.tl.y)); + } +} + +export { InfiniteCanvas, CanvasState }; diff --git a/src/public/app/widgets/type_widgets/canvas-note-utils/lib/deleteIcon.js b/src/public/app/widgets/type_widgets/canvas-note-utils/lib/deleteIcon.js new file mode 100644 index 000000000..37e85759a --- /dev/null +++ b/src/public/app/widgets/type_widgets/canvas-note-utils/lib/deleteIcon.js @@ -0,0 +1,3 @@ +var deleteIcon = "data:image/svg+xml,%3C%3Fxml version='1.0' encoding='utf-8'%3F%3E%3C!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'%3E%3Csvg version='1.1' id='Ebene_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' width='595.275px' height='595.275px' viewBox='200 215 230 470' xml:space='preserve'%3E%3Ccircle style='fill:%23F44336;' cx='299.76' cy='439.067' r='218.516'/%3E%3Cg%3E%3Crect x='267.162' y='307.978' transform='matrix(0.7071 -0.7071 0.7071 0.7071 -222.6202 340.6915)' style='fill:white;' width='65.545' height='262.18'/%3E%3Crect x='266.988' y='308.153' transform='matrix(0.7071 0.7071 -0.7071 0.7071 398.3889 -83.3116)' style='fill:white;' width='65.544' height='262.179'/%3E%3C/g%3E%3C/svg%3E"; + +export default deleteIcon; \ No newline at end of file diff --git a/src/public/app/widgets/type_widgets/canvas-note-utils/lib/lodash.cloneDeep.js b/src/public/app/widgets/type_widgets/canvas-note-utils/lib/lodash.cloneDeep.js new file mode 100644 index 000000000..5a9cf9cf9 --- /dev/null +++ b/src/public/app/widgets/type_widgets/canvas-note-utils/lib/lodash.cloneDeep.js @@ -0,0 +1,1748 @@ +/** + * lodash (Custom Build) + * Build: `lodash modularize exports="npm" -o ./` + * Copyright jQuery Foundation and other contributors + * Released under MIT license + * Based on Underscore.js 1.8.3 + * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors + */ + +/** Used as the size to enable large array optimizations. */ +var LARGE_ARRAY_SIZE = 200; + +/** Used to stand-in for `undefined` hash values. */ +var HASH_UNDEFINED = '__lodash_hash_undefined__'; + +/** Used as references for various `Number` constants. */ +var MAX_SAFE_INTEGER = 9007199254740991; + +/** `Object#toString` result references. */ +var argsTag = '[object Arguments]', + arrayTag = '[object Array]', + boolTag = '[object Boolean]', + dateTag = '[object Date]', + errorTag = '[object Error]', + funcTag = '[object Function]', + genTag = '[object GeneratorFunction]', + mapTag = '[object Map]', + numberTag = '[object Number]', + objectTag = '[object Object]', + promiseTag = '[object Promise]', + regexpTag = '[object RegExp]', + setTag = '[object Set]', + stringTag = '[object String]', + symbolTag = '[object Symbol]', + weakMapTag = '[object WeakMap]'; + +var arrayBufferTag = '[object ArrayBuffer]', + dataViewTag = '[object DataView]', + float32Tag = '[object Float32Array]', + float64Tag = '[object Float64Array]', + int8Tag = '[object Int8Array]', + int16Tag = '[object Int16Array]', + int32Tag = '[object Int32Array]', + uint8Tag = '[object Uint8Array]', + uint8ClampedTag = '[object Uint8ClampedArray]', + uint16Tag = '[object Uint16Array]', + uint32Tag = '[object Uint32Array]'; + +/** + * Used to match `RegExp` + * [syntax characters](http://ecma-international.org/ecma-262/7.0/#sec-patterns). + */ +var reRegExpChar = /[\\^$.*+?()[\]{}|]/g; + +/** Used to match `RegExp` flags from their coerced string values. */ +var reFlags = /\w*$/; + +/** Used to detect host constructors (Safari). */ +var reIsHostCtor = /^\[object .+?Constructor\]$/; + +/** Used to detect unsigned integer values. */ +var reIsUint = /^(?:0|[1-9]\d*)$/; + +/** Used to identify `toStringTag` values supported by `_.clone`. */ +var cloneableTags = {}; +cloneableTags[argsTag] = cloneableTags[arrayTag] = +cloneableTags[arrayBufferTag] = cloneableTags[dataViewTag] = +cloneableTags[boolTag] = cloneableTags[dateTag] = +cloneableTags[float32Tag] = cloneableTags[float64Tag] = +cloneableTags[int8Tag] = cloneableTags[int16Tag] = +cloneableTags[int32Tag] = cloneableTags[mapTag] = +cloneableTags[numberTag] = cloneableTags[objectTag] = +cloneableTags[regexpTag] = cloneableTags[setTag] = +cloneableTags[stringTag] = cloneableTags[symbolTag] = +cloneableTags[uint8Tag] = cloneableTags[uint8ClampedTag] = +cloneableTags[uint16Tag] = cloneableTags[uint32Tag] = true; +cloneableTags[errorTag] = cloneableTags[funcTag] = +cloneableTags[weakMapTag] = false; + +/** Detect free variable `global` from Node.js. */ +var freeGlobal = typeof global == 'object' && global && global.Object === Object && global; + +/** Detect free variable `self`. */ +var freeSelf = typeof self == 'object' && self && self.Object === Object && self; + +/** Used as a reference to the global object. */ +var root = freeGlobal || freeSelf || Function('return this')(); + +/** Detect free variable `exports`. */ +var freeExports = typeof exports == 'object' && exports && !exports.nodeType && exports; + +/** Detect free variable `module`. */ +var freeModule = freeExports && typeof module == 'object' && module && !module.nodeType && module; + +/** Detect the popular CommonJS extension `module.exports`. */ +var moduleExports = freeModule && freeModule.exports === freeExports; + +/** + * Adds the key-value `pair` to `map`. + * + * @private + * @param {Object} map The map to modify. + * @param {Array} pair The key-value pair to add. + * @returns {Object} Returns `map`. + */ +function addMapEntry(map, pair) { + // Don't return `map.set` because it's not chainable in IE 11. + map.set(pair[0], pair[1]); + return map; +} + +/** + * Adds `value` to `set`. + * + * @private + * @param {Object} set The set to modify. + * @param {*} value The value to add. + * @returns {Object} Returns `set`. + */ +function addSetEntry(set, value) { + // Don't return `set.add` because it's not chainable in IE 11. + set.add(value); + return set; +} + +/** + * A specialized version of `_.forEach` for arrays without support for + * iteratee shorthands. + * + * @private + * @param {Array} [array] The array to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @returns {Array} Returns `array`. + */ +function arrayEach(array, iteratee) { + var index = -1, + length = array ? array.length : 0; + + while (++index < length) { + if (iteratee(array[index], index, array) === false) { + break; + } + } + return array; +} + +/** + * Appends the elements of `values` to `array`. + * + * @private + * @param {Array} array The array to modify. + * @param {Array} values The values to append. + * @returns {Array} Returns `array`. + */ +function arrayPush(array, values) { + var index = -1, + length = values.length, + offset = array.length; + + while (++index < length) { + array[offset + index] = values[index]; + } + return array; +} + +/** + * A specialized version of `_.reduce` for arrays without support for + * iteratee shorthands. + * + * @private + * @param {Array} [array] The array to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @param {*} [accumulator] The initial value. + * @param {boolean} [initAccum] Specify using the first element of `array` as + * the initial value. + * @returns {*} Returns the accumulated value. + */ +function arrayReduce(array, iteratee, accumulator, initAccum) { + var index = -1, + length = array ? array.length : 0; + + if (initAccum && length) { + accumulator = array[++index]; + } + while (++index < length) { + accumulator = iteratee(accumulator, array[index], index, array); + } + return accumulator; +} + +/** + * The base implementation of `_.times` without support for iteratee shorthands + * or max array length checks. + * + * @private + * @param {number} n The number of times to invoke `iteratee`. + * @param {Function} iteratee The function invoked per iteration. + * @returns {Array} Returns the array of results. + */ +function baseTimes(n, iteratee) { + var index = -1, + result = Array(n); + + while (++index < n) { + result[index] = iteratee(index); + } + return result; +} + +/** + * Gets the value at `key` of `object`. + * + * @private + * @param {Object} [object] The object to query. + * @param {string} key The key of the property to get. + * @returns {*} Returns the property value. + */ +function getValue(object, key) { + return object == null ? undefined : object[key]; +} + +/** + * Checks if `value` is a host object in IE < 9. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a host object, else `false`. + */ +function isHostObject(value) { + // Many host objects are `Object` objects that can coerce to strings + // despite having improperly defined `toString` methods. + var result = false; + if (value != null && typeof value.toString != 'function') { + try { + result = !!(value + ''); + } catch (e) {} + } + return result; +} + +/** + * Converts `map` to its key-value pairs. + * + * @private + * @param {Object} map The map to convert. + * @returns {Array} Returns the key-value pairs. + */ +function mapToArray(map) { + var index = -1, + result = Array(map.size); + + map.forEach(function(value, key) { + result[++index] = [key, value]; + }); + return result; +} + +/** + * Creates a unary function that invokes `func` with its argument transformed. + * + * @private + * @param {Function} func The function to wrap. + * @param {Function} transform The argument transform. + * @returns {Function} Returns the new function. + */ +function overArg(func, transform) { + return function(arg) { + return func(transform(arg)); + }; +} + +/** + * Converts `set` to an array of its values. + * + * @private + * @param {Object} set The set to convert. + * @returns {Array} Returns the values. + */ +function setToArray(set) { + var index = -1, + result = Array(set.size); + + set.forEach(function(value) { + result[++index] = value; + }); + return result; +} + +/** Used for built-in method references. */ +var arrayProto = Array.prototype, + funcProto = Function.prototype, + objectProto = Object.prototype; + +/** Used to detect overreaching core-js shims. */ +var coreJsData = root['__core-js_shared__']; + +/** Used to detect methods masquerading as native. */ +var maskSrcKey = (function() { + var uid = /[^.]+$/.exec(coreJsData && coreJsData.keys && coreJsData.keys.IE_PROTO || ''); + return uid ? ('Symbol(src)_1.' + uid) : ''; +}()); + +/** Used to resolve the decompiled source of functions. */ +var funcToString = funcProto.toString; + +/** Used to check objects for own properties. */ +var hasOwnProperty = objectProto.hasOwnProperty; + +/** + * Used to resolve the + * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring) + * of values. + */ +var objectToString = objectProto.toString; + +/** Used to detect if a method is native. */ +var reIsNative = RegExp('^' + + funcToString.call(hasOwnProperty).replace(reRegExpChar, '\\$&') + .replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g, '$1.*?') + '$' +); + +/** Built-in value references. */ +var Buffer = moduleExports ? root.Buffer : undefined, + Symbol = root.Symbol, + Uint8Array = root.Uint8Array, + getPrototype = overArg(Object.getPrototypeOf, Object), + objectCreate = Object.create, + propertyIsEnumerable = objectProto.propertyIsEnumerable, + splice = arrayProto.splice; + +/* Built-in method references for those with the same name as other `lodash` methods. */ +var nativeGetSymbols = Object.getOwnPropertySymbols, + nativeIsBuffer = Buffer ? Buffer.isBuffer : undefined, + nativeKeys = overArg(Object.keys, Object); + +/* Built-in method references that are verified to be native. */ +var DataView = getNative(root, 'DataView'), + Map = getNative(root, 'Map'), + Promise = getNative(root, 'Promise'), + Set = getNative(root, 'Set'), + WeakMap = getNative(root, 'WeakMap'), + nativeCreate = getNative(Object, 'create'); + +/** Used to detect maps, sets, and weakmaps. */ +var dataViewCtorString = toSource(DataView), + mapCtorString = toSource(Map), + promiseCtorString = toSource(Promise), + setCtorString = toSource(Set), + weakMapCtorString = toSource(WeakMap); + +/** Used to convert symbols to primitives and strings. */ +var symbolProto = Symbol ? Symbol.prototype : undefined, + symbolValueOf = symbolProto ? symbolProto.valueOf : undefined; + +/** + * Creates a hash object. + * + * @private + * @constructor + * @param {Array} [entries] The key-value pairs to cache. + */ +function Hash(entries) { + var index = -1, + length = entries ? entries.length : 0; + + this.clear(); + while (++index < length) { + var entry = entries[index]; + this.set(entry[0], entry[1]); + } +} + +/** + * Removes all key-value entries from the hash. + * + * @private + * @name clear + * @memberOf Hash + */ +function hashClear() { + this.__data__ = nativeCreate ? nativeCreate(null) : {}; +} + +/** + * Removes `key` and its value from the hash. + * + * @private + * @name delete + * @memberOf Hash + * @param {Object} hash The hash to modify. + * @param {string} key The key of the value to remove. + * @returns {boolean} Returns `true` if the entry was removed, else `false`. + */ +function hashDelete(key) { + return this.has(key) && delete this.__data__[key]; +} + +/** + * Gets the hash value for `key`. + * + * @private + * @name get + * @memberOf Hash + * @param {string} key The key of the value to get. + * @returns {*} Returns the entry value. + */ +function hashGet(key) { + var data = this.__data__; + if (nativeCreate) { + var result = data[key]; + return result === HASH_UNDEFINED ? undefined : result; + } + return hasOwnProperty.call(data, key) ? data[key] : undefined; +} + +/** + * Checks if a hash value for `key` exists. + * + * @private + * @name has + * @memberOf Hash + * @param {string} key The key of the entry to check. + * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`. + */ +function hashHas(key) { + var data = this.__data__; + return nativeCreate ? data[key] !== undefined : hasOwnProperty.call(data, key); +} + +/** + * Sets the hash `key` to `value`. + * + * @private + * @name set + * @memberOf Hash + * @param {string} key The key of the value to set. + * @param {*} value The value to set. + * @returns {Object} Returns the hash instance. + */ +function hashSet(key, value) { + var data = this.__data__; + data[key] = (nativeCreate && value === undefined) ? HASH_UNDEFINED : value; + return this; +} + +// Add methods to `Hash`. +Hash.prototype.clear = hashClear; +Hash.prototype['delete'] = hashDelete; +Hash.prototype.get = hashGet; +Hash.prototype.has = hashHas; +Hash.prototype.set = hashSet; + +/** + * Creates an list cache object. + * + * @private + * @constructor + * @param {Array} [entries] The key-value pairs to cache. + */ +function ListCache(entries) { + var index = -1, + length = entries ? entries.length : 0; + + this.clear(); + while (++index < length) { + var entry = entries[index]; + this.set(entry[0], entry[1]); + } +} + +/** + * Removes all key-value entries from the list cache. + * + * @private + * @name clear + * @memberOf ListCache + */ +function listCacheClear() { + this.__data__ = []; +} + +/** + * Removes `key` and its value from the list cache. + * + * @private + * @name delete + * @memberOf ListCache + * @param {string} key The key of the value to remove. + * @returns {boolean} Returns `true` if the entry was removed, else `false`. + */ +function listCacheDelete(key) { + var data = this.__data__, + index = assocIndexOf(data, key); + + if (index < 0) { + return false; + } + var lastIndex = data.length - 1; + if (index == lastIndex) { + data.pop(); + } else { + splice.call(data, index, 1); + } + return true; +} + +/** + * Gets the list cache value for `key`. + * + * @private + * @name get + * @memberOf ListCache + * @param {string} key The key of the value to get. + * @returns {*} Returns the entry value. + */ +function listCacheGet(key) { + var data = this.__data__, + index = assocIndexOf(data, key); + + return index < 0 ? undefined : data[index][1]; +} + +/** + * Checks if a list cache value for `key` exists. + * + * @private + * @name has + * @memberOf ListCache + * @param {string} key The key of the entry to check. + * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`. + */ +function listCacheHas(key) { + return assocIndexOf(this.__data__, key) > -1; +} + +/** + * Sets the list cache `key` to `value`. + * + * @private + * @name set + * @memberOf ListCache + * @param {string} key The key of the value to set. + * @param {*} value The value to set. + * @returns {Object} Returns the list cache instance. + */ +function listCacheSet(key, value) { + var data = this.__data__, + index = assocIndexOf(data, key); + + if (index < 0) { + data.push([key, value]); + } else { + data[index][1] = value; + } + return this; +} + +// Add methods to `ListCache`. +ListCache.prototype.clear = listCacheClear; +ListCache.prototype['delete'] = listCacheDelete; +ListCache.prototype.get = listCacheGet; +ListCache.prototype.has = listCacheHas; +ListCache.prototype.set = listCacheSet; + +/** + * Creates a map cache object to store key-value pairs. + * + * @private + * @constructor + * @param {Array} [entries] The key-value pairs to cache. + */ +function MapCache(entries) { + var index = -1, + length = entries ? entries.length : 0; + + this.clear(); + while (++index < length) { + var entry = entries[index]; + this.set(entry[0], entry[1]); + } +} + +/** + * Removes all key-value entries from the map. + * + * @private + * @name clear + * @memberOf MapCache + */ +function mapCacheClear() { + this.__data__ = { + 'hash': new Hash, + 'map': new (Map || ListCache), + 'string': new Hash + }; +} + +/** + * Removes `key` and its value from the map. + * + * @private + * @name delete + * @memberOf MapCache + * @param {string} key The key of the value to remove. + * @returns {boolean} Returns `true` if the entry was removed, else `false`. + */ +function mapCacheDelete(key) { + return getMapData(this, key)['delete'](key); +} + +/** + * Gets the map value for `key`. + * + * @private + * @name get + * @memberOf MapCache + * @param {string} key The key of the value to get. + * @returns {*} Returns the entry value. + */ +function mapCacheGet(key) { + return getMapData(this, key).get(key); +} + +/** + * Checks if a map value for `key` exists. + * + * @private + * @name has + * @memberOf MapCache + * @param {string} key The key of the entry to check. + * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`. + */ +function mapCacheHas(key) { + return getMapData(this, key).has(key); +} + +/** + * Sets the map `key` to `value`. + * + * @private + * @name set + * @memberOf MapCache + * @param {string} key The key of the value to set. + * @param {*} value The value to set. + * @returns {Object} Returns the map cache instance. + */ +function mapCacheSet(key, value) { + getMapData(this, key).set(key, value); + return this; +} + +// Add methods to `MapCache`. +MapCache.prototype.clear = mapCacheClear; +MapCache.prototype['delete'] = mapCacheDelete; +MapCache.prototype.get = mapCacheGet; +MapCache.prototype.has = mapCacheHas; +MapCache.prototype.set = mapCacheSet; + +/** + * Creates a stack cache object to store key-value pairs. + * + * @private + * @constructor + * @param {Array} [entries] The key-value pairs to cache. + */ +function Stack(entries) { + this.__data__ = new ListCache(entries); +} + +/** + * Removes all key-value entries from the stack. + * + * @private + * @name clear + * @memberOf Stack + */ +function stackClear() { + this.__data__ = new ListCache; +} + +/** + * Removes `key` and its value from the stack. + * + * @private + * @name delete + * @memberOf Stack + * @param {string} key The key of the value to remove. + * @returns {boolean} Returns `true` if the entry was removed, else `false`. + */ +function stackDelete(key) { + return this.__data__['delete'](key); +} + +/** + * Gets the stack value for `key`. + * + * @private + * @name get + * @memberOf Stack + * @param {string} key The key of the value to get. + * @returns {*} Returns the entry value. + */ +function stackGet(key) { + return this.__data__.get(key); +} + +/** + * Checks if a stack value for `key` exists. + * + * @private + * @name has + * @memberOf Stack + * @param {string} key The key of the entry to check. + * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`. + */ +function stackHas(key) { + return this.__data__.has(key); +} + +/** + * Sets the stack `key` to `value`. + * + * @private + * @name set + * @memberOf Stack + * @param {string} key The key of the value to set. + * @param {*} value The value to set. + * @returns {Object} Returns the stack cache instance. + */ +function stackSet(key, value) { + var cache = this.__data__; + if (cache instanceof ListCache) { + var pairs = cache.__data__; + if (!Map || (pairs.length < LARGE_ARRAY_SIZE - 1)) { + pairs.push([key, value]); + return this; + } + cache = this.__data__ = new MapCache(pairs); + } + cache.set(key, value); + return this; +} + +// Add methods to `Stack`. +Stack.prototype.clear = stackClear; +Stack.prototype['delete'] = stackDelete; +Stack.prototype.get = stackGet; +Stack.prototype.has = stackHas; +Stack.prototype.set = stackSet; + +/** + * Creates an array of the enumerable property names of the array-like `value`. + * + * @private + * @param {*} value The value to query. + * @param {boolean} inherited Specify returning inherited property names. + * @returns {Array} Returns the array of property names. + */ +function arrayLikeKeys(value, inherited) { + // Safari 8.1 makes `arguments.callee` enumerable in strict mode. + // Safari 9 makes `arguments.length` enumerable in strict mode. + var result = (isArray(value) || isArguments(value)) + ? baseTimes(value.length, String) + : []; + + var length = result.length, + skipIndexes = !!length; + + for (var key in value) { + if ((inherited || hasOwnProperty.call(value, key)) && + !(skipIndexes && (key == 'length' || isIndex(key, length)))) { + result.push(key); + } + } + return result; +} + +/** + * Assigns `value` to `key` of `object` if the existing value is not equivalent + * using [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero) + * for equality comparisons. + * + * @private + * @param {Object} object The object to modify. + * @param {string} key The key of the property to assign. + * @param {*} value The value to assign. + */ +function assignValue(object, key, value) { + var objValue = object[key]; + if (!(hasOwnProperty.call(object, key) && eq(objValue, value)) || + (value === undefined && !(key in object))) { + object[key] = value; + } +} + +/** + * Gets the index at which the `key` is found in `array` of key-value pairs. + * + * @private + * @param {Array} array The array to inspect. + * @param {*} key The key to search for. + * @returns {number} Returns the index of the matched value, else `-1`. + */ +function assocIndexOf(array, key) { + var length = array.length; + while (length--) { + if (eq(array[length][0], key)) { + return length; + } + } + return -1; +} + +/** + * The base implementation of `_.assign` without support for multiple sources + * or `customizer` functions. + * + * @private + * @param {Object} object The destination object. + * @param {Object} source The source object. + * @returns {Object} Returns `object`. + */ +function baseAssign(object, source) { + return object && copyObject(source, keys(source), object); +} + +/** + * The base implementation of `_.clone` and `_.cloneDeep` which tracks + * traversed objects. + * + * @private + * @param {*} value The value to clone. + * @param {boolean} [isDeep] Specify a deep clone. + * @param {boolean} [isFull] Specify a clone including symbols. + * @param {Function} [customizer] The function to customize cloning. + * @param {string} [key] The key of `value`. + * @param {Object} [object] The parent object of `value`. + * @param {Object} [stack] Tracks traversed objects and their clone counterparts. + * @returns {*} Returns the cloned value. + */ +function baseClone(value, isDeep, isFull, customizer, key, object, stack) { + var result; + if (customizer) { + result = object ? customizer(value, key, object, stack) : customizer(value); + } + if (result !== undefined) { + return result; + } + if (!isObject(value)) { + return value; + } + var isArr = isArray(value); + if (isArr) { + result = initCloneArray(value); + if (!isDeep) { + return copyArray(value, result); + } + } else { + var tag = getTag(value), + isFunc = tag == funcTag || tag == genTag; + + if (isBuffer(value)) { + return cloneBuffer(value, isDeep); + } + if (tag == objectTag || tag == argsTag || (isFunc && !object)) { + if (isHostObject(value)) { + return object ? value : {}; + } + result = initCloneObject(isFunc ? {} : value); + if (!isDeep) { + return copySymbols(value, baseAssign(result, value)); + } + } else { + if (!cloneableTags[tag]) { + return object ? value : {}; + } + result = initCloneByTag(value, tag, baseClone, isDeep); + } + } + // Check for circular references and return its corresponding clone. + stack || (stack = new Stack); + var stacked = stack.get(value); + if (stacked) { + return stacked; + } + stack.set(value, result); + + if (!isArr) { + var props = isFull ? getAllKeys(value) : keys(value); + } + arrayEach(props || value, function(subValue, key) { + if (props) { + key = subValue; + subValue = value[key]; + } + // Recursively populate clone (susceptible to call stack limits). + assignValue(result, key, baseClone(subValue, isDeep, isFull, customizer, key, value, stack)); + }); + return result; +} + +/** + * The base implementation of `_.create` without support for assigning + * properties to the created object. + * + * @private + * @param {Object} prototype The object to inherit from. + * @returns {Object} Returns the new object. + */ +function baseCreate(proto) { + return isObject(proto) ? objectCreate(proto) : {}; +} + +/** + * The base implementation of `getAllKeys` and `getAllKeysIn` which uses + * `keysFunc` and `symbolsFunc` to get the enumerable property names and + * symbols of `object`. + * + * @private + * @param {Object} object The object to query. + * @param {Function} keysFunc The function to get the keys of `object`. + * @param {Function} symbolsFunc The function to get the symbols of `object`. + * @returns {Array} Returns the array of property names and symbols. + */ +function baseGetAllKeys(object, keysFunc, symbolsFunc) { + var result = keysFunc(object); + return isArray(object) ? result : arrayPush(result, symbolsFunc(object)); +} + +/** + * The base implementation of `getTag`. + * + * @private + * @param {*} value The value to query. + * @returns {string} Returns the `toStringTag`. + */ +function baseGetTag(value) { + return objectToString.call(value); +} + +/** + * The base implementation of `_.isNative` without bad shim checks. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a native function, + * else `false`. + */ +function baseIsNative(value) { + if (!isObject(value) || isMasked(value)) { + return false; + } + var pattern = (isFunction(value) || isHostObject(value)) ? reIsNative : reIsHostCtor; + return pattern.test(toSource(value)); +} + +/** + * The base implementation of `_.keys` which doesn't treat sparse arrays as dense. + * + * @private + * @param {Object} object The object to query. + * @returns {Array} Returns the array of property names. + */ +function baseKeys(object) { + if (!isPrototype(object)) { + return nativeKeys(object); + } + var result = []; + for (var key in Object(object)) { + if (hasOwnProperty.call(object, key) && key != 'constructor') { + result.push(key); + } + } + return result; +} + +/** + * Creates a clone of `buffer`. + * + * @private + * @param {Buffer} buffer The buffer to clone. + * @param {boolean} [isDeep] Specify a deep clone. + * @returns {Buffer} Returns the cloned buffer. + */ +function cloneBuffer(buffer, isDeep) { + if (isDeep) { + return buffer.slice(); + } + var result = new buffer.constructor(buffer.length); + buffer.copy(result); + return result; +} + +/** + * Creates a clone of `arrayBuffer`. + * + * @private + * @param {ArrayBuffer} arrayBuffer The array buffer to clone. + * @returns {ArrayBuffer} Returns the cloned array buffer. + */ +function cloneArrayBuffer(arrayBuffer) { + var result = new arrayBuffer.constructor(arrayBuffer.byteLength); + new Uint8Array(result).set(new Uint8Array(arrayBuffer)); + return result; +} + +/** + * Creates a clone of `dataView`. + * + * @private + * @param {Object} dataView The data view to clone. + * @param {boolean} [isDeep] Specify a deep clone. + * @returns {Object} Returns the cloned data view. + */ +function cloneDataView(dataView, isDeep) { + var buffer = isDeep ? cloneArrayBuffer(dataView.buffer) : dataView.buffer; + return new dataView.constructor(buffer, dataView.byteOffset, dataView.byteLength); +} + +/** + * Creates a clone of `map`. + * + * @private + * @param {Object} map The map to clone. + * @param {Function} cloneFunc The function to clone values. + * @param {boolean} [isDeep] Specify a deep clone. + * @returns {Object} Returns the cloned map. + */ +function cloneMap(map, isDeep, cloneFunc) { + var array = isDeep ? cloneFunc(mapToArray(map), true) : mapToArray(map); + return arrayReduce(array, addMapEntry, new map.constructor); +} + +/** + * Creates a clone of `regexp`. + * + * @private + * @param {Object} regexp The regexp to clone. + * @returns {Object} Returns the cloned regexp. + */ +function cloneRegExp(regexp) { + var result = new regexp.constructor(regexp.source, reFlags.exec(regexp)); + result.lastIndex = regexp.lastIndex; + return result; +} + +/** + * Creates a clone of `set`. + * + * @private + * @param {Object} set The set to clone. + * @param {Function} cloneFunc The function to clone values. + * @param {boolean} [isDeep] Specify a deep clone. + * @returns {Object} Returns the cloned set. + */ +function cloneSet(set, isDeep, cloneFunc) { + var array = isDeep ? cloneFunc(setToArray(set), true) : setToArray(set); + return arrayReduce(array, addSetEntry, new set.constructor); +} + +/** + * Creates a clone of the `symbol` object. + * + * @private + * @param {Object} symbol The symbol object to clone. + * @returns {Object} Returns the cloned symbol object. + */ +function cloneSymbol(symbol) { + return symbolValueOf ? Object(symbolValueOf.call(symbol)) : {}; +} + +/** + * Creates a clone of `typedArray`. + * + * @private + * @param {Object} typedArray The typed array to clone. + * @param {boolean} [isDeep] Specify a deep clone. + * @returns {Object} Returns the cloned typed array. + */ +function cloneTypedArray(typedArray, isDeep) { + var buffer = isDeep ? cloneArrayBuffer(typedArray.buffer) : typedArray.buffer; + return new typedArray.constructor(buffer, typedArray.byteOffset, typedArray.length); +} + +/** + * Copies the values of `source` to `array`. + * + * @private + * @param {Array} source The array to copy values from. + * @param {Array} [array=[]] The array to copy values to. + * @returns {Array} Returns `array`. + */ +function copyArray(source, array) { + var index = -1, + length = source.length; + + array || (array = Array(length)); + while (++index < length) { + array[index] = source[index]; + } + return array; +} + +/** + * Copies properties of `source` to `object`. + * + * @private + * @param {Object} source The object to copy properties from. + * @param {Array} props The property identifiers to copy. + * @param {Object} [object={}] The object to copy properties to. + * @param {Function} [customizer] The function to customize copied values. + * @returns {Object} Returns `object`. + */ +function copyObject(source, props, object, customizer) { + object || (object = {}); + + var index = -1, + length = props.length; + + while (++index < length) { + var key = props[index]; + + var newValue = customizer + ? customizer(object[key], source[key], key, object, source) + : undefined; + + assignValue(object, key, newValue === undefined ? source[key] : newValue); + } + return object; +} + +/** + * Copies own symbol properties of `source` to `object`. + * + * @private + * @param {Object} source The object to copy symbols from. + * @param {Object} [object={}] The object to copy symbols to. + * @returns {Object} Returns `object`. + */ +function copySymbols(source, object) { + return copyObject(source, getSymbols(source), object); +} + +/** + * Creates an array of own enumerable property names and symbols of `object`. + * + * @private + * @param {Object} object The object to query. + * @returns {Array} Returns the array of property names and symbols. + */ +function getAllKeys(object) { + return baseGetAllKeys(object, keys, getSymbols); +} + +/** + * Gets the data for `map`. + * + * @private + * @param {Object} map The map to query. + * @param {string} key The reference key. + * @returns {*} Returns the map data. + */ +function getMapData(map, key) { + var data = map.__data__; + return isKeyable(key) + ? data[typeof key == 'string' ? 'string' : 'hash'] + : data.map; +} + +/** + * Gets the native function at `key` of `object`. + * + * @private + * @param {Object} object The object to query. + * @param {string} key The key of the method to get. + * @returns {*} Returns the function if it's native, else `undefined`. + */ +function getNative(object, key) { + var value = getValue(object, key); + return baseIsNative(value) ? value : undefined; +} + +/** + * Creates an array of the own enumerable symbol properties of `object`. + * + * @private + * @param {Object} object The object to query. + * @returns {Array} Returns the array of symbols. + */ +var getSymbols = nativeGetSymbols ? overArg(nativeGetSymbols, Object) : stubArray; + +/** + * Gets the `toStringTag` of `value`. + * + * @private + * @param {*} value The value to query. + * @returns {string} Returns the `toStringTag`. + */ +var getTag = baseGetTag; + +// Fallback for data views, maps, sets, and weak maps in IE 11, +// for data views in Edge < 14, and promises in Node.js. +if ((DataView && getTag(new DataView(new ArrayBuffer(1))) != dataViewTag) || + (Map && getTag(new Map) != mapTag) || + (Promise && getTag(Promise.resolve()) != promiseTag) || + (Set && getTag(new Set) != setTag) || + (WeakMap && getTag(new WeakMap) != weakMapTag)) { + getTag = function(value) { + var result = objectToString.call(value), + Ctor = result == objectTag ? value.constructor : undefined, + ctorString = Ctor ? toSource(Ctor) : undefined; + + if (ctorString) { + switch (ctorString) { + case dataViewCtorString: return dataViewTag; + case mapCtorString: return mapTag; + case promiseCtorString: return promiseTag; + case setCtorString: return setTag; + case weakMapCtorString: return weakMapTag; + } + } + return result; + }; +} + +/** + * Initializes an array clone. + * + * @private + * @param {Array} array The array to clone. + * @returns {Array} Returns the initialized clone. + */ +function initCloneArray(array) { + var length = array.length, + result = array.constructor(length); + + // Add properties assigned by `RegExp#exec`. + if (length && typeof array[0] == 'string' && hasOwnProperty.call(array, 'index')) { + result.index = array.index; + result.input = array.input; + } + return result; +} + +/** + * Initializes an object clone. + * + * @private + * @param {Object} object The object to clone. + * @returns {Object} Returns the initialized clone. + */ +function initCloneObject(object) { + return (typeof object.constructor == 'function' && !isPrototype(object)) + ? baseCreate(getPrototype(object)) + : {}; +} + +/** + * Initializes an object clone based on its `toStringTag`. + * + * **Note:** This function only supports cloning values with tags of + * `Boolean`, `Date`, `Error`, `Number`, `RegExp`, or `String`. + * + * @private + * @param {Object} object The object to clone. + * @param {string} tag The `toStringTag` of the object to clone. + * @param {Function} cloneFunc The function to clone values. + * @param {boolean} [isDeep] Specify a deep clone. + * @returns {Object} Returns the initialized clone. + */ +function initCloneByTag(object, tag, cloneFunc, isDeep) { + var Ctor = object.constructor; + switch (tag) { + case arrayBufferTag: + return cloneArrayBuffer(object); + + case boolTag: + case dateTag: + return new Ctor(+object); + + case dataViewTag: + return cloneDataView(object, isDeep); + + case float32Tag: case float64Tag: + case int8Tag: case int16Tag: case int32Tag: + case uint8Tag: case uint8ClampedTag: case uint16Tag: case uint32Tag: + return cloneTypedArray(object, isDeep); + + case mapTag: + return cloneMap(object, isDeep, cloneFunc); + + case numberTag: + case stringTag: + return new Ctor(object); + + case regexpTag: + return cloneRegExp(object); + + case setTag: + return cloneSet(object, isDeep, cloneFunc); + + case symbolTag: + return cloneSymbol(object); + } +} + +/** + * Checks if `value` is a valid array-like index. + * + * @private + * @param {*} value The value to check. + * @param {number} [length=MAX_SAFE_INTEGER] The upper bounds of a valid index. + * @returns {boolean} Returns `true` if `value` is a valid index, else `false`. + */ +function isIndex(value, length) { + length = length == null ? MAX_SAFE_INTEGER : length; + return !!length && + (typeof value == 'number' || reIsUint.test(value)) && + (value > -1 && value % 1 == 0 && value < length); +} + +/** + * Checks if `value` is suitable for use as unique object key. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is suitable, else `false`. + */ +function isKeyable(value) { + var type = typeof value; + return (type == 'string' || type == 'number' || type == 'symbol' || type == 'boolean') + ? (value !== '__proto__') + : (value === null); +} + +/** + * Checks if `func` has its source masked. + * + * @private + * @param {Function} func The function to check. + * @returns {boolean} Returns `true` if `func` is masked, else `false`. + */ +function isMasked(func) { + return !!maskSrcKey && (maskSrcKey in func); +} + +/** + * Checks if `value` is likely a prototype object. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a prototype, else `false`. + */ +function isPrototype(value) { + var Ctor = value && value.constructor, + proto = (typeof Ctor == 'function' && Ctor.prototype) || objectProto; + + return value === proto; +} + +/** + * Converts `func` to its source code. + * + * @private + * @param {Function} func The function to process. + * @returns {string} Returns the source code. + */ +function toSource(func) { + if (func != null) { + try { + return funcToString.call(func); + } catch (e) {} + try { + return (func + ''); + } catch (e) {} + } + return ''; +} + +/** + * This method is like `_.clone` except that it recursively clones `value`. + * + * @static + * @memberOf _ + * @since 1.0.0 + * @category Lang + * @param {*} value The value to recursively clone. + * @returns {*} Returns the deep cloned value. + * @see _.clone + * @example + * + * var objects = [{ 'a': 1 }, { 'b': 2 }]; + * + * var deep = _.cloneDeep(objects); + * console.log(deep[0] === objects[0]); + * // => false + */ +function cloneDeep(value) { + return baseClone(value, true, true); +} + +/** + * Performs a + * [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero) + * comparison between two values to determine if they are equivalent. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to compare. + * @param {*} other The other value to compare. + * @returns {boolean} Returns `true` if the values are equivalent, else `false`. + * @example + * + * var object = { 'a': 1 }; + * var other = { 'a': 1 }; + * + * _.eq(object, object); + * // => true + * + * _.eq(object, other); + * // => false + * + * _.eq('a', 'a'); + * // => true + * + * _.eq('a', Object('a')); + * // => false + * + * _.eq(NaN, NaN); + * // => true + */ +function eq(value, other) { + return value === other || (value !== value && other !== other); +} + +/** + * Checks if `value` is likely an `arguments` object. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an `arguments` object, + * else `false`. + * @example + * + * _.isArguments(function() { return arguments; }()); + * // => true + * + * _.isArguments([1, 2, 3]); + * // => false + */ +function isArguments(value) { + // Safari 8.1 makes `arguments.callee` enumerable in strict mode. + return isArrayLikeObject(value) && hasOwnProperty.call(value, 'callee') && + (!propertyIsEnumerable.call(value, 'callee') || objectToString.call(value) == argsTag); +} + +/** + * Checks if `value` is classified as an `Array` object. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an array, else `false`. + * @example + * + * _.isArray([1, 2, 3]); + * // => true + * + * _.isArray(document.body.children); + * // => false + * + * _.isArray('abc'); + * // => false + * + * _.isArray(_.noop); + * // => false + */ +var isArray = Array.isArray; + +/** + * Checks if `value` is array-like. A value is considered array-like if it's + * not a function and has a `value.length` that's an integer greater than or + * equal to `0` and less than or equal to `Number.MAX_SAFE_INTEGER`. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is array-like, else `false`. + * @example + * + * _.isArrayLike([1, 2, 3]); + * // => true + * + * _.isArrayLike(document.body.children); + * // => true + * + * _.isArrayLike('abc'); + * // => true + * + * _.isArrayLike(_.noop); + * // => false + */ +function isArrayLike(value) { + return value != null && isLength(value.length) && !isFunction(value); +} + +/** + * This method is like `_.isArrayLike` except that it also checks if `value` + * is an object. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an array-like object, + * else `false`. + * @example + * + * _.isArrayLikeObject([1, 2, 3]); + * // => true + * + * _.isArrayLikeObject(document.body.children); + * // => true + * + * _.isArrayLikeObject('abc'); + * // => false + * + * _.isArrayLikeObject(_.noop); + * // => false + */ +function isArrayLikeObject(value) { + return isObjectLike(value) && isArrayLike(value); +} + +/** + * Checks if `value` is a buffer. + * + * @static + * @memberOf _ + * @since 4.3.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a buffer, else `false`. + * @example + * + * _.isBuffer(new Buffer(2)); + * // => true + * + * _.isBuffer(new Uint8Array(2)); + * // => false + */ +var isBuffer = nativeIsBuffer || stubFalse; + +/** + * Checks if `value` is classified as a `Function` object. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a function, else `false`. + * @example + * + * _.isFunction(_); + * // => true + * + * _.isFunction(/abc/); + * // => false + */ +function isFunction(value) { + // The use of `Object#toString` avoids issues with the `typeof` operator + // in Safari 8-9 which returns 'object' for typed array and other constructors. + var tag = isObject(value) ? objectToString.call(value) : ''; + return tag == funcTag || tag == genTag; +} + +/** + * Checks if `value` is a valid array-like length. + * + * **Note:** This method is loosely based on + * [`ToLength`](http://ecma-international.org/ecma-262/7.0/#sec-tolength). + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a valid length, else `false`. + * @example + * + * _.isLength(3); + * // => true + * + * _.isLength(Number.MIN_VALUE); + * // => false + * + * _.isLength(Infinity); + * // => false + * + * _.isLength('3'); + * // => false + */ +function isLength(value) { + return typeof value == 'number' && + value > -1 && value % 1 == 0 && value <= MAX_SAFE_INTEGER; +} + +/** + * Checks if `value` is the + * [language type](http://www.ecma-international.org/ecma-262/7.0/#sec-ecmascript-language-types) + * of `Object`. (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`) + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an object, else `false`. + * @example + * + * _.isObject({}); + * // => true + * + * _.isObject([1, 2, 3]); + * // => true + * + * _.isObject(_.noop); + * // => true + * + * _.isObject(null); + * // => false + */ +function isObject(value) { + var type = typeof value; + return !!value && (type == 'object' || type == 'function'); +} + +/** + * Checks if `value` is object-like. A value is object-like if it's not `null` + * and has a `typeof` result of "object". + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is object-like, else `false`. + * @example + * + * _.isObjectLike({}); + * // => true + * + * _.isObjectLike([1, 2, 3]); + * // => true + * + * _.isObjectLike(_.noop); + * // => false + * + * _.isObjectLike(null); + * // => false + */ +function isObjectLike(value) { + return !!value && typeof value == 'object'; +} + +/** + * Creates an array of the own enumerable property names of `object`. + * + * **Note:** Non-object values are coerced to objects. See the + * [ES spec](http://ecma-international.org/ecma-262/7.0/#sec-object.keys) + * for more details. + * + * @static + * @since 0.1.0 + * @memberOf _ + * @category Object + * @param {Object} object The object to query. + * @returns {Array} Returns the array of property names. + * @example + * + * function Foo() { + * this.a = 1; + * this.b = 2; + * } + * + * Foo.prototype.c = 3; + * + * _.keys(new Foo); + * // => ['a', 'b'] (iteration order is not guaranteed) + * + * _.keys('hi'); + * // => ['0', '1'] + */ +function keys(object) { + return isArrayLike(object) ? arrayLikeKeys(object) : baseKeys(object); +} + +/** + * This method returns a new empty array. + * + * @static + * @memberOf _ + * @since 4.13.0 + * @category Util + * @returns {Array} Returns the new empty array. + * @example + * + * var arrays = _.times(2, _.stubArray); + * + * console.log(arrays); + * // => [[], []] + * + * console.log(arrays[0] === arrays[1]); + * // => false + */ +function stubArray() { + return []; +} + +/** + * This method returns `false`. + * + * @static + * @memberOf _ + * @since 4.13.0 + * @category Util + * @returns {boolean} Returns `false`. + * @example + * + * _.times(2, _.stubFalse); + * // => [false, false] + */ +function stubFalse() { + return false; +} + +export default cloneDeep; diff --git a/src/public/app/widgets/type_widgets/canvas-note-utils/lib/lodash.debounce.js b/src/public/app/widgets/type_widgets/canvas-note-utils/lib/lodash.debounce.js new file mode 100644 index 000000000..a930f7732 --- /dev/null +++ b/src/public/app/widgets/type_widgets/canvas-note-utils/lib/lodash.debounce.js @@ -0,0 +1,70 @@ +/** + * Returns a function, that, as long as it continues to be invoked, will not + * be triggered. The function will be called after it stops being called for + * N milliseconds. If `immediate` is passed, trigger the function on the + * leading edge, instead of the trailing. The function also has a property 'clear' + * that is a function which will clear the timer to prevent previously scheduled executions. + * + * @source underscore.js + * @see http://unscriptable.com/2009/03/20/debouncing-javascript-methods/ + * @param {Function} function to wrap + * @param {Number} timeout in ms (`100`) + * @param {Boolean} whether to execute at the beginning (`false`) + * @api public + */ +function debounce(func, wait, immediate){ + var timeout, args, context, timestamp, result; + if (null == wait) wait = 100; + + function later() { + var last = Date.now() - timestamp; + + if (last < wait && last >= 0) { + timeout = setTimeout(later, wait - last); + } else { + timeout = null; + if (!immediate) { + result = func.apply(context, args); + context = args = null; + } + } + }; + + var debounced = function(){ + context = this; + args = arguments; + timestamp = Date.now(); + var callNow = immediate && !timeout; + if (!timeout) timeout = setTimeout(later, wait); + if (callNow) { + result = func.apply(context, args); + context = args = null; + } + + return result; + }; + + debounced.clear = function() { + if (timeout) { + clearTimeout(timeout); + timeout = null; + } + }; + + debounced.flush = function() { + if (timeout) { + result = func.apply(context, args); + context = args = null; + + clearTimeout(timeout); + timeout = null; + } + }; + + return debounced; +}; + +// Adds compatibility for ES modules +debounce.debounce = debounce; + +export default debounce; diff --git a/src/public/app/widgets/type_widgets/canvas-note-utils/lib/lodash.throttle.js b/src/public/app/widgets/type_widgets/canvas-note-utils/lib/lodash.throttle.js new file mode 100644 index 000000000..91309ead0 --- /dev/null +++ b/src/public/app/widgets/type_widgets/canvas-note-utils/lib/lodash.throttle.js @@ -0,0 +1,439 @@ +/** + * lodash (Custom Build) + * Build: `lodash modularize exports="npm" -o ./` + * Copyright jQuery Foundation and other contributors + * Released under MIT license + * Based on Underscore.js 1.8.3 + * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors + */ + +/** Used as the `TypeError` message for "Functions" methods. */ +var FUNC_ERROR_TEXT = 'Expected a function'; + +/** Used as references for various `Number` constants. */ +var NAN = 0 / 0; + +/** `Object#toString` result references. */ +var symbolTag = '[object Symbol]'; + +/** Used to match leading and trailing whitespace. */ +var reTrim = /^\s+|\s+$/g; + +/** Used to detect bad signed hexadecimal string values. */ +var reIsBadHex = /^[-+]0x[0-9a-f]+$/i; + +/** Used to detect binary string values. */ +var reIsBinary = /^0b[01]+$/i; + +/** Used to detect octal string values. */ +var reIsOctal = /^0o[0-7]+$/i; + +/** Built-in method references without a dependency on `root`. */ +var freeParseInt = parseInt; + +/** Detect free variable `global` from Node.js. */ +var freeGlobal = typeof global == 'object' && global && global.Object === Object && global; + +/** Detect free variable `self`. */ +var freeSelf = typeof self == 'object' && self && self.Object === Object && self; + +/** Used as a reference to the global object. */ +var root = freeGlobal || freeSelf || Function('return this')(); + +/** Used for built-in method references. */ +var objectProto = Object.prototype; + +/** + * Used to resolve the + * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring) + * of values. + */ +var objectToString = objectProto.toString; + +/* Built-in method references for those with the same name as other `lodash` methods. */ +var nativeMax = Math.max, + nativeMin = Math.min; + +/** + * Gets the timestamp of the number of milliseconds that have elapsed since + * the Unix epoch (1 January 1970 00:00:00 UTC). + * + * @static + * @memberOf _ + * @since 2.4.0 + * @category Date + * @returns {number} Returns the timestamp. + * @example + * + * _.defer(function(stamp) { + * console.log(_.now() - stamp); + * }, _.now()); + * // => Logs the number of milliseconds it took for the deferred invocation. + */ +var now = function() { + return root.Date.now(); +}; + +/** + * Creates a debounced function that delays invoking `func` until after `wait` + * milliseconds have elapsed since the last time the debounced function was + * invoked. The debounced function comes with a `cancel` method to cancel + * delayed `func` invocations and a `flush` method to immediately invoke them. + * Provide `options` to indicate whether `func` should be invoked on the + * leading and/or trailing edge of the `wait` timeout. The `func` is invoked + * with the last arguments provided to the debounced function. Subsequent + * calls to the debounced function return the result of the last `func` + * invocation. + * + * **Note:** If `leading` and `trailing` options are `true`, `func` is + * invoked on the trailing edge of the timeout only if the debounced function + * is invoked more than once during the `wait` timeout. + * + * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred + * until to the next tick, similar to `setTimeout` with a timeout of `0`. + * + * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/) + * for details over the differences between `_.debounce` and `_.throttle`. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Function + * @param {Function} func The function to debounce. + * @param {number} [wait=0] The number of milliseconds to delay. + * @param {Object} [options={}] The options object. + * @param {boolean} [options.leading=false] + * Specify invoking on the leading edge of the timeout. + * @param {number} [options.maxWait] + * The maximum time `func` is allowed to be delayed before it's invoked. + * @param {boolean} [options.trailing=true] + * Specify invoking on the trailing edge of the timeout. + * @returns {Function} Returns the new debounced function. + * @example + * + * // Avoid costly calculations while the window size is in flux. + * jQuery(window).on('resize', _.debounce(calculateLayout, 150)); + * + * // Invoke `sendMail` when clicked, debouncing subsequent calls. + * jQuery(element).on('click', _.debounce(sendMail, 300, { + * 'leading': true, + * 'trailing': false + * })); + * + * // Ensure `batchLog` is invoked once after 1 second of debounced calls. + * var debounced = _.debounce(batchLog, 250, { 'maxWait': 1000 }); + * var source = new EventSource('/stream'); + * jQuery(source).on('message', debounced); + * + * // Cancel the trailing debounced invocation. + * jQuery(window).on('popstate', debounced.cancel); + */ +function debounce(func, wait, options) { + var lastArgs, + lastThis, + maxWait, + result, + timerId, + lastCallTime, + lastInvokeTime = 0, + leading = false, + maxing = false, + trailing = true; + + if (typeof func != 'function') { + throw new TypeError(FUNC_ERROR_TEXT); + } + wait = toNumber(wait) || 0; + if (isObject(options)) { + leading = !!options.leading; + maxing = 'maxWait' in options; + maxWait = maxing ? nativeMax(toNumber(options.maxWait) || 0, wait) : maxWait; + trailing = 'trailing' in options ? !!options.trailing : trailing; + } + + function invokeFunc(time) { + var args = lastArgs, + thisArg = lastThis; + + lastArgs = lastThis = undefined; + lastInvokeTime = time; + result = func.apply(thisArg, args); + return result; + } + + function leadingEdge(time) { + // Reset any `maxWait` timer. + lastInvokeTime = time; + // Start the timer for the trailing edge. + timerId = setTimeout(timerExpired, wait); + // Invoke the leading edge. + return leading ? invokeFunc(time) : result; + } + + function remainingWait(time) { + var timeSinceLastCall = time - lastCallTime, + timeSinceLastInvoke = time - lastInvokeTime, + result = wait - timeSinceLastCall; + + return maxing ? nativeMin(result, maxWait - timeSinceLastInvoke) : result; + } + + function shouldInvoke(time) { + var timeSinceLastCall = time - lastCallTime, + timeSinceLastInvoke = time - lastInvokeTime; + + // Either this is the first call, activity has stopped and we're at the + // trailing edge, the system time has gone backwards and we're treating + // it as the trailing edge, or we've hit the `maxWait` limit. + return (lastCallTime === undefined || (timeSinceLastCall >= wait) || + (timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait)); + } + + function timerExpired() { + var time = now(); + if (shouldInvoke(time)) { + return trailingEdge(time); + } + // Restart the timer. + timerId = setTimeout(timerExpired, remainingWait(time)); + } + + function trailingEdge(time) { + timerId = undefined; + + // Only invoke if we have `lastArgs` which means `func` has been + // debounced at least once. + if (trailing && lastArgs) { + return invokeFunc(time); + } + lastArgs = lastThis = undefined; + return result; + } + + function cancel() { + if (timerId !== undefined) { + clearTimeout(timerId); + } + lastInvokeTime = 0; + lastArgs = lastCallTime = lastThis = timerId = undefined; + } + + function flush() { + return timerId === undefined ? result : trailingEdge(now()); + } + + function debounced() { + var time = now(), + isInvoking = shouldInvoke(time); + + lastArgs = arguments; + lastThis = this; + lastCallTime = time; + + if (isInvoking) { + if (timerId === undefined) { + return leadingEdge(lastCallTime); + } + if (maxing) { + // Handle invocations in a tight loop. + timerId = setTimeout(timerExpired, wait); + return invokeFunc(lastCallTime); + } + } + if (timerId === undefined) { + timerId = setTimeout(timerExpired, wait); + } + return result; + } + debounced.cancel = cancel; + debounced.flush = flush; + return debounced; +} + +/** + * Creates a throttled function that only invokes `func` at most once per + * every `wait` milliseconds. The throttled function comes with a `cancel` + * method to cancel delayed `func` invocations and a `flush` method to + * immediately invoke them. Provide `options` to indicate whether `func` + * should be invoked on the leading and/or trailing edge of the `wait` + * timeout. The `func` is invoked with the last arguments provided to the + * throttled function. Subsequent calls to the throttled function return the + * result of the last `func` invocation. + * + * **Note:** If `leading` and `trailing` options are `true`, `func` is + * invoked on the trailing edge of the timeout only if the throttled function + * is invoked more than once during the `wait` timeout. + * + * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred + * until to the next tick, similar to `setTimeout` with a timeout of `0`. + * + * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/) + * for details over the differences between `_.throttle` and `_.debounce`. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Function + * @param {Function} func The function to throttle. + * @param {number} [wait=0] The number of milliseconds to throttle invocations to. + * @param {Object} [options={}] The options object. + * @param {boolean} [options.leading=true] + * Specify invoking on the leading edge of the timeout. + * @param {boolean} [options.trailing=true] + * Specify invoking on the trailing edge of the timeout. + * @returns {Function} Returns the new throttled function. + * @example + * + * // Avoid excessively updating the position while scrolling. + * jQuery(window).on('scroll', _.throttle(updatePosition, 100)); + * + * // Invoke `renewToken` when the click event is fired, but not more than once every 5 minutes. + * var throttled = _.throttle(renewToken, 300000, { 'trailing': false }); + * jQuery(element).on('click', throttled); + * + * // Cancel the trailing throttled invocation. + * jQuery(window).on('popstate', throttled.cancel); + */ +function throttle(func, wait, options) { + var leading = true, + trailing = true; + + if (typeof func != 'function') { + throw new TypeError(FUNC_ERROR_TEXT); + } + if (isObject(options)) { + leading = 'leading' in options ? !!options.leading : leading; + trailing = 'trailing' in options ? !!options.trailing : trailing; + } + return debounce(func, wait, { + 'leading': leading, + 'maxWait': wait, + 'trailing': trailing + }); +} + +/** + * Checks if `value` is the + * [language type](http://www.ecma-international.org/ecma-262/7.0/#sec-ecmascript-language-types) + * of `Object`. (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`) + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an object, else `false`. + * @example + * + * _.isObject({}); + * // => true + * + * _.isObject([1, 2, 3]); + * // => true + * + * _.isObject(_.noop); + * // => true + * + * _.isObject(null); + * // => false + */ +function isObject(value) { + var type = typeof value; + return !!value && (type == 'object' || type == 'function'); +} + +/** + * Checks if `value` is object-like. A value is object-like if it's not `null` + * and has a `typeof` result of "object". + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is object-like, else `false`. + * @example + * + * _.isObjectLike({}); + * // => true + * + * _.isObjectLike([1, 2, 3]); + * // => true + * + * _.isObjectLike(_.noop); + * // => false + * + * _.isObjectLike(null); + * // => false + */ +function isObjectLike(value) { + return !!value && typeof value == 'object'; +} + +/** + * Checks if `value` is classified as a `Symbol` primitive or object. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a symbol, else `false`. + * @example + * + * _.isSymbol(Symbol.iterator); + * // => true + * + * _.isSymbol('abc'); + * // => false + */ +function isSymbol(value) { + return typeof value == 'symbol' || + (isObjectLike(value) && objectToString.call(value) == symbolTag); +} + +/** + * Converts `value` to a number. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to process. + * @returns {number} Returns the number. + * @example + * + * _.toNumber(3.2); + * // => 3.2 + * + * _.toNumber(Number.MIN_VALUE); + * // => 5e-324 + * + * _.toNumber(Infinity); + * // => Infinity + * + * _.toNumber('3.2'); + * // => 3.2 + */ +function toNumber(value) { + if (typeof value == 'number') { + return value; + } + if (isSymbol(value)) { + return NAN; + } + if (isObject(value)) { + var other = typeof value.valueOf == 'function' ? value.valueOf() : value; + value = isObject(other) ? (other + '') : other; + } + if (typeof value != 'string') { + return value === 0 ? value : +value; + } + value = value.replace(reTrim, ''); + var isBinary = reIsBinary.test(value); + return (isBinary || reIsOctal.test(value)) + ? freeParseInt(value.slice(2), isBinary ? 2 : 8) + : (reIsBadHex.test(value) ? NAN : +value); +} + +export default throttle; diff --git a/src/public/app/widgets/type_widgets/canvas-note-utils/lib/sleep.js b/src/public/app/widgets/type_widgets/canvas-note-utils/lib/sleep.js new file mode 100644 index 000000000..1077c579a --- /dev/null +++ b/src/public/app/widgets/type_widgets/canvas-note-utils/lib/sleep.js @@ -0,0 +1,7 @@ +export const sleep = (time) => { + return new Promise((resolve) => { + setTimeout(resolve, time); + }); +}; + +export default sleep; \ No newline at end of file diff --git a/src/public/app/widgets/type_widgets/canvas-note-utils/utils.js b/src/public/app/widgets/type_widgets/canvas-note-utils/utils.js new file mode 100644 index 000000000..bd623871c --- /dev/null +++ b/src/public/app/widgets/type_widgets/canvas-note-utils/utils.js @@ -0,0 +1,46 @@ +function addDemoContent(canvas) { + var comicSansText = new fabric.Text("I'm in Comic Sans", { + fontFamily: 'Comic Sans MS', + left: 100, + top: 100, + }); + canvas.add(comicSansText); + var demoLine = new fabric.Line([30, 30, 150, 210], { + fill: 'green', + stroke: 'blue', + strokeWidth: 5, + selectable: false, + evented: false, + }); + canvas.add(demoLine); +} + +function addBg(canvas) { + // Add BG + var bg = new fabric.Rect({ + width: 1500, + height: 1500, + // stroke: 'Fuchsia', + // strokeWidth: 10, + fill: '#FCFFEB', + evented: false, + selectable: false, + }); + // bg.fill = new fabric.Pattern( + // { + // source: + // 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAASElEQVQ4y2NkYGD4z0A6+M3AwMBKrGJWBgYGZiibEQ0zIInDaCaoelYyHYcX/GeitomjBo4aOGrgQBj4b7RwGFwGsjAwMDAAAD2/BjgezgsZAAAAAElFTkSuQmCC', + // }, + // function () { + // bg.dirty = true; + // canvas.requestRenderAll(); + // }, + // ); + bg.canvas = canvas; + canvas.backgroundImage = bg; +} + +export { + addBg, + addDemoContent, +}; diff --git a/src/public/app/widgets/type_widgets/canvas_note.js b/src/public/app/widgets/type_widgets/canvas_note.js index 1d5cfe49b..ec32be0bb 100644 --- a/src/public/app/widgets/type_widgets/canvas_note.js +++ b/src/public/app/widgets/type_widgets/canvas_note.js @@ -1,78 +1,66 @@ import libraryLoader from "../../services/library_loader.js"; import TypeWidget from "./type_widget.js"; import appContext from "../../services/app_context.js"; -import EraserBrushFactory from './canvas-note-utils/EraserBrush.js'; +import {InfiniteCanvas} from './canvas-note-utils/infinite-drawing-canvas.js'; +import { initButtons, initPens } from './canvas-note-utils/gui.js'; +import _debounce from './canvas-note-utils/lib/lodash.debounce.js'; const TPL = ` -
- - -
- -
- - - 30
- - -
- - -
- - - 0
- - - 0
+
+
+ +
+
-
- - - - - - - -
-
- -
-
`; + +
+ + + + Pens: + + + + +
+ + + + + + + Shapes: + +
+ Mode: + + + + +
+ Canvas: + Enlarge px + + + + + Crop: + +
+ + +
+`; export default class CanvasNoteTypeWidget extends TypeWidget { + constructor() { + super(); + + this.initCanvas = this.initCanvas.bind(this); + } static getType() { return "canvas-note"; } @@ -84,16 +72,17 @@ export default class CanvasNoteTypeWidget extends TypeWidget { .requireLibrary(libraryLoader.CANVAS_NOTE) .then(() => { console.log("fabric.js-loaded") - this.initFabric(); + this.initCanvas(); }); return this.$widget; } async doRefresh(note) { + // get note from backend and put into canvas const noteComplement = await this.tabContext.getNoteComplement(); - if (this.__canvas && noteComplement.content) { - this.__canvas.loadFromJSON(noteComplement.content); + if (this.canvas && noteComplement.content) { + this.canvas.loadFromJSON(noteComplement.content); } console.log('doRefresh', note, noteComplement); } @@ -111,246 +100,22 @@ export default class CanvasNoteTypeWidget extends TypeWidget { this.spacedUpdate.scheduleUpdate(); } - initFabric() { - const self = this; - const canvas = this.__canvas = new fabric.Canvas('c', { - isDrawingMode: true - }); - fabric.Object.prototype.transparentCorners = false; + initCanvas() { + const myCanvas = new InfiniteCanvas( + $('.canvasElement'), + $('#parentContainer'), + $('#canvasContainer'), + ); - canvas.on('after:render', () => { - self.saveData(); - }); + this.infiniteCanvas = myCanvas.initFabric(); + this.canvas = this.infiniteCanvas.$canvas; - window.addEventListener('resize', resizeCanvas, false); + this.canvas.setWidth(myCanvas.width); + this.canvas.setHeight(myCanvas.height); - function resizeCanvas() { - const width = $('.note-detail-canvas-note').width(); - const height = $('.note-detail-canvas-note').height() - console.log(`setting canvas to ${width} x ${height}px`) - canvas.setWidth(width); - canvas.setHeight(height); - canvas.renderAll(); - } - - // resize on init - resizeCanvas(); - - const {EraserBrush} = EraserBrushFactory(fabric); - - var drawingModeEl = $('#drawing-mode'), - drawingOptionsEl = $('#drawing-mode-options'), - drawingColorEl = $('#drawing-color'), - drawingShadowColorEl = $('#drawing-shadow-color'), - drawingLineWidthEl = $('#drawing-line-width'), - drawingShadowWidth = $('#drawing-shadow-width'), - drawingShadowOffset = $('#drawing-shadow-offset'), - saveCanvas = $('#save-canvas'), - refreshCanvas = $('#refresh-canvas'), - clearEl = $('#clear-canvas'), - undo = $('#undo'), - redo = $('#redo') - ; - - const deletedItems = []; - - undo.on('click', function () { - // Source: https://stackoverflow.com/a/28666556 - var lastItemIndex = (canvas.getObjects().length - 1); - var item = canvas.item(lastItemIndex); - - deletedItems.push(item); - // if(item.get('type') === 'path') { - canvas.remove(item); - canvas.renderAll(); - // } - }) - - redo.on('click', function () { - const lastItem = deletedItems.pop(); - if (lastItem) { - canvas.add(lastItem); - canvas.renderAll(); - } - }) - - clearEl.on('click', function () { - console.log('cE-oC'); - canvas.clear() - }); - - saveCanvas.on('click', function () { - console.log('sC-oC'); - const canvasContent = canvas.toJSON(); - console.log('Canvas JSON', canvasContent); - self.saveData(); - }); - refreshCanvas.on('click', function () { - console.log('rC-oC'); - self.doRefresh('no note entity needed for refresh, only noteComplement'); - }); - drawingModeEl.on('click', function () { - canvas.isDrawingMode = !canvas.isDrawingMode; - if (canvas.isDrawingMode) { - drawingModeEl.html('Cancel drawing mode'); - drawingOptionsEl.css('display', ''); - } else { - drawingModeEl.html('Enter drawing mode'); - drawingOptionsEl.css('display', 'none'); - } - }); - // - // if (fabric.PatternBrush) { - // var vLinePatternBrush = new fabric.PatternBrush(canvas); - // vLinePatternBrush.getPatternSrc = function () { - // - // var patternCanvas = fabric.document.createElement('canvas'); - // patternCanvas.width = patternCanvas.height = 10; - // var ctx = patternCanvas.getContext('2d'); - // - // ctx.strokeStyle = this.color; - // ctx.lineWidth = 5; - // ctx.beginPath(); - // ctx.moveTo(0, 5); - // ctx.lineTo(10, 5); - // ctx.closePath(); - // ctx.stroke(); - // - // return patternCanvas; - // }; - // - // var hLinePatternBrush = new fabric.PatternBrush(canvas); - // hLinePatternBrush.getPatternSrc = function () { - // - // var patternCanvas = fabric.document.createElement('canvas'); - // patternCanvas.width = patternCanvas.height = 10; - // var ctx = patternCanvas.getContext('2d'); - // - // ctx.strokeStyle = this.color; - // ctx.lineWidth = 5; - // ctx.beginPath(); - // ctx.moveTo(5, 0); - // ctx.lineTo(5, 10); - // ctx.closePath(); - // ctx.stroke(); - // - // return patternCanvas; - // }; - // - // var squarePatternBrush = new fabric.PatternBrush(canvas); - // squarePatternBrush.getPatternSrc = function () { - // - // var squareWidth = 10, squareDistance = 2; - // - // var patternCanvas = fabric.document.createElement('canvas'); - // patternCanvas.width = patternCanvas.height = squareWidth + squareDistance; - // var ctx = patternCanvas.getContext('2d'); - // - // ctx.fillStyle = this.color; - // ctx.fillRect(0, 0, squareWidth, squareWidth); - // - // return patternCanvas; - // }; - // - // var diamondPatternBrush = new fabric.PatternBrush(canvas); - // diamondPatternBrush.getPatternSrc = function () { - // - // var squareWidth = 10, squareDistance = 5; - // var patternCanvas = fabric.document.createElement('canvas'); - // var rect = new fabric.Rect({ - // width: squareWidth, - // height: squareWidth, - // angle: 45, - // fill: this.color - // }); - // - // var canvasWidth = rect.getBoundingRect().width; - // - // patternCanvas.width = patternCanvas.height = canvasWidth + squareDistance; - // rect.set({left: canvasWidth / 2, top: canvasWidth / 2}); - // - // var ctx = patternCanvas.getContext('2d'); - // rect.render(ctx); - // - // return patternCanvas; - // }; - // - // // var img = new Image(); - // // img.src = './libraries/canvas-note/honey_im_subtle.png'; - // - // // var texturePatternBrush = new fabric.PatternBrush(canvas); - // // texturePatternBrush.source = img; - // } - - $('#drawing-mode-selector').change(function () { - if (false) { - } - // else if (this.value === 'hline') { - // canvas.freeDrawingBrush = vLinePatternBrush; - // } else if (this.value === 'vline') { - // canvas.freeDrawingBrush = hLinePatternBrush; - // } else if (this.value === 'square') { - // canvas.freeDrawingBrush = squarePatternBrush; - // } else if (this.value === 'diamond') { - // canvas.freeDrawingBrush = diamondPatternBrush; - // } - // else if (this.value === 'texture') { - // canvas.freeDrawingBrush = texturePatternBrush; - // } - else if (this.value === "Eraser") { - // to use it, just set the brush - const eraserBrush = new EraserBrush(canvas); - eraserBrush.width = parseInt(drawingLineWidthEl.val(), 10) || 1; - eraserBrush.color = 'rgb(236,195,195)'; // erser works with opacity! - canvas.freeDrawingBrush = eraserBrush; - canvas.isDrawingMode = true; - } else { - canvas.freeDrawingBrush = new fabric[this.value + 'Brush'](canvas); - canvas.freeDrawingBrush.color = drawingColorEl.val(); - canvas.freeDrawingBrush.width = parseInt(drawingLineWidthEl.val(), 10) || 1; - canvas.freeDrawingBrush.shadow = new fabric.Shadow({ - blur: parseInt(drawingShadowWidth.val(), 10) || 0, - offsetX: 0, - offsetY: 0, - affectStroke: true, - color: drawingShadowColorEl.val(), - }); - } - - - }); - - drawingColorEl.change(function () { - canvas.freeDrawingBrush.color = this.value; - }); - drawingShadowColorEl.change(function () { - canvas.freeDrawingBrush.shadow.color = this.value; - }) - drawingLineWidthEl.change(function () { - canvas.freeDrawingBrush.width = parseInt(this.value, 10) || 1; - drawingLineWidthEl.prev().html(this.value); - }); - drawingShadowWidth.change(function () { - canvas.freeDrawingBrush.shadow.blur = parseInt(this.value, 10) || 0; - drawingShadowWidth.prev().html(this.value); - }); - drawingShadowOffset.change(function () { - canvas.freeDrawingBrush.shadow.offsetX = parseInt(this.value, 10) || 0; - canvas.freeDrawingBrush.shadow.offsetY = parseInt(this.value, 10) || 0; - drawingShadowOffset.prev().html(this.value); - }) - - if (canvas.freeDrawingBrush) { - canvas.freeDrawingBrush.color = drawingColorEl.value; - canvas.freeDrawingBrush.width = parseInt(drawingLineWidthEl.value, 10) || 1; - canvas.freeDrawingBrush.shadow = new fabric.Shadow({ - blur: parseInt(drawingShadowWidth.value, 10) || 0, - offsetX: 0, - offsetY: 0, - affectStroke: true, - color: drawingShadowColorEl.value, - }); - } + // Buttons + initButtons(this.infiniteCanvas); + initPens(this.infiniteCanvas); } }