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) => {
|
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
|
* On mouseup after drawing the path on contextTop canvas
|
||||||
*
|
* we use the points captured to create an new fabric path object
|
||||||
* Made it so that the bound is calculated on the original only
|
* and add it to the fabric canvas.
|
||||||
*
|
|
||||||
* 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, {
|
_finalizeAndAddPath: async function () {
|
||||||
original: null,
|
var ctx = this.canvas.contextTop;
|
||||||
erasedPath: null,
|
ctx.closePath();
|
||||||
initialize: function (original, erasedPath, options, isAlreadyGrouped) {
|
if (this.decimate) {
|
||||||
this.original = original;
|
this._points = this.decimatePoints(this._points, this.decimate);
|
||||||
this.erasedPath = erasedPath;
|
}
|
||||||
this.callSuper('initialize', [this.original, this.erasedPath], options, isAlreadyGrouped);
|
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) {
|
// use globalCompositeOperation to 'fake' eraser
|
||||||
const aX = [],
|
var path = this.createPath(pathData);
|
||||||
aY = [],
|
path.globalCompositeOperation = 'destination-out';
|
||||||
props = ['tr', 'br', 'bl', 'tl'],
|
path.selectable = false;
|
||||||
jLen = props.length,
|
path.evented = false;
|
||||||
ignoreZoom = true;
|
path.absolutePositioned = true;
|
||||||
|
|
||||||
let o = this.original;
|
// grab all the objects that intersects with the path, filter out objects
|
||||||
o.setCoords(ignoreZoom);
|
// that are not desired, such as Text and IText
|
||||||
for (let j = 0; j < jLen; j++) {
|
// otherwise text might get erased (under some circumstances, this might be desired?!)
|
||||||
const prop = props[j];
|
const objects = this.canvas.getObjects().filter((obj) => {
|
||||||
aX.push(o.oCoords[prop].x);
|
if (obj instanceof fabric.Textbox) return false;
|
||||||
aY.push(o.oCoords[prop].y);
|
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);
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
return { EraserBrush, ErasedGroup };
|
||||||
* 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};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default EraserBrushFactory;
|
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 libraryLoader from "../../services/library_loader.js";
|
||||||
import TypeWidget from "./type_widget.js";
|
import TypeWidget from "./type_widget.js";
|
||||||
import appContext from "../../services/app_context.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 = `
|
const TPL = `
|
||||||
<div class="note-detail-canvas-note note-detail-printable">
|
<div id="parentContainer" class="note-detail-canvas-note note-detail-printable"
|
||||||
|
style="resize: both; overflow:auto; width: 100%; height: 80%; border: 4px double red;">
|
||||||
<style>
|
<div id="canvasContainer" style="width: 1500px; height: 1500px;">
|
||||||
#drawing-mode {
|
<canvas id="c" class="canvasElement" style="border:1px solid #aaa; width: 1500px; height: 1500px"></canvas>
|
||||||
margin-bottom: 10px;
|
</div>
|
||||||
vertical-align: top;
|
<br />
|
||||||
}
|
|
||||||
#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>
|
</div>
|
||||||
<div style="display: block; position: relative; margin-left: 10px">
|
|
||||||
<button id="drawing-mode" class="btn btn-info">Cancel drawing mode</button>
|
<div id="pens-and-markers">
|
||||||
<button id="clear-canvas" class="btn btn-info">Clear</button>
|
<!-- Drawing:-->
|
||||||
<button id="save-canvas" class="btn btn-info">Save</button>
|
<!-- <button id="undo" disabled><i class='bx bx-undo'></i></button>-->
|
||||||
<button id="refresh-canvas" class="btn btn-info">Refresh</button>
|
<!-- <button id="redo" disabled><i class='bx bx-redo'></i></button>-->
|
||||||
<button id="undo"><-</button>
|
Pens:
|
||||||
<button id="redo">-></button>
|
<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>
|
||||||
</div>
|
<button id="pen-3" class="btn btn-info"><i class='bx bx-pencil' style="border-left: 3px solid green"></i></button>
|
||||||
<div id="canvasWrapper" style="display: inline-block; background-color: red; border: 3px double black">
|
<button id="pen-4" class="btn btn-info"><i class='bx bx-pencil' style="border-left: 3px solid blue"></i></button>
|
||||||
<canvas id="c" style="border:1px solid #aaa; position: absolute; touch-action:none; user-select: none;"></canvas>
|
<br />
|
||||||
</div>
|
<button id="marker-1" class="btn btn-info"><i class='bx bx-pen' style="border-left: 7px solid yellow"></i></button>
|
||||||
</div>`;
|
<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 {
|
export default class CanvasNoteTypeWidget extends TypeWidget {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.initCanvas = this.initCanvas.bind(this);
|
||||||
|
}
|
||||||
static getType() {
|
static getType() {
|
||||||
return "canvas-note";
|
return "canvas-note";
|
||||||
}
|
}
|
||||||
|
@ -84,16 +72,17 @@ export default class CanvasNoteTypeWidget extends TypeWidget {
|
||||||
.requireLibrary(libraryLoader.CANVAS_NOTE)
|
.requireLibrary(libraryLoader.CANVAS_NOTE)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
console.log("fabric.js-loaded")
|
console.log("fabric.js-loaded")
|
||||||
this.initFabric();
|
this.initCanvas();
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.$widget;
|
return this.$widget;
|
||||||
}
|
}
|
||||||
|
|
||||||
async doRefresh(note) {
|
async doRefresh(note) {
|
||||||
|
// get note from backend and put into canvas
|
||||||
const noteComplement = await this.tabContext.getNoteComplement();
|
const noteComplement = await this.tabContext.getNoteComplement();
|
||||||
if (this.__canvas && noteComplement.content) {
|
if (this.canvas && noteComplement.content) {
|
||||||
this.__canvas.loadFromJSON(noteComplement.content);
|
this.canvas.loadFromJSON(noteComplement.content);
|
||||||
}
|
}
|
||||||
console.log('doRefresh', note, noteComplement);
|
console.log('doRefresh', note, noteComplement);
|
||||||
}
|
}
|
||||||
|
@ -111,246 +100,22 @@ export default class CanvasNoteTypeWidget extends TypeWidget {
|
||||||
this.spacedUpdate.scheduleUpdate();
|
this.spacedUpdate.scheduleUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
initFabric() {
|
initCanvas() {
|
||||||
const self = this;
|
const myCanvas = new InfiniteCanvas(
|
||||||
const canvas = this.__canvas = new fabric.Canvas('c', {
|
$('.canvasElement'),
|
||||||
isDrawingMode: true
|
$('#parentContainer'),
|
||||||
});
|
$('#canvasContainer'),
|
||||||
fabric.Object.prototype.transparentCorners = false;
|
);
|
||||||
|
|
||||||
canvas.on('after:render', () => {
|
this.infiniteCanvas = myCanvas.initFabric();
|
||||||
self.saveData();
|
this.canvas = this.infiniteCanvas.$canvas;
|
||||||
});
|
|
||||||
|
|
||||||
window.addEventListener('resize', resizeCanvas, false);
|
this.canvas.setWidth(myCanvas.width);
|
||||||
|
this.canvas.setHeight(myCanvas.height);
|
||||||
|
|
||||||
function resizeCanvas() {
|
// Buttons
|
||||||
const width = $('.note-detail-canvas-note').width();
|
initButtons(this.infiniteCanvas);
|
||||||
const height = $('.note-detail-canvas-note').height()
|
initPens(this.infiniteCanvas);
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue