basic infinite-drawing-canvas integration

This commit is contained in:
thfrei 2020-11-07 23:06:34 +01:00 committed by Tom
parent 2f2d8327e4
commit 5ebe717da8
11 changed files with 3287 additions and 422 deletions

View file

@ -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;

View file

@ -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;

View 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;
});
};

View file

@ -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 };

View file

@ -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

View file

@ -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;

View file

@ -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;

View file

@ -0,0 +1,7 @@
export const sleep = (time) => {
return new Promise((resolve) => {
setTimeout(resolve, time);
});
};
export default sleep;

View file

@ -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,
};

View file

@ -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);
}
}