Skip to content
Snippets Groups Projects
canvas.js 3.89 KiB
Newer Older
  • Learn to ignore specific revisions
  • // 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"
    
    
    const SVG_URL = "http://www.w3.org/2000/svg"
    
    
    // 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 pathGroupElems = new Map()
    
    export var stroke_colour = "blue"
    
    export const MIN_STROKE_RADIUS = 0.1
    export const MAX_STROKE_RADIUS = 3.9
    
    const getStrokeRadius = (pressure) => {
      return MIN_STROKE_RADIUS + pressure * (MAX_STROKE_RADIUS - MIN_STROKE_RADIUS)
    }
    
    export const input = new EventTarget()
    
    const createPathElem = (d, width) => {
      const pathGroupElem = document.createElementNS(SVG_URL, "path")
      pathGroupElem.setAttribute("stroke-width", width)
      pathGroupElem.setAttribute("d", d)
      return pathGroupElem
    }
    
    export const renderPath = (id, points) => {
      points = points.filter(([x]) => x != null)
    
    Giovanni Caruso's avatar
    Giovanni Caruso committed
      var colour = ""
    
      // Split up points into completely non-erased segments.
      let segments = [[]]
      for (const point of points) {
    
    Giovanni Caruso's avatar
    Giovanni Caruso committed
        colour = point[3]
        if (point[4] != false) {
    
          segments[segments.length - 1].push(point)
        } else {
          segments.push([])
        }
    
      segments = segments.filter((a) => a.length > 0)
    
      let pathGroupElem = pathGroupElems.get(id)
    
      if (segments.length == 0) {
        if (pathGroupElem != null) {
          canvas.removeChild(pathGroupElem)
          pathGroupElems.delete(id)
        }
        return
    
      if (pathGroupElem == null) {
        pathGroupElem = document.createElementNS(SVG_URL, "g")
    
    Giovanni Caruso's avatar
    Giovanni Caruso committed
        pathGroupElem.setAttribute("stroke", colour)
    
        pathGroupElem.setAttribute("fill", "none")
        pathGroupElem.setAttribute("stroke-linecap", "round")
        pathGroupElem.setAttribute("pointer-events", "none")
        canvas.appendChild(pathGroupElem)
        pathGroupElems.set(id, pathGroupElem)
      }
    
      pathGroupElem.innerHTML = ""
    
      for (const subpath of segments) {
        if (subpath.length == 1) {
          const circleElem = document.createElementNS(SVG_URL, "circle")
          circleElem.setAttribute("stroke", "none")
    
    Giovanni Caruso's avatar
    Giovanni Caruso committed
          circleElem.setAttribute("fill", colour)
    
          circleElem.setAttribute("cx", subpath[0][0])
          circleElem.setAttribute("cy", subpath[0][1])
          circleElem.setAttribute("r", getStrokeRadius(subpath[0][2]))
          pathGroupElem.appendChild(circleElem)
        } else {
          // Further split up segments based on thickness.
          const subpath_ = subpath.slice()
          let w = subpath_[0][2]
          for (let i = 1; i < subpath_.length; i++) {
            if (subpath_[i][2] != w) {
              const d = lineFn([...subpath_.splice(0, i), subpath_[0]])
              pathGroupElem.appendChild(createPathElem(d, getStrokeRadius(w) * 2))
              w = subpath_[0][2]
              i = 1
            }
    
          const d = lineFn(subpath_)
          pathGroupElem.appendChild(createPathElem(d, getStrokeRadius(w) * 2))
    
        }
      }
    }
    
    export const clear = () => {
    
      pathGroupElems.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())
    
    
    export function setStrokeColour(colour) {
      stroke_colour = colour
    }