-
Yuriy Maksymets authoredYuriy Maksymets authored
app.js 9.89 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 * 5
function interpolatedCoordinate(start, end, length) {
const dx = end[0] - start[0]
const dy = end[1] - start[1]
const dist = getDistance(start, end)
const ratio = length / dist
return [start[0] + dx * ratio, start[1] + dy * ratio]
}
function eraseAt(x, y, room) {
let mousePos = [x, y]
room.getPaths().forEach((points, pathID) => {
points.forEach((point, i) => {
const distanceToPoint = getDistance(mousePos, point)
if (distanceToPoint <= ERASERRADIUS) {
room.erasePoint(pathID, i)
let prev, next
if (i > 0) {
prev = i - 1
}
if (i < points.length - 1) {
next = i + 1
}
if (prev !== undefined) {
const interpolatedPoint = interpolatedCoordinate(
point,
points[prev],
ERASERRADIUS,
)
room.insertIntoPath(pathID, prev, interpolatedPoint)
}
if (next !== undefined) {
const interpolatedPoint = interpolatedCoordinate(
point,
points[next],
ERASERRADIUS,
)
room.insertIntoPath(pathID, next, interpolatedPoint)
}
}
})
})
}
const SVG_URL = "http://www.w3.org/2000/svg"
const SVG = {
create: {
circle: () => SVG.create.elem("circle"),
group: () => SVG.create.elem("g"),
path: () => SVG.create.elem("path"),
elem: (elemName) => document.createElementNS(SVG_URL, elemName),
},
}
const TEST_ERASE_INTERVAL = {
0: [0.1, 0.9],
1: [0.01, 0.99999],
}
function erasurePoints(pointA, pointB, start, fin) {
if (start >= fin) return
if (start <= 0) start = 0
if (fin >= 1) fin = 1
const [xa, ya] = pointA
const [xb, yb] = pointB
const dx = xb - xa
const dy = yb - ya
const pointOnLine = (percent) => [xa + percent * dx, ya + percent * dy]
return [start, fin].map(pointOnLine)
}
const addOrUpdatePathElem = (pathElems, id, points) => {
let pathElem = pathElems.get(id)
if (pathElem == null) {
pathElem = SVG.create.group()
console.log(pathElem)
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])
const originalID = (i) => i - 1
let subPath = []
let toAdd = undefined
for (let i = 0; i < points.length; i++) {
const point = points[i]
console.log(subPath)
if (toAdd) {
subPath.push(toAdd)
toAdd = undefined
}
if (point[0] === undefined) {
continue
}
if (point[2] === false) {
// End of subpath
if (subPath.length == 1) {
const subpathElem = SVG.create.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) {
const subpathElem = SVG.create.path()
subpathElem.setAttribute("d", lineFn(subPath))
pathElem.appendChild(subpathElem)
}
subPath = []
continue
}
// Valid point inside a subpath
subPath.push(point)
const eraseInterval = TEST_ERASE_INTERVAL[originalID(i)]
if (eraseInterval) {
if (i === points.length - 1) {
continue
}
const nextPoint = points[i + 1]
const [start, fin] = erasurePoints(
point,
nextPoint,
eraseInterval[0],
eraseInterval[1],
)
subPath.push(start)
const subpathElem = SVG.create.path()
subpathElem.setAttribute("d", lineFn(subPath))
pathElem.appendChild(subpathElem)
subPath = []
toAdd = fin
}
}
return pathElem
}
const getDistance = (a, b) => {
return Math.sqrt((a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2)
}
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)
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"
}
})
const pathElems = new Map()
room.addEventListener("addOrUpdatePath", ({ detail: { id, points } }) => {
addOrUpdatePathElem(pathElems, id, points)
})
let currentTool = tools.ERASER
const pathIDsByPointerID = new Map()
const canvasOnPointerEnter = (e) => {
if (~e.buttons & 1) {
return
}
const mousePos = [e.offsetX, e.offsetY]
if (currentTool == tools.PEN) {
pathIDsByPointerID.set(e.pointerId, room.addPath(mousePos))
} else if (currentTool == tools.ERASER) {
eraseAt(mousePos[0], mousePos[1], room)
}
}
const canvasOnPointerLeave = (e) => {
pathIDsByPointerID.delete(e.pointerId)
}
const canvasOnPointerMove = (e) => {
if (~e.buttons & 1) {
return
}
const mousePos = [e.offsetX, e.offsetY]
if (currentTool == tools.PEN) {
room.extendPath(pathIDsByPointerID.get(e.pointerId), mousePos)
} else if (currentTool == tools.ERASER) {
room.getPaths().forEach((points, pathID) => {
points.forEach((point, i) => {
if (getDistance(mousePos, point) <= ERASERRADIUS) {
room.erasePoint(pathID, i)
}
})
})
}
}
const peerButtonOnClick = () => {
const peerID = HTML.peerIDElem.value
if (peerID == "") {
return
}
room.inviteUser(peerID)
HTML.peerIDElem.value = ""
}
const penButtonOnClick = () => {
currentTool = tools.PEN
HTML.penButton.classList.add("selected")
HTML.eraserButton.classList.remove("selected")
}
const eraserButtonOnClick = () => {
currentTool = tools.ERASER
HTML.penButton.classList.remove("selected")
HTML.eraserButton.classList.add("selected")
}
HTML.canvas.addEventListener("pointerdown", canvasOnPointerEnter)
HTML.canvas.addEventListener("pointerenter", canvasOnPointerEnter)
HTML.canvas.addEventListener("pointerup", canvasOnPointerLeave)
HTML.canvas.addEventListener("pointerleave", canvasOnPointerLeave)
HTML.canvas.addEventListener("pointermove", canvasOnPointerMove)
HTML.penButton.addEventListener("click", penButtonOnClick)
HTML.eraserButton.addEventListener("click", eraserButtonOnClick)
HTML.peerButton.addEventListener("click", peerButtonOnClick)
HTML.roomConnectButton.removeEventListener("click", handleRoomConnectClick)
const roomConnectButtonOnClick = () => {
const selectedRoomID = HTML.roomIDElem.value
if (!selectedRoomID || selectedRoomID == room.name) {
return
}
HTML.canvas.removeEventListener("pointerdown", canvasOnPointerEnter)
HTML.canvas.removeEventListener("pointerenter", canvasOnPointerEnter)
HTML.canvas.removeEventListener("pointerup", canvasOnPointerLeave)
HTML.canvas.removeEventListener("pointerleave", canvasOnPointerLeave)
HTML.canvas.removeEventListener("pointermove", canvasOnPointerMove)
HTML.peerButton.removeEventListener("click", peerButtonOnClick)
HTML.penButton.removeEventListener("click", penButtonOnClick)
HTML.eraserButton.removeEventListener("click", eraserButtonOnClick)
HTML.roomConnectButton.removeEventListener(
"click",
roomConnectButtonOnClick,
)
room.disconnect()
room = null
handleRoomConnectClick()
}
HTML.roomConnectButton.addEventListener("click", roomConnectButtonOnClick)
let pid = room.addPath([100, 100])
room.extendPath(pid, [100, 100])
room.extendPath(pid, [150, 150])
room.extendPath(pid, [100, 200])
}
function handleRoomConnectionError(err) {
alert(`Error connecting to a room:\n${err}`)
}
function connectToARoom(roomID) {
connect(roomID)
.then(handleRoomConnectionEstablished)
.catch(handleRoomConnectionError)
}
HTML.canvas.addEventListener("touchmove", (e) => e.preventDefault())
HTML.roomConnectButton.addEventListener("click", handleRoomConnectClick, {
once: true,
})
connectToARoom(TEST_ROOM)