Newer
Older
import { line, curveBasis } from "d3-shape"
import * as HTML from "./elements.js"
const TEST_ROOM = "imperial"
const {
computeErasureIntervals,
combineErasureIntervals,
} = require("./erasure")
const tools = {
PEN: "pen",
ERASER: "eraser",
}
const STROKECOLOUR = "blue"
const STROKERADIUS = 2
const ERASERRADIUS = STROKERADIUS * 10
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 = ""
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
}
const smoothLine = line()
.x((d) => d[0])
.y((d) => d[1])
.curve(curveBasis)
function generateSvgPath(subPath) {
const subpathElem = SVG.create.path()
subpathElem.setAttribute("d", smoothLine(subPath))
return subpathElem
}
function generateSvgForSubpath(subPath) {
return subPath.length === 1
? generateSvgPoint(subPath)
: generateSvgPath(subPath)
}
function isInvalidPoint(point) {
return !point || point[0] === undefined
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
function eraseEverythingAtPosition(x, y, radius, room) {
let mousePos = [x, y]
room.getPaths().forEach((points, pathID) => {
const prevPathIntervals =
(room.erasureIntervals || { [pathID]: {} })[pathID] || {}
const newPathIntervals = computeErasureIntervals(
points,
mousePos,
radius,
prevPathIntervals,
)
const erasureIntervalsForPath = combineErasureIntervals(
prevPathIntervals,
newPathIntervals,
)
Object.keys(erasureIntervalsForPath).forEach((pointID) =>
room.extendErasureIntervals(
pathID,
pointID,
erasureIntervalsForPath[pointID],
),
)
})
}
function getEraseIntervalsForPointInPath(intervals, pathID, pointID) {
if (!intervals) return undefined
const eraseIntervalsForPath = intervals[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, eraseIntervals) {
if (points.length < 2) return true
const penultimatePointIndex = points.length - 2
const penPointEraseIntervals = getEraseIntervalsForPointInPath(
eraseIntervals,
pathID,
penultimatePointIndex,
)
if (
penPointEraseIntervals &&
penPointEraseIntervals.length &&
penPointEraseIntervals.some(
(interval) => interval[1] >= 1 - POINT_ERASE_LIMIT,
)
) {
return false
}
return true
}
function generatePointsForPathElem(
pathElem,
pathID,
dataPoints,
erasureIntervals,
) {
const points = dataPoints
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 = getEraseIntervalsForPointInPath(
erasureIntervals,
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, erasureIntervals)) {
appendToWholePath(subPath)
}
const addOrUpdatePathElem = (pathElems, id, points, erasureIntervals) => {
const pathElem = getOrAddPathElem(pathElems, id)
if (!points.length) {
return pathElem
}
generatePointsForPathElem(pathElem, id, points, erasureIntervals)
Moritz Langenstein
committed
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, room.erasureIntervals)
room.addEventListener(
"removedIntervalsChange",
({ detail: { id, intervals, points } }) => {
room.erasureIntervals[id] = combineErasureIntervals(
room.erasureIntervals[id] || {},
addOrUpdatePathElem(pathElems, id, points, room.erasureIntervals)
let currentTool = tools.PEN
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) {
eraseEverythingAtPosition(mousePos[0], mousePos[1], ERASERRADIUS, room)
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) {
eraseEverythingAtPosition(e.offsetX, e.offsetY, ERASERRADIUS, 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)
}
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)