From 0469962c5e8b96831b382479cf8210ab756c7794 Mon Sep 17 00:00:00 2001
From: Tom Free <7283497+thfrei@users.noreply.github.com>
Date: Thu, 25 Nov 2021 13:45:58 +0100
Subject: [PATCH] wip: canvas-note patch
Conflicts:
src/public/app/services/library_loader.js
src/public/app/services/tree_context_menu.js
src/public/app/widgets/note_actions.js
src/services/consistency_checks.js
src/services/utils.js
---
src/public/app/services/library_loader.js | 12 +-
src/public/app/services/tree_context_menu.js | 3 +-
.../canvas-note-utils/EraserBrush.js | 128 +++++++
.../app/widgets/type_widgets/canvas_note.js | 356 ++++++++++++++++++
src/routes/api/notes.js | 2 +
src/services/backend_script_api.js | 2 +-
src/services/export/single.js | 2 +-
src/services/notes.js | 4 +-
src/services/utils.js | 4 +-
9 files changed, 505 insertions(+), 8 deletions(-)
create mode 100644 src/public/app/widgets/type_widgets/canvas-note-utils/EraserBrush.js
create mode 100644 src/public/app/widgets/type_widgets/canvas_note.js
diff --git a/src/public/app/services/library_loader.js b/src/public/app/services/library_loader.js
index b6f0dbc8e..b1b6d1911 100644
--- a/src/public/app/services/library_loader.js
+++ b/src/public/app/services/library_loader.js
@@ -56,6 +56,15 @@ const MERMAID = {
js: [ "libraries/mermaid.min.js" ]
}
+const CANVAS_NOTE = {
+ js: [
+ "libraries/canvas-note/fabric.4.0.0-beta.12.min.js",
+ ],
+ // css: [
+ // "stylesheets/somestyle.css"
+ // ]
+};
+
async function requireLibrary(library) {
if (library.css) {
library.css.map(cssUrl => requireCss(cssUrl));
@@ -106,5 +115,6 @@ export default {
KATEX,
WHEEL_ZOOM,
FORCE_GRAPH,
- MERMAID
+ MERMAID,
+ CANVAS_NOTE
}
diff --git a/src/public/app/services/tree_context_menu.js b/src/public/app/services/tree_context_menu.js
index f22de134b..c27266111 100644
--- a/src/public/app/services/tree_context_menu.js
+++ b/src/public/app/services/tree_context_menu.js
@@ -33,7 +33,8 @@ class TreeContextMenu {
{ title: "Note Map", command: command, type: "note-map", uiIcon: "map-alt" },
{ title: "Render HTML note", command: command, type: "render", uiIcon: "extension" },
{ title: "Book", command: command, type: "book", uiIcon: "book" },
- { title: "Mermaid diagram", command: command, type: "mermaid", uiIcon: "selection" }
+ { title: "Mermaid diagram", command: command, type: "mermaid", uiIcon: "selection" },
+ { title: "Canvas Note", command: command, type: "canvas-note", uiIcon: "map-alt" },
];
}
diff --git a/src/public/app/widgets/type_widgets/canvas-note-utils/EraserBrush.js b/src/public/app/widgets/type_widgets/canvas-note-utils/EraserBrush.js
new file mode 100644
index 000000000..1e966788f
--- /dev/null
+++ b/src/public/app/widgets/type_widgets/canvas-note-utils/EraserBrush.js
@@ -0,0 +1,128 @@
+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.oCoords[prop].x);
+ aY.push(o.oCoords[prop].y);
+ }
+
+ 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, {
+
+ /**
+ * 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;
diff --git a/src/public/app/widgets/type_widgets/canvas_note.js b/src/public/app/widgets/type_widgets/canvas_note.js
new file mode 100644
index 000000000..1d5cfe49b
--- /dev/null
+++ b/src/public/app/widgets/type_widgets/canvas_note.js
@@ -0,0 +1,356 @@
+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';
+
+
+const TPL = `
+
`;
+
+export default class CanvasNoteTypeWidget extends TypeWidget {
+ static getType() {
+ return "canvas-note";
+ }
+
+ doRender() {
+ this.$widget = $(TPL);
+
+ libraryLoader
+ .requireLibrary(libraryLoader.CANVAS_NOTE)
+ .then(() => {
+ console.log("fabric.js-loaded")
+ this.initFabric();
+ });
+
+ return this.$widget;
+ }
+
+ async doRefresh(note) {
+ const noteComplement = await this.tabContext.getNoteComplement();
+ if (this.__canvas && noteComplement.content) {
+ this.__canvas.loadFromJSON(noteComplement.content);
+ }
+ console.log('doRefresh', note, noteComplement);
+ }
+
+ /**
+ * Function gets data that will be sent via spacedUpdate.scheduleUpdate();
+ */
+ getContent() {
+ const content = JSON.stringify(this.__canvas.toJSON());
+ console.log('gC', content);
+ return content;
+ }
+
+ saveData() {
+ this.spacedUpdate.scheduleUpdate();
+ }
+
+ initFabric() {
+ const self = this;
+ const canvas = this.__canvas = new fabric.Canvas('c', {
+ isDrawingMode: true
+ });
+ fabric.Object.prototype.transparentCorners = false;
+
+ canvas.on('after:render', () => {
+ self.saveData();
+ });
+
+ window.addEventListener('resize', resizeCanvas, false);
+
+ 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,
+ });
+ }
+ }
+}
+
diff --git a/src/routes/api/notes.js b/src/routes/api/notes.js
index f602189e2..29f0e3508 100644
--- a/src/routes/api/notes.js
+++ b/src/routes/api/notes.js
@@ -217,6 +217,8 @@ function changeTitle(req) {
noteService.triggerNoteTitleChanged(note);
}
+ console.log(note, await note.getContent());
+
return note;
}
diff --git a/src/services/backend_script_api.js b/src/services/backend_script_api.js
index 757c067bb..305a39c94 100644
--- a/src/services/backend_script_api.js
+++ b/src/services/backend_script_api.js
@@ -210,7 +210,7 @@ function BackendScriptApi(currentNote, apiParams) {
* @property {string} parentNoteId - MANDATORY
* @property {string} title - MANDATORY
* @property {string|buffer} content - MANDATORY
- * @property {string} type - text, code, file, image, search, book, relation-map - MANDATORY
+ * @property {string} type - text, code, file, image, search, book, relation-map, canvas-note - MANDATORY
* @property {string} mime - value is derived from default mimes for type
* @property {boolean} isProtected - default is false
* @property {boolean} isExpanded - default is false
diff --git a/src/services/export/single.js b/src/services/export/single.js
index 2937b40d7..f7ab87dc4 100644
--- a/src/services/export/single.js
+++ b/src/services/export/single.js
@@ -41,7 +41,7 @@ function exportSingleNote(taskContext, branch, format, res) {
extension = mimeTypes.extension(note.mime) || 'code';
mime = note.mime;
}
- else if (note.type === 'relation-map' || note.type === 'search') {
+ else if (note.type === 'relation-map' || note.type === 'canas-note' || note.type === 'search') {
payload = content;
extension = 'json';
mime = 'application/json';
diff --git a/src/services/notes.js b/src/services/notes.js
index 5609e0582..4d62ae274 100644
--- a/src/services/notes.js
+++ b/src/services/notes.js
@@ -52,7 +52,7 @@ function deriveMime(type, mime) {
mime = 'text/html';
} else if (type === 'code' || type === 'mermaid') {
mime = 'text/plain';
- } else if (['relation-map', 'search'].includes(type)) {
+ } else if (['relation-map', 'search', 'canvas-note'].includes(type)) {
mime = 'application/json';
} else if (['render', 'book'].includes(type)) {
mime = '';
@@ -83,7 +83,7 @@ function copyChildAttributes(parentNote, childNote) {
* - {string} parentNoteId
* - {string} title
* - {*} content
- * - {string} type - text, code, file, image, search, book, relation-map, render
+ * - {string} type - text, code, file, image, search, book, relation-map, canvas-note, render
*
* Following are optional (have defaults)
* - {string} mime - value is derived from default mimes for type
diff --git a/src/services/utils.js b/src/services/utils.js
index f4f31c69e..d50716381 100644
--- a/src/services/utils.js
+++ b/src/services/utils.js
@@ -168,7 +168,7 @@ const STRING_MIME_TYPES = [
function isStringNote(type, mime) {
// render and book are string note in the sense that they are expected to contain empty string
- return ["text", "code", "relation-map", "search", "render", "book", "mermaid"].includes(type)
+ return ["text", "code", "relation-map", "search", "render", "book", "mermaid", "canvas-note"].includes(type)
|| mime.startsWith('text/')
|| STRING_MIME_TYPES.includes(mime);
}
@@ -192,7 +192,7 @@ function formatDownloadTitle(filename, type, mime) {
if (type === 'text') {
return filename + '.html';
- } else if (['relation-map', 'search'].includes(type)) {
+ } else if (['relation-map', 'canvas-note', 'search'].includes(type)) {
return filename + '.json';
} else {
if (!mime) {