Newer
Older
import { line, curveLinear } from "d3-shape"
import * as HTML from "./elements.js"
const TEST_ROOM = "imperial"
const {
computeErasureIntervals,
combineErasureIntervals,
} = require("./erasure")
// 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 * 10
// 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) => {
const t = computeErasureIntervals(points, mousePos, ERASERRADIUS)
if (!TEST_ERASE_INTERVAL[pathID]) TEST_ERASE_INTERVAL[pathID] = {}
TEST_ERASE_INTERVAL[pathID] = combineErasureIntervals(
TEST_ERASE_INTERVAL[pathID],
t,
)
room.dispatchEvent(
new CustomEvent("addOrUpdatePath", {
detail: { id: pathID, points },
}),
)
points.forEach((point) => {
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),
},
}
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)
function getOrAddPathElem(pathElems, id) {
let pathElem = pathElems.get(id)
if (pathElem == null) {
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)
Moritz Langenstein
committed
pathElem.innerHTML = ""
return pathElem
}
function generateSvgPoint(subPath) {
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)
return subpathElem
}
function generateSvgPath(subPath) {
const subpathElem = SVG.create.path()
subpathElem.setAttribute("d", lineFn(subPath))
return subpathElem
}
function generateSvgForSubpath(subPath) {
return subPath.length === 1
? generateSvgPoint(subPath)
: generateSvgPath(subPath)
}
function isInvalidPoint(point) {
return point[0] === undefined
}
function getEraseIntervals(pathID, pointID) {
const eraseIntervalsForPath = TEST_ERASE_INTERVAL[pathID]
if (!eraseIntervalsForPath) return undefined
return eraseIntervalsForPath[pointID]
}
const POINT_ERASE_LIMIT = 0.0001
function pointWasErased(eraseIntervals) {
return (
eraseIntervals.length &&
eraseIntervals[0] &&
eraseIntervals[0][0] <= POINT_ERASE_LIMIT
)
}
function needToDrawLastPoint(points, pathID) {
if (points.length < 2) return true
const penultimatePointIndex = points.length - 2
const penPointEraseIntervals = getEraseIntervals(
pathID,
penultimatePointIndex,
)
if (
penPointEraseIntervals &&
penPointEraseIntervals.length &&
penPointEraseIntervals.some(
(interval) => interval[1] >= 1 - POINT_ERASE_LIMIT,
)
) {
return false
}
return true
}
function generatePointsForPathElem(pathElem, pathID, dataPoints) {
const points = dataPoints
document.getElementById("console").innerText = JSON.stringify(
TEST_ERASE_INTERVAL,
)
const appendToWholePath = (subPath) => {
if (!(subPath && subPath.length)) return
const subpathElem = generateSvgForSubpath(subPath)
pathElem.appendChild(subpathElem)
}
let subPath = []
for (let i = 0; i < points.length; i++) {
const point = points[i]
if (isInvalidPoint(point)) {
continue
Moritz Langenstein
committed
}
const eraseIntervals = getEraseIntervals(pathID, i)
if (!eraseIntervals || nextPoint === undefined) {
subPath.push(point)
continue
}
if (!pointWasErased(eraseIntervals) || subPath.length) {
subPath.push(point)
}
for (const eraseInterval of eraseIntervals) {
if (!eraseInterval) continue
const erasedIntervalBounds = erasurePoints(
point,
nextPoint,
eraseInterval,
)
if (!(erasedIntervalBounds && erasedIntervalBounds.length)) continue
const [endOfDrawnSegment, startOfNewSegment] = erasedIntervalBounds
if (eraseInterval[0] > POINT_ERASE_LIMIT) {
subPath.push(endOfDrawnSegment)
}
appendToWholePath(subPath)
if (eraseInterval[1] < 1 - POINT_ERASE_LIMIT) {
subPath = [startOfNewSegment]
} else {
subPath = []
}
Moritz Langenstein
committed
}
if (needToDrawLastPoint(points, pathID)) {
appendToWholePath(subPath)
}
}
const addOrUpdatePathElem = (pathElems, id, points) => {
const pathElem = getOrAddPathElem(pathElems, id)
if (!points.length) {
return pathElem
}
generatePointsForPathElem(pathElem, id, points)
Moritz Langenstein
committed
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"
}
Moritz Langenstein
committed
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 }) => {
})
room.addEventListener("userJoin", ({ detail: id }) => {
if (HTML.connectedPeers.children.length == 0) {
Moritz Langenstein
committed
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)
Moritz Langenstein
committed
if (HTML.connectedPeers.children.length == 0) {
Moritz Langenstein
committed
HTML.connectedPeers.innerHTML = "No peers are connected"
}
room.addEventListener("addOrUpdatePath", ({ detail: { id, points } }) => {
addOrUpdatePathElem(pathElems, id, points)
const pathIDsByPointerID = new Map()
Moritz Langenstein
committed
const canvasOnPointerEnter = (e) => {
if (~e.buttons & 1) {
Moritz Langenstein
committed
return
}
const mousePos = [e.offsetX, e.offsetY]
Moritz Langenstein
committed
if (currentTool == tools.PEN) {
pathIDsByPointerID.set(e.pointerId, room.addPath(mousePos))
} else if (currentTool == tools.ERASER) {
Moritz Langenstein
committed
}
Moritz Langenstein
committed
const canvasOnPointerLeave = (e) => {
pathIDsByPointerID.delete(e.pointerId)
Moritz Langenstein
committed
const canvasOnPointerMove = (e) => {
if (~e.buttons & 1) {
const mousePos = [e.offsetX, e.offsetY]
if (currentTool == tools.PEN) {
room.extendPath(pathIDsByPointerID.get(e.pointerId), mousePos)
} else if (currentTool == tools.ERASER) {
eraseAt(e.offsetX, e.offsetY, room)
const peerButtonOnClick = () => {
const peerID = HTML.peerIDElem.value
if (peerID == "") {
return
}
room.inviteUser(peerID)
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()
HTML.roomConnectButton.addEventListener("click", roomConnectButtonOnClick)
room.extendPath(pid, [800, 200])
room.extendPath(pid, [100, 300])
room.extendPath(pid, [800, 400])
room.extendPath(pid, [800, 450])
room.extendPath(pid, [800, 500])
// let pid = room.addPath([100, 100])
// room.extendPath(pid, [110, 105])
// room.extendPath(pid, [120, 100])
// room.extendPath(pid, [130, 105])
// room.extendPath(pid, [140, 100])
}
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)