// Room connection and synchronisation. // Translate local canvas input events to draw messages and send to the room. // Get back room updates and invoke the local canvas renderer. import * as canvas from "./canvas.js" import * as HTML from "./elements.js" import { connect } from "./room.js" const TEST_ROOM = "imperial" let room = null const onRoomConnect = (room_) => { room = room_ HTML.connectedRoomID.textContent = room.name HTML.connectedRoomInfoContainer.style.display = "block" 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 = "" } getOrInsertPeerById(id) updateOverallStatusIcon() }) room.addEventListener("userLeave", ({ detail: id }) => { HTML.connectedPeers.removeChild(getOrInsertPeerById(id)) if (HTML.connectedPeers.children.length == 0) { HTML.connectedPeers.innerHTML = "No peers are connected" } updateOverallStatusIcon() }) room.addEventListener("weSyncedWithPeer", ({ detail: id }) => { getOrInsertPeerById(id).children[1].className = "peer-status synced" updateOverallStatusIcon() }) room.addEventListener("waitingForSyncStep", ({ detail: id }) => { getOrInsertPeerById(id).children[2].className = "peer-status negotiating" updateOverallStatusIcon() }) room.addEventListener("peerSyncedWithUs", ({ detail: id }) => { getOrInsertPeerById(id).children[2].className = "peer-status synced" updateOverallStatusIcon() }) room.addEventListener("addOrUpdatePath", ({ detail: { id, points } }) => { canvas.renderPath(id, points) }) } const tryRoomConnect = async (roomID) => { return await connect(roomID) .then(onRoomConnect) .catch((err) => alert(`Error connecting to a room:\n${err}`)) } const ERASER_RADIUS = 10 const tools = { PEN: Symbol("pen"), ERASER: Symbol("eraser"), } let currentTool = tools.PEN const pathIDsByPointerID = new Map() HTML.penButton.addEventListener("click", () => { if (currentTool == tools.PEN) { showElement(HTML.penProperties) } else { currentTool = tools.PEN HTML.penButton.classList.add("selected") HTML.eraserButton.classList.remove("selected") } }) HTML.closeButton.forEach((element) => { element.addEventListener("click", () => { hideElement(element.parentNode.parentNode.parentNode) }) }) window.addEventListener("click", (event) => { if (event.target == HTML.penProperties) { hideElement(HTML.penProperties) } else if (event.target == HTML.palette) { hideElement(HTML.palette) hideElement(HTML.penProperties) } }) HTML.rectangle.addEventListener("click", () => { showElement(HTML.palette) }) const svg = HTML.wheel.children for (let i = 1; i < svg.length; i++) { svg[i].addEventListener("click", (event) => { const paletteColour = event.target.getAttribute("fill") HTML.rectangle.style.backgroundColor = paletteColour HTML.picker.value = paletteColour HTML.labelColours.style.backgroundColor = paletteColour canvas.setStrokeColour(paletteColour) hideElement(HTML.palette) }) } function showElement(element) { element.style.display = "block" } function hideElement(element) { element.style.display = "none" } HTML.picker.addEventListener("change", () => { const paletteColour = event.target.value HTML.rectangle.style.backgroundColor = paletteColour HTML.labelColour.style.backgroundColor = paletteColour canvas.setStrokeColour(paletteColour) }) HTML.output.innerHTML = HTML.slider.value HTML.slider.oninput = function() { HTML.output.innerHTML = this.value canvas.setStrokeRadius(this.value / 10) } var x = window.matchMedia( "only screen and (orientation: landscape) and (max-width: 600px)", ) x.addListener(() => { if (x.matches) { HTML.wheel.setAttribute("viewBox", "-50 10 200 100") HTML.palette.setAttribute("style", "padding-top: 50px") } else { HTML.wheel.setAttribute("viewBox", "0 10 100 100") } }) function showElement(element) { element.style.display = "block" } function hideElement(element) { element.style.display = "none" } HTML.picker.addEventListener("change", () => { var paletteColour = event.target.value HTML.rectangle.style.backgroundColor = paletteColour HTML.labelColours.style.backgroundColor = paletteColour canvas.setStrokeColour(paletteColour) }) HTML.eraserButton.addEventListener("click", () => { currentTool = tools.ERASER HTML.penButton.classList.remove("selected") HTML.eraserButton.classList.add("selected") }) HTML.peerButton.addEventListener("click", () => { const peerID = HTML.peerIDElem.value if (room == null || peerID == "") { return } room.inviteUser(peerID) HTML.peerIDElem.value = "" }) const onRoomJoinEnter = () => { const selectedRoomID = HTML.roomIDElem.value if (!selectedRoomID || selectedRoomID == room.name) { return } if (room != null) { room.disconnect() room = null } canvas.clear() HTML.connectedPeers.innerHTML = "No peers are connected" tryRoomConnect(selectedRoomID) } HTML.roomConnectButton.addEventListener("click", onRoomJoinEnter) HTML.roomIDElem.addEventListener("keydown", (event) => { if (event.key == "Enter") { event.target.blur() onRoomJoinEnter() } }) const getOrInsertPeerById = (id) => { for (const peerElem of HTML.connectedPeers.children) { const peerId = peerElem.children[0].innerHTML if (peerId == id) { return peerElem } } const peerElem = document.createElement("li") const peerId = document.createElement("div") peerId.style.display = "inline" peerId.innerHTML = id const ourStatus = document.createElement("div") ourStatus.className = "peer-status unsynced" const theirStatus = document.createElement("div") theirStatus.className = "peer-status unsynced" peerElem.appendChild(peerId) peerElem.appendChild(ourStatus) peerElem.appendChild(theirStatus) HTML.connectedPeers.appendChild(peerElem) return peerElem } const updateOverallStatusIcon = () => { for (const peerElem of HTML.connectedPeers.children) { if ( !peerElem.children[1].classList.contains("synced") || !peerElem.children[2].classList.contains("synced") ) { HTML.overallStatusIcon.className = "synchronising" HTML.overallStatusIconImage.src = "synchronising.svg" return } } HTML.overallStatusIcon.className = "synchronised" HTML.overallStatusIconImage.src = "synchronised.svg" } const getDistance = (a, b) => { return Math.sqrt( (a[0] - b[0]) * (a[0] - b[0]) + (a[1] - b[1]) * (a[1] - b[1]), ) } const erasePoint = ([x, y]) => { if (room == null) { return } room.getPaths().forEach((points, pathID) => { points.forEach((point, i) => { if (getDistance([x, y], point) <= ERASER_RADIUS) { room.erasePoint(pathID, i) } }) }) } canvas.input.addEventListener("strokestart", ({ detail: e }) => { if (room == null) { return } const mousePos = [e.offsetX, e.offsetY] if (currentTool == tools.PEN) { pathIDsByPointerID.set( e.pointerId, room.addPath([ ...mousePos, e.pressure, canvas.getStrokeColour(), canvas.stroke_radius, ]), ) } else if (currentTool == tools.ERASER) { erasePoint(mousePos) } }) canvas.input.addEventListener("strokeend", ({ detail: e }) => { pathIDsByPointerID.delete(e.pointerId) }) canvas.input.addEventListener("strokemove", ({ detail: e }) => { if (room == null) { return } const mousePos = [e.offsetX, e.offsetY] if (currentTool == tools.PEN) { room.extendPath(pathIDsByPointerID.get(e.pointerId), [ ...mousePos, e.pressure, canvas.getStrokeColour(), canvas.stroke_radius, ]) } else if (currentTool == tools.ERASER) { erasePoint(mousePos) } }) tryRoomConnect(TEST_ROOM)