diff --git a/package-lock.json b/package-lock.json index a03b932625a3a145d196e3007068f0b44d2eea7d..439e3b2c49bc1e9818cd91aa57fd5a55690137db 100644 --- a/package-lock.json +++ b/package-lock.json @@ -609,9 +609,9 @@ "dev": true }, "@types/node": { - "version": "12.12.12", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.12.tgz", - "integrity": "sha512-MGuvYJrPU0HUwqF7LqvIj50RZUX23Z+m583KBygKYUZLlZ88n6w28XRNJRJgsHukLEnLz6w6SvxZoLgbr5wLqQ==" + "version": "12.12.14", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.14.tgz", + "integrity": "sha512-u/SJDyXwuihpwjXy7hOOghagLEV1KdAST6syfnOk6QZAMzZuWZqXy5aYYZbh8Jdpd4escVFP0MvftHNDb9pruA==" }, "@types/stack-utils": { "version": "1.0.1", @@ -859,9 +859,9 @@ }, "dependencies": { "acorn": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.3.0.tgz", - "integrity": "sha512-/czfa8BwS88b9gWQVhc8eknunSA2DoJpJyTQkhheIf5E48u1N0R4q/YxxsAeqRrmK9TQ/uYfgLDfZo91UlANIA==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.0.tgz", + "integrity": "sha512-gac8OEcQ2Li1dxIEWGZzsp2BitJxwkwcOm0zHAJLcPJaVvm58FRnk6RkuLRpU1EujipU2ZFODv2P9DLMfnV8mw==", "dev": true } } @@ -1182,9 +1182,9 @@ "dev": true }, "aws4": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", - "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.9.0.tgz", + "integrity": "sha512-Uvq6hVe90D0B2WEnUqtdgY1bATGz3mw33nH9Y+dmA+w5DHvUmBgkr5rM/KCHpCsiFNRUfokW/szpPPgMK2hm4A==", "dev": true }, "babel-code-frame": { @@ -2600,9 +2600,9 @@ "dev": true }, "caniuse-lite": { - "version": "1.0.30001011", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001011.tgz", - "integrity": "sha512-h+Eqyn/YA6o6ZTqpS86PyRmNWOs1r54EBDcd2NTwwfsXQ8re1B38SnB+p2RKF8OUsyEIjeDU8XGec1RGO/wYCg==", + "version": "1.0.30001012", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001012.tgz", + "integrity": "sha512-7RR4Uh04t9K1uYRWzOJmzplgEOAXbfK72oVNokCdMzA67trrhPzy93ahKk1AWHiA0c58tD2P+NHqxrA8FZ+Trg==", "dev": true }, "canvas-renderer": { @@ -3488,9 +3488,9 @@ "dev": true }, "electron-to-chromium": { - "version": "1.3.312", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.312.tgz", - "integrity": "sha512-/Nk6Hvwt+RfS9X3oA4IXpWqpcnS7cdWsTMP4AmrP8hPpxtZbHemvTEYzjAKghk28aS9zIV8NwGHNt8H+6OmJug==", + "version": "1.3.314", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.314.tgz", + "integrity": "sha512-IKDR/xCxKFhPts7h+VaSXS02Z1mznP3fli1BbXWXeN89i2gCzKraU8qLpEid8YzKcmZdZD3Mly3cn5/lY9xsBQ==", "dev": true }, "elegant-spinner": { @@ -3710,18 +3710,18 @@ } }, "es-abstract": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.16.0.tgz", - "integrity": "sha512-xdQnfykZ9JMEiasTAJZJdMWCQ1Vm00NBw79/AWi7ELfZuuPCSOMDZbT9mkOfSctVtfhb+sAAzrm+j//GjjLHLg==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.16.2.tgz", + "integrity": "sha512-jYo/J8XU2emLXl3OLwfwtuFfuF2w6DYPs+xy9ZfVyPkDcrauu6LYrw/q2TyCtrbc/KUdCiC5e9UajRhgNkVopA==", "dev": true, "requires": { - "es-to-primitive": "^1.2.0", + "es-to-primitive": "^1.2.1", "function-bind": "^1.1.1", "has": "^1.0.3", - "has-symbols": "^1.0.0", + "has-symbols": "^1.0.1", "is-callable": "^1.1.4", "is-regex": "^1.0.4", - "object-inspect": "^1.6.0", + "object-inspect": "^1.7.0", "object-keys": "^1.1.1", "string.prototype.trimleft": "^2.1.0", "string.prototype.trimright": "^2.1.0" @@ -3771,9 +3771,9 @@ } }, "eslint": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.7.0.tgz", - "integrity": "sha512-dQpj+PaHKHfXHQ2Imcw5d853PTvkUGbHk/MR68KQUl98EgKDCdh4vLRH1ZxhqeQjQFJeg8fgN0UwmNhN3l8dDQ==", + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.7.1.tgz", + "integrity": "sha512-UWzBS79pNcsDSxgxbdjkmzn/B6BhsXMfUaOHnNwyE8nD+Q6pyT96ow2MccVayUTV4yMid4qLhMiQaywctRkBLA==", "dev": true, "requires": { "@babel/code-frame": "^7.0.0", @@ -8330,9 +8330,9 @@ "dev": true }, "resolve": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.12.2.tgz", - "integrity": "sha512-cAVTI2VLHWYsGOirfeYVVQ7ZDejtQ9fp4YhYckWDEkFfqbVjaT11iM8k6xSAfGFMM+gDpZjMnFssPu8we+mqFw==", + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.13.1.tgz", + "integrity": "sha512-CxqObCX8K8YtAhOBRg+lrcdn+LK+WYOS8tSjqSFbjtrI5PnS63QPhZl4+yKfrU9tdsbMu9Anr/amegT87M9Z6w==", "dev": true, "requires": { "path-parse": "^1.0.6" @@ -8900,9 +8900,9 @@ } }, "socket.io-adapter": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-1.1.1.tgz", - "integrity": "sha1-KoBeihTWNyEk3ZFZrUUC+MsH8Gs=" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-1.1.2.tgz", + "integrity": "sha512-WzZRUj1kUjrTIrUKpZLEzFZ1OLj5FwLlAFQs9kuZJzJi5DKdU7FsWc36SNmA8iDOtwBQyT8FkrriRM8vXLYz8g==" }, "socket.io-client": { "version": "2.3.0", @@ -9467,9 +9467,9 @@ }, "dependencies": { "@types/node": { - "version": "10.17.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.5.tgz", - "integrity": "sha512-RElZIr/7JreF1eY6oD5RF3kpmdcreuQPjg5ri4oQ5g9sq7YWU8HkfB3eH8GwAwxf5OaCh0VPi7r4N/yoTGelrA==", + "version": "10.17.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.6.tgz", + "integrity": "sha512-0a2X6cgN3RdPBL2MIlR6Lt0KlM7fOFsutuXcdglcOq6WvLnYXgPQSh0Mx6tO1KCAE8MxbHSOSTWDoUxRq+l3DA==", "dev": true }, "ci-info": { @@ -9663,9 +9663,9 @@ } }, "execa": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-3.3.0.tgz", - "integrity": "sha512-j5Vit5WZR/cbHlqU97+qcnw9WHRCIL4V1SVe75VcHcD1JRBdt8fv0zw89b7CQHQdUHTt2VjuhcF5ibAgVOxqpg==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-3.4.0.tgz", + "integrity": "sha512-r9vdGQk4bmCuK1yKQu1KTwcT2zwfWdbdaXfCtAh+5nU/4fSX+JAb7vZGvI5naJrQlvONrEB20jeruESI69530g==", "dev": true, "requires": { "cross-spawn": "^7.0.0", @@ -10324,9 +10324,9 @@ "dev": true }, "uglify-js": { - "version": "3.6.9", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.9.tgz", - "integrity": "sha512-pcnnhaoG6RtrvHJ1dFncAe8Od6Nuy30oaJ82ts6//sGSXOP5UjBMEthiProjXmMNHOfd93sqlkztifFMcb+4yw==", + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.7.0.tgz", + "integrity": "sha512-PC/ee458NEMITe1OufAjal65i6lB58R1HWMRcxwvdz1UopW0DYqlRL3xdu3IcTvTXsB02CRHykidkTRL+A3hQA==", "dev": true, "optional": true, "requires": { @@ -10618,9 +10618,9 @@ }, "dependencies": { "acorn": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.3.0.tgz", - "integrity": "sha512-/czfa8BwS88b9gWQVhc8eknunSA2DoJpJyTQkhheIf5E48u1N0R4q/YxxsAeqRrmK9TQ/uYfgLDfZo91UlANIA==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.0.tgz", + "integrity": "sha512-gac8OEcQ2Li1dxIEWGZzsp2BitJxwkwcOm0zHAJLcPJaVvm58FRnk6RkuLRpU1EujipU2ZFODv2P9DLMfnV8mw==", "dev": true }, "eslint-scope": { @@ -10657,9 +10657,9 @@ }, "dependencies": { "acorn": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.3.0.tgz", - "integrity": "sha512-/czfa8BwS88b9gWQVhc8eknunSA2DoJpJyTQkhheIf5E48u1N0R4q/YxxsAeqRrmK9TQ/uYfgLDfZo91UlANIA==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.0.tgz", + "integrity": "sha512-gac8OEcQ2Li1dxIEWGZzsp2BitJxwkwcOm0zHAJLcPJaVvm58FRnk6RkuLRpU1EujipU2ZFODv2P9DLMfnV8mw==", "dev": true } } diff --git a/public/index.html b/public/index.html index ca2ae6044aece77c536d264245695f3d3464af17..2286a49b8b334ee832cf9b3635e11d9d77cb7534 100644 --- a/public/index.html +++ b/public/index.html @@ -197,13 +197,21 @@ </div> </div> </div> - <button id="eraser-tool"><i class="fa fa-eraser"></i></button> + <button id="eraser-tool"> + <i class="fa fa-eraser"></i> + </button> <div id="status-info"> - <div id="user-avatar"></div> + <button id="fast-undo-tool" class="disabled"> + <i class="fa fa-fast-backward"></i> + </button> + <button id="undo-tool" class="disabled"> + <i class="fa fa-backward"></i> + </button> <div id="connected-room-info"> - Room <i class="fa fa-globe">: </i> + Room: <span id="connected-room-id"></span> </div> + <div id="user-avatar"></div> </div> </div> </div> diff --git a/public/styles.css b/public/styles.css index d1f4a1381ed965be737d4eb3082e5c50a941ad2f..f4c1de641ba7745eae2a6ac5c3326f21553bdeb0 100644 --- a/public/styles.css +++ b/public/styles.css @@ -33,14 +33,8 @@ body { align-items: center; background-color: #4f4f4fb7; border-radius: 4px; - margin-left: 8px; justify-content: center; - height: 44px; -} - -#connected-room-info:hover { - background-color: #4f4f4f !important; - transition-duration: 0.4s; + padding: 0.75em; } button.selected { @@ -269,6 +263,11 @@ button.selected { border-radius: 4px; } +#room-connect:hover { + background-color: #4f4f4f !important; + transition-duration: 0.4s; +} + #pen-tool { background-color: #2f2f2f; color: white; @@ -285,6 +284,10 @@ button.selected { transition-duration: 0.4s; } +#pen-tool > i { + padding: 0 1px; +} + #eraser-tool { background-color: #2f2f2f; color: white; @@ -293,6 +296,7 @@ button.selected { border: none; cursor: pointer; border-radius: 50%; + margin-right: 8px; } #eraser-tool:hover { @@ -300,6 +304,58 @@ button.selected { transition-duration: 0.4s; } +#eraser-tool > i { + padding: 0 0.5px; +} + +#undo-tool { + background-color: #2f2f2f; + color: white; + padding: 10px; + font-size: 16px; + border: none; + cursor: pointer; + border-radius: 50%; + margin-right: 8px; +} + +#undo-tool:hover { + background-color: #4f4f4f !important; + transition-duration: 0.4s; +} + +#undo-tool.disabled { + display: none; +} + +#undo-tool > i { + padding: 0 3px 0 0; +} + +#fast-undo-tool { + background-color: #2f2f2f; + color: white; + padding: 10px; + font-size: 16px; + border: none; + cursor: pointer; + border-radius: 50%; + margin-right: 8px; +} + +#fast-undo-tool:hover { + background-color: #4f4f4f !important; + transition-duration: 0.4s; +} + +#fast-undo-tool.disabled { + display: none; +} + +#fast-undo-tool > i { + padding: 0 1px; +} + .properties { display: none; position: fixed; @@ -547,11 +603,8 @@ button.selected { align-items: center; background-color: #4f4f4fb7; border-radius: 4px; -} - -#user-avatar:hover { - background-color: #4f4f4f !important; - transition-duration: 0.4s; + margin-left: 0.75em; + padding: 0 0.75em 0 0; } #status-info { diff --git a/src/app.js b/src/app.js index 62626b62832d8be6b263f7489d3858eeaf8fea11..4367171fd94c405ca120d0152251580f76805b91 100644 --- a/src/app.js +++ b/src/app.js @@ -158,6 +158,11 @@ const onRoomConnect = (room_) => { canvas.renderPath(id, points, room.erasureIntervals[id] || []) }, ) + + room.addEventListener("undoEnabled", () => { + HTML.fastUndoButton.classList.remove("disabled") + HTML.undoButton.classList.remove("disabled") + }) } const tryRoomConnect = async (roomID) => { @@ -190,12 +195,35 @@ const onRoomJoinEnter = () => { canvas.clear() HTML.connectedPeers.innerHTML = "No peers are connected" + HTML.fastUndoButton.classList.add("disabled") + HTML.undoButton.classList.add("disabled") tryRoomConnect(selectedRoomID) } HTML.roomConnectButton.addEventListener("click", onRoomJoinEnter) +HTML.fastUndoButton.addEventListener("click", () => { + if (room == null) return + + room.fastUndo() + + if (!room.canUndo()) { + HTML.fastUndoButton.classList.add("disabled") + HTML.undoButton.classList.add("disabled") + } +}) +HTML.undoButton.addEventListener("click", () => { + if (room == null) return + + room.undo() + + if (!room.canUndo()) { + HTML.fastUndoButton.classList.add("disabled") + HTML.undoButton.classList.add("disabled") + } +}) + HTML.roomIDElem.addEventListener("keydown", (event) => { if (event.key == "Enter") { event.target.blur() diff --git a/src/elements.js b/src/elements.js index b1902a21ff6bd350fede2d44e7300c243ab7efa3..d2dd34fb019b214355e71fcbbc0586963bccb9d2 100644 --- a/src/elements.js +++ b/src/elements.js @@ -13,6 +13,9 @@ export const canvas = document.getElementById("canvas") export const penButton = document.getElementById("pen-tool") export const eraserButton = document.getElementById("eraser-tool") +export const fastUndoButton = document.getElementById("fast-undo-tool") +export const undoButton = document.getElementById("undo-tool") + export const roomIDElem = document.getElementById("room-id") export const roomConnectButton = document.getElementById("room-connect") export const connectedRoomID = document.getElementById("connected-room-id") diff --git a/src/room.js b/src/room.js index a158b5a902f43fa963f10023c76a2d63bbdee235..d39589111bdb4a6e4b3a83e08aed05f4c2420c5c 100644 --- a/src/room.js +++ b/src/room.js @@ -24,6 +24,7 @@ class Room extends EventTarget { this._y = null this.ownID = null this.erasureIntervals = {} + this.undoStack = [] } disconnect() { @@ -36,11 +37,25 @@ class Room extends EventTarget { this.shared.strokePoints.set(id, Y.Array).push([[x, y, w, colour]]) this.shared.eraseIntervals.set(id, Y.Union) + this.undoStack.push([id, 0, 0]) + + this.dispatchEvent(new CustomEvent("undoEnabled")) + return id } extendPath(id, [x, y, w, colour]) { - this.shared.strokePoints.get(id).push([[x, y, w, colour]]) + const path = this.shared.strokePoints.get(id) + + path.push([[x, y, w, colour]]) + + if (path.length == 2) { + this.undoStack[this.undoStack.length - 1] = [id, 0, 1] + } else { + this.undoStack.push([id, path.length - 2, path.length - 1]) + } + + this.dispatchEvent(new CustomEvent("undoEnabled")) } extendErasureIntervals(pathID, pointID, newIntervals) { @@ -49,6 +64,40 @@ class Room extends EventTarget { .merge(flattenErasureIntervals({ [pointID]: newIntervals })) } + undo() { + const operation = this.undoStack.pop() + + if (!operation) return + + const [id, ...interval] = operation + + this.shared.eraseIntervals.get(id).merge([interval]) + } + + fastUndo() { + let from = this.undoStack.length - 1 + + if (from < 0) return + + // eslint-disable-next-line no-unused-vars + const [id, _, end] = this.undoStack[from] + + for (; from >= 0; from--) { + if (this.undoStack[from][0] != id) { + from++ + break + } + } + + this.undoStack = this.undoStack.slice(0, Math.max(0, from)) + + this.shared.eraseIntervals.get(id).merge([[0, end]]) + } + + canUndo() { + return this.undoStack.length > 0 + } + getPaths() { const paths = new Map()