Index: lams_build/conf/whiteboard/scripts/s_whiteboard.js =================================================================== diff -u -r1cb71fb7cfd13ce1de802d0a069ebe849466a7fc -r63061fed615977a3a7e768fa11cf4e7f0db9dade --- lams_build/conf/whiteboard/scripts/s_whiteboard.js (.../s_whiteboard.js) (revision 1cb71fb7cfd13ce1de802d0a069ebe849466a7fc) +++ lams_build/conf/whiteboard/scripts/s_whiteboard.js (.../s_whiteboard.js) (revision 63061fed615977a3a7e768fa11cf4e7f0db9dade) @@ -59,6 +59,7 @@ if (!savedUndos[wid]) { savedUndos[wid] = []; } + console.log("undo wid: " + wid, savedUndos[wid]); let savedBoard = this.loadStoredData(wid); for (var i = savedUndos[wid].length - 1; i >= 0; i--) { if (savedUndos[wid][i]["username"] == username) { @@ -161,6 +162,15 @@ return; } savedBoards[targetWid] = sourceData.slice(); + + // LAMS: after load prefix author so his steps can not be undone + for (var i = 0; i < savedBoards[targetWid].length; i++) { + let username = savedBoards[targetWid][i]["username"]; + if (username && !username.startsWith('authored-')) { + savedBoards[targetWid][i]["username"] = 'authored-' + username; + } + } + this.saveToDB(targetWid); }, saveData: function(wid, data, processEmbeddedImages) { Index: lams_build/conf/whiteboard/src/js/whiteboard.js =================================================================== diff -u --- lams_build/conf/whiteboard/src/js/whiteboard.js (revision 0) +++ lams_build/conf/whiteboard/src/js/whiteboard.js (revision 63061fed615977a3a7e768fa11cf4e7f0db9dade) @@ -0,0 +1,1523 @@ +import { dom } from "@fortawesome/fontawesome-svg-core"; +import Point from "./classes/Point"; +import ReadOnlyService from "./services/ReadOnlyService"; +import InfoService from "./services/InfoService"; +import ThrottlingService from "./services/ThrottlingService"; +import ConfigService from "./services/ConfigService"; +import html2canvas from "html2canvas"; + +const RAD_TO_DEG = 180.0 / Math.PI; +const DEG_TO_RAD = Math.PI / 180.0; +const _45_DEG_IN_RAD = 45 * DEG_TO_RAD; + +const whiteboard = { + canvas: null, + ctx: null, + drawcolor: "black", + previousToolHtmlElem: null, // useful for handling read-only mode + tool: "mouse", + thickness: 4, + /** + * @type Point + */ + prevPos: new Point(0, 0), + /** + * @type Point + */ + startCoords: new Point(0, 0), + drawFlag: false, + oldGCO: null, + mouseover: false, + lineCap: "round", //butt, square + backgroundGrid: null, + canvasElement: null, + cursorContainer: null, + imgContainer: null, + svgContainer: null, //For draw prev + mouseOverlay: null, + ownCursor: null, + penSmoothLastCoords: [], + svgLine: null, + svgRect: null, + svgCirle: null, + drawBuffer: [], + undoBuffer: [], + drawId: 0, //Used for undo/redo functions + imgDragActive: false, + latestActiveTextBoxId: false, //The id of the latest clicked Textbox (for font and color change) + pressedKeys: {}, + settings: { + whiteboardId: "0", + username: "unknown", + sendFunction: null, + backgroundGridUrl: "./images/gb_grid.png", + }, + lastPointerSentTime: 0, + /** + * @type Point + */ + lastPointerPosition: new Point(0, 0), + loadWhiteboard: function (whiteboardContainer, newSettings) { + const svgns = "http://www.w3.org/2000/svg"; + const _this = this; + for (const i in newSettings) { + this.settings[i] = newSettings[i]; + } + this.settings["username"] = this.settings["username"].replace(/[^0-9a-z]/gi, ""); + + //background grid (repeating image) and smallest screen indication + _this.backgroundGrid = $( + `
` + ); + // container for background images + _this.imgContainer = $( + '
' + ); + // whiteboard canvas + _this.canvasElement = $( + '' + ); + // SVG container holding drawing or moving previews + _this.svgContainer = $( + '' + ); + // drag and drop indicator, hidden by default + _this.dropIndicator = $( + '
' + ); + // container for other users cursors + _this.cursorContainer = $( + '
' + ); + // container for texts by users + _this.textContainer = $( + '
' + ); + // mouse overlay for draw callbacks + _this.mouseOverlay = $( + '
' + ); + + $(whiteboardContainer) + .append(_this.backgroundGrid) + .append(_this.imgContainer) + .append(_this.canvasElement) + .append(_this.svgContainer) + .append(_this.dropIndicator) + .append(_this.cursorContainer) + .append(_this.textContainer) + .append(_this.mouseOverlay); + + // render newly added icons + dom.i2svg(); + + this.canvas = $("#whiteboardCanvas")[0]; + this.canvas.height = $(window).height(); + this.canvas.width = $(window).width(); + this.ctx = this.canvas.getContext("2d"); + this.oldGCO = this.ctx.globalCompositeOperation; + + window.addEventListener("resize", function () { + // Handle resize + const dbCp = JSON.parse(JSON.stringify(_this.drawBuffer)); // Copy the buffer + _this.canvas.width = $(window).width(); + _this.canvas.height = $(window).height(); // Set new canvas height + _this.drawBuffer = []; + _this.textContainer.empty(); + _this.loadData(dbCp); // draw old content in + }); + + $(_this.mouseOverlay).on("mousedown touchstart", function (e) { + _this.mousedown(e); + }); + + _this.mousedown = function (e) { + if (_this.imgDragActive || _this.drawFlag) { + return; + } + if (ReadOnlyService.readOnlyActive) return; + + _this.drawFlag = true; + + const currentPos = Point.fromEvent(e); + + if (_this.tool === "pen") { + _this.penSmoothLastCoords = [ + currentPos.x, + currentPos.y, + currentPos.x, + currentPos.y, + currentPos.x, + currentPos.y, + ]; + } else if (_this.tool === "eraser") { + _this.drawEraserLine( + currentPos.x, + currentPos.y, + currentPos.x, + currentPos.y, + _this.thickness + ); + _this.sendFunction({ + t: _this.tool, + d: [currentPos.x, currentPos.y, currentPos.x, currentPos.y], + th: _this.thickness, + }); + } else if (_this.tool === "line") { + _this.startCoords = currentPos; + _this.svgLine = document.createElementNS(svgns, "line"); + _this.svgLine.setAttribute("stroke", "gray"); + _this.svgLine.setAttribute("stroke-dasharray", "5, 5"); + _this.svgLine.setAttribute("x1", currentPos.x); + _this.svgLine.setAttribute("y1", currentPos.y); + _this.svgLine.setAttribute("x2", currentPos.x); + _this.svgLine.setAttribute("y2", currentPos.y); + _this.svgContainer.append(_this.svgLine); + } else if (_this.tool === "rect" || _this.tool === "recSelect") { + _this.svgContainer.find("rect").remove(); + _this.svgRect = document.createElementNS(svgns, "rect"); + _this.svgRect.setAttribute("stroke", "gray"); + _this.svgRect.setAttribute("stroke-dasharray", "5, 5"); + _this.svgRect.setAttribute("style", "fill-opacity:0.0;"); + _this.svgRect.setAttribute("x", currentPos.x); + _this.svgRect.setAttribute("y", currentPos.y); + _this.svgRect.setAttribute("width", 0); + _this.svgRect.setAttribute("height", 0); + _this.svgContainer.append(_this.svgRect); + _this.startCoords = currentPos; + } else if (_this.tool === "circle") { + _this.svgCirle = document.createElementNS(svgns, "circle"); + _this.svgCirle.setAttribute("stroke", "gray"); + _this.svgCirle.setAttribute("stroke-dasharray", "5, 5"); + _this.svgCirle.setAttribute("style", "fill-opacity:0.0;"); + _this.svgCirle.setAttribute("cx", currentPos.x); + _this.svgCirle.setAttribute("cy", currentPos.y); + _this.svgCirle.setAttribute("r", 0); + _this.svgContainer.append(_this.svgCirle); + _this.startCoords = currentPos; + } + + _this.prevPos = currentPos; + }; + + _this.textContainer.on("mousemove touchmove", function (e) { + e.preventDefault(); + + if (_this.imgDragActive || !$(e.target).hasClass("textcontainer")) { + return; + } + if (ReadOnlyService.readOnlyActive) return; + + const currentPos = Point.fromEvent(e); + + ThrottlingService.throttle(currentPos, () => { + _this.lastPointerPosition = currentPos; + _this.sendFunction({ + t: "cursor", + event: "move", + d: [currentPos.x, currentPos.y], + username: _this.settings.username, + }); + }); + }); + + _this.mouseOverlay.on("mousemove touchmove", function (e) { + e.preventDefault(); + if (ReadOnlyService.readOnlyActive) return; + _this.triggerMouseMove(e); + }); + + _this.mouseOverlay.on("mouseup touchend touchcancel", function (e) { + _this.mouseup(e); + }); + + _this.mouseup = function (e) { + if (_this.imgDragActive) { + return; + } + if (ReadOnlyService.readOnlyActive) return; + _this.drawFlag = false; + _this.drawId++; + _this.ctx.globalCompositeOperation = _this.oldGCO; + + let currentPos = Point.fromEvent(e); + + if (currentPos.isZeroZero) { + _this.sendFunction({ + t: "cursor", + event: "out", + username: _this.settings.username, + }); + } + + if (_this.tool === "line") { + if (_this.pressedKeys.shift) { + currentPos = _this.getRoundedAngles(currentPos); + } + _this.drawPenLine( + currentPos.x, + currentPos.y, + _this.startCoords.x, + _this.startCoords.y, + _this.drawcolor, + _this.thickness + ); + _this.sendFunction({ + t: _this.tool, + d: [currentPos.x, currentPos.y, _this.startCoords.x, _this.startCoords.y], + c: _this.drawcolor, + th: _this.thickness, + }); + _this.svgContainer.find("line").remove(); + } else if (_this.tool === "pen") { + _this.drawId--; + _this.pushPointSmoothPen(currentPos.x, currentPos.y); + _this.drawId++; + } else if (_this.tool === "rect") { + if (_this.pressedKeys.shift) { + if ( + (currentPos.x - _this.startCoords.x) * + (currentPos.y - _this.startCoords.y) > + 0 + ) { + currentPos = new Point( + currentPos.x, + _this.startCoords.y + (currentPos.x - _this.startCoords.x) + ); + } else { + currentPos = new Point( + currentPos.x, + _this.startCoords.y - (currentPos.x - _this.startCoords.x) + ); + } + } + _this.drawRec( + _this.startCoords.x, + _this.startCoords.y, + currentPos.x, + currentPos.y, + _this.drawcolor, + _this.thickness + ); + _this.sendFunction({ + t: _this.tool, + d: [_this.startCoords.x, _this.startCoords.y, currentPos.x, currentPos.y], + c: _this.drawcolor, + th: _this.thickness, + }); + _this.svgContainer.find("rect").remove(); + } else if (_this.tool === "circle") { + const r = currentPos.distTo(_this.startCoords); + _this.drawCircle( + _this.startCoords.x, + _this.startCoords.y, + r, + _this.drawcolor, + _this.thickness + ); + _this.sendFunction({ + t: _this.tool, + d: [_this.startCoords.x, _this.startCoords.y, r], + c: _this.drawcolor, + th: _this.thickness, + }); + _this.svgContainer.find("circle").remove(); + } else if (_this.tool === "recSelect") { + _this.imgDragActive = true; + if (_this.pressedKeys.shift) { + if ( + (currentPos.x - _this.startCoords.x) * + (currentPos.y - _this.startCoords.y) > + 0 + ) { + currentPos = new Point( + currentPos.x, + _this.startCoords.y + (currentPos.x - _this.startCoords.x) + ); + } else { + currentPos = new Point( + currentPos.x, + _this.startCoords.y - (currentPos.x - _this.startCoords.x) + ); + } + } + + const width = Math.abs(_this.startCoords.x - currentPos.x); + const height = Math.abs(_this.startCoords.y - currentPos.y); + const left = + _this.startCoords.x < currentPos.x ? _this.startCoords.x : currentPos.x; + const top = _this.startCoords.y < currentPos.y ? _this.startCoords.y : currentPos.y; + _this.mouseOverlay.css({ cursor: "default" }); + const imgDiv = $( + `
+ +
+ + +
+
` + ); + const dragCanvas = $(imgDiv).find("canvas"); + const dragOutOverlay = $( + `
` + ); + _this.mouseOverlay.append(dragOutOverlay); + _this.mouseOverlay.append(imgDiv); + + const destCanvasContext = dragCanvas[0].getContext("2d"); + destCanvasContext.drawImage( + _this.canvas, + left, + top, + width, + height, + 0, + 0, + width, + height + ); + imgDiv + .find(".xCanvasBtn") + .off("click") + .click(function () { + _this.imgDragActive = false; + _this.refreshCursorAppearance(); + imgDiv.remove(); + dragOutOverlay.remove(); + }); + imgDiv + .find(".addToCanvasBtn") + .off("click") + .click(function () { + _this.imgDragActive = false; + _this.refreshCursorAppearance(); + const p = imgDiv.position(); + const leftT = Math.round(p.left * 100) / 100; + const topT = Math.round(p.top * 100) / 100; + _this.drawId++; + _this.sendFunction({ + t: _this.tool, + d: [left, top, leftT, topT, width, height], + }); + _this.dragCanvasRectContent(left, top, leftT, topT, width, height); + imgDiv.remove(); + dragOutOverlay.remove(); + }); + imgDiv.draggable(); + _this.svgContainer.find("rect").remove(); + } + }; + + _this.mouseOverlay.on("mouseout", function (e) { + if (ReadOnlyService.readOnlyActive) return; + _this.triggerMouseOut(); + }); + + _this.mouseOverlay.on("mouseover", function (e) { + if (ReadOnlyService.readOnlyActive) return; + _this.triggerMouseOver(); + }); + + // On text container click (Add a new textbox) + _this.textContainer.on("click", function (e) { + const currentPos = Point.fromEvent(e); + const fontsize = _this.thickness * 0.5; + const txId = "tx" + +new Date(); + const isStickyNote = _this.tool === "stickynote"; + _this.sendFunction({ + t: "addTextBox", + d: [ + _this.drawcolor, + _this.textboxBackgroundColor, + fontsize, + currentPos.x, + currentPos.y, + txId, + isStickyNote, + ], + }); + _this.addTextBox( + _this.drawcolor, + _this.textboxBackgroundColor, + fontsize, + currentPos.x, + currentPos.y, + txId, + isStickyNote, + true + ); + }); + }, + /** + * For drawing lines at 0,45,90° .... + * @param {Point} currentPos + * @returns {Point} + */ + getRoundedAngles: function (currentPos) { + const { startCoords } = this; + + // these transformations operate in the standard coordinate system + // y goes from bottom to up, x goes left to right + const dx = currentPos.x - startCoords.x; // browser x is reversed + const dy = startCoords.y - currentPos.y; + + const angle = Math.atan2(dy, dx); + const angle45 = Math.round(angle / _45_DEG_IN_RAD) * _45_DEG_IN_RAD; + + const dist = currentPos.distTo(startCoords); + let outX = startCoords.x + dist * Math.cos(angle45); + let outY = startCoords.y - dist * Math.sin(angle45); + + return new Point(outX, outY); + }, + triggerMouseMove: function (e) { + const _this = this; + if (_this.imgDragActive) { + return; + } + + let currentPos = Point.fromEvent(e); + + window.requestAnimationFrame(function () { + // update position + currentPos = Point.fromEvent(e); + + if (_this.drawFlag) { + if (_this.tool === "pen") { + _this.pushPointSmoothPen(currentPos.x, currentPos.y); + } else if (_this.tool === "eraser") { + _this.drawEraserLine( + currentPos.x, + currentPos.y, + _this.prevPos.x, + _this.prevPos.y, + _this.thickness + ); + _this.sendFunction({ + t: _this.tool, + d: [currentPos.x, currentPos.y, _this.prevPos.x, _this.prevPos.y], + th: _this.thickness, + }); + } + } + + if (_this.tool === "eraser") { + const left = currentPos.x - _this.thickness; + const top = currentPos.y - _this.thickness; + if (_this.ownCursor) _this.ownCursor.css({ top: top + "px", left: left + "px" }); + } else if (_this.tool === "pen") { + const left = currentPos.x - _this.thickness / 2; + const top = currentPos.y - _this.thickness / 2; + if (_this.ownCursor) _this.ownCursor.css({ top: top + "px", left: left + "px" }); + } else if (_this.tool === "line") { + if (_this.svgLine) { + let posToUse = currentPos; + if (_this.pressedKeys.shift) { + posToUse = _this.getRoundedAngles(currentPos); + } + _this.svgLine.setAttribute("x2", posToUse.x); + _this.svgLine.setAttribute("y2", posToUse.y); + } + } else if (_this.tool === "rect" || (_this.tool === "recSelect" && _this.drawFlag)) { + if (_this.svgRect) { + const width = Math.abs(currentPos.x - _this.startCoords.x); + let height = Math.abs(currentPos.y - _this.startCoords.y); + if (_this.pressedKeys.shift) { + height = width; + const x = + currentPos.x < _this.startCoords.x + ? _this.startCoords.x - width + : _this.startCoords.x; + const y = + currentPos.y < _this.startCoords.y + ? _this.startCoords.y - width + : _this.startCoords.y; + _this.svgRect.setAttribute("x", x); + _this.svgRect.setAttribute("y", y); + } else { + const x = + currentPos.x < _this.startCoords.x ? currentPos.x : _this.startCoords.x; + const y = + currentPos.y < _this.startCoords.y ? currentPos.y : _this.startCoords.y; + _this.svgRect.setAttribute("x", x); + _this.svgRect.setAttribute("y", y); + } + + _this.svgRect.setAttribute("width", width); + _this.svgRect.setAttribute("height", height); + } + } else if (_this.tool === "circle") { + const r = currentPos.distTo(_this.startCoords); + if (_this.svgCirle) { + _this.svgCirle.setAttribute("r", r); + } + } + + _this.prevPos = currentPos; + }); + + ThrottlingService.throttle(currentPos, () => { + _this.lastPointerPosition = currentPos; + _this.sendFunction({ + t: "cursor", + event: "move", + d: [currentPos.x, currentPos.y], + username: _this.settings.username, + }); + }); + }, + triggerMouseOver: function () { + var _this = this; + if (_this.imgDragActive) { + return; + } + if (!_this.mouseover) { + var color = _this.drawcolor; + var widthHeight = _this.thickness; + if (_this.tool === "eraser") { + color = "#00000000"; + widthHeight = widthHeight * 2; + } + if (_this.tool === "eraser" || _this.tool === "pen") { + _this.ownCursor = $( + '
' + ); + _this.cursorContainer.append(_this.ownCursor); + } + } + _this.mouseover = true; + }, + triggerMouseOut: function () { + var _this = this; + if (_this.imgDragActive) { + return; + } + _this.drawFlag = false; + _this.mouseover = false; + _this.ctx.globalCompositeOperation = _this.oldGCO; + if (_this.ownCursor) _this.ownCursor.remove(); + _this.svgContainer.find("line").remove(); + _this.svgContainer.find("rect").remove(); + _this.svgContainer.find("circle").remove(); + _this.sendFunction({ t: "cursor", event: "out" }); + }, + redrawMouseCursor: function () { + const _this = this; + _this.triggerMouseOut(); + _this.triggerMouseOver(); + _this.triggerMouseMove({ offsetX: _this.prevPos.x, offsetY: _this.prevPos.y }); + }, + delKeyAction: function () { + var _this = this; + $.each(_this.mouseOverlay.find(".dragOutOverlay"), function () { + var width = $(this).width(); + var height = $(this).height(); + var p = $(this).position(); + var left = Math.round(p.left * 100) / 100; + var top = Math.round(p.top * 100) / 100; + _this.drawId++; + _this.sendFunction({ t: "eraseRec", d: [left, top, width, height] }); + _this.eraseRec(left, top, width, height); + }); + _this.mouseOverlay.find(".xCanvasBtn").click(); //Remove all current drops + _this.textContainer + .find("#" + _this.latestActiveTextBoxId) + .find(".removeIcon") + .click(); + }, + escKeyAction: function () { + var _this = this; + if (!_this.drawFlag) { + _this.svgContainer.empty(); + } + _this.mouseOverlay.find(".xCanvasBtn").click(); //Remove all current drops + }, + pushPointSmoothPen: function (X, Y) { + var _this = this; + if (_this.penSmoothLastCoords.length >= 8) { + _this.penSmoothLastCoords = [ + _this.penSmoothLastCoords[2], + _this.penSmoothLastCoords[3], + _this.penSmoothLastCoords[4], + _this.penSmoothLastCoords[5], + _this.penSmoothLastCoords[6], + _this.penSmoothLastCoords[7], + ]; + } + _this.penSmoothLastCoords.push(X, Y); + if (_this.penSmoothLastCoords.length >= 8) { + _this.drawPenSmoothLine(_this.penSmoothLastCoords, _this.drawcolor, _this.thickness); + _this.sendFunction({ + t: _this.tool, + d: _this.penSmoothLastCoords, + c: _this.drawcolor, + th: _this.thickness, + }); + } + }, + dragCanvasRectContent: function (xf, yf, xt, yt, width, height) { + var tempCanvas = document.createElement("canvas"); + tempCanvas.width = width; + tempCanvas.height = height; + var tempCanvasContext = tempCanvas.getContext("2d"); + tempCanvasContext.drawImage(this.canvas, xf, yf, width, height, 0, 0, width, height); + this.eraseRec(xf, yf, width, height); + this.ctx.drawImage(tempCanvas, xt, yt); + }, + eraseRec: function (fromX, fromY, width, height) { + var _this = this; + _this.ctx.beginPath(); + _this.ctx.rect(fromX, fromY, width, height); + _this.ctx.fillStyle = "rgba(0,0,0,1)"; + _this.ctx.globalCompositeOperation = "destination-out"; + _this.ctx.fill(); + _this.ctx.closePath(); + _this.ctx.globalCompositeOperation = _this.oldGCO; + }, + drawPenLine: function (fromX, fromY, toX, toY, color, thickness) { + var _this = this; + _this.ctx.beginPath(); + _this.ctx.moveTo(fromX, fromY); + _this.ctx.lineTo(toX, toY); + _this.ctx.strokeStyle = color; + _this.ctx.lineWidth = thickness; + _this.ctx.lineCap = _this.lineCap; + _this.ctx.stroke(); + _this.ctx.closePath(); + }, + drawPenSmoothLine: function (coords, color, thickness) { + var _this = this; + var xm1 = coords[0]; + var ym1 = coords[1]; + var x0 = coords[2]; + var y0 = coords[3]; + var x1 = coords[4]; + var y1 = coords[5]; + var x2 = coords[6]; + var y2 = coords[7]; + var length = Math.sqrt(Math.pow(x0 - x1, 2) + Math.pow(y0 - y1, 2)); + var steps = Math.ceil(length / 5); + _this.ctx.beginPath(); + _this.ctx.moveTo(x0, y0); + if (steps == 0) { + _this.ctx.lineTo(x0, y0); + } + for (var i = 0; i < steps; i++) { + var point = lanczosInterpolate(xm1, ym1, x0, y0, x1, y1, x2, y2, (i + 1) / steps); + _this.ctx.lineTo(point[0], point[1]); + } + _this.ctx.strokeStyle = color; + _this.ctx.lineWidth = thickness; + _this.ctx.lineCap = _this.lineCap; + _this.ctx.stroke(); + _this.ctx.closePath(); + }, + drawEraserLine: function (fromX, fromY, toX, toY, thickness) { + var _this = this; + _this.ctx.beginPath(); + _this.ctx.moveTo(fromX, fromY); + _this.ctx.lineTo(toX, toY); + _this.ctx.strokeStyle = "rgba(0,0,0,1)"; + _this.ctx.lineWidth = thickness * 2; + _this.ctx.lineCap = _this.lineCap; + _this.ctx.globalCompositeOperation = "destination-out"; + _this.ctx.stroke(); + _this.ctx.closePath(); + _this.ctx.globalCompositeOperation = _this.oldGCO; + }, + drawRec: function (fromX, fromY, toX, toY, color, thickness) { + var _this = this; + toX = toX - fromX; + toY = toY - fromY; + _this.ctx.beginPath(); + _this.ctx.rect(fromX, fromY, toX, toY); + _this.ctx.strokeStyle = color; + _this.ctx.lineWidth = thickness; + _this.ctx.lineCap = _this.lineCap; + _this.ctx.stroke(); + _this.ctx.closePath(); + }, + drawCircle: function (fromX, fromY, radius, color, thickness) { + var _this = this; + _this.ctx.beginPath(); + _this.ctx.arc(fromX, fromY, radius, 0, 2 * Math.PI, false); + _this.ctx.lineWidth = thickness; + _this.ctx.strokeStyle = color; + _this.ctx.stroke(); + }, + clearWhiteboard: function () { + var _this = this; + if (ReadOnlyService.readOnlyActive) return; + _this.canvas.height = _this.canvas.height; + _this.imgContainer.empty(); + _this.textContainer.empty(); + _this.sendFunction({ t: "clear" }); + _this.drawBuffer = []; + _this.undoBuffer = []; + _this.drawId = 0; + }, + setStrokeThickness(thickness) { + var _this = this; + _this.thickness = thickness; + + if ((_this.tool == "text" || this.tool === "stickynote") && _this.latestActiveTextBoxId) { + _this.sendFunction({ + t: "setTextboxFontSize", + d: [_this.latestActiveTextBoxId, thickness], + }); + _this.setTextboxFontSize(_this.latestActiveTextBoxId, thickness); + } + }, + addImgToCanvasByUrl: function (url) { + var _this = this; + var oldTool = _this.tool; + + const { imageURL } = ConfigService; + var finalURL = url; + if (imageURL && url.startsWith("/uploads/")) { + finalURL = imageURL + url; + } + + _this.setTool("mouse"); //Set to mouse tool while dropping to prevent errors + _this.imgDragActive = true; + _this.mouseOverlay.css({ cursor: "default" }); + var imgDiv = $( + '
' + + '' + + '
' + + ' ' + + ' ' + + '' + + "
" + + '' + + '
' + + "
" + ); + imgDiv + .find(".xCanvasBtn") + .off("click") + .click(function () { + _this.imgDragActive = false; + _this.refreshCursorAppearance(); + imgDiv.remove(); + _this.setTool(oldTool); + }); + var rotationAngle = 0; + var recoupLeft = 0; + var recoupTop = 0; + var p = imgDiv.position(); + var left = 200; + var top = 200; + imgDiv + .find(".addToCanvasBtn,.addToBackgroundBtn") + .off("click") + .click(function () { + var draw = $(this).attr("draw"); + _this.imgDragActive = false; + + var width = imgDiv.width(); + var height = imgDiv.height(); + + if (draw == "1") { + //draw image to canvas + _this.drawImgToCanvas(finalURL, width, height, left, top, rotationAngle); + } else { + //Add image to background + _this.drawImgToBackground(finalURL, width, height, left, top, rotationAngle); + } + _this.sendFunction({ + t: "addImgBG", + draw: draw, + url: finalURL, + d: [width, height, left, top, rotationAngle], + }); + _this.drawId++; + imgDiv.remove(); + _this.refreshCursorAppearance(); + _this.setTool(oldTool); + }); + _this.mouseOverlay.append(imgDiv); + + imgDiv.draggable({ + start: function (event, ui) { + var left = parseInt($(this).css("left"), 10); + left = isNaN(left) ? 0 : left; + var top = parseInt($(this).css("top"), 10); + top = isNaN(top) ? 0 : top; + recoupLeft = left - ui.position.left; + recoupTop = top - ui.position.top; + }, + drag: function (event, ui) { + ui.position.left += recoupLeft; + ui.position.top += recoupTop; + }, + stop: function (event, ui) { + left = ui.position.left; + top = ui.position.top; + }, + }); + imgDiv.resizable(); + var params = { + // Callback fired on rotation start. + start: function (event, ui) {}, + // Callback fired during rotation. + rotate: function (event, ui) { + //console.log(ui) + }, + // Callback fired on rotation end. + stop: function (event, ui) { + rotationAngle = ui.angle.current; + }, + handle: imgDiv.find(".rotationHandle"), + }; + imgDiv.rotatable(params); + + // render newly added icons + dom.i2svg(); + }, + drawImgToBackground(url, width, height, left, top, rotationAngle) { + this.imgContainer.append( + '' + ); + }, + addTextBox( + textcolor, + textboxBackgroundColor, + fontsize, + left, + top, + txId, + isStickyNote, + newLocalBox + ) { + var _this = this; + console.log(isStickyNote); + var cssclass = "textBox"; + if (isStickyNote) { + cssclass += " stickyNote"; + } + var textBox = $( + '
' + + '
' + + '
x
' + + '
' + + "
" + ); + _this.latestActiveTextBoxId = txId; + textBox.click(function (e) { + e.preventDefault(); + _this.latestActiveTextBoxId = txId; + return false; + }); + textBox.on("mousemove touchmove", function (e) { + e.preventDefault(); + if (_this.imgDragActive) { + return; + } + var textBoxPosition = textBox.position(); + var currX = e.offsetX + textBoxPosition.left; + var currY = e.offsetY + textBoxPosition.top; + if ($(e.target).hasClass("removeIcon")) { + currX += textBox.width() - 4; + } + + const newPointerPosition = new Point(currX, currY); + + ThrottlingService.throttle(newPointerPosition, () => { + _this.lastPointerPosition = newPointerPosition; + _this.sendFunction({ + t: "cursor", + event: "move", + d: [newPointerPosition.x, newPointerPosition.y], + username: _this.settings.username, + }); + }); + }); + this.textContainer.append(textBox); + textBox.draggable({ + handle: ".moveIcon", + stop: function () { + var textBoxPosition = textBox.position(); + _this.sendFunction({ + t: "setTextboxPosition", + d: [txId, textBoxPosition.top, textBoxPosition.left], + }); + }, + drag: function () { + var textBoxPosition = textBox.position(); + _this.sendFunction({ + t: "setTextboxPosition", + d: [txId, textBoxPosition.top, textBoxPosition.left], + }); + }, + }); + textBox.find(".textContent").on("input", function () { + var text = btoa(unescape(encodeURIComponent($(this).html()))); //Get html and make encode base64 also take care of the charset + _this.sendFunction({ t: "setTextboxText", d: [txId, text] }); + }); + textBox + .find(".removeIcon") + .off("click") + .click(function (e) { + $("#" + txId).remove(); + _this.sendFunction({ t: "removeTextbox", d: [txId] }); + e.preventDefault(); + return false; + }); + if (newLocalBox) { + //per https://stackoverflow.com/questions/2388164/set-focus-on-div-contenteditable-element + setTimeout(() => { + textBox.find(".textContent").focus(); + }, 0); + } + if (this.tool === "text" || this.tool === "stickynote") { + textBox.addClass("active"); + } + + // render newly added icons + dom.i2svg(); + }, + setTextboxText(txId, text) { + $("#" + txId) + .find(".textContent") + .html(decodeURIComponent(escape(atob(text)))); //Set decoded base64 as html + }, + removeTextbox(txId) { + $("#" + txId).remove(); + }, + setTextboxPosition(txId, top, left) { + $("#" + txId).css({ top: top + "px", left: left + "px" }); + }, + setTextboxFontSize(txId, fontSize) { + $("#" + txId) + .find(".textContent") + .css({ "font-size": fontSize + "em" }); + }, + setTextboxFontColor(txId, color) { + $("#" + txId) + .find(".textContent") + .css({ color: color }); + }, + setTextboxBackgroundColor(txId, textboxBackgroundColor) { + $("#" + txId) + .find(".textContent") + .css({ "background-color": textboxBackgroundColor }); + }, + drawImgToCanvas(url, width, height, left, top, rotationAngle, doneCallback) { + var _this = this; + var img = document.createElement("img"); + img.onload = function () { + rotationAngle = rotationAngle ? rotationAngle : 0; + if (rotationAngle === 0) { + _this.ctx.drawImage(img, left, top, width, height); + } else { + _this.ctx.save(); + _this.ctx.translate(left + width / 2, top + height / 2); + _this.ctx.rotate(rotationAngle); + _this.ctx.drawImage(img, -(width / 2), -(height / 2), width, height); + _this.ctx.restore(); + } + if (doneCallback) { + doneCallback(); + } + }; + img.src = url; + }, + undoWhiteboard: function (username) { + //Not call this directly because you will get out of sync whit others... + var _this = this; + if (!username) { + username = _this.settings.username; + } + for (var i = _this.drawBuffer.length - 1; i >= 0; i--) { + if (_this.drawBuffer[i]["username"] == username) { + var drawId = _this.drawBuffer[i]["drawId"]; + for (var i = _this.drawBuffer.length - 1; i >= 0; i--) { + if ( + _this.drawBuffer[i]["drawId"] == drawId && + _this.drawBuffer[i]["username"] == username + ) { + _this.undoBuffer.push(_this.drawBuffer[i]); + _this.drawBuffer.splice(i, 1); + } + } + break; + } + } + if (_this.undoBuffer.length > 1000) { + _this.undoBuffer.splice(0, _this.undoBuffer.length - 1000); + } + _this.canvas.height = _this.canvas.height; + _this.imgContainer.empty(); + _this.loadDataInSteps(_this.drawBuffer, false, function (stepData) { + //Nothing to do + }); + }, + redoWhiteboard: function (username) { + //Not call this directly because you will get out of sync whit others... + var _this = this; + if (!username) { + username = _this.settings.username; + } + for (var i = _this.undoBuffer.length - 1; i >= 0; i--) { + if (_this.undoBuffer[i]["username"] == username) { + var drawId = _this.undoBuffer[i]["drawId"]; + for (var i = _this.undoBuffer.length - 1; i >= 0; i--) { + if ( + _this.undoBuffer[i]["drawId"] == drawId && + _this.undoBuffer[i]["username"] == username + ) { + _this.drawBuffer.push(_this.undoBuffer[i]); + _this.undoBuffer.splice(i, 1); + } + } + break; + } + } + _this.canvas.height = _this.canvas.height; + _this.imgContainer.empty(); + _this.loadDataInSteps(_this.drawBuffer, false, function (stepData) { + //Nothing to do + }); + }, + undoWhiteboardClick: function () { + if (ReadOnlyService.readOnlyActive) return; + this.sendFunction({ t: "undo" }); + this.undoWhiteboard(); + }, + redoWhiteboardClick: function () { + if (ReadOnlyService.readOnlyActive) return; + this.sendFunction({ t: "redo" }); + this.redoWhiteboard(); + }, + setTool: function (tool) { + this.tool = tool; + if (this.tool === "text" || this.tool === "stickynote") { + $(".textBox").addClass("active"); + this.textContainer.appendTo($(whiteboardContainer)); //Bring textContainer to the front + } else { + $(".textBox").removeClass("active"); + this.mouseOverlay.appendTo($(whiteboardContainer)); + } + this.refreshCursorAppearance(); + this.mouseOverlay.find(".xCanvasBtn").click(); + this.latestActiveTextBoxId = null; + }, + setDrawColor(color) { + var _this = this; + _this.drawcolor = color; + $("#whiteboardColorpicker").css({ background: color }); + if ((_this.tool == "text" || this.tool === "stickynote") && _this.latestActiveTextBoxId) { + _this.sendFunction({ + t: "setTextboxFontColor", + d: [_this.latestActiveTextBoxId, color], + }); + _this.setTextboxFontColor(_this.latestActiveTextBoxId, color); + } + }, + setTextBackgroundColor(textboxBackgroundColor) { + var _this = this; + _this.textboxBackgroundColor = textboxBackgroundColor; + $("#textboxBackgroundColorPicker").css({ background: textboxBackgroundColor }); + if ((_this.tool == "text" || this.tool === "stickynote") && _this.latestActiveTextBoxId) { + _this.sendFunction({ + t: "setTextboxBackgroundColor", + d: [_this.latestActiveTextBoxId, textboxBackgroundColor], + }); + _this.setTextboxBackgroundColor(_this.latestActiveTextBoxId, textboxBackgroundColor); + } + }, + updateSmallestScreenResolution() { + const { smallestScreenResolution } = InfoService; + const { showSmallestScreenIndicator } = ConfigService; + if (showSmallestScreenIndicator && smallestScreenResolution) { + const { w: width, h: height } = smallestScreenResolution; + this.backgroundGrid.empty(); + if (width < $(window).width() || height < $(window).height()) { + this.backgroundGrid.append( + '
' + ); + this.backgroundGrid.append( + '
smallest screen participating
' + ); + } + } + }, + handleEventsAndData: function (content, isNewData, doneCallback) { + var _this = this; + var tool = content["t"]; + var data = content["d"]; + var color = content["c"]; + var username = content["username"]; + var thickness = content["th"]; + + window.requestAnimationFrame(function () { + if (tool === "line" || tool === "pen") { + if (data.length == 4) { + //Only used for old json imports + _this.drawPenLine(data[0], data[1], data[2], data[3], color, thickness); + } else { + _this.drawPenSmoothLine(data, color, thickness); + } + } else if (tool === "rect") { + _this.drawRec(data[0], data[1], data[2], data[3], color, thickness); + } else if (tool === "circle") { + _this.drawCircle(data[0], data[1], data[2], color, thickness); + } else if (tool === "eraser") { + _this.drawEraserLine(data[0], data[1], data[2], data[3], thickness); + } else if (tool === "eraseRec") { + _this.eraseRec(data[0], data[1], data[2], data[3]); + } else if (tool === "recSelect") { + _this.dragCanvasRectContent(data[0], data[1], data[2], data[3], data[4], data[5]); + } else if (tool === "addImgBG") { + if (content["draw"] == "1") { + _this.drawImgToCanvas( + content["url"], + data[0], + data[1], + data[2], + data[3], + data[4], + doneCallback + ); + } else { + _this.drawImgToBackground( + content["url"], + data[0], + data[1], + data[2], + data[3], + data[4] + ); + } + } else if (tool === "addTextBox") { + _this.addTextBox(data[0], data[1], data[2], data[3], data[4], data[5], data[6]); + } else if (tool === "setTextboxText") { + _this.setTextboxText(data[0], data[1]); + } else if (tool === "removeTextbox") { + _this.removeTextbox(data[0]); + } else if (tool === "setTextboxPosition") { + _this.setTextboxPosition(data[0], data[1], data[2]); + } else if (tool === "setTextboxFontSize") { + _this.setTextboxFontSize(data[0], data[1]); + } else if (tool === "setTextboxFontColor") { + _this.setTextboxFontColor(data[0], data[1]); + } else if (tool === "setTextboxBackgroundColor") { + _this.setTextboxBackgroundColor(data[0], data[1]); + } else if (tool === "clear") { + _this.canvas.height = _this.canvas.height; + _this.imgContainer.empty(); + _this.textContainer.empty(); + _this.drawBuffer = []; + _this.undoBuffer = []; + _this.drawId = 0; + } else if (tool === "cursor" && _this.settings) { + if (content["event"] === "move") { + if (_this.cursorContainer.find("." + content["username"]).length >= 1) { + _this.cursorContainer + .find("." + content["username"]) + .css({ left: data[0] + "px", top: data[1] - 15 + "px" }); + } else { + _this.cursorContainer.append( + '
' + + '
' + + decodeURIComponent(atob(content["username"])) + + "
" + ); + } + } else { + _this.cursorContainer.find("." + content["username"]).remove(); + } + } else if (tool === "undo") { + _this.undoWhiteboard(username); + } else if (tool === "redo") { + _this.redoWhiteboard(username); + } + }); + + if ( + isNewData && + [ + "line", + "pen", + "rect", + "circle", + "eraser", + "addImgBG", + "recSelect", + "eraseRec", + "addTextBox", + "setTextboxText", + "removeTextbox", + "setTextboxPosition", + "setTextboxFontSize", + "setTextboxFontColor", + "setTextboxBackgroundColor", + ].includes(tool) + ) { + content["drawId"] = content["drawId"] ? content["drawId"] : _this.drawId; + content["username"] = content["username"] + ? content["username"] + : _this.settings.username; + _this.drawBuffer.push(content); + } + }, + userLeftWhiteboard(username) { + this.cursorContainer.find("." + username).remove(); + }, + refreshUserBadges() { + this.cursorContainer.find(".userbadge").remove(); + }, + getImageDataBase64(options, callback) { + var _this = this; + var width = this.mouseOverlay.width(); + var height = this.mouseOverlay.height(); + var copyCanvas = document.createElement("canvas"); + copyCanvas.width = width; + copyCanvas.height = height; + var imageFormat = options.imageFormat || "png"; + var drawBackgroundGrid = options.drawBackgroundGrid || false; + + var brackGroundImg = new Image(); + brackGroundImg.src = _this.settings.backgroundGridUrl; + + brackGroundImg.onload = function () { + var destCtx = copyCanvas.getContext("2d"); //Draw the maincanvas to the exportcanvas + + if (imageFormat === "jpeg") { + //Set white background for jpeg images + destCtx.fillStyle = "#FFFFFF"; + destCtx.fillRect(0, 0, width, height); + } + + if (drawBackgroundGrid) { + destCtx.globalAlpha = 0.8; + var ptrn = destCtx.createPattern(brackGroundImg, "repeat"); // Create a pattern with this image, and set it to "repeat". + destCtx.fillStyle = ptrn; + destCtx.fillRect(0, 0, copyCanvas.width, copyCanvas.height); // context.fillRect(x, y, width, height); + destCtx.globalAlpha = 1; + } + + $.each(_this.imgContainer.find("img"), function () { + //Draw Backgroundimages to the export canvas + var width = $(this).width(); + var height = $(this).height(); + var p = $(this).position(); + var left = Math.round(p.left * 100) / 100; + var top = Math.round(p.top * 100) / 100; + destCtx.drawImage(this, left, top, width, height); + }); + + //Copy drawings + destCtx.drawImage(_this.canvas, 0, 0); + + var textBoxCnt = 0; + $.each($(".textBox"), function () { + //Draw the text on top + textBoxCnt++; + + var textContainer = $(this); + var p = textContainer.position(); + + var left = Math.round(p.left * 100) / 100; + var top = Math.round(p.top * 100) / 100; + + html2canvas(this, { + backgroundColor: "rgba(0, 0, 0, 0)", + removeContainer: true, + }).then(function (canvas) { + console.log("canvas", canvas); + + destCtx.drawImage(canvas, left, top); + textBoxCnt--; + checkForReturn(); + }); + }); + + function checkForReturn() { + if (textBoxCnt == 0) { + var url = copyCanvas.toDataURL("image/" + imageFormat); + callback(url); + } + } + checkForReturn(); + }; + }, + getImageDataJson() { + var sendObj = []; + for (var i = 0; i < this.drawBuffer.length; i++) { + sendObj.push(JSON.parse(JSON.stringify(this.drawBuffer[i]))); + delete sendObj[i]["username"]; + delete sendObj[i]["wid"]; + delete sendObj[i]["drawId"]; + } + return JSON.stringify(sendObj, null, 2); + }, + loadData: function (content) { + var _this = this; + _this.loadDataInSteps(content, true, function (stepData) { + if ( + stepData["username"] == _this.settings.username && + _this.drawId < stepData["drawId"] + ) { + _this.drawId = stepData["drawId"] + 1; + } + }); + }, + loadDataInSteps(content, isNewData, callAfterEveryStep) { + var _this = this; + + function lData(index) { + for (var i = index; i < content.length; i++) { + if (content[i]["t"] === "addImgBG" && content[i]["draw"] == "1") { + _this.handleEventsAndData(content[i], isNewData, function () { + callAfterEveryStep(content[i], i); + lData(i + 1); + }); + break; + } else { + _this.handleEventsAndData(content[i], isNewData); + callAfterEveryStep(content[i], i); + } + } + } + lData(0); + }, + loadJsonData(content, doneCallback) { + var _this = this; + + _this.loadDataInSteps(content, false, function (stepData, index) { + _this.sendFunction(stepData); + if (index >= content.length - 1) { + //Done with all data + _this.drawId++; + + // LAMS: after load prefix author so his steps can not be undone + for (var i = 0; i < _this.drawBuffer.length; i++) { + let username = _this.drawBuffer[i]["username"]; + if (username && !username.startsWith('authored-')) { + _this.drawBuffer[i]["username"] = 'authored-' + username; + } + } + + if (doneCallback) { + doneCallback(); + } + } + }); + }, + sendFunction: function (content) { + //Sends every draw to server + var _this = this; + content["wid"] = _this.settings.whiteboardId; + content["username"] = _this.settings.username; + content["drawId"] = _this.drawId; + + var tool = content["t"]; + if (_this.settings.sendFunction) { + _this.settings.sendFunction(content); + } + if ( + [ + "line", + "pen", + "rect", + "circle", + "eraser", + "addImgBG", + "recSelect", + "eraseRec", + "addTextBox", + "setTextboxText", + "removeTextbox", + "setTextboxPosition", + "setTextboxFontSize", + "setTextboxFontColor", + "setTextboxBackgroundColor", + ].includes(tool) + ) { + _this.drawBuffer.push(content); + } + }, + refreshCursorAppearance() { + //Set cursor depending on current active tool + var _this = this; + if (_this.tool === "pen" || _this.tool === "eraser") { + _this.mouseOverlay.css({ cursor: "none" }); + } else if (_this.tool === "mouse") { + this.mouseOverlay.css({ cursor: "default" }); + } else { + //Line, Rec, Circle, Cutting + _this.mouseOverlay.css({ cursor: "crosshair" }); + } + }, +}; + +function lanczosKernel(x) { + if (x == 0) { + return 1.0; + } + return (2 * Math.sin(Math.PI * x) * Math.sin((Math.PI * x) / 2)) / Math.pow(Math.PI * x, 2); +} + +function lanczosInterpolate(xm1, ym1, x0, y0, x1, y1, x2, y2, a) { + var cm1 = lanczosKernel(1 + a); + var c0 = lanczosKernel(a); + var c1 = lanczosKernel(1 - a); + var c2 = lanczosKernel(2 - a); + var delta = (cm1 + c0 + c1 + c2 - 1) / 4; + cm1 -= delta; + c0 -= delta; + c1 -= delta; + c2 -= delta; + return [cm1 * xm1 + c0 * x0 + c1 * x1 + c2 * x2, cm1 * ym1 + c0 * y0 + c1 * y1 + c2 * y2]; +} + +export default whiteboard;