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"
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 let stroke_radius = 1
export const MIN_STROKE_RADIUS = 0.1
export const MAX_STROKE_RADIUS = 3.9
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
const MAX_POINT_DISTANCE = 5
const MAX_PRESSURE_DELTA = 0.05
// Interpolate a path so that:
// - The distance between two adjacent points is capped at MAX_POINT_DISTANCE.
// - The pressure delta between two adjacent points is capped at
// MAX_PRESSURE_DELTA
// If paths are too choppy, try decreasing these constants.
const smoothPath = ([...path]) => {
// Apply MAX_POINT_DISTANCE.
for (let i = 1; i < path.length; i++) {
const dx = path[i][0] - path[i - 1][0]
const dy = path[i][1] - path[i - 1][1]
const dw = path[i][2] - path[i - 1][2]
const distance = Math.hypot(dx, dy)
const segmentsToSplit = Math.ceil(distance / MAX_POINT_DISTANCE)
const newPoints = []
for (let j = 1; j < segmentsToSplit; j++) {
newPoints.push([
path[i - 1][0] + (dx / segmentsToSplit) * j,
path[i - 1][1] + (dy / segmentsToSplit) * j,
path[i - 1][2] + (dw / segmentsToSplit) * j,
path[i - 1][3],
path[i - 1][4],
])
}
path.splice(i, 0, ...newPoints)
i += newPoints.length
}
// Apply MAX_PRESSURE_DELTA.
for (let i = 1; i < path.length; i++) {
const dx = path[i][0] - path[i - 1][0]
const dy = path[i][1] - path[i - 1][1]
const dw = path[i][2] - path[i - 1][2]
const normdw = dw / Math.max(0.05, Math.max(path[i][2], path[i - 1][2]))
const segmentsToSplit = Math.ceil(normdw / MAX_PRESSURE_DELTA)
const newPoints = []
for (let j = 1; j < segmentsToSplit; j++) {
newPoints.push([
path[i - 1][0] + (dx / segmentsToSplit) * j,
path[i - 1][1] + (dy / segmentsToSplit) * j,
path[i - 1][2] + (dw / segmentsToSplit) * j,
path[i - 1][3],
path[i - 1][4],
])
}
path.splice(i, 0, ...newPoints)
i += newPoints.length
}
return path
}
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)
// Split up points into completely non-erased segments.
let segments = [[]]
for (const point of points) {
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")
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")
circleElem.setAttribute("cx", subpath[0][0])
circleElem.setAttribute("cy", subpath[0][1])
circleElem.setAttribute("r", subpath[0][2])
pathGroupElem.appendChild(circleElem)
} else {
// Further split up segments based on thickness.
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, w * 2))
w = subpath_[0][2]
i = 1
}
const d = lineFn(subpath_)
pathGroupElem.appendChild(createPathElem(d, w * 2))
}
}
}
export const clear = () => {
pathGroupElems.clear()
canvas.innerHTML = ""
Iurii Maksymets
committed
// 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 }))
Iurii Maksymets
committed
}
// 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
}
export function getStrokeColour() {
return stroke_colour
export function setStrokeRadius(radius) {
stroke_radius = radius
const calculateStrokeRadius = (pressure, radius) => {
return (
MIN_STROKE_RADIUS +
(radius + pressure) * (MAX_STROKE_RADIUS - MIN_STROKE_RADIUS)
)
}
export function getStrokeRadius(pressure) {
return calculateStrokeRadius(pressure, stroke_radius)
}