UNPKG

@zag-js/signature-pad

Version:

Core logic for the signature-pad widget implemented as a state machine

139 lines (138 loc) 3.84 kB
// src/signature-pad.machine.ts import { createMachine } from "@zag-js/core"; import { getRelativePoint, trackPointerMove } from "@zag-js/dom-query"; import getStroke from "perfect-freehand"; import { getSvgPathFromStroke } from "./get-svg-path.mjs"; import * as dom from "./signature-pad.dom.mjs"; var machine = createMachine({ props({ props }) { return { defaultPaths: [], ...props, drawing: { size: 2, simulatePressure: false, thinning: 0.7, smoothing: 0.4, streamline: 0.6, ...props.drawing }, translations: { control: "signature pad", clearTrigger: "clear signature", ...props.translations } }; }, initialState() { return "idle"; }, context({ prop, bindable }) { return { paths: bindable(() => ({ defaultValue: prop("defaultPaths"), value: prop("paths"), sync: true, onChange(value) { prop("onDraw")?.({ paths: value }); } })), currentPoints: bindable(() => ({ defaultValue: [] })), currentPath: bindable(() => ({ defaultValue: null })) }; }, computed: { isInteractive: ({ prop }) => !(prop("disabled") || prop("readOnly")), isEmpty: ({ context }) => context.get("paths").length === 0 }, on: { CLEAR: { actions: ["clearPoints", "invokeOnDrawEnd", "focusCanvasEl"] } }, states: { idle: { on: { POINTER_DOWN: { target: "drawing", actions: ["addPoint"] } } }, drawing: { effects: ["trackPointerMove"], on: { POINTER_MOVE: { actions: ["addPoint", "invokeOnDraw"] }, POINTER_UP: { target: "idle", actions: ["endStroke", "invokeOnDrawEnd"] } } } }, implementations: { effects: { trackPointerMove({ scope, send }) { const doc = scope.getDoc(); return trackPointerMove(doc, { onPointerMove({ event, point }) { const controlEl = dom.getControlEl(scope); if (!controlEl) return; const { offset } = getRelativePoint(point, controlEl); send({ type: "POINTER_MOVE", point: offset, pressure: event.pressure }); }, onPointerUp() { send({ type: "POINTER_UP" }); } }); } }, actions: { addPoint({ context, event, prop }) { const nextPoints = [...context.get("currentPoints"), event.point]; context.set("currentPoints", nextPoints); const stroke = getStroke(nextPoints, prop("drawing")); context.set("currentPath", getSvgPathFromStroke(stroke)); }, endStroke({ context }) { const nextPaths = [...context.get("paths"), context.get("currentPath")]; context.set("paths", nextPaths); context.set("currentPoints", []); context.set("currentPath", null); }, clearPoints({ context }) { context.set("currentPoints", []); context.set("paths", []); context.set("currentPath", null); }, focusCanvasEl({ scope }) { queueMicrotask(() => { scope.getActiveElement()?.focus({ preventScroll: true }); }); }, invokeOnDraw({ context, prop }) { prop("onDraw")?.({ paths: [...context.get("paths"), context.get("currentPath")] }); }, invokeOnDrawEnd({ context, prop, scope, computed }) { prop("onDrawEnd")?.({ paths: [...context.get("paths")], getDataUrl(type, quality = 0.92) { if (computed("isEmpty")) return Promise.resolve(""); return dom.getDataUrl(scope, { type, quality }); } }); } } } }); export { machine };