diff --git a/package-lock.json b/package-lock.json index 6796e5ac07630fa91a9c03e636cb4a8b617aa03e..faa11a84c55bcfbed36e95737ea505d9261087c1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -484,9 +484,9 @@ } }, "@types/babel__traverse": { - "version": "7.0.7", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.0.7.tgz", - "integrity": "sha512-CeBpmX1J8kWLcDEnI3Cl2Eo6RfbGvzUctA+CjZUhOKDFbLfcr7fc4usEqLNWetrlJd7RhAkyYe2czXop4fICpw==", + "version": "7.0.8", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.0.8.tgz", + "integrity": "sha512-yGeB2dHEdvxjP0y4UbRtQaSkXJ9649fYCmIdRoul5kfAoGCwxuCbMhag0k3RPfnuh9kPGm8x89btcfDEXdVWGw==", "dev": true, "requires": { "@babel/types": "^7.3.0" @@ -518,9 +518,9 @@ } }, "@types/node": { - "version": "12.12.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.7.tgz", - "integrity": "sha512-E6Zn0rffhgd130zbCbAr/JdXfXkoOUFAKNs/rF8qnafSJ8KYaA/j3oz7dcwal+lYjLA7xvdd5J4wdYpCTlP8+w==" + "version": "12.12.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.11.tgz", + "integrity": "sha512-O+x6uIpa6oMNTkPuHDa9MhMMehlxLAd5QcOvKRjAFsBVpeFWTOPnXbDvILvFgFFZfQ1xh1EZi1FbXxUix+zpsQ==" }, "@types/stack-utils": { "version": "1.0.1", @@ -817,12 +817,12 @@ "dev": true }, "ansi-escapes": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.2.1.tgz", - "integrity": "sha512-Cg3ymMAdN10wOk/VYfLV7KCQyv7EDirJ64500sU7n9UlmioEtDuU5Gd+hj73hXSU/ex7tHJSssmyftDdkMLO8Q==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.0.tgz", + "integrity": "sha512-EiYhwo0v255HUL6eDyuLrXEkTi7WwVCLAw+SeOQ7M7qdun1z1pum4DEm/nuqIVbPvi9RPPc9k9LbyBv6H0DwVg==", "dev": true, "requires": { - "type-fest": "^0.5.2" + "type-fest": "^0.8.1" } }, "ansi-regex": { @@ -1911,14 +1911,14 @@ "dev": true }, "d3-path": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.8.tgz", - "integrity": "sha512-J6EfUNwcMQ+aM5YPOB8ZbgAZu6wc82f/0WFxrxwV6Ll8wBwLaHLKCqQ5Imub02JriCVVdPjgI+6P3a4EWJCxAg==" + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==" }, "d3-shape": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.5.tgz", - "integrity": "sha512-VKazVR3phgD+MUCldapHD7P9kcrvPcexeX/PkMJmkUov4JM8IxsSg1DvbYoYich9AtdTsa5nNk2++ImPiDiSxg==", + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", "requires": { "d3-path": "1" } @@ -2150,9 +2150,9 @@ "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" }, "ejs": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-2.7.2.tgz", - "integrity": "sha512-rHGwtpl67oih3xAHbZlpw5rQAt+YV1mSCu2fUZ9XNrfaGEhom7E+AUiMci+ByP4aSfuAWx7hE0BPuJLMrpXwOw==", + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-2.7.4.tgz", + "integrity": "sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA==", "dev": true }, "elliptic": { @@ -2447,9 +2447,9 @@ } }, "eslint-config-prettier": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.5.0.tgz", - "integrity": "sha512-cjXp8SbO9VFGW/Z7mbTydqS9to8Z58E5aYhj3e1+Hx7lS9s6gL5ILKNpCqZAFOVYRcSkWPFYljHrEh8QFEK5EQ==", + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.7.0.tgz", + "integrity": "sha512-FamQVKM3jjUVwhG4hEMnbtsq7xOIDm+SY5iBPfR8gKsJoAB2IQnNF+bk1+8Fy44Nq7PPJaLvkRxILYdJWoguKQ==", "dev": true, "requires": { "get-stdin": "^6.0.0" @@ -3703,9 +3703,9 @@ } }, "handlebars": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.5.2.tgz", - "integrity": "sha512-29Zxv/cynYB7mkT1rVWQnV7mGX6v7H/miQ6dbEpYTKq5eJBN7PsRB+ViYJlcT6JINTSu4dVB9kOqEun78h6Exg==", + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.5.3.tgz", + "integrity": "sha512-3yPecJoJHK/4c6aZhSvxOyG4vJKDshV36VHp0iVCDVh7o9w2vwi3NSnL2MMPj3YdduqaBcu7cGbggJQM0br9xA==", "dev": true, "requires": { "neo-async": "^2.6.0", @@ -3767,9 +3767,9 @@ "dev": true }, "has-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", - "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", "dev": true }, "has-value": { @@ -4226,12 +4226,12 @@ "dev": true }, "is-symbol": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz", - "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", + "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", "dev": true, "requires": { - "has-symbols": "^1.0.0" + "has-symbols": "^1.0.1" } }, "is-typedarray": { @@ -6349,9 +6349,9 @@ "dev": true }, "resolve": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.12.0.tgz", - "integrity": "sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.12.2.tgz", + "integrity": "sha512-cAVTI2VLHWYsGOirfeYVVQ7ZDejtQ9fp4YhYckWDEkFfqbVjaT11iM8k6xSAfGFMM+gDpZjMnFssPu8we+mqFw==", "dev": true, "requires": { "path-parse": "^1.0.6" @@ -7532,9 +7532,9 @@ } }, "type-fest": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.5.2.tgz", - "integrity": "sha512-DWkS49EQKVX//Tbupb9TFa19c7+MK1XmzkrZUR8TAktmE/DizXoaoJV6TZ/tSIPXipqNiRI6CyAe7x69Jb6RSw==", + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", "dev": true }, "type-is": { diff --git a/public/index.html b/public/index.html index ca2ae6044aece77c536d264245695f3d3464af17..dab54f181c4cc70663bc9a45d5c76b581bbc0e9a 100644 --- a/public/index.html +++ b/public/index.html @@ -57,7 +57,9 @@ size="25" color="gray" /> - <button id="room-connect">Connect <i class="fa fa-link"></i></button> + <button id="room-connect"> + Connect <i class="fa fa-link" style="padding: 0 1.5px"></i> + </button> </div> <div id="tools-panel"> <button id="pen-tool" class="selected"> @@ -197,13 +199,21 @@ </div> </div> </div> - <button id="eraser-tool"><i class="fa fa-eraser"></i></button> + <button id="eraser-tool"> + <i class="fa fa-eraser" style="padding: 0 1px"></i> + </button> <div id="status-info"> - <div id="user-avatar"></div> + <button id="fast-undo-tool" class="disabled"> + <i class="fa fa-fast-backward" style="padding: 0 2.5px"></i> + </button> + <button id="undo-tool" class="disabled"> + <i class="fa fa-backward" style="padding: 0 4px 0 1px"></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..578139c8e20b4454ff96fe53c1a2d9e883eced18 100644 --- a/public/styles.css +++ b/public/styles.css @@ -33,15 +33,14 @@ body { align-items: center; background-color: #4f4f4fb7; border-radius: 4px; - margin-left: 8px; justify-content: center; - height: 44px; + padding: 0.75em; } -#connected-room-info:hover { +/*#connected-room-info:hover { background-color: #4f4f4f !important; transition-duration: 0.4s; -} +}*/ button.selected { background-color: gray !important; @@ -269,6 +268,11 @@ button.selected { border-radius: 4px; } +#room-connect:hover { + background-color: #4f4f4f !important; + transition-duration: 0.4s; +} + #pen-tool { background-color: #2f2f2f; color: white; @@ -293,6 +297,7 @@ button.selected { border: none; cursor: pointer; border-radius: 50%; + margin-right: 8px; } #eraser-tool:hover { @@ -300,6 +305,46 @@ button.selected { transition-duration: 0.4s; } +#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; +} + +#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; +} + .properties { display: none; position: fixed; @@ -547,12 +592,14 @@ button.selected { align-items: center; background-color: #4f4f4fb7; border-radius: 4px; + margin-left: 0.75em; + padding: 0 0.75em 0 0; } -#user-avatar:hover { +/*#user-avatar:hover { background-color: #4f4f4f !important; transition-duration: 0.4s; -} +}*/ #status-info { display: flex; diff --git a/src/app.js b/src/app.js index 62626b62832d8be6b263f7489d3858eeaf8fea11..10ab8c2cac911c8c1075876b18683de8b32b6081 100644 --- a/src/app.js +++ b/src/app.js @@ -158,6 +158,16 @@ const onRoomConnect = (room_) => { canvas.renderPath(id, points, room.erasureIntervals[id] || []) }, ) + + room.addEventListener("undoEnabled", () => { + HTML.fastUndoButton.classList.remove("disabled") + HTML.undoButton.classList.remove("disabled") + }) + + room.addEventListener("undoDisabled", () => { + HTML.fastUndoButton.classList.add("disabled") + HTML.undoButton.classList.add("disabled") + }) } const tryRoomConnect = async (roomID) => { @@ -190,12 +200,20 @@ 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", + () => room != null && room.fastUndo(), +) +HTML.undoButton.addEventListener("click", () => room != null && room.undo()) + 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..c0325bb3e821168b29c82201da34d0443a99d3db 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,44 @@ 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]) + + if (this.undoStack.length <= 0) { + this.dispatchEvent(new CustomEvent("undoDisabled")) + } + } + + 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]]) + + if (this.undoStack.length <= 0) { + this.dispatchEvent(new CustomEvent("undoDisabled")) + } + } + getPaths() { const paths = new Map()