import { line, curveLinear } from "d3-shape" import * as HTML from "./elements.js" import { connect } from "./room.js" const TEST_ROOM = "imperial" // 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)") HTML.canvas.appendChild(pathElem) pathElems.set(id, pathElem) } pathElem.innerHTML = "" if (points.length == 0) { return pathElem } // Push a fake path split to generate the last path points.push([-1, -1, false]) let subpath = [] for (let point of points) { if (point[0] === undefined) { continue } 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) } subpath = [] continue } subpath.push(point) } return pathElem } const getDistance = (a, b) => { return Math.sqrt( (a[0] - b[0]) * (a[0] - b[0]) + (a[1] - b[1]) * (a[1] - b[1]), ) } function setElemVisible(elem, visible = true) { if (!(elem && elem.style)) return elem.style.display = visible ? "block" : "none" } function showConnectedRoom(roomID) { HTML.connectedRoomID.textContent = roomID setElemVisible(HTML.connectedRoomInfoContainer) } function setBlankUIState() { while (HTML.canvas.children[1]) { HTML.canvas.removeChild(HTML.canvas.children[1]) } HTML.connectedPeers.innerHTML = "No peers are connected" } function handleRoomConnectClick() { const selectedRoomID = HTML.roomIDElem.value if (!selectedRoomID) return setBlankUIState() connectToARoom(selectedRoomID) } function handleRoomConnectionEstablished(room) { showConnectedRoom(room.name) let userInput = false let currentTool = tools.PEN let currentPathID = null HTML.userIDElem.value = room.ownID || "" room.addEventListener("allocateOwnID", ({ detail: id }) => { HTML.userIDElem.value = id }) room.addEventListener("userJoin", ({ detail: id }) => { if (HTML.connectedPeers.children.length === 0) { HTML.connectedPeers.innerHTML = "" } const peerElem = document.createElement("li") peerElem.innerHTML = id HTML.connectedPeers.appendChild(peerElem) }) room.addEventListener("userLeave", ({ detail: id }) => { for (let peerElem of HTML.connectedPeers.children) { if (peerElem.innerHTML == id) { HTML.connectedPeers.removeChild(peerElem) } } if (HTML.connectedPeers.children.length === 0) { HTML.connectedPeers.innerHTML = "No peers are connected" } }) room.addEventListener("addOrUpdatePath", ({ detail: { id, points } }) => { addOrUpdatePathElem(id, points) }) const canvasOnMouseDown = (e) => { userInput = true let mouse = [e.offsetX, e.offsetY] 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) } }) }) } } HTML.canvas.addEventListener("mousedown", canvasOnMouseDown) const canvasOnMouseLeave = () => { userInput = false } HTML.canvas.addEventListener("mouseleave", canvasOnMouseLeave) const canvasOnMouseEnter = (e) => { if (e.buttons === 0) { userInput = false return } userInput = true let mouse = [e.offsetX, e.offsetY] 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) } }) }) } } HTML.canvas.addEventListener("mouseenter", canvasOnMouseEnter) const canvasOnMouseUp = () => { userInput = false } HTML.canvas.addEventListener("mouseup", canvasOnMouseUp) const canvasOnMouseMove = (e) => { if (!userInput) { return } let mouse = [e.offsetX, e.offsetY] 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) } }) }) } } HTML.canvas.addEventListener("mousemove", canvasOnMouseMove) const peerButtonOnClick = () => { const peerID = HTML.peerIDElem.value if (peerID == "") { return } room.inviteUser(peerID) HTML.peerIDElem.value = "" } HTML.peerButton.addEventListener("click", peerButtonOnClick) const penButtonOnClick = () => { currentTool = tools.PEN HTML.penButton.classList.add("selected") HTML.eraserButton.classList.remove("selected") } HTML.penButton.addEventListener("click", penButtonOnClick) const eraserButtonOnClick = () => { currentTool = tools.ERASER HTML.penButton.classList.remove("selected") HTML.eraserButton.classList.add("selected") } HTML.eraserButton.addEventListener("click", eraserButtonOnClick) HTML.roomConnectButton.removeEventListener("click", handleRoomConnectClick) const roomConnectButtonOnClick = () => { HTML.canvas.removeEventListener("mousedown", canvasOnMouseDown) HTML.canvas.removeEventListener("mouseleave", canvasOnMouseLeave) HTML.canvas.removeEventListener("mouseenter", canvasOnMouseEnter) HTML.canvas.removeEventListener("mouseup", canvasOnMouseUp) HTML.canvas.removeEventListener("mousemove", canvasOnMouseMove) HTML.peerButton.removeEventListener("click", peerButtonOnClick) HTML.penButton.removeEventListener("click", penButtonOnClick) HTML.eraserButton.removeEventListener("click", eraserButtonOnClick) room.disconnect() room = undefined handleRoomConnectClick() } HTML.roomConnectButton.addEventListener("click", roomConnectButtonOnClick, { once: true, }) } function handleRoomConnectionError(err) { alert(`Error connecting to a room:\n${err}`) } function connectToARoom(roomID) { connect(roomID) .then(handleRoomConnectionEstablished) .catch(handleRoomConnectionError) } HTML.roomConnectButton.addEventListener("click", handleRoomConnectClick, { once: true, }) connectToARoom(TEST_ROOM)