Skip to content
Snippets Groups Projects
Commit 6e4359ca authored by Nayeem Rahman's avatar Nayeem Rahman
Browse files

Split canvas.js from app.js

parent 22531aba
No related branches found
No related tags found
1 merge request!29Split canvas.js from app.js
Pipeline #101870 passed
import { line, curveLinear } from "d3-shape"
// 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"
// 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)
let room = null
const tools = {
PEN: "pen",
ERASER: "eraser",
}
const STROKECOLOUR = "blue"
const STROKERADIUS = 2
const ERASERRADIUS = STROKERADIUS * 2
const addOrUpdatePathElem = (pathElems, 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 (const point of points) {
if (point[0] === undefined) {
continue
}
if (point[2] === false) {
if (subpath.length == 1) {
const 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) {
const 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]),
)
}
const onRoomConnect = (room_) => {
room = room_
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.connectedRoomID.textContent = room.name
HTML.connectedRoomInfoContainer.style.display = "block"
HTML.userIDElem.value = room.ownID || ""
room.addEventListener("allocateOwnID", ({ detail: id }) => {
......@@ -152,133 +43,112 @@ function handleRoomConnectionEstablished(room) {
}
})
const pathElems = new Map()
room.addEventListener("addOrUpdatePath", ({ detail: { id, points } }) => {
addOrUpdatePathElem(pathElems, id, points)
canvas.renderPath(id, points)
})
}
let currentTool = tools.PEN
const pathIDsByPointerID = new Map()
const tryRoomConnect = async (roomID) => {
return await connect(roomID)
.then(onRoomConnect)
.catch((err) => alert(`Error connecting to a room:\n${err}`))
}
const canvasOnPointerEnter = (e) => {
if (~e.buttons & 1) {
return
}
const ERASER_RADIUS = canvas.STROKE_RADIUS * 2
const tools = {
PEN: Symbol("pen"),
ERASER: Symbol("eraser"),
}
let currentTool = tools.PEN
const pathIDsByPointerID = new Map()
const mousePos = [e.offsetX, e.offsetY]
HTML.penButton.addEventListener("click", () => {
currentTool = tools.PEN
HTML.penButton.classList.add("selected")
HTML.eraserButton.classList.remove("selected")
})
if (currentTool == tools.PEN) {
pathIDsByPointerID.set(e.pointerId, room.addPath(mousePos))
} else if (currentTool == tools.ERASER) {
room.getPaths().forEach((points, pathID) => {
points.forEach((point, i) => {
if (getDistance(mousePos, point) <= ERASERRADIUS) {
room.erasePoint(pathID, i)
}
})
})
}
}
HTML.eraserButton.addEventListener("click", () => {
currentTool = tools.ERASER
HTML.penButton.classList.remove("selected")
HTML.eraserButton.classList.add("selected")
})
const canvasOnPointerLeave = (e) => {
pathIDsByPointerID.delete(e.pointerId)
HTML.peerButton.addEventListener("click", () => {
const peerID = HTML.peerIDElem.value
if (room == null || peerID == "") {
return
}
room.inviteUser(peerID)
HTML.peerIDElem.value = ""
})
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)
}
})
})
}
HTML.roomConnectButton.addEventListener("click", () => {
const selectedRoomID = HTML.roomIDElem.value
if (!selectedRoomID || selectedRoomID == room.name) {
return
}
const peerButtonOnClick = () => {
const peerID = HTML.peerIDElem.value
if (peerID == "") {
return
}
room.inviteUser(peerID)
HTML.peerIDElem.value = ""
if (room != null) {
room.disconnect()
room = null
}
const penButtonOnClick = () => {
currentTool = tools.PEN
canvas.clear()
HTML.connectedPeers.innerHTML = "No peers are connected"
HTML.penButton.classList.add("selected")
HTML.eraserButton.classList.remove("selected")
}
tryRoomConnect(selectedRoomID)
})
const eraserButtonOnClick = () => {
currentTool = tools.ERASER
const getDistance = (a, b) => {
return Math.sqrt(
(a[0] - b[0]) * (a[0] - b[0]) + (a[1] - b[1]) * (a[1] - b[1]),
)
}
HTML.penButton.classList.remove("selected")
HTML.eraserButton.classList.add("selected")
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)
}
})
})
}
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,
)
canvas.input.addEventListener("strokestart", ({ detail: e }) => {
if (room == null) {
return
}
room.disconnect()
room = null
const mousePos = [e.offsetX, e.offsetY]
handleRoomConnectClick()
if (currentTool == tools.PEN) {
pathIDsByPointerID.set(e.pointerId, room.addPath(mousePos))
} else if (currentTool == tools.ERASER) {
erasePoint(mousePos)
}
HTML.roomConnectButton.addEventListener("click", roomConnectButtonOnClick)
}
})
function handleRoomConnectionError(err) {
alert(`Error connecting to a room:\n${err}`)
}
canvas.input.addEventListener("strokeend", ({ detail: e }) => {
pathIDsByPointerID.delete(e.pointerId)
})
function connectToARoom(roomID) {
connect(roomID)
.then(handleRoomConnectionEstablished)
.catch(handleRoomConnectionError)
}
canvas.input.addEventListener("strokemove", ({ detail: e }) => {
if (room == null) {
return
}
HTML.canvas.addEventListener("touchmove", (e) => e.preventDefault())
const mousePos = [e.offsetX, e.offsetY]
HTML.roomConnectButton.addEventListener("click", handleRoomConnectClick, {
once: true,
if (currentTool == tools.PEN) {
room.extendPath(pathIDsByPointerID.get(e.pointerId), mousePos)
} else if (currentTool == tools.ERASER) {
erasePoint(mousePos)
}
})
connectToARoom(TEST_ROOM)
tryRoomConnect(TEST_ROOM)
// Local canvas rendering.
// Emit input events and receive draw calls seperately - these must be piped
// together externally if desired.
import { line, curveLinear } from "d3-shape"
import { canvas } from "./elements.js"
// 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 pathElems = new Map()
export const STROKE_COLOUR = "blue"
export const STROKE_RADIUS = 2
export const input = new EventTarget()
export const renderPath = (id, points) => {
let pathElem = pathElems.get(id)
if (pathElem == null) {
pathElem = document.createElementNS("http://www.w3.org/2000/svg", "g")
pathElem.setAttribute("stroke", STROKE_COLOUR)
pathElem.setAttribute("stroke-width", STROKE_RADIUS * 2)
pathElem.setAttribute("fill", "none")
pathElem.setAttribute("pointer-events", "none")
pathElem.setAttribute("marker-start", "url(#dot)")
pathElem.setAttribute("marker-end", "url(#dot)")
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 (const point of points) {
if (point[0] === undefined) {
continue
}
if (point[2] === false) {
if (subpath.length == 1) {
const subpathElem = document.createElementNS(
"http://www.w3.org/2000/svg",
"circle",
)
subpathElem.setAttribute("stroke", "none")
subpathElem.setAttribute("fill", STROKE_COLOUR)
subpathElem.setAttribute("cx", subpath[0][0])
subpathElem.setAttribute("cy", subpath[0][1])
subpathElem.setAttribute("r", STROKE_RADIUS)
pathElem.appendChild(subpathElem)
} else if (subpath.length > 0) {
const 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
}
export const clear = () => {
while (canvas.children[1]) {
canvas.removeChild(canvas.children[1])
}
}
// Note that the PointerEvent is passed as the detail in these 'stroke events'.
canvas.addEventListener("pointerdown", (e) => {
if (e.buttons & 1) {
input.dispatchEvent(new CustomEvent("strokestart", { detail: e }))
}
})
canvas.addEventListener("pointerenter", (e) => {
if (e.buttons & 1) {
input.dispatchEvent(new CustomEvent("strokestart", { detail: e }))
}
})
canvas.addEventListener("pointerup", (e) => {
if (e.buttons & 1) {
input.dispatchEvent(new CustomEvent("strokeend", { detail: e }))
}
})
canvas.addEventListener("pointerleave", (e) => {
if (e.buttons & 1) {
input.dispatchEvent(new CustomEvent("strokeend", { detail: e }))
}
})
canvas.addEventListener("pointermove", (e) => {
if (e.buttons & 1) {
input.dispatchEvent(new CustomEvent("strokemove", { detail: e }))
}
})
canvas.addEventListener("touchmove", (e) => e.preventDefault())
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment