-
Moritz Langenstein authoredMoritz Langenstein authored
app.js 7.63 KiB
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 (const 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)