diff --git a/.gitignore b/.gitignore index f13064819488f1b136bbc6ac9e0b1326ac5124e0..63f3ad56b484ab5b28efdf6446016e0128452f41 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ src/liowebrtc src/rtcpeerconnection src/signalbuddy +src/yjs ### macOS ### # General diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 56e8e90ebe05d31d0722e3f9158fc3926d9b9bef..c334e6cb01556c29d2e001314e3e524aeb943ef1 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -20,6 +20,7 @@ submodule_fetch: - src/liowebrtc - src/rtcpeerconnection - src/signalbuddy + - src/yjs node_install: stage: deps @@ -33,6 +34,7 @@ node_install: - src/liowebrtc - src/rtcpeerconnection - src/signalbuddy + - src/yjs dev_node_install: stage: deps @@ -46,6 +48,7 @@ dev_node_install: - src/liowebrtc - src/rtcpeerconnection - src/signalbuddy + - src/yjs check_format: stage: check diff --git a/.gitmodules b/.gitmodules index 378c66afc8eff5619a5903583531a433c601e60a..ddaddd31d0dc09cb5d1af99e79a14a5669247859 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,3 +7,6 @@ [submodule "src/liowebrtc"] path = src/liowebrtc url = git@gitlab.doc.ic.ac.uk:sweng-group-15/liowebrtc.git +[submodule "src/yjs"] + path = src/yjs + url = git@gitlab.doc.ic.ac.uk:sweng-group-15/yjs.git diff --git a/package-lock.json b/package-lock.json index e070bb2184dc24fa20b31f9d09a19d0467fb5f7a..15cbdc85de4de88e8475ba0f7cc92af9022f7a13 100644 --- a/package-lock.json +++ b/package-lock.json @@ -713,9 +713,9 @@ } }, "acorn-jsx": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.0.2.tgz", - "integrity": "sha512-tiNTrP1MP0QrChmD2DdupCr6HWSFeKVw5d/dHTu4Y7rkAkRhU/Dt7dphAfIUyxtHpl/eBVip5uTNSpQJHylpAw==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.1.0.tgz", + "integrity": "sha512-tMUqwBWfLFbJbizRmEcWSLw6HnFzfdJs2sOJEOwwtVPMoH/0Ay+E703oZz78VSXZiiDcZrQ5XKjPIUQixhmgVw==", "dev": true }, "acorn-walk": { @@ -1112,9 +1112,9 @@ "integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==" }, "bluebird": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.0.tgz", - "integrity": "sha512-aBQ1FxIa7kSWCcmKHlcHFlT2jt6J/l4FzC7KcPELkOJOsPOb/bccdhmIrKDfXhwFrmc7vDoDrrepFvGqjyXGJg==", + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.1.tgz", + "integrity": "sha512-DdmyoGCleJnkbp3nkbxTLJ18rjDsE4yCggEwKNXkeV123sPNfOCYeDoeuOY+F2FrSjO1YXcTU+dsy96KMy+gcg==", "dev": true }, "bn.js": { @@ -4808,13 +4808,13 @@ "liowebrtc": { "version": "file:src/liowebrtc", "requires": { - "attachmediastream": "^2.0.4", + "attachmediastream": "^2.1.0", "filetransfer": "^2.0.4", "hark": "^1.2.0", "mockconsole": "0.0.1", "rtcpeerconnection": "file:src/rtcpeerconnection", "socket.io-client": "^2.3.0", - "webrtc-adapter": "^4.0.0", + "webrtc-adapter": "^7.3.0", "wildemitter": "^1.2.0" } }, @@ -4880,11 +4880,6 @@ "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", "dev": true }, - "lodash.clonedeep": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", - "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=" - }, "lodash.sortby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", @@ -6268,6 +6263,31 @@ "lodash.clonedeep": "^4.3.2", "sdp-jingle-json": "^3.0.0", "wildemitter": "1.x" + }, + "dependencies": { + "lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=" + }, + "sdp-jingle-json": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/sdp-jingle-json/-/sdp-jingle-json-3.1.0.tgz", + "integrity": "sha512-Uu+FelZD/edNoOc64NwQP8jjbBVMggAaErGU+2cSxPZgyReJTtqtp5287p2vu7bHubERxEbiW0H1pC2fnH5GEA==" + }, + "wildemitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/wildemitter/-/wildemitter-1.2.1.tgz", + "integrity": "sha512-UMmSUoIQSir+XbBpTxOTS53uJ8s/lVhADCkEbhfRjUGFDPme/XGOb0sBWLx5sTz7Wx/2+TlAw1eK9O5lw5PiEw==" + } + } + }, + "rtcpeerconnection-shim": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/rtcpeerconnection-shim/-/rtcpeerconnection-shim-1.2.15.tgz", + "integrity": "sha512-C6DxhXt7bssQ1nHb154lqeL0SXz5Dx4RczXZu2Aa/L1NJFnEVDxFwCBo3fqtuljhHIGceg5JKBV4XJ0gW5JKyw==", + "requires": { + "sdp": "^2.6.0" } }, "run-async": { @@ -6363,11 +6383,6 @@ "resolved": "https://registry.npmjs.org/sdp/-/sdp-2.10.0.tgz", "integrity": "sha512-H+VjfyQpRz9GezhshJmkXTtCAT9/2g9az3GFDPYfGOz0eAOQU1fCrL3S9Dq/eUT9FtOyLi/czdR9PzK3fKUYOQ==" }, - "sdp-jingle-json": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/sdp-jingle-json/-/sdp-jingle-json-3.1.0.tgz", - "integrity": "sha512-Uu+FelZD/edNoOc64NwQP8jjbBVMggAaErGU+2cSxPZgyReJTtqtp5287p2vu7bHubERxEbiW0H1pC2fnH5GEA==" - }, "semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", @@ -7081,9 +7096,9 @@ "dev": true }, "terser": { - "version": "4.3.8", - "resolved": "https://registry.npmjs.org/terser/-/terser-4.3.8.tgz", - "integrity": "sha512-otmIRlRVmLChAWsnSFNO0Bfk6YySuBp6G9qrHiJwlLDd4mxe2ta4sjI7TzIR+W1nBMjilzrMcPOz9pSusgx3hQ==", + "version": "4.3.9", + "resolved": "https://registry.npmjs.org/terser/-/terser-4.3.9.tgz", + "integrity": "sha512-NFGMpHjlzmyOtPL+fDw3G7+6Ueh/sz4mkaUYa4lJCxOPTNzd0Uj0aZJOmsDYoSQyfuVoWDMSWTPU3huyOm2zdA==", "dev": true, "requires": { "commander": "^2.20.0", @@ -7307,9 +7322,9 @@ "dev": true }, "uglify-js": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.1.tgz", - "integrity": "sha512-+dSJLJpXBb6oMHP+Yvw8hUgElz4gLTh82XuX68QiJVTXaE5ibl6buzhNkQdYhBlIhozWOC9ge16wyRmjG4TwVQ==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.2.tgz", + "integrity": "sha512-+gh/xFte41GPrgSMJ/oJVq15zYmqr74pY9VoM69UzMzq9NFk4YDylclb1/bhEzZSaUQjbW5RvniHeq1cdtRYjw==", "dev": true, "optional": true, "requires": { @@ -7552,9 +7567,9 @@ "dev": true }, "webpack": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.41.1.tgz", - "integrity": "sha512-ak7u4tUu/U63sCVxA571IuPZO/Q0pZ9cEXKg+R/woxkDzVovq57uB6L2Hlg/pC8LCU+TWpvtcYwsstivQwMJmw==", + "version": "4.41.2", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.41.2.tgz", + "integrity": "sha512-Zhw69edTGfbz9/8JJoyRQ/pq8FYUoY0diOXqW0T6yhgdhCv6wr0hra5DwwWexNRns2Z2+gsnrNcbe9hbGBgk/A==", "dev": true, "requires": { "@webassemblyjs/ast": "1.8.5", @@ -7688,11 +7703,12 @@ } }, "webrtc-adapter": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-4.2.2.tgz", - "integrity": "sha1-F4lsBHCE/UxWeVigzUMh4X8ydzw=", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-7.3.0.tgz", + "integrity": "sha512-pKcwt6IR6RLCD6jlcdOOi88iVwdzppHlkOhtgTSuZHtYTxdD09t5fA1Di7GJU7je8oHcCBlNfb7zwBsetERnmQ==", "requires": { - "sdp": "^2.1.0" + "rtcpeerconnection-shim": "^1.2.15", + "sdp": "^2.10.0" } }, "whatwg-encoding": { @@ -7905,9 +7921,7 @@ "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=" }, "yjs": { - "version": "12.3.3", - "resolved": "https://registry.npmjs.org/yjs/-/yjs-12.3.3.tgz", - "integrity": "sha1-e+wU1Zr+Fm1ozCsnQTGTwOW6ckw=", + "version": "file:src/yjs", "requires": { "debug": "^2.6.3" } diff --git a/package.json b/package.json index f63bbd65ea08152fe06ae877e4db5722d85b0755..2d7731f14ed8d7c64462f24f3610549c3c2db076 100644 --- a/package.json +++ b/package.json @@ -10,9 +10,9 @@ "scripts": { "build": "webpack src/app.js -o public/js/app.js", "start": "node --experimental-modules src/server.js", - "test": "jest --testPathIgnorePatterns src/liowebrtc src/rtcpeerconnection src/signalbuddy", - "test-changed": "jest --only-changed --testPathIgnorePatterns src/liowebrtc src/rtcpeerconnection src/signalbuddy", - "test-coverage": "jest --coverage --testPathIgnorePatterns src/liowebrtc src/rtcpeerconnection src/signalbuddy", + "test": "jest --testPathIgnorePatterns src/liowebrtc src/rtcpeerconnection src/signalbuddy src/yjs", + "test-changed": "jest --only-changed --testPathIgnorePatterns src/liowebrtc src/rtcpeerconnection src/signalbuddy src/yjs", + "test-coverage": "jest --coverage --testPathIgnorePatterns src/liowebrtc src/rtcpeerconnection src/signalbuddy src/yjs", "lint": "jshint .", "validate": "npm ls" }, @@ -22,11 +22,11 @@ "liowebrtc": "file:src/liowebrtc", "signalbuddy": "file:src/signalbuddy", "uuid": "^3.3.3", - "webrtc-adapter": "^4.0.0", + "webrtc-adapter": "^7.3.0", "y-array": "^10.1.4", "y-map": "^10.1.3", "y-memory": "^8.0.9", - "yjs": "^12.3.3" + "yjs": "file:src/yjs" }, "devDependencies": { "eslint": "^6.5.1", diff --git a/public/index.html b/public/index.html index 27fb261049f824a9d5b36554cad399c35c59b644..d023da56ddca6ac2ddee27dd0ae12f8299bba802 100644 --- a/public/index.html +++ b/public/index.html @@ -18,6 +18,11 @@ ) } </script> + <style> + button.selected { + background-color: lightgray; + } + </style> </head> <body> <div> @@ -31,15 +36,23 @@ Connected peers: <ul id="connected-peers"></ul> </div> - <button id="pen">Pen</button> - <button id="eraser">Eraser</button> + <button id="pen-tool" class="selected">Pen</button> + <button id="eraser-tool">Eraser</button> - <svg - id="whiteboard" - width="100%" - height="100%" - style="position: fixed" - ></svg> + <svg id="canvas" width="100%" height="100%" style="position: fixed"> + <defs> + <marker + id="dot" + markerUnits="userSpaceOnUse" + markerWidth="4" + markerHeight="4" + refX="2" + refY="2" + > + <circle cx="2" cy="2" r="2" fill="blue" /> + </marker> + </defs> + </svg> <script src="js/app.js"></script> </body> diff --git a/src/app.js b/src/app.js index e9d3e9386fa77c66106c7ef6a06477201343abee..efa9f2bc574e3144cf69ac8d4dcb4b8fb4988772 100644 --- a/src/app.js +++ b/src/app.js @@ -1,284 +1,202 @@ -import { line, curveBasis } from "d3-shape" -import uuidv4 from "uuid/v4" -import yMemory from "y-memory" -import yMap from "y-map" -import yArray from "y-array" -import Y from "yjs" - -import yWebrtc from "./y-webrtc/index.js" - -yMemory(Y) -yMap(Y) -yArray(Y) -yWebrtc(Y) - -Y({ - db: { - name: "memory", - }, - connector: { - name: "webrtc", - url: "/", - room: "imperial", - }, - share: { - drawing: "Map", - }, -}).then((y) => { - const userIDElem = document.getElementById("user-id") - const peerIDElem = document.getElementById("peer-id") - const peerButton = document.getElementById("peer-connect") - const connectedP = document.getElementById("connected-peers") - const penButton = document.getElementById("pen") - const eraserButton = document.getElementById("eraser") - - userIDElem.value = y.db.userId - - y.connector.onUserEvent(function(event) { - switch (event.action) { - case "userID": - userIDElem.value = event.id - break - case "userJoined": - var peerElem = document.createElement("li") - peerElem.innerHTML = event.user - connectedP.appendChild(peerElem) - break - case "userLeft": - for (var peer of connectedP.children) { - if (peer.innerHTML == event.user) { - connectedP.removeChild(peer) - } - } - break - } - }) - - peerButton.onclick = function() { - const peerID = peerIDElem.value - - if (peerID == "") { - return - } - - y.connector.connectToPeer(peerID) - - peerIDElem.value = "" +import { line, curveLinear } from "d3-shape" + +import { + canvas, + connectedPeers, + peerButton, + peerIDElem, + userIDElem, + penButton, + eraserButton, +} from "./elements.js" +import { connect } from "./room.js" + +// TODO: switch to curve interpolation that respects mouse points based on velocity +const lineFn = line() + .x((d) => d[0]) + .y((d) => d[1]) + .curve(curveLinear) + +const tools = { + PEN: "pen", + ERASER: "eraser", +} + +const STROKECOLOUR = "blue" +const STROKERADIUS = 2 +const ERASERRADIUS = STROKERADIUS * 2 + +const pathElems = new Map() + +const addOrUpdatePathElem = (id, points) => { + let pathElem = pathElems.get(id) + + if (pathElem == null) { + pathElem = document.createElementNS("http://www.w3.org/2000/svg", "g") + + pathElem.setAttribute("stroke", STROKECOLOUR) + pathElem.setAttribute("stroke-width", STROKERADIUS * 2) + pathElem.setAttribute("fill", "none") + pathElem.setAttribute("pointer-events", "none") + pathElem.setAttribute("marker-start", "url(#dot)") + pathElem.setAttribute("marker-end", "url(#dot)") + + canvas.appendChild(pathElem) + pathElems.set(id, pathElem) } - // Used to check what kind of tool is selected - var addingLine = true - var removingLine = false - - const CHECKRADIUS = 10 + pathElem.innerHTML = "" - penButton.onclick = function() { - // If pen tool selected - if (!addingLine) { - addingLine = true - removingLine = false - } - } - - eraserButton.onclick = function() { - // If eraser tool selected - if (!removingLine) { - removingLine = true - addingLine = false - } + if (points.length == 0) { + return pathElem } - const whiteboard = document.getElementById("whiteboard") + // Push a fake path split to generate the last path + points.push([-1, -1, false]) - var input = false - var paths = new Map() - var pathID + let subpath = [] - function createOrUpdatePath(uid, points) { - const lineFn = line() - .x((d) => d[0]) - .y((d) => d[1]) - .curve(curveBasis) - - var path = paths.get(uid) - - if (path === undefined) { - path = document.createElementNS("http://www.w3.org/2000/svg", "path") - - path.setAttribute("stroke", "blue") - path.setAttribute("stroke-width", 3) - path.setAttribute("fill", "none") - path.setAttribute("pointer-events", "none") - - whiteboard.appendChild(path) - - paths.set(uid, path) + for (let point of points) { + if (point[0] === undefined) { + continue } - points = points.toArray().filter((point) => point !== undefined) + if (point[2] === false) { + if (subpath.length === 1) { + let subpathElem = document.createElementNS( + "http://www.w3.org/2000/svg", + "circle", + ) + + subpathElem.setAttribute("stroke", "none") + subpathElem.setAttribute("fill", STROKECOLOUR) + subpathElem.setAttribute("cx", subpath[0][0]) + subpathElem.setAttribute("cy", subpath[0][1]) + subpathElem.setAttribute("r", STROKERADIUS) + + pathElem.appendChild(subpathElem) + } else if (subpath.length > 0) { + let subpathElem = document.createElementNS( + "http://www.w3.org/2000/svg", + "path", + ) + + subpathElem.setAttribute("d", lineFn(subpath)) + + pathElem.appendChild(subpathElem) + } - if (points.length <= 0) { - path.removeAttribute("d") + subpath = [] - return path + continue } - path.setAttribute("d", lineFn(points)) - - console.log(points) - console.log(path) - - return path + subpath.push(point) } - function removeOrUpdatePath(uid) { - var path = paths.get(uid) + return pathElem +} - if (path !== undefined) { - paths.delete(path) - } - } +const getDistance = (a, b) => { + return Math.sqrt( + (a[0] - b[0]) * (a[0] - b[0]) + (a[1] - b[1]) * (a[1] - b[1]), + ) +} - whiteboard.onmousedown = function(e) { - input = true - - const mouse = { - x: e.offsetX, - y: e.offsetY, - } +connect("imperial").then((room) => { + let userInput = false + let currentTool = tools.PEN + let currentPathID = null - if (addingLine) { - pathID = uuidv4() - const sharedPath = y.share.drawing.set(pathID, Y.Array) - sharedPath.push([[mouse.x, mouse.y, true]]) - } else if (removingLine) { - // Iterate over all the possible paths in the Map - const mapPaths = y.share.drawing.keys() - for (var mapPath of mapPaths) { - var found = false - // Get the array of coordinates - var mouseYArray = y.share.drawing.get(mapPath) - var mouseArray = mouseYArray.toArray() - - // Check the array for current position - for (var i = 0; i < mouseArray.length; i++) { - var point = mouseArray[i] - if (checkRadius(point, mouse)) { - // Delete point - point[2] = false - // Update map - mouseYArray.insert(i, point) - y.share.drawing.set(mapPath, mouseYArray) - found = true - break - } - } - if (found) { - break - } - } - } - } + userIDElem.value = room.ownID || "" + room.addEventListener("allocateOwnID", ({ detail: id }) => { + userIDElem.value = id + }) - /* Helper function that checks whether a point is within the mouse radius */ - function checkRadius(mouseMap, mouse) { - var mouseMapX = mouseMap[0] - var mouseMapY = mouseMap[1] - var mouseX = mouse.x - var mouseY = mouse.y - - for (var i = 0; i < CHECKRADIUS; i++) { - // Chech x-axis - if (mouseX + i == mouseMapX) { - return true - } else if (mouseX - i == mouseMapX) { - return true - } + room.addEventListener("userJoin", ({ detail: id }) => { + const peerElem = document.createElement("li") + peerElem.innerHTML = id + connectedPeers.appendChild(peerElem) + }) - // Check y-axis - if (mouseY + i == mouseMapY) { - return true - } else if (mouseY - i == mouseMapY) { - return true + room.addEventListener("userLeave", ({ detail: id }) => { + for (const peerElem of connectedPeers.children) { + if (peerElem.innerHTML == id) { + connectedPeers.removeChild(peerElem) } } + }) - return false - } + room.addEventListener("addOrUpdatePath", ({ detail: { id, points } }) => { + addOrUpdatePathElem(id, points) + }) - whiteboard.onmouseup = function() { - input = false - } + canvas.addEventListener("mousedown", (e) => { + userInput = true - whiteboard.onmousemove = function(e) { - const mouse = { - x: e.offsetX, - y: e.offsetY, - } + let mouse = [e.offsetX, e.offsetY] - if (input) { - if (addingLine) { - const sharedPath = y.share.drawing.get(pathID) - sharedPath.push([[mouse.x, mouse.y, true]]) - } else if (removingLine) { - // Iterate over all the possible paths in the Map - const mapPaths = y.share.drawing.keys() - for (var mapPath of mapPaths) { - var found = false - // Get the array of coordinates - var mouseYArray = y.share.drawing.get(mapPath) - var mouseArray = mouseYArray.toArray() - - // Check the array for current position - for (var i = 0; i < mouseArray.length; i++) { - var point = mouseArray[i] - if (checkRadius(point, mouse)) { - // Delete point - point[2] = false - // Update map - mouseYArray.insert(i, point) - y.share.drawing.set(mapPath, mouseYArray) - found = true - break - } + if (currentTool === tools.PEN) { + currentPathID = room.addPath(mouse) + } else if (currentTool === tools.ERASER) { + room.getPaths().forEach((points, pathID) => { + points.forEach((point, i) => { + if (getDistance(mouse, point) <= ERASERRADIUS) { + room.erasePoint(pathID, i) } - if (found) { - break - } - } - } + }) + }) } - } - - y.share.drawing.observe(function(lineEvent) { - const lineID = lineEvent.name + }) - switch (lineEvent.type) { - case "add": - createOrUpdatePath(lineID, lineEvent.value) + canvas.addEventListener("mouseleave", () => { + userInput = false + }) - lineEvent.value.observe(function(pointEvent) { - switch (pointEvent.type) { - case "insert": - createOrUpdatePath(lineID, pointEvent.object) - break - } - }) + canvas.addEventListener("mouseup", () => { + userInput = false + }) - break + canvas.addEventListener("mousemove", (e) => { + if (!userInput) { + return + } - case "update": - removeOrUpdatePath(lineID) + let mouse = [e.offsetX, e.offsetY] - lineEvent.value.observe(function(pointEvent) { - switch (pointEvent.type) { - case "insert": - removeOrUpdatePath(lineID) - break + if (currentTool === tools.PEN) { + room.extendPath(currentPathID, mouse) + } else if (currentTool === tools.ERASER) { + room.getPaths().forEach((points, pathID) => { + points.forEach((point, i) => { + if (getDistance(mouse, point) <= ERASERRADIUS) { + room.erasePoint(pathID, i) } }) + }) + } + }) - break + peerButton.addEventListener("click", () => { + const peerID = peerIDElem.value + if (peerID == "") { + return } + room.inviteUser(peerID) + peerIDElem.value = "" + }) + + penButton.addEventListener("click", () => { + currentTool = tools.PEN + + penButton.classList.add("selected") + eraserButton.classList.remove("selected") + }) + + eraserButton.addEventListener("click", () => { + currentTool = tools.ERASER + + penButton.classList.remove("selected") + eraserButton.classList.add("selected") }) }) diff --git a/src/elements.js b/src/elements.js new file mode 100644 index 0000000000000000000000000000000000000000..71d88d82b0e4a8d90248b063f9c54a5690d56dbc --- /dev/null +++ b/src/elements.js @@ -0,0 +1,7 @@ +export const userIDElem = document.getElementById("user-id") +export const peerIDElem = document.getElementById("peer-id") +export const peerButton = document.getElementById("peer-connect") +export const connectedPeers = document.getElementById("connected-peers") +export const canvas = document.getElementById("canvas") +export const penButton = document.getElementById("pen-tool") +export const eraserButton = document.getElementById("eraser-tool") diff --git a/src/liowebrtc b/src/liowebrtc index afa8666f796bdd40cc263354917632ea671dfee2..f0b57a9258b117b97e1b793de27f05b0c9d5e51f 160000 --- a/src/liowebrtc +++ b/src/liowebrtc @@ -1 +1 @@ -Subproject commit afa8666f796bdd40cc263354917632ea671dfee2 +Subproject commit f0b57a9258b117b97e1b793de27f05b0c9d5e51f diff --git a/src/room.js b/src/room.js new file mode 100644 index 0000000000000000000000000000000000000000..367e136d9520f064ea07dbf96862e0f0a689ca2a --- /dev/null +++ b/src/room.js @@ -0,0 +1,134 @@ +import uuidv4 from "uuid/v4" +import yArray from "y-array" +import yMap from "y-map" +import yMemory from "y-memory" +import Y from "yjs" + +import yWebrtc from "./y-webrtc/index.js" + +yMemory(Y) +yMap(Y) +yArray(Y) +yWebrtc(Y) + +class Room extends EventTarget { + constructor(name) { + super() + this.name = name + this._y = null + this.ownID = null + } + + addPath([x, y]) { + const id = uuidv4() + this._y.share.strokeAdd.set(id, Y.Array).push([[x, y]]) + return id + } + + extendPath(id, [x, y]) { + this._y.share.strokeAdd.get(id).push([[x, y]]) + } + + getPaths() { + let paths = new Map() + + for (let id of this._y.share.strokeAdd.keys()) { + paths.set(id, this._generatePath(id)) + } + + return paths + } + + erasePoint(id, idx) { + let eraseSet = this._y.share.strokeErase.get(id) + + if (!eraseSet) { + eraseSet = this._y.share.strokeErase.set(id, Y.Map) + } + + eraseSet.set(idx.toString(), true) + } + + // Generate an array of points [x, y, exist] by merging the path's add and erase sets + _generatePath(id) { + let addSet = this._y.share.strokeAdd.get(id) + + if (addSet === undefined) { + return [] + } + + let eraseSet = this._y.share.strokeErase.get(id) || { get: () => false } + + return addSet + .toArray() + .map((p = [], i) => [p[0], p[1], !eraseSet.get(i.toString())]) + } + + inviteUser(id) { + this._y.connector.connectToPeer(id) + } + + async _initialise() { + this._y = await Y({ + db: { + name: "memory", + }, + connector: { + name: "webrtc", + url: "/", + room: this.name, + onUserEvent: (event) => { + if (event.action == "userID") { + const { id } = event + this.ownID = id + this.dispatchEvent(new CustomEvent("allocateOwnID", { detail: id })) + } else if (event.action == "userJoined") { + const { user: id } = event + this.dispatchEvent(new CustomEvent("userJoin", { detail: id })) + } else if (event.action == "userLeft") { + const { user: id } = event + this.dispatchEvent(new CustomEvent("userLeave", { detail: id })) + } + }, + }, + share: { + strokeAdd: "Map", + strokeErase: "Map", + }, + }) + this._y.share.strokeAdd.observe((lineEvent) => { + if (lineEvent.type == "add") { + const points = this._generatePath(lineEvent.name) + const detail = { id: lineEvent.name, points } + this.dispatchEvent(new CustomEvent("addOrUpdatePath", { detail })) + lineEvent.value.observe((pointEvent) => { + if (pointEvent.type == "insert") { + const points = this._generatePath(lineEvent.name) + const detail = { id: lineEvent.name, points } + this.dispatchEvent(new CustomEvent("addOrUpdatePath", { detail })) + } + }) + } + }) + this._y.share.strokeErase.observe((lineEvent) => { + if (lineEvent.type == "add") { + const points = this._generatePath(lineEvent.name) + const detail = { id: lineEvent.name, points } + this.dispatchEvent(new CustomEvent("addOrUpdatePath", { detail })) + lineEvent.value.observe((pointEvent) => { + if (pointEvent.type == "add") { + const points = this._generatePath(lineEvent.name) + const detail = { id: lineEvent.name, points } + this.dispatchEvent(new CustomEvent("addOrUpdatePath", { detail })) + } + }) + } + }) + } +} + +export const connect = async (roomName) => { + const room = new Room(roomName) + await room._initialise() + return room +} diff --git a/src/y-webrtc/index.js b/src/y-webrtc/index.js index dab556d3d4a1498f17c9d08ff28d21af35ea71fa..d860ec2079d63fcf2f2216b753a3715ca62db8bb 100644 --- a/src/y-webrtc/index.js +++ b/src/y-webrtc/index.js @@ -14,11 +14,18 @@ function extend(Y) { super(y, options) this.webrtcOptions = options + if (options.onUserEvent) { + this.onUserEvent(options.onUserEvent) + } this.initialiseConnection() + + window.addEventListener("unload", () => { + this.y.destroy() + }) } initialiseConnection() { - const webrtc = new LioWebRTC({ + this.webrtc = new LioWebRTC({ url: this.webrtcOptions.url, dataOnly: true, /*network: { @@ -27,57 +34,110 @@ function extend(Y) { },*/ }) - this.webrtc = webrtc + this.peers = new Set() - webrtc.on("ready", () => { - webrtc.joinRoom(this.webrtcOptions.room) + this.webrtc.on("ready", () => { + this.webrtc.joinRoom(this.webrtcOptions.room) + }) - webrtc.connection.on("message", (data) => - console.log("socket.io", data), - ) + this.webrtc.on("joinedRoom", () => { + this.checkAndEnsureUser() }) - webrtc.on("joinedRoom", () => { - const id = webrtc.getMyId() + this.webrtc.on("channelOpen", (dataChannel, peer) => { + this.checkAndEnsureUser() + + // Start a handshake to ensure both sides are able to use the channel + function handshake(peer) { + const _peer = this.webrtc.getPeerById(peer.id) + + if (!_peer || _peer !== peer) { + return + } + + if (this.peers.has(peer.id)) { + return + } + + console.log("ping", peer.id) + + // Initial message in the handshake + this.webrtc.whisper(peer, "tw-ml", "tw") - for (let f of this.userEventListeners) { - f({ action: "userID", id: id }) + setTimeout(handshake.bind(this, peer), 500) } - this.setUserId(id) + setTimeout(handshake.bind(this, peer), 100) }) - // Cannot use createdPeer here as y-js will then try to send data before the channel is open - webrtc.on("channelOpen", (dataChannel, peer) => { - console.log( - "createdPeer", - peer.id, - this.webrtc.getPeers().map((peer) => peer.id), - ) - this.userJoined(peer.id, "master") - }) + this.webrtc.on("receivedPeerData", (type, message, peer) => { + this.checkAndEnsureUser() + + if (message.type !== "update") { + console.log("receivedData", peer.id, message) + } - webrtc.on("receivedPeerData", (type, message, peer) => { - if (message.type !== "update") - console.log( - "receivedData", - peer.id, - message, - this.webrtc.getPeers().map((peer) => peer.id), - ) - this.receiveMessage(peer.id, message) + if (type === "y-js") { + this.checkAndInsertPeer(peer.id) + this.receiveMessage(peer.id, message) + } else if (type === "tw-ml") { + if (message === "tw") { + // Response message in the handshake + this.webrtc.whisper(peer, "tw-ml", "ml") + } else if (message == "ml") { + // Handshake completed + this.checkAndInsertPeer(peer.id) + } + } }) - webrtc.on("channelClose", (dataChannel, peer) => { - console.log( - "removedPeer", - peer.id, - this.webrtc.getPeers().map((peer) => peer.id), - ) - this.userLeft(peer.id) + this.webrtc.on("channelClose", (dataChannel, peer) => { + this.checkAndEnsureUser() + this.checkAndRemovePeer(peer.id) }) } + // Ensure that y-js is up to date on the user's id + checkAndEnsureUser() { + const id = this.webrtc.getMyId() + + if (this.y.db.userId === id) { + return + } + + for (let f of this.userEventListeners) { + f({ action: "userID", id: id }) + } + + this.setUserId(id) + } + + // Ensure that y-js knows that the peer has joined + checkAndInsertPeer(uid) { + if (this.peers.has(uid)) { + return + } + + this.peers.add(uid) + + console.log("createdPeer", uid) + + this.userJoined(uid, "master") + } + + // Ensure that y-js knows that the peer has left + checkAndRemovePeer(uid) { + if (!this.peers.has(uid)) { + return + } + + this.peers.delete(uid) + + console.log("removedPeer", uid) + + this.userLeft(uid) + } + connectToPeer(/*uid*/) { // currently deprecated } @@ -95,22 +155,19 @@ function extend(Y) { } send(uid, message) { - console.log( - "send", - uid, - message, - this.webrtc.getPeers().map((peer) => peer.id), - ) + // y-js db transactions can send messages after a peer has disconnected + if (!this.peers.has(uid) || !this.webrtc.getPeerById(uid)) { + return + } + + console.log("send", uid, message) + this.webrtc.whisper(this.webrtc.getPeerById(uid), "y-js", message) } broadcast(message) { - if (message.type !== "update") - console.log( - "broadcast", - message, - this.webrtc.getPeers().map((peer) => peer.id), - ) + if (message.type !== "update") console.log("broadcast", message) + this.webrtc.shout("y-js", message) } diff --git a/src/yjs b/src/yjs new file mode 160000 index 0000000000000000000000000000000000000000..d30d6db92c8930e7d2381846d3c161b1a787393b --- /dev/null +++ b/src/yjs @@ -0,0 +1 @@ +Subproject commit d30d6db92c8930e7d2381846d3c161b1a787393b