mirror of
https://github.com/zadam/trilium.git
synced 2024-09-23 00:56:00 +08:00
basic infinite-drawing-canvas integration
This commit is contained in:
parent
2f2d8327e4
commit
5ebe717da8
|
@ -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;
|
||||
|
|
|
@ -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;
|
193
src/public/app/widgets/type_widgets/canvas-note-utils/gui.js
Normal file
193
src/public/app/widgets/type_widgets/canvas-note-utils/gui.js
Normal file
|
@ -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(`<img src="${imageSrc}" height="100" />`);
|
||||
$('#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;
|
||||
});
|
||||
};
|
|
@ -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 };
|
|
@ -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;
|
File diff suppressed because it is too large
Load diff
|
@ -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;
|
|
@ -0,0 +1,439 @@
|
|||
/**
|
||||
* lodash (Custom Build) <https://lodash.com/>
|
||||
* Build: `lodash modularize exports="npm" -o ./`
|
||||
* Copyright jQuery Foundation and other contributors <https://jquery.org/>
|
||||
* Released under MIT license <https://lodash.com/license>
|
||||
* Based on Underscore.js 1.8.3 <http://underscorejs.org/LICENSE>
|
||||
* 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;
|
|
@ -0,0 +1,7 @@
|
|||
export const sleep = (time) => {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, time);
|
||||
});
|
||||
};
|
||||
|
||||
export default sleep;
|
|
@ -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,
|
||||
};
|
|
@ -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 = `
|
||||
<div class="note-detail-canvas-note note-detail-printable">
|
||||
|
||||
<style>
|
||||
#drawing-mode {
|
||||
margin-bottom: 10px;
|
||||
vertical-align: top;
|
||||
}
|
||||
#drawing-mode-options {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
margin-bottom: 10px;
|
||||
margin-top: 10px;
|
||||
background: #f5f2f0;
|
||||
padding: 10px;
|
||||
}
|
||||
label {
|
||||
display: inline-block; width: 130px;
|
||||
}
|
||||
.info {
|
||||
display: inline-block;
|
||||
width: 25px;
|
||||
background: #ffc;
|
||||
}
|
||||
#bd-wrapper {
|
||||
min-width: 1500px;
|
||||
}
|
||||
</style>
|
||||
<div id="drawing-mode-options">
|
||||
<label for="drawing-mode-selector">Mode:</label>
|
||||
<select id="drawing-mode-selector">
|
||||
<option>Pencil</option>
|
||||
<option>Circle</option>
|
||||
<option>Spray</option>
|
||||
<option>Pattern</option>
|
||||
<option>Eraser</option>
|
||||
</select><br>
|
||||
|
||||
<label for="drawing-line-width">Line width:</label>
|
||||
<span class="info">30</span><input type="range" value="30" min="0" max="150" id="drawing-line-width"><br>
|
||||
|
||||
<label for="drawing-color">Line color:</label>
|
||||
<input type="color" value="#005E7A" id="drawing-color"><br>
|
||||
|
||||
<label for="drawing-shadow-color">Shadow color:</label>
|
||||
<input type="color" value="#005E7A" id="drawing-shadow-color"><br>
|
||||
|
||||
<label for="drawing-shadow-width">Shadow width:</label>
|
||||
<span class="info">0</span><input type="range" value="0" min="0" max="50" id="drawing-shadow-width"><br>
|
||||
|
||||
<label for="drawing-shadow-offset">Shadow offset:</label>
|
||||
<span class="info">0</span><input type="range" value="0" min="0" max="50" id="drawing-shadow-offset"><br>
|
||||
<div id="parentContainer" class="note-detail-canvas-note note-detail-printable"
|
||||
style="resize: both; overflow:auto; width: 100%; height: 80%; border: 4px double red;">
|
||||
<div id="canvasContainer" style="width: 1500px; height: 1500px;">
|
||||
<canvas id="c" class="canvasElement" style="border:1px solid #aaa; width: 1500px; height: 1500px"></canvas>
|
||||
</div>
|
||||
<br />
|
||||
</div>
|
||||
<div style="display: block; position: relative; margin-left: 10px">
|
||||
<button id="drawing-mode" class="btn btn-info">Cancel drawing mode</button>
|
||||
<button id="clear-canvas" class="btn btn-info">Clear</button>
|
||||
<button id="save-canvas" class="btn btn-info">Save</button>
|
||||
<button id="refresh-canvas" class="btn btn-info">Refresh</button>
|
||||
<button id="undo"><-</button>
|
||||
<button id="redo">-></button>
|
||||
|
||||
</div>
|
||||
<div id="canvasWrapper" style="display: inline-block; background-color: red; border: 3px double black">
|
||||
<canvas id="c" style="border:1px solid #aaa; position: absolute; touch-action:none; user-select: none;"></canvas>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
<div id="pens-and-markers">
|
||||
<!-- Drawing:-->
|
||||
<!-- <button id="undo" disabled><i class='bx bx-undo'></i></button>-->
|
||||
<!-- <button id="redo" disabled><i class='bx bx-redo'></i></button>-->
|
||||
Pens:
|
||||
<button id="pen-1" class="btn btn-info"><i class='bx bx-pencil' style="border-left: 3px solid black"></i></button>
|
||||
<button id="pen-2" class="btn btn-info"><i class='bx bx-pencil' style="border-left: 3px solid red"></i></button>
|
||||
<button id="pen-3" class="btn btn-info"><i class='bx bx-pencil' style="border-left: 3px solid green"></i></button>
|
||||
<button id="pen-4" class="btn btn-info"><i class='bx bx-pencil' style="border-left: 3px solid blue"></i></button>
|
||||
<br />
|
||||
<button id="marker-1" class="btn btn-info"><i class='bx bx-pen' style="border-left: 7px solid yellow"></i></button>
|
||||
<button id="marker-2" class="btn btn-info"><i class='bx bx-pen' style="border-left: 7px solid wheat"></i></button>
|
||||
<button id="marker-3" class="btn btn-info"><i class='bx bx-pen'
|
||||
style="border-left: 7px solid rgba(51,204,0, 0.5)"></i></button>
|
||||
<button id="marker-4" class="btn btn-info"><i class='bx bx-pen' style="border-left: 7px solid skyblue"></i></button>
|
||||
<button id="eraser" class="btn btn-info"><i class='bx bx-eraser' style="border-left: 7px solid black"></i></button>
|
||||
<button id="eraser-path" class="btn btn-info"><i class='bx bx-eraser' style="border-left: 7px dashed rgba(236,195,220, 20)"><i class='bx bx-shape-polygon' ></i></i></button>
|
||||
Shapes:
|
||||
<button id="text-1" class="btn btn-info"><i class='bx bx-text' style="border-left: 3px solid black"></i></button>
|
||||
<br />
|
||||
Mode:
|
||||
<button id="mode-select" class="btn btn-info"><i class='bx bx-pointer'></i></button>
|
||||
<!-- <button id="mode-1" class="btn btn-info"><i class='bx bx-mouse'></i></button> -->
|
||||
<button id="mode-drawWithTouch" class="btn btn-info"><i class='bx bxs-hand-up'></i> Draw with Touch</button>
|
||||
<!-- <button id="mode-3" class="btn btn-info"><i class='bx bx-stats'></i>Pen-Touch-Mouse</button> -->
|
||||
<br />
|
||||
Canvas:
|
||||
Enlarge <input type="number" value=100 id="enlargeValue" style="width: 60px" />px
|
||||
<button id="enlarge-left" class="btn btn-info"><i class='bx bxs-dock-left'></i></button>
|
||||
<button id="enlarge-top" class="btn btn-info"><i class='bx bxs-dock-left bx-rotate-90' ></i></button>
|
||||
<button id="enlarge-bottom" class="btn btn-info"><i class='bx bxs-dock-left bx-rotate-270' ></i></button>
|
||||
<button id="enlarge-right" class="btn btn-info"><i class='bx bxs-dock-left bx-rotate-180' ></i></button>
|
||||
Crop:
|
||||
<button id="crop-canvas" class="btn btn-info"><i class='bx bx-crop'></i></button>
|
||||
<br />
|
||||
<button id="zoom-100" class="btn btn-info">Zoom 100%</button>
|
||||
<button id="clear-canvas" class="btn btn-info">Clear</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue