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 = ` +
+ + +
+ +
+ + + 30
+ + +
+ + +
+ + + 0
+ + + 0
+
+
+ + + + + + + +
+
+ +
+
`; + +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) {