diff --git a/src/app.js b/src/app.js index 113f96e93883308fc4188b757b8e7b16e3a1adde..d23b052a5549939affeb9521c51635d0513bd24d 100644 --- a/src/app.js +++ b/src/app.js @@ -1,13 +1,34 @@ // Room connection and synchronisation. -// Translate local canvas input events to draw messages and send to the room. +// Translate local canvas input events to draw messages in light of current tool +// selections 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" +import * as toolSelection from "./tool-selection.js" const TEST_ROOM = "imperial" +const MIN_PRESSURE_FACTOR = 0.1 +const MAX_PRESSURE_FACTOR = 1.5 + +// This is a quadratic such that: +// - getPressureFactor(0.0) = MIN_PRESSURE_FACTOR +// - getPressureFactor(0.5) = 1.0 +// - getPressureFactor(1.0) = MAX_PRESSURE_FACTOR +// For sensible results, maintain that: +// - 0.0 <= MIN_PRESSURE_FACTOR <= 1.0 +// - 1.0 <= MAX_PRESSURE_FACTOR +// For intuitive results, maintain that: +// - MAX_PRESSURE_FACTOR <= ~2.0 +const getPressureFactor = (pressure) => { + const a = 2 * (MAX_PRESSURE_FACTOR + MIN_PRESSURE_FACTOR) - 4 + const b = -MAX_PRESSURE_FACTOR - 3 * MIN_PRESSURE_FACTOR + 4 + const c = MIN_PRESSURE_FACTOR + return a * pressure ** 2 + b * pressure + c +} + let room = null const onRoomConnect = (room_) => { @@ -89,102 +110,8 @@ const tryRoomConnect = async (roomID) => { .catch((err) => alert(`Error connecting to a room:\n${err}`)) } -const ERASER_RADIUS = 10 -const tools = { - PEN: Symbol("pen"), - ERASER: Symbol("eraser"), -} -let currentTool = tools.PEN const pathIDsByPointerID = new Map() -HTML.penButton.addEventListener("click", () => { - if (currentTool == tools.PEN) { - showElement(HTML.penProperties) - } else { - currentTool = tools.PEN - HTML.penButton.classList.add("selected") - HTML.eraserButton.classList.remove("selected") - } -}) - -HTML.closeButton.forEach((element) => { - element.addEventListener("click", () => { - hideElement(element.parentNode.parentNode.parentNode) - }) -}) - -window.addEventListener("click", (event) => { - if (event.target == HTML.penProperties) { - hideElement(HTML.penProperties) - } else if (event.target == HTML.palette) { - hideElement(HTML.palette) - hideElement(HTML.penProperties) - } -}) - -HTML.rectangle.addEventListener("click", () => { - showElement(HTML.palette) -}) - -const svg = HTML.wheel.children -for (let i = 1; i < svg.length; i++) { - svg[i].addEventListener("click", (event) => { - const paletteColour = event.target.getAttribute("fill") - HTML.rectangle.style.backgroundColor = paletteColour - HTML.picker.value = paletteColour - HTML.labelColours.style.backgroundColor = paletteColour - canvas.setStrokeColour(paletteColour) - hideElement(HTML.palette) - }) -} - -function showElement(element) { - element.style.display = "block" -} - -function hideElement(element) { - element.style.display = "none" -} - -HTML.picker.addEventListener("change", () => { - const paletteColour = event.target.value - HTML.rectangle.style.backgroundColor = paletteColour - HTML.labelColours.style.backgroundColor = paletteColour - canvas.setStrokeColour(paletteColour) -}) - -HTML.output.innerHTML = HTML.slider.value - -HTML.slider.oninput = function() { - HTML.output.innerHTML = this.value - canvas.setStrokeRadius(this.value / 10) -} - -const x = window.matchMedia( - "only screen and (orientation: landscape) and (max-width: 600px)", -) -x.addListener(() => { - if (x.matches) { - HTML.wheel.setAttribute("viewBox", "-50 10 200 100") - HTML.palette.setAttribute("style", "padding-top: 50px") - } else { - HTML.wheel.setAttribute("viewBox", "0 10 100 100") - } -}) - -HTML.picker.addEventListener("change", () => { - const paletteColour = event.target.value - HTML.rectangle.style.backgroundColor = paletteColour - HTML.labelColours.style.backgroundColor = paletteColour - canvas.setStrokeColour(paletteColour) -}) - -HTML.eraserButton.addEventListener("click", () => { - currentTool = tools.ERASER - HTML.penButton.classList.remove("selected") - HTML.eraserButton.classList.add("selected") -}) - HTML.peerButton.addEventListener("click", () => { const peerID = HTML.peerIDElem.value if (room == null || peerID == "") { @@ -293,7 +220,7 @@ const erasePoint = ([x, y]) => { } room.getPaths().forEach((points, pathID) => { points.forEach((point, i) => { - if (getDistance([x, y], point) <= ERASER_RADIUS) { + if (getDistance([x, y], point) <= toolSelection.getEraseRadius()) { room.erasePoint(pathID, i) } }) @@ -304,19 +231,18 @@ canvas.input.addEventListener("strokestart", ({ detail: e }) => { if (room == null) { return } - + const currentTool = toolSelection.getTool() const mousePos = [e.offsetX, e.offsetY] - - if (currentTool == tools.PEN) { + if (currentTool == toolSelection.Tools.PEN) { pathIDsByPointerID.set( e.pointerId, room.addPath([ ...mousePos, - canvas.getStrokeRadius(e.pressure), - canvas.getStrokeColour(), + toolSelection.getStrokeRadius() * getPressureFactor(e.pressure), + toolSelection.getStrokeColour(), ]), ) - } else if (currentTool == tools.ERASER) { + } else if (currentTool == toolSelection.Tools.ERASER) { erasePoint(mousePos) } }) @@ -329,16 +255,15 @@ canvas.input.addEventListener("strokemove", ({ detail: e }) => { if (room == null) { return } - + const currentTool = toolSelection.getTool() const mousePos = [e.offsetX, e.offsetY] - - if (currentTool == tools.PEN) { + if (currentTool == toolSelection.Tools.PEN) { room.extendPath(pathIDsByPointerID.get(e.pointerId), [ ...mousePos, - canvas.getStrokeRadius(e.pressure), - canvas.getStrokeColour(), + toolSelection.getStrokeRadius() * getPressureFactor(e.pressure), + toolSelection.getStrokeColour(), ]) - } else if (currentTool == tools.ERASER) { + } else if (currentTool == toolSelection.Tools.ERASER) { erasePoint(mousePos) } }) diff --git a/src/canvas.js b/src/canvas.js index 5aceb59fdb0b37a8a7b7b7749204d627a87f3b16..d04e6fa2f7ef3d03a9fbc041f622618010c2df25 100644 --- a/src/canvas.js +++ b/src/canvas.js @@ -16,9 +16,6 @@ const lineFn = line() const pathGroupElems = new Map() -let strokeColour = "#0000ff" -let strokeRadius = 5 - const MAX_POINT_DISTANCE = 5 const MAX_RADIUS_DELTA = 0.05 @@ -168,38 +165,3 @@ canvas.addEventListener("pointerup", dispatchPointerEvent("strokeend")) canvas.addEventListener("pointerleave", dispatchPointerEvent("strokeend")) canvas.addEventListener("pointermove", dispatchPointerEvent("strokemove")) canvas.addEventListener("touchmove", (e) => e.preventDefault()) - -export function setStrokeColour(colour) { - strokeColour = colour -} - -export function getStrokeColour() { - return strokeColour -} - -const MIN_PRESSURE_FACTOR = 0.1 -const MAX_PRESSURE_FACTOR = 1.5 - -// This is a quadratic such that: -// - getPressureFactor(0.0) = MIN_PRESSURE_FACTOR -// - getPressureFactor(0.5) = 1.0 -// - getPressureFactor(1.0) = MAX_PRESSURE_FACTOR -// For sensible results, maintain that: -// - 0.0 <= MIN_PRESSURE_FACTOR <= 1.0 -// - 1.0 <= MAX_PRESSURE_FACTOR -// For intuitive results, maintain that: -// - MAX_PRESSURE_FACTOR <= ~2.0 -const getPressureFactor = (pressure) => { - const a = 2 * (MAX_PRESSURE_FACTOR + MIN_PRESSURE_FACTOR) - 4 - const b = -MAX_PRESSURE_FACTOR - 3 * MIN_PRESSURE_FACTOR + 4 - const c = MIN_PRESSURE_FACTOR - return a * pressure ** 2 + b * pressure + c -} - -export function getStrokeRadius(pressure) { - return strokeRadius * getPressureFactor(pressure) -} - -export function setStrokeRadius(radius) { - strokeRadius = radius -} diff --git a/src/tool-selection.js b/src/tool-selection.js new file mode 100644 index 0000000000000000000000000000000000000000..1d2be7cd3dd4c1eba9823d2fe234ee0714a77c82 --- /dev/null +++ b/src/tool-selection.js @@ -0,0 +1,98 @@ +import * as HTML from "./elements.js" + +export const Tools = Object.freeze({ + PEN: Symbol("pen"), + ERASER: Symbol("eraser"), +}) + +let tool = Tools.PEN +let strokeColour = "#0000ff" +let strokeRadius = 5 +// TODO: The erase radius should also be selectable. +const ERASE_RADIUS = 10 + +export const getTool = () => tool +export const getStrokeColour = () => strokeColour +export const getStrokeRadius = () => strokeRadius +export const getEraseRadius = () => ERASE_RADIUS + +const showElement = (element) => { + element.style.display = "block" +} + +const hideElement = (element) => { + element.style.display = "none" +} + +HTML.penButton.addEventListener("click", () => { + if (tool == Tools.PEN) { + showElement(HTML.penProperties) + } else { + tool = Tools.PEN + HTML.penButton.classList.add("selected") + HTML.eraserButton.classList.remove("selected") + } +}) + +HTML.eraserButton.addEventListener("click", () => { + tool = Tools.ERASER + HTML.penButton.classList.remove("selected") + HTML.eraserButton.classList.add("selected") +}) + +HTML.picker.addEventListener("change", () => { + const paletteColour = event.target.value + HTML.rectangle.style.backgroundColor = paletteColour + HTML.labelColours.style.backgroundColor = paletteColour + strokeColour = paletteColour +}) + +HTML.slider.oninput = function() { + HTML.output.innerHTML = this.value + strokeRadius = this.value / 10 +} + +HTML.output.innerHTML = HTML.slider.value + +const x = window.matchMedia( + "only screen and (orientation: landscape) and (max-width: 600px)", +) +x.addListener(() => { + if (x.matches) { + HTML.wheel.setAttribute("viewBox", "-50 10 200 100") + HTML.palette.setAttribute("style", "padding-top: 50px") + } else { + HTML.wheel.setAttribute("viewBox", "0 10 100 100") + } +}) + +HTML.closeButton.forEach((element) => { + element.addEventListener("click", () => { + hideElement(element.parentNode.parentNode.parentNode) + }) +}) + +window.addEventListener("click", (event) => { + if (event.target == HTML.penProperties) { + hideElement(HTML.penProperties) + } else if (event.target == HTML.palette) { + hideElement(HTML.palette) + hideElement(HTML.penProperties) + } +}) + +HTML.rectangle.addEventListener("click", () => { + showElement(HTML.palette) +}) + +const svg = HTML.wheel.children +for (let i = 1; i < svg.length; i++) { + svg[i].addEventListener("click", (event) => { + const paletteColour = event.target.getAttribute("fill") + HTML.rectangle.style.backgroundColor = paletteColour + HTML.picker.value = paletteColour + HTML.labelColours.style.backgroundColor = paletteColour + strokeColour = paletteColour + hideElement(HTML.palette) + }) +}