Skip to content
Snippets Groups Projects
canvas.js 3.02 KiB
Newer Older
// 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("stroke-linecap", "round")
    pathElem.setAttribute("pointer-events", "none")

    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 = () => {
  pathElems.clear()
// Necessary since buttons property is non standard on iOS versions < 13.2
function checkValidPointerEvent(e) {
  return e.buttons & 1 || e.pointerType === "touch"
}

const dispatchPointerEvent = (name) => (e) => {
  if (checkValidPointerEvent(e)) {
    input.dispatchEvent(new CustomEvent(name, { detail: e }))
}

// Note that the PointerEvent is passed as the detail in these 'stroke events'.
canvas.addEventListener("pointerdown", dispatchPointerEvent("strokestart"))
canvas.addEventListener("pointerenter", dispatchPointerEvent("strokestart"))
canvas.addEventListener("pointerup", dispatchPointerEvent("strokeend"))
canvas.addEventListener("pointerleave", dispatchPointerEvent("strokeend"))
canvas.addEventListener("pointermove", dispatchPointerEvent("strokemove"))
canvas.addEventListener("touchmove", (e) => e.preventDefault())