@tremolo-ui/dom 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/canvas/context.ts","../src/canvas/animation.ts","../src/midi/access.ts","../src/midi/message.ts","../src/midi/input.ts","../src/pointer/drag.ts","../src/piano/input.ts","../src/xy.ts","../src/pointer/dragValue.ts","../src/pointer/wheel.ts"],"sourcesContent":["/**\n * The properties of a 2D context that survive `save()` / `restore()`, and so\n * are what has to be carried across a resize by hand: setting `canvas.width`\n * resets the context to its defaults.\n *\n * @see https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/save\n */\nexport const drawingState = [\n 'strokeStyle',\n 'fillStyle',\n 'globalAlpha',\n 'lineWidth',\n 'lineCap',\n 'lineJoin',\n 'miterLimit',\n 'lineDashOffset',\n 'shadowOffsetX',\n 'shadowOffsetY',\n 'shadowBlur',\n 'shadowColor',\n 'globalCompositeOperation',\n 'font',\n 'textAlign',\n 'textBaseline',\n 'direction',\n 'imageSmoothingEnabled',\n] as const\n\nexport type DrawingState = (typeof drawingState)[number]\nexport type DrawingStateValue = CanvasRenderingContext2D[DrawingState]\nexport type DrawingContext = Pick<CanvasRenderingContext2D, DrawingState>\n\nexport function isDrawingState(value: unknown): value is DrawingState {\n const names: readonly string[] = drawingState\n return typeof value == 'string' && names.includes(value)\n}\n\n/** Copy the drawing state off a context, to put back after a resize. */\nexport function readDrawingState(\n context: CanvasRenderingContext2D,\n): DrawingContext {\n const state = {} as DrawingContext\n for (const property of drawingState) {\n ;(state[property] as DrawingStateValue) = context[property]\n }\n return state\n}\n\n/** Put a state read by {@link readDrawingState} back onto a context. */\nexport function writeDrawingState(\n context: CanvasRenderingContext2D,\n state: DrawingContext,\n) {\n for (const property of drawingState) {\n ;(context[property] as DrawingStateValue) = state[property]\n }\n}\n\n/**\n * Give the canvas a backing store of `width * dpr` by `height * dpr` device\n * pixels while keeping it `width` by `height` in CSS pixels, and scale the\n * context so that drawing code can work in CSS pixels throughout.\n *\n * Assigning to `canvas.width` clears the canvas and resets the context, so\n * this both resizes and re-establishes the transform.\n */\nexport function applyDevicePixelRatio(\n canvas: HTMLCanvasElement,\n context: CanvasRenderingContext2D,\n width: number,\n height: number,\n dpr: number,\n) {\n canvas.width = width * dpr\n canvas.height = height * dpr\n // Reset the current transformation matrix to the identity matrix\n context.setTransform(1, 0, 0, 1, 0, 0)\n context.scale(dpr, dpr)\n canvas.style.width = `${width}px`\n canvas.style.height = `${height}px`\n}\n","import {\n applyDevicePixelRatio,\n readDrawingState,\n writeDrawingState,\n type DrawingContext,\n} from './context'\n\n/** What the canvas looked like when a frame was drawn. */\nexport interface AnimationFrame {\n /** Width of the canvas in CSS pixels. */\n width: number\n /** Height of the canvas in CSS pixels. */\n height: number\n /** Frames drawn so far, starting at 0. */\n count: number\n /** Milliseconds since the previous frame. */\n deltaTime: number\n /** Milliseconds since the instance was created. */\n elapsedTime: number\n /** Frames per second implied by `deltaTime`. */\n fps: number\n}\n\nexport type CanvasDrawFunction = (\n context: CanvasRenderingContext2D,\n frame: AnimationFrame,\n) => void\n\nexport type CanvasInitFunction = (\n context: CanvasRenderingContext2D,\n size: { width: number; height: number },\n) => void\n\nexport interface AnimationCanvasOptions {\n /** Called for every frame. */\n draw: CanvasDrawFunction\n\n /** Called once, after the first size is known and before the first frame. */\n init?: CanvasInitFunction\n\n /**\n * Keep drawing on every animation frame. When off, a frame is drawn only on\n * a resize or an explicit {@link AnimationCanvasInstance.redraw}.\n *\n * @default true\n */\n animate?: boolean\n\n /**\n * Size in CSS pixels. Ignored when `relativeSize` is on.\n *\n * @default { width: 100, height: 100 }\n */\n size?: { width: number; height: number }\n\n /**\n * Follow the size of the canvas's parent element instead of `size`.\n *\n * Fixed for the lifetime of the instance: it decides whether a\n * `ResizeObserver` is attached.\n *\n * @default false\n */\n relativeSize?: boolean\n\n /**\n * Carry the drawing across a resize, so that the canvas does not blank for a\n * frame while the new size is drawn.\n *\n * @default true\n */\n reduceFlickering?: boolean\n\n /**\n * Passed to `getContext('2d', …)`. Read once when the context is created, so\n * it is fixed for the lifetime of the instance.\n *\n * @see https://developer.mozilla.org/docs/Web/API/HTMLCanvasElement/getContext#contextattributes\n */\n contextAttributes?: CanvasRenderingContext2DSettings\n}\n\nexport interface AnimationCanvasInstance {\n /**\n * Replace the given options. Lets a wrapper feed a fresh `draw` in on every\n * render without restarting the animation, so the frame count and the\n * elapsed time keep running.\n *\n * `relativeSize` and `contextAttributes` are fixed for the lifetime of the\n * instance and are ignored here.\n *\n * While `animate` is off this also draws a frame, since nothing else would.\n */\n update: (options: Partial<AnimationCanvasOptions>) => void\n\n /** Draw one frame now, whether or not `animate` is on. */\n redraw: () => void\n\n destroy: () => void\n}\n\n/**\n * Drive a canvas from `requestAnimationFrame`, keeping its backing store in\n * step with the device pixel ratio and, optionally, with the size of its\n * parent.\n *\n * Drawing code works in CSS pixels: the context is scaled by the device pixel\n * ratio, and `width` / `height` of each {@link AnimationFrame} are CSS pixels.\n */\nexport function createAnimationCanvas(\n canvas: HTMLCanvasElement,\n options: AnimationCanvasOptions,\n): AnimationCanvasInstance {\n let opts = options\n\n const context2d = canvas.getContext('2d', opts.contextAttributes)\n if (!context2d) {\n throw new Error('createAnimationCanvas: cannot get a 2d context')\n }\n const context: CanvasRenderingContext2D = context2d\n\n const relativeSize = opts.relativeSize ?? false\n\n /** Off-document canvas holding the drawing while the real one is resized. */\n let memo: HTMLCanvasElement | null = null\n let memoContext: CanvasRenderingContext2D | null = null\n\n let width = 0\n let height = 0\n /** Whether a size has been applied, so a frame can be drawn. */\n let sized = false\n let initialized = false\n\n let frameId: number | null = null\n let count = -1\n const startTime = performance.now()\n let previousTime = startTime\n\n function devicePixelRatio() {\n return globalThis.devicePixelRatio || 1\n }\n\n /**\n * Copy the canvas onto the memo canvas at its full device resolution, 1:1.\n *\n * Assigning to `memo.width` resets the memo's transform to the identity, so\n * the copy neither scales nor resamples.\n */\n function takeSnapshot(): DrawingContext | null {\n if (!(opts.reduceFlickering ?? true)) return null\n if (!memo) {\n memo = globalThis.document?.createElement('canvas') ?? null\n memoContext = memo?.getContext('2d', opts.contextAttributes) ?? null\n }\n if (!memo || !memoContext) return null\n\n const state = readDrawingState(context)\n memo.width = canvas.width\n memo.height = canvas.height\n if (canvas.width > 0 && canvas.height > 0) {\n memoContext.drawImage(canvas, 0, 0)\n }\n return state\n }\n\n /**\n * Put the snapshot back, given the size in CSS pixels it was taken at.\n *\n * The context is scaled to CSS pixels, so drawing the snapshot at its old\n * CSS size leaves it exactly where it was. While the device pixel ratio\n * stays put that is a 1:1 copy of device pixels, and when it changes the\n * snapshot is rescaled once, from its full resolution.\n */\n function restoreSnapshot(\n state: DrawingContext | null,\n previousWidth: number,\n previousHeight: number,\n ) {\n if (!state || !memo || !memoContext) return\n if (memo.width <= 0 || memo.height <= 0) return\n writeDrawingState(context, state)\n context.drawImage(memo, 0, 0, previousWidth, previousHeight)\n }\n\n function applySize(w: number, h: number) {\n const dpr = devicePixelRatio()\n const previousWidth = width\n const previousHeight = height\n const state = takeSnapshot()\n applyDevicePixelRatio(canvas, context, w, h, dpr)\n width = w\n height = h\n sized = true\n restoreSnapshot(state, previousWidth, previousHeight)\n }\n\n function drawFrame() {\n if (!sized) return\n if (!initialized) {\n initialized = true\n opts.init?.(context, { width, height })\n }\n const now = performance.now()\n const deltaTime = now - previousTime\n previousTime = now\n count += 1\n opts.draw(context, {\n width,\n height,\n count,\n deltaTime,\n elapsedTime: now - startTime,\n fps: 1000 / deltaTime,\n })\n }\n\n function tick() {\n // Scheduled before drawing so that a slow frame does not delay the next\n // request, matching how requestAnimationFrame loops are usually written.\n frameId = requestAnimationFrame(tick)\n drawFrame()\n }\n\n function startLoop() {\n if (frameId != null) return\n previousTime = performance.now()\n frameId = requestAnimationFrame(tick)\n }\n\n function stopLoop() {\n if (frameId == null) return\n cancelAnimationFrame(frameId)\n frameId = null\n }\n\n let observer: ResizeObserver | null = null\n\n if (relativeSize) {\n const parent = canvas.parentElement\n if (!parent) {\n throw new Error(\n 'createAnimationCanvas: relativeSize needs the canvas to have a parent element',\n )\n }\n // The observer reports the current size as soon as it starts, so it is the\n // single source of the size — including the first one. Reading the parent\n // here instead would mix the padding box with the content box the observer\n // reports.\n observer = new ResizeObserver((entries) => {\n for (const entry of entries) {\n const { width: w, height: h } = entry.contentRect\n applySize(w, h)\n // With the loop running the next frame covers the new size already.\n if (!(opts.animate ?? true)) drawFrame()\n }\n })\n observer.observe(parent)\n } else {\n const { width: w = 100, height: h = 100 } = opts.size ?? {}\n applySize(w, h)\n }\n\n if (opts.animate ?? true) {\n startLoop()\n } else if (sized) {\n drawFrame()\n }\n\n return {\n update: (next) => {\n const wasAnimating = opts.animate ?? true\n opts = {\n ...opts,\n ...next,\n relativeSize,\n contextAttributes: opts.contextAttributes,\n }\n\n if (!relativeSize) {\n const { width: w = 100, height: h = 100 } = opts.size ?? {}\n if (w != width || h != height) applySize(w, h)\n }\n\n const animating = opts.animate ?? true\n if (animating && !wasAnimating) startLoop()\n if (!animating && wasAnimating) stopLoop()\n\n // Without a loop running, a resize and this call are the only things\n // that can put anything new on the canvas — so a new `draw` has to be\n // painted here. That is what makes a canvas driven by state rather than\n // by time work: the wrapper re-renders, pushes the new handler in, and\n // the frame it draws reflects it.\n if (!animating) drawFrame()\n },\n redraw: drawFrame,\n destroy: () => {\n stopLoop()\n observer?.disconnect()\n observer = null\n memo = null\n memoContext = null\n },\n }\n}\n","/** @private */\nexport const PERMISSION_DENIED = 'PERMISSION_DENIED'\n/** @private */\nexport const NOT_SUPPORTED = 'NOT_SUPPORTED'\n\n/** @private */\nexport type MIDIAccessError = typeof PERMISSION_DENIED | typeof NOT_SUPPORTED\n\nexport type MIDIAccessState = {\n readonly midiAccess: MIDIAccess | null\n readonly error: MIDIAccessError | null\n}\n\nconst INITIAL_STATE: MIDIAccessState = {\n midiAccess: null,\n error: null,\n}\n\nexport interface MIDIAccessInstance {\n /** Request MIDI access. Safe to call more than once. */\n request: () => void\n getState: () => MIDIAccessState\n /** Snapshot for server side rendering. Always the initial state. */\n getServerState: () => MIDIAccessState\n /** @returns unsubscribe function */\n subscribe: (listener: () => void) => () => void\n destroy: () => void\n}\n\n/**\n * Request MIDI access in the browser.\n *\n * The returned instance holds the state and notifies subscribers when it changes,\n * so it can be consumed from any framework.\n */\nexport function createMIDIAccess(): MIDIAccessInstance {\n let state = INITIAL_STATE\n let destroyed = false\n const listeners = new Set<() => void>()\n\n function setState(next: MIDIAccessState) {\n state = next\n for (const listener of listeners) listener()\n }\n\n function request() {\n if (destroyed) return\n if (typeof navigator === 'undefined' || !navigator.requestMIDIAccess) {\n setState({ ...state, error: NOT_SUPPORTED })\n return\n }\n navigator\n .requestMIDIAccess()\n .then((access) => {\n if (destroyed) return\n setState({ midiAccess: access, error: null })\n })\n .catch(() => {\n if (destroyed) return\n setState({ ...state, error: PERMISSION_DENIED })\n })\n }\n\n return {\n request,\n getState: () => state,\n getServerState: () => INITIAL_STATE,\n subscribe: (listener) => {\n listeners.add(listener)\n return () => {\n listeners.delete(listener)\n }\n },\n destroy: () => {\n destroyed = true\n listeners.clear()\n },\n }\n}\n","export interface MIDIMessageInstance {\n destroy: () => void\n}\n\n/**\n * Listen to raw `midimessage` events on every input of a MIDIAccess.\n *\n * Use this when you need more detail than {@link createMIDIInput} provides.\n */\nexport function createMIDIMessage(\n midiAccess: MIDIAccess | null,\n onMIDIMessage: (event: MIDIMessageEvent) => void,\n): MIDIMessageInstance {\n if (!midiAccess) {\n return { destroy: () => {} }\n }\n\n const inputs = [...midiAccess.inputs.values()]\n for (const input of inputs) {\n input.addEventListener('midimessage', onMIDIMessage)\n }\n\n return {\n destroy: () => {\n for (const input of inputs) {\n input.removeEventListener('midimessage', onMIDIMessage)\n }\n },\n }\n}\n","import { createMIDIMessage, type MIDIMessageInstance } from './message'\n\nconst MIDI_EVENT_TO_NUMBER = {\n NOTE_ON: 0x90,\n NOTE_OFF: 0x80,\n PITCH_BEND: 0xe0,\n}\n\nexport type MIDIInputHandlers = {\n onNoteOnEvent?: (note: number, velocity: number) => void\n onNoteOffEvent?: (note: number) => void\n onPitchBendEvent?: (msb: number, lsb: number) => void\n}\n\nexport type MIDIInputInstance = MIDIMessageInstance\n\n/**\n * Handle note on/off and pitch bend events. To be used with {@link createMIDIAccess}.\n * Internally uses {@link createMIDIMessage}.\n */\nexport function createMIDIInput(\n midiAccess: MIDIAccess | null,\n { onNoteOnEvent, onNoteOffEvent, onPitchBendEvent }: MIDIInputHandlers,\n): MIDIInputInstance {\n return createMIDIMessage(midiAccess, (event) => {\n if (!event.data) return\n // event.data[0] ... command\n // event.data[1] ... note, MSB (Most Significant Byte)\n // event.data[2] ... velocity, LSB (Least Significant Byte)\n const kind = event.data[0] & 0xf0\n\n if (\n kind == MIDI_EVENT_TO_NUMBER.NOTE_OFF ||\n (kind == MIDI_EVENT_TO_NUMBER.NOTE_ON && event.data[2] == 0)\n ) {\n onNoteOffEvent?.(event.data[1])\n } else if (kind == MIDI_EVENT_TO_NUMBER.NOTE_ON) {\n onNoteOnEvent?.(event.data[1], event.data[2])\n } else if (kind == MIDI_EVENT_TO_NUMBER.PITCH_BEND) {\n onPitchBendEvent?.(event.data[1], event.data[2])\n }\n })\n}\n","export type DragState = {\n /** Total movement from the drag start, in screen coordinates. */\n x: number\n y: number\n /** Movement since the previous event, in screen coordinates. */\n deltaX: number\n deltaY: number\n /** Pointer position in viewport coordinates. */\n clientX: number\n clientY: number\n /**\n * Which pointer this is. Always the same value for one drag; with\n * {@link DragOptions.multiPointer} it tells concurrent drags apart.\n */\n pointerId: number\n event: PointerEvent\n}\n\nexport type DragOptions = {\n /**\n * Minimum movement in pixels before `onDrag` fires. Movement below it is\n * carried over to the next event rather than discarded, so a slow drag still\n * reports once it adds up.\n *\n * Prevents `onDrag` from firing on, for example, a double click.\n *\n * @default 0\n */\n threshold?: number\n\n /**\n * CSS cursor to show while dragging. Applied to the element itself: pointer\n * capture keeps it in effect even once the pointer leaves the element, so\n * there is no need to touch the document.\n *\n * With {@link DragOptions.multiPointer} it is set for the first pointer and\n * restored once the last one is up.\n */\n cursor?: string\n\n /**\n * Track every pointer that goes down on the element, rather than only the\n * first. Each one gets its own `onDragStart` / `onDrag` / `onDragEnd` and\n * carries its own totals; {@link DragState.pointerId} says which is which.\n *\n * Fixed for the lifetime of the instance: switching part way through a drag\n * has no meaning, so `update()` ignores it.\n *\n * @default false\n */\n multiPointer?: boolean\n\n onDragStart?: (state: DragState) => void\n onDrag?: (state: DragState) => void\n onDragEnd?: (state: DragState) => void\n}\n\nexport interface DragInstance {\n /**\n * Replace the given options. Lets a wrapper feed fresh handlers in without\n * tearing down the listeners, which would abort a drag in progress.\n *\n * `multiPointer` is fixed for the lifetime of the instance and is ignored\n * here.\n */\n update: (options: DragOptions) => void\n destroy: () => void\n}\n\n/**\n * Applied to the element for the lifetime of the instance.\n *\n * `touch-action` stops touch dragging from scrolling the page. That alone makes\n * the browser treat a long press as the start of a text selection, so selection\n * and the iOS callout are suppressed here too.\n */\nconst MANAGED_STYLES = [\n ['touch-action', 'none'],\n ['user-select', 'none'],\n ['-webkit-user-select', 'none'],\n ['-webkit-touch-callout', 'none'],\n] as const\n\ntype CaptureTarget = {\n setPointerCapture?: (pointerId: number) => void\n releasePointerCapture?: (pointerId: number) => void\n hasPointerCapture?: (pointerId: number) => boolean\n}\n\n/** What one pointer needs to report its own movement. */\ntype PointerState = {\n /** Where the listeners for this pointer live. */\n moveTarget: EventTarget\n startX: number\n startY: number\n lastX: number\n lastY: number\n}\n\n/**\n * Track a pointer drag on an element.\n *\n * Uses Pointer Events only, and pointer capture so that the drag keeps working\n * once the pointer leaves the element. For the lifetime of the instance the\n * element gets `touch-action: none` so that touch dragging does not scroll the\n * page, plus `user-select: none` so that a long press does not start a text\n * selection instead.\n *\n * One pointer at a time by default; see {@link DragOptions.multiPointer}.\n */\nexport function createDrag(\n element: Element,\n options: DragOptions = {},\n): DragInstance {\n let opts = options\n const multiPointer = options.multiPointer ?? false\n const capture = element as CaptureTarget\n const style = (element as Partial<HTMLElement>).style\n\n const previousStyles = new Map<string, string>()\n if (style) {\n for (const [property, value] of MANAGED_STYLES) {\n previousStyles.set(property, style.getPropertyValue(property))\n style.setProperty(property, value)\n }\n }\n\n const pointers = new Map<number, PointerState>()\n /**\n * How many pointers each target carries. Adding the same listener twice is a\n * no-op and removing it once removes it for good, so a target is subscribed\n * to while at least one pointer is on it and no longer.\n */\n const targets = new Map<EventTarget, number>()\n let previousCursor: string | undefined\n\n function state(\n event: PointerEvent,\n pointer: PointerState,\n deltaX: number,\n deltaY: number,\n ): DragState {\n return {\n x: event.screenX - pointer.startX,\n y: event.screenY - pointer.startY,\n deltaX,\n deltaY,\n clientX: event.clientX,\n clientY: event.clientY,\n pointerId: event.pointerId,\n event,\n }\n }\n\n /**\n * `user-select: none` only makes the element's own text unselectable; the\n * browser still starts a selection on long press and grabs whatever text it\n * finds nearby. Cancelling the selection outright is what actually stops it.\n */\n function preventSelectStart(event: Event) {\n event.preventDefault()\n }\n\n function retainTarget(target: EventTarget) {\n const count = targets.get(target) ?? 0\n if (count === 0) {\n target.addEventListener('pointermove', handlePointerMove)\n target.addEventListener('pointerup', handlePointerUp)\n target.addEventListener('pointercancel', handlePointerUp)\n }\n targets.set(target, count + 1)\n }\n\n function releaseTarget(target: EventTarget) {\n const count = targets.get(target) ?? 0\n if (count > 1) {\n targets.set(target, count - 1)\n return\n }\n target.removeEventListener('pointermove', handlePointerMove)\n target.removeEventListener('pointerup', handlePointerUp)\n target.removeEventListener('pointercancel', handlePointerUp)\n targets.delete(target)\n }\n\n function handlePointerDown(event: Event) {\n const pointerEvent = event as PointerEvent\n const pointerId = pointerEvent.pointerId\n // Without multiPointer only one pointer drives the drag; ignore the rest.\n if (pointers.has(pointerId)) return\n if (!multiPointer && pointers.size > 0) return\n\n const isFirst = pointers.size === 0\n if (isFirst && opts.cursor && style) {\n previousCursor = style.cursor\n style.cursor = opts.cursor\n }\n\n capture.setPointerCapture?.(pointerId)\n // With pointer capture the element receives the rest of the gesture.\n // Without it (older engines, jsdom) fall back to the window.\n const moveTarget =\n capture.hasPointerCapture?.(pointerId) === true\n ? element\n : (globalThis.window ?? element)\n\n const pointer: PointerState = {\n moveTarget,\n startX: pointerEvent.screenX,\n startY: pointerEvent.screenY,\n lastX: pointerEvent.screenX,\n lastY: pointerEvent.screenY,\n }\n pointers.set(pointerId, pointer)\n\n retainTarget(moveTarget)\n if (isFirst) {\n globalThis.document?.addEventListener('selectstart', preventSelectStart)\n }\n\n opts.onDragStart?.(state(pointerEvent, pointer, 0, 0))\n }\n\n function handlePointerMove(event: Event) {\n const pointerEvent = event as PointerEvent\n const pointer = pointers.get(pointerEvent.pointerId)\n if (!pointer) return\n\n const deltaX = pointerEvent.screenX - pointer.lastX\n const deltaY = pointerEvent.screenY - pointer.lastY\n\n // Movement below the threshold accumulates until it crosses it. Dropping it\n // instead would swallow a slow drag entirely: pointer coordinates are\n // fractional, so each event can move less than a pixel and never report.\n const threshold = opts.threshold ?? 0\n if (Math.abs(deltaX) < threshold && Math.abs(deltaY) < threshold) return\n\n pointer.lastX = pointerEvent.screenX\n pointer.lastY = pointerEvent.screenY\n\n opts.onDrag?.(state(pointerEvent, pointer, deltaX, deltaY))\n }\n\n function handlePointerUp(event: Event) {\n const pointerEvent = event as PointerEvent\n const pointer = pointers.get(pointerEvent.pointerId)\n if (!pointer) return\n\n const deltaX = pointerEvent.screenX - pointer.lastX\n const deltaY = pointerEvent.screenY - pointer.lastY\n const finalState = state(pointerEvent, pointer, deltaX, deltaY)\n\n stopTracking(pointerEvent.pointerId)\n opts.onDragEnd?.(finalState)\n }\n\n function stopTracking(pointerId: number) {\n const pointer = pointers.get(pointerId)\n if (!pointer) return\n\n capture.releasePointerCapture?.(pointerId)\n releaseTarget(pointer.moveTarget)\n pointers.delete(pointerId)\n\n if (pointers.size > 0) return\n\n globalThis.document?.removeEventListener('selectstart', preventSelectStart)\n if (previousCursor !== undefined && style) {\n style.cursor = previousCursor\n previousCursor = undefined\n }\n }\n\n element.addEventListener('pointerdown', handlePointerDown)\n\n return {\n update: (next) => {\n opts = { ...opts, ...next, multiPointer }\n },\n destroy: () => {\n for (const pointerId of [...pointers.keys()]) stopTracking(pointerId)\n element.removeEventListener('pointerdown', handlePointerDown)\n if (style) {\n for (const [property] of MANAGED_STYLES) {\n const previous = previousStyles.get(property)\n if (previous) {\n style.setProperty(property, previous)\n } else {\n style.removeProperty(property)\n }\n }\n }\n },\n }\n}\n","import { noteAt, type PianoLayout } from '@tremolo-ui/functions'\n\nimport { createDrag } from '../pointer/drag'\n\n/**\n * What asked for a note to sound.\n *\n * A note stops only once everything that asked for it has let go, so two\n * fingers on one key, or a key held by both the mouse and a MIDI keyboard,\n * behave the way they look.\n *\n * Pointers use `pointer:<pointerId>`; everything else names its own source.\n */\nexport type NoteSource = string\n\nexport interface PianoInputOptions {\n /** Geometry of the drawn keyboard, used to find the note under a pointer. */\n layout: PianoLayout\n\n /**\n * Let a pointer slide from one key to the next while it is down. With it off,\n * the key that was pressed keeps sounding until the pointer is released.\n *\n * @default true\n */\n glissando?: boolean\n\n /**\n * Highest note that can sound. Above it a note is refused, whichever source\n * asks for it.\n *\n * @default 127\n */\n midiMax?: number\n\n /** Called when a note starts sounding, not for each source that asks. */\n onPlayNote?: (note: number, velocity?: number) => void\n /** Called once the last source holding a note has let go. */\n onStopNote?: (note: number) => void\n /** Called whenever {@link PianoInputInstance.activeNotes} would change. */\n onActiveNotesChange?: (notes: number[]) => void\n}\n\nexport interface PianoInputInstance {\n /**\n * Replace the given options. Lets a wrapper feed a fresh layout in without\n * tearing down the listeners, which would abort a drag in progress.\n */\n update: (options: Partial<PianoInputOptions>) => void\n\n /**\n * Start a note from something other than a pointer: a keyboard shortcut, a\n * MIDI message, an imperative call.\n */\n noteOn: (\n note: number,\n options?: { source?: NoteSource; velocity?: number },\n ) => void\n /** Release a note held by `source`. */\n noteOff: (note: number, options?: { source?: NoteSource }) => void\n\n /** The notes currently sounding, ascending. */\n activeNotes: () => number[]\n\n destroy: () => void\n}\n\nconst DEFAULT_SOURCE: NoteSource = 'api'\n\n/**\n * Drive a piano keyboard with pointers, and own what is sounding.\n *\n * Every way of playing a note goes through the one instance — pointers here,\n * keyboard shortcuts and MIDI through {@link PianoInputInstance.noteOn} — so\n * there is a single answer to what is currently held, and drawing the keys is\n * left entirely to the wrapper.\n *\n * Tracks every pointer at once, so a chord can be played with several fingers.\n */\nexport function createPianoInput(\n element: Element,\n options: PianoInputOptions,\n): PianoInputInstance {\n let opts = options\n\n /** Which sources are holding each sounding note. */\n const held = new Map<number, Set<NoteSource>>()\n /** The note each pointer is currently on. */\n const pointerNotes = new Map<number, number>()\n\n function activeNotes(): number[] {\n return [...held.keys()].sort((a, b) => a - b)\n }\n\n function noteOn(\n note: number,\n {\n source = DEFAULT_SOURCE,\n velocity,\n }: { source?: NoteSource; velocity?: number } = {},\n ) {\n if (note > (opts.midiMax ?? 127)) return\n\n const sources = held.get(note)\n if (sources) {\n // Already sounding: remember the extra holder and leave it alone.\n sources.add(source)\n return\n }\n\n held.set(note, new Set([source]))\n opts.onPlayNote?.(note, velocity)\n opts.onActiveNotesChange?.(activeNotes())\n }\n\n function noteOff(\n note: number,\n { source = DEFAULT_SOURCE }: { source?: NoteSource } = {},\n ) {\n const sources = held.get(note)\n if (!sources) return\n\n sources.delete(source)\n if (sources.size > 0) return\n\n held.delete(note)\n opts.onStopNote?.(note)\n opts.onActiveNotesChange?.(activeNotes())\n }\n\n /** The note under a pointer, or null where the pointer is off the keys. */\n function noteUnder(clientX: number, clientY: number): number | null {\n const { left, top, height } = element.getBoundingClientRect()\n return noteAt(clientX - left, clientY - top, height, opts.layout)\n }\n\n /** Move a pointer onto a note, releasing whatever it held before. */\n function movePointer(pointerId: number, note: number | null) {\n const source = `pointer:${pointerId}`\n const previous = pointerNotes.get(pointerId)\n if (previous === note) return\n\n if (previous !== undefined) noteOff(previous, { source })\n\n if (note === null) {\n pointerNotes.delete(pointerId)\n } else {\n pointerNotes.set(pointerId, note)\n noteOn(note, { source })\n }\n }\n\n const drag = createDrag(element, {\n multiPointer: true,\n onDragStart: (state) =>\n movePointer(state.pointerId, noteUnder(state.clientX, state.clientY)),\n onDrag: (state) => {\n // Without glissando the pressed key holds until the pointer is released,\n // so where the pointer wanders to does not matter.\n if (opts.glissando === false) return\n movePointer(state.pointerId, noteUnder(state.clientX, state.clientY))\n },\n onDragEnd: (state) => movePointer(state.pointerId, null),\n })\n\n return {\n update: (next) => {\n opts = { ...opts, ...next }\n },\n noteOn,\n noteOff,\n activeNotes,\n destroy: () => {\n drag.destroy()\n // Anything still held is released, so a caller that mirrors these\n // callbacks into a synth is not left with a stuck note.\n for (const note of activeNotes()) {\n held.delete(note)\n opts.onStopNote?.(note)\n }\n pointerNotes.clear()\n opts.onActiveNotesChange?.([])\n },\n }\n}\n","/**\n * A pair of per-axis values. The tuple elements are labelled, so editors show\n * `[x: number, y: number]` rather than a bare pair.\n */\nexport type XY<T> = [x: T, y: T]\n\n/**\n * A setting that may be given once for both axes, or per axis.\n *\n * A single value is told from a pair with `Array.isArray`, so the single form\n * is only offered while `T` cannot itself be an array. Where it can, the pair\n * is the only way to write it, since a lone array would be read as a pair.\n */\nexport type XYInput<T> = [T] extends [readonly unknown[]]\n ? readonly [x: T, y: T]\n : T | readonly [x: T, y: T]\n\n// `Array.isArray` narrows a mutable tuple on its own, but not a readonly one,\n// hence the explicit predicate.\nfunction isPair<T>(\n value: T | readonly [x: T, y: T],\n): value is readonly [x: T, y: T] {\n return Array.isArray(value)\n}\n\n/**\n * Spread a setting that may have been given as a single value.\n *\n * The parameter is written out rather than taken as `XYInput<T>`: a\n * conditional type cannot be narrowed, so the constraint stays where it is\n * declared and this takes both forms.\n *\n * The pair is copied rather than passed along, so that the result is a tuple\n * the caller owns even when a readonly one was given.\n */\nexport function toXY<T>(value: T | readonly [x: T, y: T]): XY<T> {\n return isPair(value) ? [value[0], value[1]] : [value, value]\n}\n","import {\n clamp,\n linearScale,\n normalizeValue,\n stepValue,\n type ValueRange,\n} from '@tremolo-ui/functions'\n\nimport { toXY, type XY, type XYInput } from '../xy'\n\nimport { createDrag, type DragState } from './drag'\n\n/**\n * How the 0-1 travel of one axis maps onto a value.\n *\n * Extends `ValueRange` so that a drag and an `applyDelta` nudge from a wheel\n * or an arrow key share one description of the scaling.\n */\nexport interface AxisOptions extends ValueRange {\n /**\n * Flip the axis so that its far end is `min`.\n *\n * Positions follow the screen: x grows to the right, y downwards. A vertical\n * slider whose maximum is at the top therefore reverses its y axis.\n *\n * @default false\n */\n reverse?: boolean\n}\n\n/** Where the value of each axis currently sits, as a position (see {@link DragValueMapping}). */\nexport interface MappingContext {\n position: () => XY<number>\n}\n\n/**\n * Turns pointer movement into a position on each axis: 0 is the `min` end of\n * the travel and 1 the `max` end, before `reverse` and the scaling of\n * {@link AxisOptions} are applied.\n *\n * A mapping holds the state of the drag in progress, so an instance belongs to\n * a single {@link createDragValue} instance.\n */\nexport interface DragValueMapping {\n /**\n * @returns the position, or null when it cannot be determined and the event\n * should be ignored.\n */\n start: (state: DragState, context: MappingContext) => XY<number> | null\n move: (state: DragState, context: MappingContext) => XY<number> | null\n}\n\n/**\n * Map the pointer onto the bounding rect of an element: the value *is* the\n * position pointed at, so the middle of the element is 0.5.\n *\n * The element is read on every event, so it may be mounted after the drag is\n * set up and may change size while a drag is in progress.\n */\nexport function elementMapping(\n getElement: () => Element | null | undefined,\n): DragValueMapping {\n function positionIn(state: DragState): XY<number> | null {\n const element = getElement()\n if (!element) return null\n const { left, top, right, bottom } = element.getBoundingClientRect()\n // A collapsed element has no travel to normalize against, and\n // normalizeValue rejects an empty range.\n return [\n right > left ? normalizeValue(state.clientX, left, right) : 0,\n bottom > top ? normalizeValue(state.clientY, top, bottom) : 0,\n ]\n }\n\n return { start: positionIn, move: positionIn }\n}\n\n/**\n * Move the value away from where it stood when the drag started, by the\n * distance dragged. The pointer position itself carries no meaning, so the\n * value can be adjusted from anywhere on the screen.\n */\nexport function relativeMapping({\n pixelRange = 100,\n}: {\n /**\n * Pixels of movement that span the whole range.\n * @default 100\n */\n pixelRange?: XYInput<number>\n} = {}): DragValueMapping {\n const [rangeX, rangeY] = toXY(pixelRange)\n let origin: XY<number> = [0, 0]\n\n return {\n start: (_state, context) => {\n origin = context.position()\n return origin\n },\n // `x` and `y` are measured from the start of the drag, so the position\n // never accumulates a rounding error of its own.\n move: (state) => [\n origin[0] + state.x / rangeX,\n origin[1] + state.y / rangeY,\n ],\n }\n}\n\nexport interface DragValueOptions {\n /** Scaling of each axis; a single value applies to both. */\n axis: XYInput<AxisOptions>\n\n /** How pointer movement becomes a position. */\n mapping: DragValueMapping\n\n /**\n * The current value of each axis. Read when a drag starts, by mappings that\n * move the value relative to it, such as {@link relativeMapping}.\n */\n getValue?: () => XY<number>\n\n /**\n * Report the value on pointer down, before any movement.\n *\n * Enable it where the pointer position *is* the value, so that a plain click\n * jumps to it. Leave it off where the element being dragged is an object in\n * its own right, so that grabbing its edge does not shift it under the\n * pointer.\n *\n * @default false\n */\n updateOnPointerDown?: boolean\n\n /** @see DragOptions.threshold */\n threshold?: number\n /** @see DragOptions.cursor */\n cursor?: string\n\n onChange?: (value: XY<number>, state: DragState) => void\n onDragStart?: (value: XY<number>, state: DragState) => void\n onDragEnd?: (value: XY<number>, state: DragState) => void\n}\n\nexport interface DragValueInstance {\n /**\n * Replace the given options. Lets a wrapper feed fresh values in without\n * tearing down the listeners, which would abort a drag in progress.\n *\n * `mapping` is fixed for the lifetime of the instance and is ignored here.\n */\n update: (options: Partial<DragValueOptions>) => void\n destroy: () => void\n}\n\n/**\n * Drive a value with a pointer drag.\n *\n * Combines {@link createDrag} with the scaling of `@tremolo-ui/functions`: the\n * mapping decides where the pointer sits on the 0-1 travel of each axis, and\n * the axis options turn that into a value.\n */\nexport function createDragValue(\n element: Element,\n options: DragValueOptions,\n): DragValueInstance {\n let opts = options\n let lastValue: XY<number> = [0, 0]\n\n const axes = () => toXY(opts.axis)\n\n function valueOf(position: XY<number>): XY<number> {\n return axes().map((axis, i) => {\n const p = axis.reverse ? 1 - position[i] : position[i]\n // A scale clamps the position, so a mapping may report outside 0-1.\n const scale = axis.scale ?? linearScale\n const value = scale.denormalize(p, axis.min, axis.max)\n const stepped = axis.step ? stepValue(value, axis.step) : value\n // Rounding to the step can leave the range.\n return clamp(stepped, axis.min, axis.max)\n }) as XY<number>\n }\n\n const context: MappingContext = {\n position: () => {\n const getValue = opts.getValue\n if (!getValue) {\n throw new Error(\n 'createDragValue: getValue is required by the given mapping',\n )\n }\n const value = getValue()\n return axes().map((axis, i) => {\n const scale = axis.scale ?? linearScale\n const n = scale.normalize(value[i], axis.min, axis.max)\n return axis.reverse ? 1 - n : n\n }) as XY<number>\n },\n }\n\n const drag = createDrag(element, {\n threshold: opts.threshold,\n cursor: opts.cursor,\n onDragStart: (state) => {\n const position = opts.mapping.start(state, context)\n if (position) {\n lastValue = valueOf(position)\n if (opts.updateOnPointerDown) opts.onChange?.(lastValue, state)\n }\n opts.onDragStart?.(lastValue, state)\n },\n onDrag: (state) => {\n const position = opts.mapping.move(state, context)\n if (!position) return\n lastValue = valueOf(position)\n opts.onChange?.(lastValue, state)\n },\n // The pointer has not moved since the last reported value, so `lastValue`\n // is where the drag ended.\n onDragEnd: (state) => opts.onDragEnd?.(lastValue, state),\n })\n\n return {\n update: (next) => {\n opts = { ...opts, ...next, mapping: opts.mapping }\n drag.update({ threshold: opts.threshold, cursor: opts.cursor })\n },\n destroy: () => drag.destroy(),\n }\n}\n","export interface WheelOptions {\n /**\n * Only report events while the focus is inside the element.\n *\n * A control that reacts to the wheel on hover alone takes the scroll away\n * from the page, so passing over one in a long form silently changes its\n * value. Requiring focus makes that an explicit act.\n *\n * The check is `contains`, not an identity test: the element that actually\n * takes focus is usually a descendant, such as a thumb or an `<input>`, and\n * a caller may have replaced it with markup of their own.\n *\n * @default false\n */\n requireFocus?: boolean\n}\n\nexport interface WheelInstance {\n /** Replace the given options, keeping the listener in place. */\n update: (options: WheelOptions) => void\n destroy: () => void\n}\n\n/**\n * Listen to wheel events on an element.\n *\n * The listener is registered with `passive: false` so that the handler can call\n * `preventDefault()` to stop the page from scrolling.\n */\nexport function createWheel(\n element: Element,\n onWheel: (event: WheelEvent) => void,\n options: WheelOptions = {},\n): WheelInstance {\n let opts = options\n\n function hasFocus() {\n const active = element.ownerDocument?.activeElement\n return !!active && element.contains(active)\n }\n\n const handler = (event: Event) => {\n if (opts.requireFocus && !hasFocus()) return\n onWheel(event as WheelEvent)\n }\n\n element.addEventListener('wheel', handler, { passive: false })\n\n return {\n update: (next) => {\n opts = { ...opts, ...next }\n },\n destroy: () => {\n // Only `capture` matters when removing, and it is false here.\n element.removeEventListener('wheel', handler)\n },\n }\n}\n"],"mappings":";;;;;;;;;AAOA,MAAa,eAAe;CAC1B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD;AAMD,SAAgB,eAAe,OAAuC;AAEpE,QAAO,OAAO,SAAS,YADU,aACQ,SAAS,MAAM;;;AAI1D,SAAgB,iBACd,SACgB;CAChB,MAAM,QAAQ,EAAE;AAChB,MAAK,MAAM,YAAY,aACnB,OAAM,YAAkC,QAAQ;AAEpD,QAAO;;;AAIT,SAAgB,kBACd,SACA,OACA;AACA,MAAK,MAAM,YAAY,aACnB,SAAQ,YAAkC,MAAM;;;;;;;;;;AAYtD,SAAgB,sBACd,QACA,SACA,OACA,QACA,KACA;AACA,QAAO,QAAQ,QAAQ;AACvB,QAAO,SAAS,SAAS;AAEzB,SAAQ,aAAa,GAAG,GAAG,GAAG,GAAG,GAAG,EAAE;AACtC,SAAQ,MAAM,KAAK,IAAI;AACvB,QAAO,MAAM,QAAQ,GAAG,MAAM;AAC9B,QAAO,MAAM,SAAS,GAAG,OAAO;;;;;;;;;;;;AC8BlC,SAAgB,sBACd,QACA,SACyB;CACzB,IAAI,OAAO;CAEX,MAAM,YAAY,OAAO,WAAW,MAAM,KAAK,kBAAkB;AACjE,KAAI,CAAC,UACH,OAAM,IAAI,MAAM,iDAAiD;CAEnE,MAAM,UAAoC;CAE1C,MAAM,eAAe,KAAK,gBAAgB;;CAG1C,IAAI,OAAiC;CACrC,IAAI,cAA+C;CAEnD,IAAI,QAAQ;CACZ,IAAI,SAAS;;CAEb,IAAI,QAAQ;CACZ,IAAI,cAAc;CAElB,IAAI,UAAyB;CAC7B,IAAI,QAAQ;CACZ,MAAM,YAAY,YAAY,KAAK;CACnC,IAAI,eAAe;CAEnB,SAAS,mBAAmB;AAC1B,SAAO,WAAW,oBAAoB;;;;;;;;CASxC,SAAS,eAAsC;AAC7C,MAAI,EAAE,KAAK,oBAAoB,MAAO,QAAO;AAC7C,MAAI,CAAC,MAAM;AACT,UAAO,WAAW,UAAU,cAAc,SAAS,IAAI;AACvD,iBAAc,MAAM,WAAW,MAAM,KAAK,kBAAkB,IAAI;;AAElE,MAAI,CAAC,QAAQ,CAAC,YAAa,QAAO;EAElC,MAAM,QAAQ,iBAAiB,QAAQ;AACvC,OAAK,QAAQ,OAAO;AACpB,OAAK,SAAS,OAAO;AACrB,MAAI,OAAO,QAAQ,KAAK,OAAO,SAAS,EACtC,aAAY,UAAU,QAAQ,GAAG,EAAE;AAErC,SAAO;;;;;;;;;;CAWT,SAAS,gBACP,OACA,eACA,gBACA;AACA,MAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,YAAa;AACrC,MAAI,KAAK,SAAS,KAAK,KAAK,UAAU,EAAG;AACzC,oBAAkB,SAAS,MAAM;AACjC,UAAQ,UAAU,MAAM,GAAG,GAAG,eAAe,eAAe;;CAG9D,SAAS,UAAU,GAAW,GAAW;EACvC,MAAM,MAAM,kBAAkB;EAC9B,MAAM,gBAAgB;EACtB,MAAM,iBAAiB;EACvB,MAAM,QAAQ,cAAc;AAC5B,wBAAsB,QAAQ,SAAS,GAAG,GAAG,IAAI;AACjD,UAAQ;AACR,WAAS;AACT,UAAQ;AACR,kBAAgB,OAAO,eAAe,eAAe;;CAGvD,SAAS,YAAY;AACnB,MAAI,CAAC,MAAO;AACZ,MAAI,CAAC,aAAa;AAChB,iBAAc;AACd,QAAK,OAAO,SAAS;IAAE;IAAO;IAAQ,CAAC;;EAEzC,MAAM,MAAM,YAAY,KAAK;EAC7B,MAAM,YAAY,MAAM;AACxB,iBAAe;AACf,WAAS;AACT,OAAK,KAAK,SAAS;GACjB;GACA;GACA;GACA;GACA,aAAa,MAAM;GACnB,KAAK,MAAO;GACb,CAAC;;CAGJ,SAAS,OAAO;AAGd,YAAU,sBAAsB,KAAK;AACrC,aAAW;;CAGb,SAAS,YAAY;AACnB,MAAI,WAAW,KAAM;AACrB,iBAAe,YAAY,KAAK;AAChC,YAAU,sBAAsB,KAAK;;CAGvC,SAAS,WAAW;AAClB,MAAI,WAAW,KAAM;AACrB,uBAAqB,QAAQ;AAC7B,YAAU;;CAGZ,IAAI,WAAkC;AAEtC,KAAI,cAAc;EAChB,MAAM,SAAS,OAAO;AACtB,MAAI,CAAC,OACH,OAAM,IAAI,MACR,gFACD;AAMH,aAAW,IAAI,gBAAgB,YAAY;AACzC,QAAK,MAAM,SAAS,SAAS;IAC3B,MAAM,EAAE,OAAO,GAAG,QAAQ,MAAM,MAAM;AACtC,cAAU,GAAG,EAAE;AAEf,QAAI,EAAE,KAAK,WAAW,MAAO,YAAW;;IAE1C;AACF,WAAS,QAAQ,OAAO;QACnB;EACL,MAAM,EAAE,OAAO,IAAI,KAAK,QAAQ,IAAI,QAAQ,KAAK,QAAQ,EAAE;AAC3D,YAAU,GAAG,EAAE;;AAGjB,KAAI,KAAK,WAAW,KAClB,YAAW;UACF,MACT,YAAW;AAGb,QAAO;EACL,SAAS,SAAS;GAChB,MAAM,eAAe,KAAK,WAAW;AACrC,UAAO;IACL,GAAG;IACH,GAAG;IACH;IACA,mBAAmB,KAAK;IACzB;AAED,OAAI,CAAC,cAAc;IACjB,MAAM,EAAE,OAAO,IAAI,KAAK,QAAQ,IAAI,QAAQ,KAAK,QAAQ,EAAE;AAC3D,QAAI,KAAK,SAAS,KAAK,OAAQ,WAAU,GAAG,EAAE;;GAGhD,MAAM,YAAY,KAAK,WAAW;AAClC,OAAI,aAAa,CAAC,aAAc,YAAW;AAC3C,OAAI,CAAC,aAAa,aAAc,WAAU;AAO1C,OAAI,CAAC,UAAW,YAAW;;EAE7B,QAAQ;EACR,eAAe;AACb,aAAU;AACV,aAAU,YAAY;AACtB,cAAW;AACX,UAAO;AACP,iBAAc;;EAEjB;;;;;AC7SH,MAAa,oBAAoB;;AAEjC,MAAa,gBAAgB;AAU7B,MAAM,gBAAiC;CACrC,YAAY;CACZ,OAAO;CACR;;;;;;;AAmBD,SAAgB,mBAAuC;CACrD,IAAI,QAAQ;CACZ,IAAI,YAAY;CAChB,MAAM,4BAAY,IAAI,KAAiB;CAEvC,SAAS,SAAS,MAAuB;AACvC,UAAQ;AACR,OAAK,MAAM,YAAY,UAAW,WAAU;;CAG9C,SAAS,UAAU;AACjB,MAAI,UAAW;AACf,MAAI,OAAO,cAAc,eAAe,CAAC,UAAU,mBAAmB;AACpE,YAAS;IAAE,GAAG;IAAO,OAAO;IAAe,CAAC;AAC5C;;AAEF,YACG,mBAAmB,CACnB,MAAM,WAAW;AAChB,OAAI,UAAW;AACf,YAAS;IAAE,YAAY;IAAQ,OAAO;IAAM,CAAC;IAC7C,CACD,YAAY;AACX,OAAI,UAAW;AACf,YAAS;IAAE,GAAG;IAAO,OAAO;IAAmB,CAAC;IAChD;;AAGN,QAAO;EACL;EACA,gBAAgB;EAChB,sBAAsB;EACtB,YAAY,aAAa;AACvB,aAAU,IAAI,SAAS;AACvB,gBAAa;AACX,cAAU,OAAO,SAAS;;;EAG9B,eAAe;AACb,eAAY;AACZ,aAAU,OAAO;;EAEpB;;;;;;;;;ACpEH,SAAgB,kBACd,YACA,eACqB;AACrB,KAAI,CAAC,WACH,QAAO,EAAE,eAAe,IAAI;CAG9B,MAAM,SAAS,CAAC,GAAG,WAAW,OAAO,QAAQ,CAAC;AAC9C,MAAK,MAAM,SAAS,OAClB,OAAM,iBAAiB,eAAe,cAAc;AAGtD,QAAO,EACL,eAAe;AACb,OAAK,MAAM,SAAS,OAClB,OAAM,oBAAoB,eAAe,cAAc;IAG5D;;;;AC1BH,MAAM,uBAAuB;CAC3B,SAAS;CACT,UAAU;CACV,YAAY;CACb;;;;;AAcD,SAAgB,gBACd,YACA,EAAE,eAAe,gBAAgB,oBACd;AACnB,QAAO,kBAAkB,aAAa,UAAU;AAC9C,MAAI,CAAC,MAAM,KAAM;EAIjB,MAAM,OAAO,MAAM,KAAK,KAAK;AAE7B,MACE,QAAQ,qBAAqB,YAC5B,QAAQ,qBAAqB,WAAW,MAAM,KAAK,MAAM,EAE1D,kBAAiB,MAAM,KAAK,GAAG;WACtB,QAAQ,qBAAqB,QACtC,iBAAgB,MAAM,KAAK,IAAI,MAAM,KAAK,GAAG;WACpC,QAAQ,qBAAqB,WACtC,oBAAmB,MAAM,KAAK,IAAI,MAAM,KAAK,GAAG;GAElD;;;;;;;;;;;ACmCJ,MAAM,iBAAiB;CACrB,CAAC,gBAAgB,OAAO;CACxB,CAAC,eAAe,OAAO;CACvB,CAAC,uBAAuB,OAAO;CAC/B,CAAC,yBAAyB,OAAO;CAClC;;;;;;;;;;;;AA6BD,SAAgB,WACd,SACA,UAAuB,EAAE,EACX;CACd,IAAI,OAAO;CACX,MAAM,eAAe,QAAQ,gBAAgB;CAC7C,MAAM,UAAU;CAChB,MAAM,QAAS,QAAiC;CAEhD,MAAM,iCAAiB,IAAI,KAAqB;AAChD,KAAI,MACF,MAAK,MAAM,CAAC,UAAU,UAAU,gBAAgB;AAC9C,iBAAe,IAAI,UAAU,MAAM,iBAAiB,SAAS,CAAC;AAC9D,QAAM,YAAY,UAAU,MAAM;;CAItC,MAAM,2BAAW,IAAI,KAA2B;;;;;;CAMhD,MAAM,0BAAU,IAAI,KAA0B;CAC9C,IAAI;CAEJ,SAAS,MACP,OACA,SACA,QACA,QACW;AACX,SAAO;GACL,GAAG,MAAM,UAAU,QAAQ;GAC3B,GAAG,MAAM,UAAU,QAAQ;GAC3B;GACA;GACA,SAAS,MAAM;GACf,SAAS,MAAM;GACf,WAAW,MAAM;GACjB;GACD;;;;;;;CAQH,SAAS,mBAAmB,OAAc;AACxC,QAAM,gBAAgB;;CAGxB,SAAS,aAAa,QAAqB;EACzC,MAAM,QAAQ,QAAQ,IAAI,OAAO,IAAI;AACrC,MAAI,UAAU,GAAG;AACf,UAAO,iBAAiB,eAAe,kBAAkB;AACzD,UAAO,iBAAiB,aAAa,gBAAgB;AACrD,UAAO,iBAAiB,iBAAiB,gBAAgB;;AAE3D,UAAQ,IAAI,QAAQ,QAAQ,EAAE;;CAGhC,SAAS,cAAc,QAAqB;EAC1C,MAAM,QAAQ,QAAQ,IAAI,OAAO,IAAI;AACrC,MAAI,QAAQ,GAAG;AACb,WAAQ,IAAI,QAAQ,QAAQ,EAAE;AAC9B;;AAEF,SAAO,oBAAoB,eAAe,kBAAkB;AAC5D,SAAO,oBAAoB,aAAa,gBAAgB;AACxD,SAAO,oBAAoB,iBAAiB,gBAAgB;AAC5D,UAAQ,OAAO,OAAO;;CAGxB,SAAS,kBAAkB,OAAc;EACvC,MAAM,eAAe;EACrB,MAAM,YAAY,aAAa;AAE/B,MAAI,SAAS,IAAI,UAAU,CAAE;AAC7B,MAAI,CAAC,gBAAgB,SAAS,OAAO,EAAG;EAExC,MAAM,UAAU,SAAS,SAAS;AAClC,MAAI,WAAW,KAAK,UAAU,OAAO;AACnC,oBAAiB,MAAM;AACvB,SAAM,SAAS,KAAK;;AAGtB,UAAQ,oBAAoB,UAAU;EAGtC,MAAM,aACJ,QAAQ,oBAAoB,UAAU,KAAK,OACvC,UACC,WAAW,UAAU;EAE5B,MAAM,UAAwB;GAC5B;GACA,QAAQ,aAAa;GACrB,QAAQ,aAAa;GACrB,OAAO,aAAa;GACpB,OAAO,aAAa;GACrB;AACD,WAAS,IAAI,WAAW,QAAQ;AAEhC,eAAa,WAAW;AACxB,MAAI,QACF,YAAW,UAAU,iBAAiB,eAAe,mBAAmB;AAG1E,OAAK,cAAc,MAAM,cAAc,SAAS,GAAG,EAAE,CAAC;;CAGxD,SAAS,kBAAkB,OAAc;EACvC,MAAM,eAAe;EACrB,MAAM,UAAU,SAAS,IAAI,aAAa,UAAU;AACpD,MAAI,CAAC,QAAS;EAEd,MAAM,SAAS,aAAa,UAAU,QAAQ;EAC9C,MAAM,SAAS,aAAa,UAAU,QAAQ;EAK9C,MAAM,YAAY,KAAK,aAAa;AACpC,MAAI,KAAK,IAAI,OAAO,GAAG,aAAa,KAAK,IAAI,OAAO,GAAG,UAAW;AAElE,UAAQ,QAAQ,aAAa;AAC7B,UAAQ,QAAQ,aAAa;AAE7B,OAAK,SAAS,MAAM,cAAc,SAAS,QAAQ,OAAO,CAAC;;CAG7D,SAAS,gBAAgB,OAAc;EACrC,MAAM,eAAe;EACrB,MAAM,UAAU,SAAS,IAAI,aAAa,UAAU;AACpD,MAAI,CAAC,QAAS;EAId,MAAM,aAAa,MAAM,cAAc,SAFxB,aAAa,UAAU,QAAQ,OAC/B,aAAa,UAAU,QAAQ,MACiB;AAE/D,eAAa,aAAa,UAAU;AACpC,OAAK,YAAY,WAAW;;CAG9B,SAAS,aAAa,WAAmB;EACvC,MAAM,UAAU,SAAS,IAAI,UAAU;AACvC,MAAI,CAAC,QAAS;AAEd,UAAQ,wBAAwB,UAAU;AAC1C,gBAAc,QAAQ,WAAW;AACjC,WAAS,OAAO,UAAU;AAE1B,MAAI,SAAS,OAAO,EAAG;AAEvB,aAAW,UAAU,oBAAoB,eAAe,mBAAmB;AAC3E,MAAI,mBAAmB,KAAA,KAAa,OAAO;AACzC,SAAM,SAAS;AACf,oBAAiB,KAAA;;;AAIrB,SAAQ,iBAAiB,eAAe,kBAAkB;AAE1D,QAAO;EACL,SAAS,SAAS;AAChB,UAAO;IAAE,GAAG;IAAM,GAAG;IAAM;IAAc;;EAE3C,eAAe;AACb,QAAK,MAAM,aAAa,CAAC,GAAG,SAAS,MAAM,CAAC,CAAE,cAAa,UAAU;AACrE,WAAQ,oBAAoB,eAAe,kBAAkB;AAC7D,OAAI,MACF,MAAK,MAAM,CAAC,aAAa,gBAAgB;IACvC,MAAM,WAAW,eAAe,IAAI,SAAS;AAC7C,QAAI,SACF,OAAM,YAAY,UAAU,SAAS;QAErC,OAAM,eAAe,SAAS;;;EAKvC;;;;AClOH,MAAM,iBAA6B;;;;;;;;;;;AAYnC,SAAgB,iBACd,SACA,SACoB;CACpB,IAAI,OAAO;;CAGX,MAAM,uBAAO,IAAI,KAA8B;;CAE/C,MAAM,+BAAe,IAAI,KAAqB;CAE9C,SAAS,cAAwB;AAC/B,SAAO,CAAC,GAAG,KAAK,MAAM,CAAC,CAAC,MAAM,GAAG,MAAM,IAAI,EAAE;;CAG/C,SAAS,OACP,MACA,EACE,SAAS,gBACT,aAC8C,EAAE,EAClD;AACA,MAAI,QAAQ,KAAK,WAAW,KAAM;EAElC,MAAM,UAAU,KAAK,IAAI,KAAK;AAC9B,MAAI,SAAS;AAEX,WAAQ,IAAI,OAAO;AACnB;;AAGF,OAAK,IAAI,MAAM,IAAI,IAAI,CAAC,OAAO,CAAC,CAAC;AACjC,OAAK,aAAa,MAAM,SAAS;AACjC,OAAK,sBAAsB,aAAa,CAAC;;CAG3C,SAAS,QACP,MACA,EAAE,SAAS,mBAA4C,EAAE,EACzD;EACA,MAAM,UAAU,KAAK,IAAI,KAAK;AAC9B,MAAI,CAAC,QAAS;AAEd,UAAQ,OAAO,OAAO;AACtB,MAAI,QAAQ,OAAO,EAAG;AAEtB,OAAK,OAAO,KAAK;AACjB,OAAK,aAAa,KAAK;AACvB,OAAK,sBAAsB,aAAa,CAAC;;;CAI3C,SAAS,UAAU,SAAiB,SAAgC;EAClE,MAAM,EAAE,MAAM,KAAK,WAAW,QAAQ,uBAAuB;AAC7D,SAAO,OAAO,UAAU,MAAM,UAAU,KAAK,QAAQ,KAAK,OAAO;;;CAInE,SAAS,YAAY,WAAmB,MAAqB;EAC3D,MAAM,SAAS,WAAW;EAC1B,MAAM,WAAW,aAAa,IAAI,UAAU;AAC5C,MAAI,aAAa,KAAM;AAEvB,MAAI,aAAa,KAAA,EAAW,SAAQ,UAAU,EAAE,QAAQ,CAAC;AAEzD,MAAI,SAAS,KACX,cAAa,OAAO,UAAU;OACzB;AACL,gBAAa,IAAI,WAAW,KAAK;AACjC,UAAO,MAAM,EAAE,QAAQ,CAAC;;;CAI5B,MAAM,OAAO,WAAW,SAAS;EAC/B,cAAc;EACd,cAAc,UACZ,YAAY,MAAM,WAAW,UAAU,MAAM,SAAS,MAAM,QAAQ,CAAC;EACvE,SAAS,UAAU;AAGjB,OAAI,KAAK,cAAc,MAAO;AAC9B,eAAY,MAAM,WAAW,UAAU,MAAM,SAAS,MAAM,QAAQ,CAAC;;EAEvE,YAAY,UAAU,YAAY,MAAM,WAAW,KAAK;EACzD,CAAC;AAEF,QAAO;EACL,SAAS,SAAS;AAChB,UAAO;IAAE,GAAG;IAAM,GAAG;IAAM;;EAE7B;EACA;EACA;EACA,eAAe;AACb,QAAK,SAAS;AAGd,QAAK,MAAM,QAAQ,aAAa,EAAE;AAChC,SAAK,OAAO,KAAK;AACjB,SAAK,aAAa,KAAK;;AAEzB,gBAAa,OAAO;AACpB,QAAK,sBAAsB,EAAE,CAAC;;EAEjC;;;;ACpKH,SAAS,OACP,OACgC;AAChC,QAAO,MAAM,QAAQ,MAAM;;;;;;;;;;;;AAa7B,SAAgB,KAAQ,OAAyC;AAC/D,QAAO,OAAO,MAAM,GAAG,CAAC,MAAM,IAAI,MAAM,GAAG,GAAG,CAAC,OAAO,MAAM;;;;;;;;;;;ACuB9D,SAAgB,eACd,YACkB;CAClB,SAAS,WAAW,OAAqC;EACvD,MAAM,UAAU,YAAY;AAC5B,MAAI,CAAC,QAAS,QAAO;EACrB,MAAM,EAAE,MAAM,KAAK,OAAO,WAAW,QAAQ,uBAAuB;AAGpE,SAAO,CACL,QAAQ,OAAO,eAAe,MAAM,SAAS,MAAM,MAAM,GAAG,GAC5D,SAAS,MAAM,eAAe,MAAM,SAAS,KAAK,OAAO,GAAG,EAC7D;;AAGH,QAAO;EAAE,OAAO;EAAY,MAAM;EAAY;;;;;;;AAQhD,SAAgB,gBAAgB,EAC9B,aAAa,QAOX,EAAE,EAAoB;CACxB,MAAM,CAAC,QAAQ,UAAU,KAAK,WAAW;CACzC,IAAI,SAAqB,CAAC,GAAG,EAAE;AAE/B,QAAO;EACL,QAAQ,QAAQ,YAAY;AAC1B,YAAS,QAAQ,UAAU;AAC3B,UAAO;;EAIT,OAAO,UAAU,CACf,OAAO,KAAK,MAAM,IAAI,QACtB,OAAO,KAAK,MAAM,IAAI,OACvB;EACF;;;;;;;;;AAwDH,SAAgB,gBACd,SACA,SACmB;CACnB,IAAI,OAAO;CACX,IAAI,YAAwB,CAAC,GAAG,EAAE;CAElC,MAAM,aAAa,KAAK,KAAK,KAAK;CAElC,SAAS,QAAQ,UAAkC;AACjD,SAAO,MAAM,CAAC,KAAK,MAAM,MAAM;GAC7B,MAAM,IAAI,KAAK,UAAU,IAAI,SAAS,KAAK,SAAS;GAGpD,MAAM,SADQ,KAAK,SAAS,aACR,YAAY,GAAG,KAAK,KAAK,KAAK,IAAI;AAGtD,UAAO,MAFS,KAAK,OAAO,UAAU,OAAO,KAAK,KAAK,GAAG,OAEpC,KAAK,KAAK,KAAK,IAAI;IACzC;;CAGJ,MAAM,UAA0B,EAC9B,gBAAgB;EACd,MAAM,WAAW,KAAK;AACtB,MAAI,CAAC,SACH,OAAM,IAAI,MACR,6DACD;EAEH,MAAM,QAAQ,UAAU;AACxB,SAAO,MAAM,CAAC,KAAK,MAAM,MAAM;GAE7B,MAAM,KADQ,KAAK,SAAS,aACZ,UAAU,MAAM,IAAI,KAAK,KAAK,KAAK,IAAI;AACvD,UAAO,KAAK,UAAU,IAAI,IAAI;IAC9B;IAEL;CAED,MAAM,OAAO,WAAW,SAAS;EAC/B,WAAW,KAAK;EAChB,QAAQ,KAAK;EACb,cAAc,UAAU;GACtB,MAAM,WAAW,KAAK,QAAQ,MAAM,OAAO,QAAQ;AACnD,OAAI,UAAU;AACZ,gBAAY,QAAQ,SAAS;AAC7B,QAAI,KAAK,oBAAqB,MAAK,WAAW,WAAW,MAAM;;AAEjE,QAAK,cAAc,WAAW,MAAM;;EAEtC,SAAS,UAAU;GACjB,MAAM,WAAW,KAAK,QAAQ,KAAK,OAAO,QAAQ;AAClD,OAAI,CAAC,SAAU;AACf,eAAY,QAAQ,SAAS;AAC7B,QAAK,WAAW,WAAW,MAAM;;EAInC,YAAY,UAAU,KAAK,YAAY,WAAW,MAAM;EACzD,CAAC;AAEF,QAAO;EACL,SAAS,SAAS;AAChB,UAAO;IAAE,GAAG;IAAM,GAAG;IAAM,SAAS,KAAK;IAAS;AAClD,QAAK,OAAO;IAAE,WAAW,KAAK;IAAW,QAAQ,KAAK;IAAQ,CAAC;;EAEjE,eAAe,KAAK,SAAS;EAC9B;;;;;;;;;;ACtMH,SAAgB,YACd,SACA,SACA,UAAwB,EAAE,EACX;CACf,IAAI,OAAO;CAEX,SAAS,WAAW;EAClB,MAAM,SAAS,QAAQ,eAAe;AACtC,SAAO,CAAC,CAAC,UAAU,QAAQ,SAAS,OAAO;;CAG7C,MAAM,WAAW,UAAiB;AAChC,MAAI,KAAK,gBAAgB,CAAC,UAAU,CAAE;AACtC,UAAQ,MAAoB;;AAG9B,SAAQ,iBAAiB,SAAS,SAAS,EAAE,SAAS,OAAO,CAAC;AAE9D,QAAO;EACL,SAAS,SAAS;AAChB,UAAO;IAAE,GAAG;IAAM,GAAG;IAAM;;EAE7B,eAAe;AAEb,WAAQ,oBAAoB,SAAS,QAAQ;;EAEhD"}
1
+ {"version":3,"file":"index.js","names":["names","pointerEvent"],"sources":["../src/canvas/context.ts","../src/canvas/animation.ts","../src/midi/access.ts","../src/midi/message.ts","../src/midi/input.ts","../src/pointer/drag.ts","../src/piano/index.ts","../src/xy.ts","../src/pointer/drag-value.ts","../src/pointer/wheel.ts","../src/selection/box.ts"],"sourcesContent":["/**\n * The assignable parts of a 2D context's drawing state. They have to be\n * carried across a resize by hand because setting `canvas.width` resets the\n * context to its defaults. The transform and line dash are handled separately\n * by {@link DrawingContext} because they are exposed through methods.\n *\n * @see https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/save\n */\nexport const drawingState = [\n 'strokeStyle',\n 'fillStyle',\n 'globalAlpha',\n 'lineWidth',\n 'lineCap',\n 'lineJoin',\n 'miterLimit',\n 'lineDashOffset',\n 'shadowOffsetX',\n 'shadowOffsetY',\n 'shadowBlur',\n 'shadowColor',\n 'globalCompositeOperation',\n 'filter',\n 'font',\n 'fontKerning',\n 'fontStretch',\n 'fontVariantCaps',\n 'textAlign',\n 'textBaseline',\n 'direction',\n 'letterSpacing',\n 'textRendering',\n 'wordSpacing',\n 'imageSmoothingEnabled',\n 'imageSmoothingQuality',\n] as const\n\nexport type DrawingState = (typeof drawingState)[number]\nexport type DrawingStateValue = CanvasRenderingContext2D[DrawingState]\nexport type DrawingContext = Pick<CanvasRenderingContext2D, DrawingState> & {\n /** The current line dash sequence. */\n lineDash: number[]\n /** The current transformation matrix. */\n transform: DOMMatrix\n}\n\nexport function isDrawingState(value: unknown): value is DrawingState {\n const names: readonly string[] = drawingState\n return typeof value === 'string' && names.includes(value)\n}\n\n/** Copy the drawing state off a context, to put back after a resize. */\nexport function readDrawingState(\n context: CanvasRenderingContext2D,\n): DrawingContext {\n const state = {} as DrawingContext\n for (const property of drawingState) {\n ;(state[property] as DrawingStateValue) = context[property]\n }\n state.lineDash = context.getLineDash()\n state.transform = context.getTransform()\n return state\n}\n\n/** Put a state read by {@link readDrawingState} back onto a context. */\nexport function writeDrawingState(\n context: CanvasRenderingContext2D,\n state: DrawingContext,\n transformScale = 1,\n) {\n for (const property of drawingState) {\n ;(context[property] as DrawingStateValue) = state[property]\n }\n context.setLineDash(state.lineDash)\n const { a, b, c, d, e, f } = state.transform\n context.setTransform(\n a * transformScale,\n b * transformScale,\n c * transformScale,\n d * transformScale,\n e * transformScale,\n f * transformScale,\n )\n}\n\n/**\n * Give the canvas a backing store of `width * dpr` by `height * dpr` device\n * pixels while keeping it `width` by `height` in CSS pixels, and scale the\n * context so that drawing code can work in CSS pixels throughout.\n *\n * Assigning to `canvas.width` clears the canvas and resets the context, so\n * this both resizes and re-establishes the transform.\n */\nexport function applyDevicePixelRatio(\n canvas: HTMLCanvasElement,\n context: CanvasRenderingContext2D,\n width: number,\n height: number,\n dpr: number,\n) {\n canvas.width = width * dpr\n canvas.height = height * dpr\n // Reset the current transformation matrix to the identity matrix\n context.setTransform(1, 0, 0, 1, 0, 0)\n context.scale(dpr, dpr)\n canvas.style.width = `${width}px`\n canvas.style.height = `${height}px`\n}\n","import {\n applyDevicePixelRatio,\n readDrawingState,\n writeDrawingState,\n type DrawingContext,\n} from './context'\n\n/** What the canvas looked like when a frame was drawn. */\nexport interface AnimationFrame {\n /** Width of the canvas in CSS pixels. */\n width: number\n /** Height of the canvas in CSS pixels. */\n height: number\n /** Frames drawn so far, starting at 0. */\n count: number\n /** Milliseconds since the previous frame. */\n deltaTime: number\n /** Milliseconds since the instance was created. */\n elapsedTime: number\n /** Frames per second implied by `deltaTime`, or 0 when no time elapsed. */\n fps: number\n}\n\nexport type CanvasDrawFunction = (\n context: CanvasRenderingContext2D,\n frame: AnimationFrame,\n) => void\n\nexport type CanvasInitFunction = (\n context: CanvasRenderingContext2D,\n size: { width: number; height: number },\n) => void\n\nexport interface AnimationCanvasOptions {\n /** Called for every frame. */\n draw: CanvasDrawFunction\n\n /** Called once, after the first size is known and before the first frame. */\n init?: CanvasInitFunction\n\n /**\n * Keep drawing on every animation frame. When off, a frame is drawn only on\n * a resize or an explicit {@link AnimationCanvasInstance.redraw}.\n *\n * @default true\n */\n animate?: boolean\n\n /**\n * Size in CSS pixels. Ignored when `resizable` is on.\n *\n * @default { width: 100, height: 100 }\n */\n size?: { width: number; height: number }\n\n /**\n * Follow the size of the canvas's parent element instead of `size`.\n *\n * Fixed for the lifetime of the instance: it decides whether a\n * `ResizeObserver` is attached.\n *\n * @default false\n */\n resizable?: boolean\n\n /**\n * Carry the drawing across a resize, so that the canvas does not blank for a\n * frame while the new size is drawn.\n *\n * @default true\n */\n reduceFlickering?: boolean\n\n /**\n * Passed to `getContext('2d', …)`. Read once when the context is created, so\n * it is fixed for the lifetime of the instance.\n *\n * @see https://developer.mozilla.org/docs/Web/API/HTMLCanvasElement/getContext#contextattributes\n */\n contextAttributes?: CanvasRenderingContext2DSettings\n}\n\nexport interface AnimationCanvasInstance {\n /**\n * Replace the given options. Lets a wrapper feed a fresh `draw` in on every\n * render without restarting the animation, so the frame count and the\n * elapsed time keep running.\n *\n * `resizable` and `contextAttributes` are fixed for the lifetime of the\n * instance and are ignored here.\n *\n * While `animate` is off this also draws a frame, since nothing else would.\n */\n update: (options: Partial<AnimationCanvasOptions>) => void\n\n /** Draw one frame now, whether or not `animate` is on. */\n redraw: () => void\n\n destroy: () => void\n}\n\n/**\n * Drive a canvas from `requestAnimationFrame`, keeping its backing store in\n * step with the device pixel ratio and, optionally, with the size of its\n * parent.\n *\n * Drawing code works in CSS pixels: the context is scaled by the device pixel\n * ratio, and `width` / `height` of each {@link AnimationFrame} are CSS pixels.\n */\nexport function createAnimationCanvas(\n canvas: HTMLCanvasElement,\n options: AnimationCanvasOptions,\n): AnimationCanvasInstance {\n let opts = options\n\n const context2d = canvas.getContext('2d', opts.contextAttributes)\n if (!context2d) {\n throw new Error('createAnimationCanvas: cannot get a 2d context')\n }\n const context: CanvasRenderingContext2D = context2d\n\n const resizable = opts.resizable ?? false\n\n /** Off-document canvas holding the drawing while the real one is resized. */\n let memo: HTMLCanvasElement | null = null\n let memoContext: CanvasRenderingContext2D | null = null\n\n let width = 0\n let height = 0\n let appliedDpr = 0\n /** Whether a size has been applied, so a frame can be drawn. */\n let sized = false\n let initialized = false\n\n let frameId: number | null = null\n let count = -1\n const startTime = performance.now()\n let previousTime = startTime\n\n function devicePixelRatio() {\n return globalThis.devicePixelRatio || 1\n }\n\n /**\n * Copy the canvas onto the memo canvas at its full device resolution, 1:1.\n *\n * Assigning to `memo.width` resets the memo's transform to the identity, so\n * the copy neither scales nor resamples.\n */\n function takeSnapshot() {\n if (!(opts.reduceFlickering ?? true)) return false\n if (!memo) {\n memo = globalThis.document?.createElement('canvas') ?? null\n memoContext = memo?.getContext('2d', opts.contextAttributes) ?? null\n }\n if (!memo || !memoContext) return false\n\n memo.width = canvas.width\n memo.height = canvas.height\n if (canvas.width > 0 && canvas.height > 0) {\n memoContext.drawImage(canvas, 0, 0)\n }\n return true\n }\n\n /**\n * Put the snapshot back, given the size in CSS pixels it was taken at.\n *\n * The context is scaled to CSS pixels, so drawing the snapshot at its old\n * CSS size leaves it exactly where it was. While the device pixel ratio\n * stays put that is a 1:1 copy of device pixels, and when it changes the\n * snapshot is rescaled once, from its full resolution.\n */\n function restoreSnapshot(\n hasSnapshot: boolean,\n previousWidth: number,\n previousHeight: number,\n ) {\n if (!hasSnapshot || !memo) return\n if (memo.width <= 0 || memo.height <= 0) return\n context.drawImage(memo, 0, 0, previousWidth, previousHeight)\n }\n\n function applySize(w: number, h: number) {\n const dpr = devicePixelRatio()\n const previousWidth = width\n const previousHeight = height\n const previousDpr = appliedDpr\n const state: DrawingContext | null = sized\n ? readDrawingState(context)\n : null\n const hasSnapshot = sized && takeSnapshot()\n applyDevicePixelRatio(canvas, context, w, h, dpr)\n width = w\n height = h\n appliedDpr = dpr\n sized = true\n // Restore pixels while the reset context still has neutral alpha,\n // compositing, filter, and shadow settings.\n restoreSnapshot(hasSnapshot, previousWidth, previousHeight)\n if (state) {\n writeDrawingState(context, state, dpr / previousDpr)\n }\n }\n\n function applyDevicePixelRatioIfNeeded() {\n if (sized && devicePixelRatio() !== appliedDpr) {\n applySize(width, height)\n }\n }\n\n function drawFrame(timestamp = performance.now()) {\n if (!sized) return\n applyDevicePixelRatioIfNeeded()\n if (!initialized) {\n initialized = true\n opts.init?.(context, { width, height })\n }\n const deltaTime = timestamp - previousTime\n previousTime = timestamp\n count += 1\n opts.draw(context, {\n width,\n height,\n count,\n deltaTime,\n elapsedTime: timestamp - startTime,\n fps: deltaTime > 0 ? 1000 / deltaTime : 0,\n })\n }\n\n function tick(timestamp: DOMHighResTimeStamp) {\n // Scheduled before drawing so that a slow frame does not delay the next\n // request, matching how requestAnimationFrame loops are usually written.\n frameId = requestAnimationFrame(tick)\n drawFrame(timestamp)\n }\n\n function startLoop() {\n if (frameId !== null) return\n previousTime = performance.now()\n frameId = requestAnimationFrame(tick)\n }\n\n function stopLoop() {\n if (frameId === null) return\n cancelAnimationFrame(frameId)\n frameId = null\n }\n\n let observer: ResizeObserver | null = null\n\n if (resizable) {\n const parent = canvas.parentElement\n if (!parent) {\n throw new Error(\n 'createAnimationCanvas: resizable needs the canvas to have a parent element',\n )\n }\n // The observer reports the current size as soon as it starts, so it is the\n // single source of the size — including the first one. Reading the parent\n // here instead would mix the padding box with the content box the observer\n // reports.\n observer = new ResizeObserver((entries) => {\n for (const entry of entries) {\n const { width: w, height: h } = entry.contentRect\n if (w !== width || h !== height || devicePixelRatio() !== appliedDpr) {\n applySize(w, h)\n }\n // With the loop running the next frame covers the new size already.\n if (!(opts.animate ?? true)) drawFrame()\n }\n })\n observer.observe(parent)\n } else {\n const { width: w = 100, height: h = 100 } = opts.size ?? {}\n applySize(w, h)\n }\n\n if (opts.animate ?? true) {\n startLoop()\n } else if (sized) {\n drawFrame()\n }\n\n return {\n update: (next) => {\n const wasAnimating = opts.animate ?? true\n opts = {\n ...opts,\n ...next,\n resizable,\n contextAttributes: opts.contextAttributes,\n }\n\n if (!resizable) {\n const { width: w = 100, height: h = 100 } = opts.size ?? {}\n if (w !== width || h !== height) applySize(w, h)\n }\n applyDevicePixelRatioIfNeeded()\n\n const animating = opts.animate ?? true\n if (animating && !wasAnimating) startLoop()\n if (!animating && wasAnimating) stopLoop()\n\n // Without a loop running, a resize and this call are the only things\n // that can put anything new on the canvas — so a new `draw` has to be\n // painted here. That is what makes a canvas driven by state rather than\n // by time work: the wrapper re-renders, pushes the new handler in, and\n // the frame it draws reflects it.\n if (!animating) drawFrame()\n },\n redraw: drawFrame,\n destroy: () => {\n stopLoop()\n observer?.disconnect()\n observer = null\n memo = null\n memoContext = null\n },\n }\n}\n","/** @private */\nexport const PERMISSION_DENIED = 'PERMISSION_DENIED'\n/** @private */\nexport const NOT_SUPPORTED = 'NOT_SUPPORTED'\n/** @private */\nexport const UNAVAILABLE = 'UNAVAILABLE'\n\n/** @private */\nexport type MIDIAccessError =\n | typeof PERMISSION_DENIED\n | typeof NOT_SUPPORTED\n | typeof UNAVAILABLE\n\nexport type MIDIAccessOptions = {\n /**\n * Ask for system exclusive messages as well.\n *\n * Browsers treat this as a separate, more sensitive permission, so leave it\n * off unless the app actually reads or sends sysex.\n *\n * @default false\n */\n sysex?: boolean\n}\n\nexport type MIDIAccessState = {\n readonly midiAccess: MIDIAccess | null\n readonly error: MIDIAccessError | null\n /**\n * The inputs currently connected, in the order MIDIAccess lists them.\n *\n * Kept up to date as devices are plugged in and unplugged, so a UI listing\n * the devices does not have to watch `statechange` itself.\n */\n readonly inputs: readonly MIDIInput[]\n}\n\nconst INITIAL_STATE: MIDIAccessState = {\n midiAccess: null,\n error: null,\n inputs: [],\n}\n\nexport interface MIDIAccessInstance {\n /** Request MIDI access. Safe to call more than once. */\n request: (options?: MIDIAccessOptions) => void\n getState: () => MIDIAccessState\n /** Snapshot for server side rendering. Always the initial state. */\n getServerState: () => MIDIAccessState\n /** @returns unsubscribe function */\n subscribe: (listener: () => void) => () => void\n destroy: () => void\n}\n\n/**\n * Which of our errors a rejected `requestMIDIAccess()` amounts to.\n *\n * The spec names the reasons, and they need telling apart: a user who said no\n * can say yes on a second ask, while a browser without the API never will.\n */\nfunction toError(reason: unknown): MIDIAccessError {\n const name =\n typeof reason === 'object' && reason !== null && 'name' in reason\n ? String((reason as { name: unknown }).name)\n : ''\n\n if (name === 'SecurityError' || name === 'NotAllowedError') {\n return PERMISSION_DENIED\n }\n if (name === 'NotSupportedError' || name === 'TypeError') {\n return NOT_SUPPORTED\n }\n // AbortError, InvalidStateError, and anything a browser makes up.\n return UNAVAILABLE\n}\n\n/**\n * Request MIDI access in the browser.\n *\n * The returned instance holds the state and notifies subscribers when it changes,\n * so it can be consumed from any framework.\n */\nexport function createMIDIAccess(): MIDIAccessInstance {\n let state = INITIAL_STATE\n let destroyed = false\n let access: MIDIAccess | null = null\n let requestGeneration = 0\n const listeners = new Set<() => void>()\n\n function setState(next: MIDIAccessState) {\n state = next\n for (const listener of listeners) listener()\n }\n\n /**\n * A new array every time, so that `useSyncExternalStore` and its equivalents\n * see the change. The identity of the state object is what they compare.\n */\n function readInputs() {\n return access ? [...access.inputs.values()] : []\n }\n\n function handleStateChange(event?: Event) {\n if (destroyed || !access) return\n if (event && (event as MIDIConnectionEvent).port?.type === 'output') return\n setState({ ...state, inputs: readInputs() })\n }\n\n function request(options: MIDIAccessOptions = {}) {\n if (destroyed) return\n const generation = ++requestGeneration\n if (typeof navigator === 'undefined' || !navigator.requestMIDIAccess) {\n setState({ ...state, error: NOT_SUPPORTED })\n return\n }\n navigator.requestMIDIAccess({ sysex: options.sysex ?? false }).then(\n (granted) => {\n if (destroyed || generation !== requestGeneration) return\n access?.removeEventListener('statechange', handleStateChange)\n access = granted\n granted.addEventListener('statechange', handleStateChange)\n setState({ midiAccess: granted, error: null, inputs: readInputs() })\n },\n (reason: unknown) => {\n if (destroyed || generation !== requestGeneration) return\n setState({ ...state, error: toError(reason) })\n },\n )\n }\n\n return {\n request,\n getState: () => state,\n getServerState: () => INITIAL_STATE,\n subscribe: (listener) => {\n listeners.add(listener)\n return () => {\n listeners.delete(listener)\n }\n },\n destroy: () => {\n destroyed = true\n requestGeneration += 1\n access?.removeEventListener('statechange', handleStateChange)\n access = null\n listeners.clear()\n },\n }\n}\n","export interface MIDIMessageInstance {\n /** Replace the handler, keeping the listeners in place. */\n update: (onMIDIMessage: (event: MIDIMessageEvent) => void) => void\n destroy: () => void\n}\n\n/**\n * Listen to raw `midimessage` events on every input of a MIDIAccess.\n *\n * The set of inputs is followed rather than sampled: MIDIAccess fires\n * `statechange` when a device is plugged in or unplugged, and the listeners\n * move with it. A keyboard connected after access was granted works without\n * the caller having to rebuild anything.\n *\n * Use this when you need more detail than {@link createMIDIInput} provides.\n */\nexport function createMIDIMessage(\n midiAccess: MIDIAccess | null,\n onMIDIMessage: (event: MIDIMessageEvent) => void,\n): MIDIMessageInstance {\n let handler = onMIDIMessage\n\n /** Inputs this instance currently listens on. */\n const attached = new Set<MIDIInput>()\n\n const listener = (event: Event) => handler(event as MIDIMessageEvent)\n\n function sync() {\n if (!midiAccess) return\n const connected = new Set(midiAccess.inputs.values())\n\n for (const input of connected) {\n if (attached.has(input)) continue\n input.addEventListener('midimessage', listener)\n attached.add(input)\n }\n for (const input of [...attached]) {\n if (connected.has(input)) continue\n input.removeEventListener('midimessage', listener)\n attached.delete(input)\n }\n }\n\n sync()\n midiAccess?.addEventListener('statechange', sync)\n\n return {\n update: (next) => {\n handler = next\n },\n destroy: () => {\n midiAccess?.removeEventListener('statechange', sync)\n for (const input of attached) {\n input.removeEventListener('midimessage', listener)\n }\n attached.clear()\n },\n }\n}\n","import { createMIDIMessage } from './message'\n\n/** Status byte of each channel message, with the channel nibble cleared. */\nconst STATUS = {\n NOTE_OFF: 0x80,\n NOTE_ON: 0x90,\n AFTERTOUCH: 0xa0,\n CONTROL_CHANGE: 0xb0,\n PROGRAM_CHANGE: 0xc0,\n CHANNEL_PRESSURE: 0xd0,\n PITCH_BEND: 0xe0,\n}\n\n/**\n * Centre of the 14-bit pitch bend range: no bend.\n *\n * The range is not symmetric — 0 is 8192 below centre and 16383 is 8191 above\n * — so a wheel at rest reports exactly this rather than half of the maximum.\n */\nexport const PITCH_BEND_CENTER = 8192\n\n/**\n * Every handler is given the channel last, as 0-15. MIDI channels are written\n * 1-16 on hardware, so add one before showing it to anyone.\n */\nexport type MIDIInputHandlers = {\n onNoteOnEvent?: (note: number, velocity: number, channel: number) => void\n onNoteOffEvent?: (note: number, channel: number) => void\n /**\n * The 14-bit bend, 0-16383, centred at {@link PITCH_BEND_CENTER}.\n *\n * The two data bytes are little-endian — the first carries the low 7 bits —\n * which is the other way round from every other message.\n */\n onPitchBendEvent?: (value: number, channel: number) => void\n /** `controller` is the CC number, `value` is 0-127. */\n onControlChangeEvent?: (\n controller: number,\n value: number,\n channel: number,\n ) => void\n onProgramChangeEvent?: (program: number, channel: number) => void\n /** Pressure for one held note (polyphonic aftertouch). */\n onAftertouchEvent?: (note: number, pressure: number, channel: number) => void\n /** Pressure for the whole channel, sent by keyboards with one sensor. */\n onChannelPressureEvent?: (pressure: number, channel: number) => void\n}\n\nexport interface MIDIInputInstance {\n /** Replace the handlers, keeping the listeners in place. */\n update: (handlers: MIDIInputHandlers) => void\n destroy: () => void\n}\n\n/**\n * Handle the channel voice messages of every connected input. To be used with\n * {@link createMIDIAccess}. Internally uses {@link createMIDIMessage}, so\n * devices plugged in later are picked up.\n *\n * System messages (clock, sysex, and the rest of `0xf0`-`0xff`) are not\n * decoded here; reach for {@link createMIDIMessage} for those.\n */\nexport function createMIDIInput(\n midiAccess: MIDIAccess | null,\n handlers: MIDIInputHandlers,\n): MIDIInputInstance {\n let current = handlers\n\n const message = createMIDIMessage(midiAccess, (event) => {\n if (!event.data || event.data.length === 0) return\n\n const status = event.data[0]\n // 0xf0 and above are system messages, which carry no channel.\n if (status >= 0xf0) return\n\n const kind = status & 0xf0\n const channel = status & 0x0f\n const first = event.data[1] ?? 0\n const second = event.data[2] ?? 0\n\n switch (kind) {\n case STATUS.NOTE_OFF:\n current.onNoteOffEvent?.(first, channel)\n break\n case STATUS.NOTE_ON:\n // A note on with zero velocity is how most devices say note off.\n if (second === 0) current.onNoteOffEvent?.(first, channel)\n else current.onNoteOnEvent?.(first, second, channel)\n break\n case STATUS.AFTERTOUCH:\n current.onAftertouchEvent?.(first, second, channel)\n break\n case STATUS.CONTROL_CHANGE:\n current.onControlChangeEvent?.(first, second, channel)\n break\n case STATUS.PROGRAM_CHANGE:\n current.onProgramChangeEvent?.(first, channel)\n break\n case STATUS.CHANNEL_PRESSURE:\n current.onChannelPressureEvent?.(first, channel)\n break\n case STATUS.PITCH_BEND:\n current.onPitchBendEvent?.((second << 7) | first, channel)\n break\n }\n })\n\n return {\n update: (next) => {\n current = next\n },\n destroy: message.destroy,\n }\n}\n","export type DragState = {\n /** Total movement from the drag start, in screen coordinates. */\n x: number\n y: number\n /** Movement since the previous event, in screen coordinates. */\n deltaX: number\n deltaY: number\n /** Pointer position in viewport coordinates. */\n clientX: number\n clientY: number\n /**\n * Which pointer this is. Always the same value for one drag; with\n * {@link DragOptions.multiPointer} it tells concurrent drags apart.\n */\n pointerId: number\n event: PointerEvent\n}\n\nexport type DragOptions = {\n /**\n * Minimum movement in pixels before `onDrag` fires. Movement below it is\n * carried over to the next event rather than discarded, so a slow drag still\n * reports once it adds up.\n *\n * Prevents `onDrag` from firing on, for example, a double click.\n *\n * @default 0\n */\n threshold?: number\n\n /**\n * CSS cursor to show while dragging. Applied to the element itself: pointer\n * capture keeps it in effect even once the pointer leaves the element, so\n * there is no need to touch the document.\n *\n * With {@link DragOptions.multiPointer} it is set for the first pointer and\n * restored once the last one is up.\n */\n cursor?: string\n\n /**\n * Decide whether a pointerdown starts a drag at all.\n *\n * Checked before anything else — **before the pointer is captured** — so\n * declining here leaves the whole gesture to whatever else is listening.\n * Deciding later would be too late: the capture has already been taken from\n * the element that was going to handle it.\n *\n * The use for it is a drag on a container that also holds draggable things\n * of its own, such as a rubber-band selection that must not begin on top of\n * one of the objects it would select.\n */\n shouldStart?: (event: PointerEvent) => boolean\n\n /**\n * Hide the pointer and read its movement directly, instead of following it\n * around the screen.\n *\n * A relative drag — a knob, a stepper — does not care where the pointer is,\n * only how far it moved, and letting it wander has two costs: the cursor\n * ends up far from what it is holding, and **the drag stops at the edge of\n * the screen**, where the operating system pins the pointer and the\n * coordinates stop changing. A fine drag reaches that edge quickly.\n *\n * **Not for a drag whose value is the position pointed at** — anything on\n * `elementMapping`. `clientX` / `clientY` freeze while the pointer is\n * locked, so there is no position left to read.\n *\n * The request needs a user gesture, which a pointerdown is, but it can still\n * be refused; the drag then carries on as an ordinary one. Read on\n * pointerdown, so `update()` reaches the next drag rather than the current.\n *\n * @default false\n */\n pointerLock?: boolean\n\n /**\n * Track every pointer that goes down on the element, rather than only the\n * first. Each one gets its own `onDragStart` / `onDrag` / `onDragEnd` and\n * carries its own totals; {@link DragState.pointerId} says which is which.\n *\n * Fixed for the lifetime of the instance: switching part way through a drag\n * has no meaning, so `update()` ignores it.\n *\n * @default false\n */\n multiPointer?: boolean\n\n onDragStart?: (state: DragState) => void\n onDrag?: (state: DragState) => void\n /**\n * Called exactly once for every drag that starts, whether tracking ends by\n * pointer release, cancellation, capture or lock loss, or destruction.\n */\n onDragEnd?: (state: DragState) => void\n}\n\nexport interface DragInstance {\n /**\n * Replace the given options. Lets a wrapper feed fresh handlers in without\n * tearing down the listeners, which would abort a drag in progress.\n *\n * `multiPointer` is fixed for the lifetime of the instance and is ignored\n * here.\n */\n update: (options: DragOptions) => void\n /** End any active drags before removing the instance. */\n destroy: () => void\n}\n\n/**\n * Applied to the element for the lifetime of the instance.\n *\n * `touch-action` stops touch dragging from scrolling the page. That alone makes\n * the browser treat a long press as the start of a text selection, so selection\n * and the iOS callout are suppressed here too.\n */\nconst MANAGED_STYLES = [\n ['touch-action', 'none'],\n ['user-select', 'none'],\n ['-webkit-user-select', 'none'],\n ['-webkit-touch-callout', 'none'],\n] as const\n\ntype ManagedStyleState = {\n count: number\n previous: Map<string, { value: string; priority: string }>\n}\n\nconst managedStyleStates = new WeakMap<Element, ManagedStyleState>()\n\ntype CaptureTarget = {\n requestPointerLock?: () => unknown\n setPointerCapture?: (pointerId: number) => void\n releasePointerCapture?: (pointerId: number) => void\n hasPointerCapture?: (pointerId: number) => boolean\n}\n\n/** What one pointer needs to report its own movement. */\ntype PointerState = {\n /** Where the listeners for this pointer live. */\n moveTarget: EventTarget\n startX: number\n startY: number\n lastX: number\n lastY: number\n /**\n * Where the travel stood when the pointer lock took effect, and how far it\n * has moved since. Screen coordinates stop changing under the lock, so from\n * that point the movement of each event is added up instead.\n *\n * The base is taken when the lock engages rather than on pointerdown: the\n * request is asynchronous, and whatever movement happened while it was in\n * flight was measured the ordinary way.\n */\n lockBaseX?: number\n lockBaseY?: number\n lockMoveX: number\n lockMoveY: number\n /** The most recent event, to end the drag with when the lock is lost. */\n lastEvent: PointerEvent\n}\n\n/**\n * Track a pointer drag on an element.\n *\n * Uses Pointer Events only, and pointer capture so that the drag keeps working\n * once the pointer leaves the element. For the lifetime of the instance the\n * element gets `touch-action: none` so that touch dragging does not scroll the\n * page, plus `user-select: none` so that a long press does not start a text\n * selection instead.\n *\n * One pointer at a time by default; see {@link DragOptions.multiPointer}.\n */\nexport function createDrag(\n element: Element,\n options: DragOptions = {},\n): DragInstance {\n let opts = options\n const multiPointer = options.multiPointer ?? false\n const capture = element as CaptureTarget\n const style = (element as Partial<HTMLElement>).style\n\n let managedStyles = managedStyleStates.get(element)\n if (style) {\n if (managedStyles) {\n managedStyles.count += 1\n } else {\n const previous = new Map<string, { value: string; priority: string }>()\n for (const [property, value] of MANAGED_STYLES) {\n previous.set(property, {\n value: style.getPropertyValue(property),\n priority: style.getPropertyPriority(property),\n })\n style.setProperty(property, value)\n }\n managedStyles = { count: 1, previous }\n managedStyleStates.set(element, managedStyles)\n }\n }\n\n const pointers = new Map<number, PointerState>()\n /**\n * How many pointers each target carries. Adding the same listener twice is a\n * no-op and removing it once removes it for good, so a target is subscribed\n * to while at least one pointer is on it and no longer.\n */\n const targets = new Map<EventTarget, number>()\n let previousCursor: string | undefined\n /** The pointer that asked for the lock, while it is still down. */\n let lockedPointerId: number | null = null\n let lockRequest = 0\n let destroyed = false\n\n function state(\n event: PointerEvent,\n pointer: PointerState,\n deltaX: number,\n deltaY: number,\n ): DragState {\n return {\n x:\n pointer.lockBaseX !== undefined\n ? pointer.lockBaseX + pointer.lockMoveX\n : event.screenX - pointer.startX,\n y:\n pointer.lockBaseY !== undefined\n ? pointer.lockBaseY + pointer.lockMoveY\n : event.screenY - pointer.startY,\n deltaX,\n deltaY,\n clientX: event.clientX,\n clientY: event.clientY,\n pointerId: event.pointerId,\n event,\n }\n }\n\n /**\n * `user-select: none` only makes the element's own text unselectable; the\n * browser still starts a selection on long press and grabs whatever text it\n * finds nearby. Cancelling the selection outright is what actually stops it.\n */\n function preventSelectStart(event: Event) {\n event.preventDefault()\n }\n\n function retainTarget(target: EventTarget) {\n const count = targets.get(target) ?? 0\n if (count === 0) {\n target.addEventListener('pointermove', handlePointerMove)\n target.addEventListener('pointerup', handlePointerUp)\n target.addEventListener('pointercancel', handlePointerUp)\n if (target === element) {\n target.addEventListener('lostpointercapture', handleLostPointerCapture)\n }\n }\n targets.set(target, count + 1)\n }\n\n function releaseTarget(target: EventTarget) {\n const count = targets.get(target) ?? 0\n if (count > 1) {\n targets.set(target, count - 1)\n return\n }\n target.removeEventListener('pointermove', handlePointerMove)\n target.removeEventListener('pointerup', handlePointerUp)\n target.removeEventListener('pointercancel', handlePointerUp)\n if (target === element) {\n target.removeEventListener('lostpointercapture', handleLostPointerCapture)\n }\n targets.delete(target)\n }\n\n function handlePointerDown(event: Event) {\n const pointerEvent = event as PointerEvent\n if (pointerEvent.button !== 0) return\n const pointerId = pointerEvent.pointerId\n // Without multiPointer only one pointer drives the drag; ignore the rest.\n if (pointers.has(pointerId)) return\n if (!multiPointer && pointers.size > 0) return\n\n if (opts.shouldStart && !opts.shouldStart(pointerEvent)) return\n\n const isFirst = pointers.size === 0\n if (isFirst && opts.cursor && style) {\n previousCursor = style.cursor\n style.cursor = opts.cursor\n }\n\n capture.setPointerCapture?.(pointerId)\n // With pointer capture the element receives the rest of the gesture.\n // Without it (older engines, jsdom) fall back to the window.\n const moveTarget =\n capture.hasPointerCapture?.(pointerId) === true\n ? element\n : (globalThis.window ?? element)\n\n const pointer: PointerState = {\n moveTarget,\n startX: pointerEvent.screenX,\n startY: pointerEvent.screenY,\n lastX: pointerEvent.screenX,\n lastY: pointerEvent.screenY,\n lockMoveX: 0,\n lockMoveY: 0,\n lastEvent: pointerEvent,\n }\n pointers.set(pointerId, pointer)\n\n retainTarget(moveTarget)\n if (isFirst) {\n globalThis.document?.addEventListener('selectstart', preventSelectStart)\n }\n\n // One pointer can be locked, so the first one takes it.\n if (isFirst && opts.pointerLock) {\n const requestId = ++lockRequest\n lockedPointerId = pointerId\n globalThis.document?.addEventListener(\n 'pointerlockchange',\n handleLockChange,\n )\n try {\n // Newer engines return a promise that rejects; older ones fire\n // `pointerlockerror` instead. Either way a refusal only means the drag\n // stays an ordinary one, so nothing here has to act on it.\n const request = capture.requestPointerLock?.() as\n | Promise<void>\n | undefined\n request?.then?.(\n () => {\n if (\n requestId === lockRequest &&\n (!pointers.has(pointerId) || destroyed) &&\n globalThis.document?.pointerLockElement === element\n ) {\n globalThis.document.exitPointerLock?.()\n }\n },\n () => {},\n )\n } catch {\n // requestPointerLock threw synchronously; same story.\n }\n }\n\n opts.onDragStart?.(state(pointerEvent, pointer, 0, 0))\n }\n\n function handlePointerMove(event: Event) {\n const pointerEvent = event as PointerEvent\n const pointer = pointers.get(pointerEvent.pointerId)\n if (!pointer) return\n\n // Under the lock the screen position no longer moves, so the movement the\n // event reports is the only thing left to read.\n const locked = pointer.lockBaseX !== undefined\n const deltaX = locked\n ? (pointerEvent.movementX ?? 0)\n : pointerEvent.screenX - pointer.lastX\n const deltaY = locked\n ? (pointerEvent.movementY ?? 0)\n : pointerEvent.screenY - pointer.lastY\n\n // Movement below the threshold accumulates until it crosses it. Dropping it\n // instead would swallow a slow drag entirely: pointer coordinates are\n // fractional, so each event can move less than a pixel and never report.\n const threshold = opts.threshold ?? 0\n if (Math.abs(deltaX) < threshold && Math.abs(deltaY) < threshold) return\n\n pointer.lastX = pointerEvent.screenX\n pointer.lastY = pointerEvent.screenY\n pointer.lastEvent = pointerEvent\n if (locked) {\n pointer.lockMoveX += deltaX\n pointer.lockMoveY += deltaY\n }\n\n opts.onDrag?.(state(pointerEvent, pointer, deltaX, deltaY))\n }\n\n function handleLockChange() {\n if (lockedPointerId === null) return\n const pointer = pointers.get(lockedPointerId)\n if (!pointer) return\n\n if (globalThis.document?.pointerLockElement === element) {\n // Engaged. Whatever moved while the request was in flight was measured\n // the ordinary way, so the travel so far becomes the base and the\n // per-event movement is added to it from here.\n pointer.lockBaseX = pointer.lastX - pointer.startX\n pointer.lockBaseY = pointer.lastY - pointer.startY\n pointer.lockMoveX = 0\n pointer.lockMoveY = 0\n return\n }\n\n // Lost without the pointer coming up: Esc, a tab switch, leaving\n // fullscreen. No pointerup is coming, so the drag ends here rather than\n // hanging on with a pointer nobody can see.\n if (pointer.lockBaseX === undefined) return\n finishDrag(lockedPointerId)\n }\n\n function handlePointerUp(event: Event) {\n const pointerEvent = event as PointerEvent\n finishDrag(pointerEvent.pointerId, pointerEvent)\n }\n\n function handleLostPointerCapture(event: Event) {\n const pointerEvent = event as PointerEvent\n finishDrag(pointerEvent.pointerId)\n }\n\n function finishDrag(pointerId: number, event?: PointerEvent) {\n const pointer = pointers.get(pointerId)\n if (!pointer) return\n\n const finalState = event\n ? state(\n event,\n pointer,\n event.screenX - pointer.lastX,\n event.screenY - pointer.lastY,\n )\n : state(pointer.lastEvent, pointer, 0, 0)\n\n stopTracking(pointerId)\n opts.onDragEnd?.(finalState)\n }\n\n function stopTracking(pointerId: number) {\n const pointer = pointers.get(pointerId)\n if (!pointer) return\n\n pointers.delete(pointerId)\n releaseTarget(pointer.moveTarget)\n try {\n if (capture.hasPointerCapture?.(pointerId) === true) {\n capture.releasePointerCapture?.(pointerId)\n }\n } catch {\n // The capture may disappear between checking and releasing it. Tracking\n // is already cleared, so the drag still ends normally.\n }\n\n if (lockedPointerId === pointerId) {\n lockedPointerId = null\n const document = globalThis.document\n document?.removeEventListener('pointerlockchange', handleLockChange)\n // Already gone when the lock is what ended the drag.\n if (document?.pointerLockElement === element) document.exitPointerLock?.()\n }\n\n if (pointers.size > 0) return\n\n globalThis.document?.removeEventListener('selectstart', preventSelectStart)\n if (previousCursor !== undefined && style) {\n style.cursor = previousCursor\n previousCursor = undefined\n }\n }\n\n element.addEventListener('pointerdown', handlePointerDown)\n\n return {\n update: (next) => {\n opts = { ...opts, ...next, multiPointer }\n },\n destroy: () => {\n if (destroyed) return\n destroyed = true\n element.removeEventListener('pointerdown', handlePointerDown)\n for (const pointerId of [...pointers.keys()]) finishDrag(pointerId)\n if (style && managedStyles && --managedStyles.count === 0) {\n for (const [property] of MANAGED_STYLES) {\n const previous = managedStyles.previous.get(property)\n if (previous?.value) {\n style.setProperty(property, previous.value, previous.priority)\n } else {\n style.removeProperty(property)\n }\n }\n managedStyleStates.delete(element)\n }\n },\n }\n}\n","import { noteAt, type PianoLayout } from '@tremolo-ui/functions'\n\nimport { createDrag } from '../pointer/drag'\n\n/**\n * What asked for a note to sound.\n *\n * A note stops only once everything that asked for it has let go, so two\n * fingers on one key, or a key held by both the mouse and a MIDI keyboard,\n * behave the way they look.\n *\n * Pointers use `pointer:<pointerId>`; everything else names its own source.\n */\nexport type NoteSource = string\n\nexport interface PianoInputOptions {\n /** Geometry of the drawn keyboard, used to find the note under a pointer. */\n layout: PianoLayout\n\n /**\n * Let a pointer slide from one key to the next while it is down. With it off,\n * the key that was pressed keeps sounding until the pointer is released.\n *\n * @default true\n */\n glissando?: boolean\n\n /**\n * Highest note that can sound. Above it a note is refused, whichever source\n * asks for it.\n *\n * @default 127\n */\n midiMax?: number\n\n /** Called when a note starts sounding, not for each source that asks. */\n onPlayNote?: (note: number, velocity?: number) => void\n /** Called once the last source holding a note has let go. */\n onStopNote?: (note: number) => void\n /** Called whenever {@link PianoInputInstance.activeNotes} would change. */\n onActiveNotesChange?: (notes: number[]) => void\n}\n\nexport interface PianoInputInstance {\n /**\n * Replace the given options. Lets a wrapper feed a fresh layout in without\n * tearing down the listeners, which would abort a drag in progress.\n */\n update: (options: Partial<PianoInputOptions>) => void\n\n /**\n * Start a note from something other than a pointer: a keyboard shortcut, a\n * MIDI message, an imperative call.\n */\n noteOn: (\n note: number,\n options?: { source?: NoteSource; velocity?: number },\n ) => void\n /** Release a note held by `source`. */\n noteOff: (note: number, options?: { source?: NoteSource }) => void\n\n /** The notes currently sounding, ascending. */\n activeNotes: () => number[]\n\n destroy: () => void\n}\n\nconst DEFAULT_SOURCE: NoteSource = 'api'\n\n/**\n * Drive a piano keyboard with pointers, and own what is sounding.\n *\n * Every way of playing a note goes through the one instance — pointers here,\n * keyboard shortcuts and MIDI through {@link PianoInputInstance.noteOn} — so\n * there is a single answer to what is currently held, and drawing the keys is\n * left entirely to the wrapper.\n *\n * Tracks every pointer at once, so a chord can be played with several fingers.\n */\nexport function createPianoInput(\n element: Element,\n options: PianoInputOptions,\n): PianoInputInstance {\n let opts = options\n\n /** Which sources are holding each sounding note. */\n const held = new Map<number, Set<NoteSource>>()\n /** The note each pointer is currently on. */\n const pointerNotes = new Map<number, number>()\n\n function activeNotes(): number[] {\n return [...held.keys()].sort((a, b) => a - b)\n }\n\n function noteOn(\n note: number,\n {\n source = DEFAULT_SOURCE,\n velocity,\n }: { source?: NoteSource; velocity?: number } = {},\n ) {\n if (!Number.isInteger(note) || note < 0 || note > 127) {\n throw new RangeError('note: requirements: an integer from 0 to 127')\n }\n if (note > (opts.midiMax ?? 127)) return\n\n const sources = held.get(note)\n if (sources) {\n // Already sounding: remember the extra holder and leave it alone.\n sources.add(source)\n return\n }\n\n held.set(note, new Set([source]))\n opts.onPlayNote?.(note, velocity)\n opts.onActiveNotesChange?.(activeNotes())\n }\n\n function noteOff(\n note: number,\n { source = DEFAULT_SOURCE }: { source?: NoteSource } = {},\n ) {\n if (!Number.isInteger(note) || note < 0 || note > 127) {\n throw new RangeError('note: requirements: an integer from 0 to 127')\n }\n const sources = held.get(note)\n if (!sources) return\n\n sources.delete(source)\n if (sources.size > 0) return\n\n held.delete(note)\n opts.onStopNote?.(note)\n opts.onActiveNotesChange?.(activeNotes())\n }\n\n /** The note under a pointer, or null where the pointer is off the keys. */\n function noteUnder(clientX: number, clientY: number): number | null {\n const { left, top, height } = element.getBoundingClientRect()\n return noteAt(clientX - left, clientY - top, height, opts.layout)\n }\n\n /** Move a pointer onto a note, releasing whatever it held before. */\n function movePointer(pointerId: number, note: number | null) {\n const source = `pointer:${pointerId}`\n const previous = pointerNotes.get(pointerId)\n if (previous === note) return\n\n if (previous !== undefined) noteOff(previous, { source })\n\n if (note === null || note > (opts.midiMax ?? 127)) {\n pointerNotes.delete(pointerId)\n } else {\n pointerNotes.set(pointerId, note)\n noteOn(note, { source })\n }\n }\n\n const drag = createDrag(element, {\n multiPointer: true,\n onDragStart: (state) =>\n movePointer(state.pointerId, noteUnder(state.clientX, state.clientY)),\n onDrag: (state) => {\n // Without glissando the pressed key holds until the pointer is released,\n // so where the pointer wanders to does not matter.\n if (opts.glissando === false) return\n movePointer(state.pointerId, noteUnder(state.clientX, state.clientY))\n },\n onDragEnd: (state) => movePointer(state.pointerId, null),\n })\n\n return {\n update: (next) => {\n opts = { ...opts, ...next }\n\n const midiMax = opts.midiMax ?? 127\n const stoppedNotes = activeNotes().filter((note) => note > midiMax)\n if (stoppedNotes.length === 0) return\n\n for (const note of stoppedNotes) {\n held.delete(note)\n opts.onStopNote?.(note)\n }\n for (const [pointerId, note] of pointerNotes) {\n if (note > midiMax) pointerNotes.delete(pointerId)\n }\n opts.onActiveNotesChange?.(activeNotes())\n },\n noteOn,\n noteOff,\n activeNotes,\n destroy: () => {\n drag.destroy()\n // Anything still held is released, so a caller that mirrors these\n // callbacks into a synth is not left with a stuck note.\n const notes = activeNotes()\n for (const note of notes) {\n held.delete(note)\n opts.onStopNote?.(note)\n }\n pointerNotes.clear()\n if (notes.length > 0) opts.onActiveNotesChange?.([])\n },\n }\n}\n","/**\n * A pair of per-axis values. The tuple elements are labelled, so editors show\n * `[x: number, y: number]` rather than a bare pair.\n */\nexport type XY<T> = [x: T, y: T]\n\n/**\n * A setting that may be given once for both axes, or per axis.\n *\n * A single value is told from a pair with `Array.isArray`, so the single form\n * is only offered while `T` cannot itself be an array. Where it can, the pair\n * is the only way to write it, since a lone array would be read as a pair.\n */\nexport type XYInput<T> = [T] extends [readonly unknown[]]\n ? readonly [x: T, y: T]\n : T | readonly [x: T, y: T]\n\n// `Array.isArray` narrows a mutable tuple on its own, but not a readonly one,\n// hence the explicit predicate.\nfunction isPair<T>(\n value: T | readonly [x: T, y: T],\n): value is readonly [x: T, y: T] {\n return Array.isArray(value)\n}\n\n/**\n * Spread a setting that may have been given as a single value.\n *\n * The parameter is written out rather than taken as `XYInput<T>`: a\n * conditional type cannot be narrowed, so the constraint stays where it is\n * declared and this takes both forms.\n *\n * The pair is copied rather than passed along, so that the result is a tuple\n * the caller owns even when a readonly one was given.\n */\nexport function toXY<T>(value: T | readonly [x: T, y: T]): XY<T> {\n return isPair(value) ? [value[0], value[1]] : [value, value]\n}\n","import {\n clamp,\n linearScale,\n normalizeValue,\n stepValue,\n toPrecision,\n type ValueRange,\n} from '@tremolo-ui/functions'\n\nimport { toXY, type XY, type XYInput } from '../xy'\n\nimport { createDrag, type DragState } from './drag'\n\n/**\n * How the 0-1 travel of one axis maps onto a value.\n *\n * Extends `ValueRange` so that a drag and an `applyDelta` nudge from a wheel\n * or an arrow key share one description of the scaling.\n */\nexport interface AxisOptions extends ValueRange {\n /**\n * Flip the axis so that its far end is `min`.\n *\n * Positions follow the screen: x grows to the right, y downwards. A vertical\n * slider whose maximum is at the top therefore reverses its y axis.\n *\n * @default false\n */\n reverse?: boolean\n}\n\n/** Where the value of each axis currently sits, as a position (see {@link DragValueMapping}). */\nexport interface MappingContext {\n position: () => XY<number>\n}\n\n/**\n * Turns pointer movement into a position on each axis: 0 is the `min` end of\n * the travel and 1 the `max` end, before `reverse` and the scaling of\n * {@link AxisOptions} are applied.\n *\n * A mapping holds the state of the drag in progress, so an instance belongs to\n * a single {@link createDragValue} instance.\n */\nexport interface DragValueMapping {\n /**\n * @returns the position, or null when it cannot be determined and the event\n * should be ignored.\n */\n start: (state: DragState, context: MappingContext) => XY<number> | null\n move: (state: DragState, context: MappingContext) => XY<number> | null\n}\n\n/**\n * Map the pointer onto the bounding rect of an element: the value *is* the\n * position pointed at, so the middle of the element is 0.5.\n *\n * The element is read on every event, so it may be mounted after the drag is\n * set up and may change size while a drag is in progress.\n */\nexport function elementMapping(\n getElement: () => Element | null | undefined,\n {\n sensitivity,\n }: {\n /**\n * How much the movement counts, read on every move. `1` is the pointer\n * position itself; `0.1` makes the same movement cover a tenth of the\n * travel, which is what a fine-adjustment modifier wants.\n *\n * Anything but `1` turns the mapping relative: the value stops being the\n * position pointed at and starts being where it stood when the sensitivity\n * changed, plus the movement since. **The pointer and the value stay apart\n * for the rest of the drag** rather than snapping back together when the\n * key is released, since snapping would move the value nobody asked to\n * move.\n */\n sensitivity?: (state: DragState) => number\n } = {},\n): DragValueMapping {\n /** The reported position when the sensitivity last changed. */\n let origin: XY<number> = [0, 0]\n /** The raw position at that same moment. */\n let anchor: XY<number> = [0, 0]\n /** The previous event, so a sensitivity change can be dated back to it. */\n let previous: XY<number> = [0, 0]\n let factor = 1\n\n function positionIn(state: DragState): XY<number> | null {\n const element = getElement()\n if (!element) return null\n const { left, top, right, bottom } = element.getBoundingClientRect()\n // A collapsed element has no travel to normalize against, and\n // normalizeValue rejects an empty range.\n return [\n right > left ? normalizeValue(state.clientX, left, right) : 0,\n bottom > top ? normalizeValue(state.clientY, top, bottom) : 0,\n ]\n }\n\n const reported = (raw: XY<number>, at: number): XY<number> => [\n origin[0] + (raw[0] - anchor[0]) * at,\n origin[1] + (raw[1] - anchor[1]) * at,\n ]\n\n return {\n start: (state) => {\n const raw = positionIn(state)\n if (!raw) return null\n // origin and anchor together, so the value is the position pointed at:\n // a plain click still lands where it was aimed.\n origin = raw\n anchor = raw\n previous = raw\n // Read here too, so a modifier already held when the pointer went down\n // applies from the first pixel rather than from the first change.\n factor = sensitivity ? sensitivity(state) : 1\n return raw\n },\n move: (state) => {\n const raw = positionIn(state)\n if (!raw) return null\n\n const next = sensitivity ? sensitivity(state) : 1\n if (next !== factor) {\n // Fold the travel so far into the origin, or the new sensitivity would\n // apply to the whole drag and the value would jump.\n //\n // Dated to the previous event rather than this one: a key produces no\n // pointer event of its own, so the change is only seen on the next\n // move, and that move's own distance belongs to the new sensitivity.\n origin = reported(previous, factor)\n anchor = previous\n factor = next\n }\n previous = raw\n\n // With no sensitivity given, origin and anchor never move apart and this\n // is the raw position, exactly as before.\n return reported(raw, factor)\n },\n }\n}\n\n/**\n * Move the value away from where it stood when the drag started, by the\n * distance dragged. The pointer position itself carries no meaning, so the\n * value can be adjusted from anywhere on the screen.\n */\nexport function relativeMapping({\n pixelRange = 100,\n sensitivity,\n}: {\n /**\n * Pixels of movement that span the whole range.\n * @default 100\n */\n pixelRange?: XYInput<number>\n /**\n * How much the movement counts, read on every move. `1` is `pixelRange` as\n * given; `0.1` makes the same movement cover a tenth of the range, which is\n * what a fine-adjustment modifier wants.\n *\n * Changing it mid-drag does not disturb the value: the travel so far is\n * folded into the origin and measuring starts again from there.\n */\n sensitivity?: (state: DragState) => number\n} = {}): DragValueMapping {\n const [baseX, baseY] = toXY(pixelRange)\n let origin: XY<number> = [0, 0]\n /** Where the current sensitivity took over, in drag coordinates. */\n let anchor: XY<number> = [0, 0]\n /** The previous event, so a sensitivity change can be dated back to it. */\n let previous: XY<number> = [0, 0]\n let factor = 1\n\n const travelled = (to: XY<number>, at: number): XY<number> => [\n baseX === 0 ? origin[0] : origin[0] + ((to[0] - anchor[0]) * at) / baseX,\n baseY === 0 ? origin[1] : origin[1] + ((to[1] - anchor[1]) * at) / baseY,\n ]\n\n return {\n start: (state, context) => {\n origin = context.position()\n anchor = [0, 0]\n previous = [0, 0]\n // Read here too, so a modifier already held when the pointer went down\n // applies from the first pixel rather than from the first change.\n factor = sensitivity ? sensitivity(state) : 1\n return origin\n },\n // Measured from the anchor rather than accumulated per event, so the\n // position picks up no rounding error of its own. The anchor only moves\n // when the sensitivity does, which is a handful of times per drag at most.\n move: (state) => {\n const next = sensitivity ? sensitivity(state) : 1\n if (next !== factor) {\n // Fold the travel so far into the origin, or the new sensitivity would\n // apply to the whole drag and the value would jump.\n //\n // Dated to the previous event rather than this one: a key produces no\n // pointer event of its own, so the change is only seen on the next\n // move, and that move's own distance belongs to the new sensitivity.\n origin = travelled(previous, factor)\n anchor = previous\n factor = next\n }\n previous = [state.x, state.y]\n return travelled([state.x, state.y], factor)\n },\n }\n}\n\nexport interface DragValueOptions {\n /** Scaling of each axis; a single value applies to both. */\n axis: XYInput<AxisOptions>\n\n /** How pointer movement becomes a position. */\n mapping: DragValueMapping\n\n /**\n * The current value of each axis. Read when a drag starts, by mappings that\n * move the value relative to it, such as {@link relativeMapping}.\n */\n getValue?: () => XY<number>\n\n /**\n * Report the value on pointer down, before any movement.\n *\n * Enable it where the pointer position *is* the value, so that a plain click\n * jumps to it. Leave it off where the element being dragged is an object in\n * its own right, so that grabbing its edge does not shift it under the\n * pointer.\n *\n * @default false\n */\n updateOnPointerDown?: boolean\n\n /** @see DragOptions.threshold */\n threshold?: number\n /** @see DragOptions.cursor */\n cursor?: string\n /**\n * @see DragOptions.pointerLock\n *\n * Only for a mapping that moves the value relative to where it stood, such\n * as {@link relativeMapping}. {@link elementMapping} reads the pointer\n * position, and there is none while it is locked.\n */\n pointerLock?: boolean\n /** @see DragOptions.shouldStart */\n shouldStart?: (event: PointerEvent) => boolean\n\n onChange?: (value: XY<number>, state: DragState) => void\n onDragStart?: (value: XY<number>, state: DragState) => void\n onDragEnd?: (value: XY<number>, state: DragState) => void\n}\n\nexport interface DragValueInstance {\n /**\n * Replace the given options. Lets a wrapper feed fresh values in without\n * tearing down the listeners, which would abort a drag in progress.\n *\n * `mapping` is fixed for the lifetime of the instance and is ignored here.\n */\n update: (options: Partial<DragValueOptions>) => void\n destroy: () => void\n}\n\n/**\n * Drive a value with a pointer drag.\n *\n * Combines {@link createDrag} with the scaling of `@tremolo-ui/functions`: the\n * mapping decides where the pointer sits on the 0-1 travel of each axis, and\n * the axis options turn that into a value.\n */\nexport function createDragValue(\n element: Element,\n options: DragValueOptions,\n): DragValueInstance {\n let opts = options\n let lastValue: XY<number> = [0, 0]\n let active = false\n\n const axes = () => toXY(opts.axis)\n\n function valueOf(position: XY<number>): XY<number> {\n return axes().map((axis, i) => {\n const p = axis.reverse ? 1 - position[i] : position[i]\n // A scale clamps the position, so a mapping may report outside 0-1.\n const scale = axis.scale ?? linearScale\n const value = scale.denormalize(p, axis.min, axis.max)\n const stepped = axis.step ? stepValue(value, axis.step) : value\n // Rounding to the step can leave the range, and so can dropping the\n // binary artefact, so the clamp comes last.\n return clamp(toPrecision(stepped), axis.min, axis.max)\n }) as XY<number>\n }\n\n const context: MappingContext = {\n position: () => {\n const getValue = opts.getValue\n if (!getValue) {\n throw new Error(\n 'createDragValue: getValue is required by the given mapping',\n )\n }\n const value = getValue()\n return axes().map((axis, i) => {\n const scale = axis.scale ?? linearScale\n const n = scale.normalize(value[i], axis.min, axis.max)\n return axis.reverse ? 1 - n : n\n }) as XY<number>\n },\n }\n\n const drag = createDrag(element, {\n threshold: opts.threshold,\n cursor: opts.cursor,\n pointerLock: opts.pointerLock,\n shouldStart: (event) => opts.shouldStart?.(event) ?? true,\n onDragStart: (state) => {\n const position = opts.mapping.start(state, context)\n if (!position) return\n active = true\n lastValue = valueOf(position)\n if (opts.updateOnPointerDown) opts.onChange?.(lastValue, state)\n opts.onDragStart?.(lastValue, state)\n },\n onDrag: (state) => {\n if (!active) return\n const position = opts.mapping.move(state, context)\n if (!position) return\n lastValue = valueOf(position)\n opts.onChange?.(lastValue, state)\n },\n onDragEnd: (state) => {\n if (!active) return\n if (\n state.event.type === 'pointerup' &&\n (state.deltaX !== 0 || state.deltaY !== 0)\n ) {\n const position = opts.mapping.move(state, context)\n if (position) {\n lastValue = valueOf(position)\n opts.onChange?.(lastValue, state)\n }\n }\n active = false\n opts.onDragEnd?.(lastValue, state)\n },\n })\n\n return {\n update: (next) => {\n opts = { ...opts, ...next, mapping: opts.mapping }\n drag.update({\n threshold: opts.threshold,\n cursor: opts.cursor,\n pointerLock: opts.pointerLock,\n })\n },\n destroy: () => drag.destroy(),\n }\n}\n","export interface WheelOptions {\n /** Replace the callback through {@link WheelInstance.update}. */\n onWheel?: (event: WheelEvent) => void\n /**\n * Only report events while the focus is inside the element.\n *\n * A control that reacts to the wheel on hover alone takes the scroll away\n * from the page, so passing over one in a long form silently changes its\n * value. Requiring focus makes that an explicit act.\n *\n * The check is `contains`, not an identity test: the element that actually\n * takes focus is usually a descendant, such as a thumb or an `<input>`, and\n * a caller may have replaced it with markup of their own.\n *\n * @default false\n */\n requireFocus?: boolean\n}\n\nexport interface WheelInstance {\n /** Replace the given options, keeping the listener in place. */\n update: (options: WheelOptions) => void\n destroy: () => void\n}\n\n/**\n * Listen to wheel events on an element.\n *\n * The listener is registered with `passive: false` so that the handler can call\n * `preventDefault()` to stop the page from scrolling.\n */\nexport function createWheel(\n element: Element,\n onWheel: (event: WheelEvent) => void,\n options: WheelOptions = {},\n): WheelInstance {\n let opts = { ...options, onWheel }\n\n function hasFocus() {\n const active = element.ownerDocument?.activeElement\n return !!active && element.contains(active)\n }\n\n const handler = (event: Event) => {\n if (opts.requireFocus && !hasFocus()) return\n opts.onWheel?.(event as WheelEvent)\n }\n\n element.addEventListener('wheel', handler, { passive: false })\n\n return {\n update: (next) => {\n opts = { ...opts, ...next }\n },\n destroy: () => {\n // Only `capture` matters when removing, and it is false here.\n element.removeEventListener('wheel', handler)\n },\n }\n}\n","import type { XY } from '../xy'\n\n/**\n * A rectangle in the 0..1 space of whatever the box is drawn over, with `y`\n * growing downwards — the same space the items are given in.\n */\nexport interface SelectionBoxRect {\n x: number\n y: number\n width: number\n height: number\n}\n\nexport interface SelectionBoxOptions<Id> {\n /**\n * Everything the box can pick up, read on every move rather than taken as a\n * snapshot: what is under the box changes while it is dragged, and an item\n * may have moved since the last one.\n */\n items: () => Iterable<readonly [Id, XY<number>]>\n /** The box as it is dragged, and `null` once it is gone. */\n onBoxChange?: (box: SelectionBoxRect | null) => void\n /** What the box covers, added to whatever it started from. */\n onSelectionChange?: (ids: Id[]) => void\n}\n\nexport interface SelectionBoxBeginOptions<Id> {\n /**\n * Add to `selection` rather than replacing it. Ctrl / meta rather than\n * shift, for the controls where shift is the fine-adjustment key.\n */\n additive?: boolean\n /** What was selected before the box started. Only read when `additive`. */\n selection?: readonly Id[]\n}\n\nexport interface SelectionBoxInstance<Id> {\n /** Replace the given options, without interrupting a box in progress. */\n update: (options: Partial<SelectionBoxOptions<Id>>) => void\n\n /** Start a box at `at`, which is one of its corners. */\n begin: (at: XY<number>, options?: SelectionBoxBeginOptions<Id>) => void\n /** Drag the opposite corner to `to`. */\n move: (to: XY<number>) => void\n /**\n * Finish the box. The selection stays as it is.\n *\n * @returns whether a box was running, so that a caller can tell a drag from\n * a press that selected nothing.\n */\n end: () => boolean\n\n /** The box being dragged, or `null` when there is none. */\n box: () => SelectionBoxRect | null\n\n destroy: () => void\n}\n\nfunction rectOf(from: XY<number>, to: XY<number>): SelectionBoxRect {\n return {\n x: Math.min(from[0], to[0]),\n y: Math.min(from[1], to[1]),\n width: Math.abs(to[0] - from[0]),\n height: Math.abs(to[1] - from[1]),\n }\n}\n\n/** Whether the box covers the point, edges included. */\nexport function selectionBoxCovers(\n box: SelectionBoxRect,\n [x, y]: XY<number>,\n): boolean {\n return (\n x >= box.x &&\n x <= box.x + box.width &&\n y >= box.y &&\n y <= box.y + box.height\n )\n}\n\n/**\n * Selecting by dragging a box over a set of items.\n *\n * The drag itself is not here: the caller owns the pointer, and reports where\n * it went in the same 0..1 space the items are given in.\n */\nexport function createSelectionBox<Id>(\n options: SelectionBoxOptions<Id>,\n): SelectionBoxInstance<Id> {\n let opts = options\n let drag: { from: XY<number>; base: readonly Id[] } | null = null\n let box: SelectionBoxRect | null = null\n\n function setBox(next: SelectionBoxRect | null) {\n box = next\n opts.onBoxChange?.(next)\n }\n\n function apply(rect: SelectionBoxRect, base: readonly Id[]) {\n const inside: Id[] = []\n for (const [id, at] of opts.items()) {\n if (selectionBoxCovers(rect, at) && !base.includes(id)) inside.push(id)\n }\n opts.onSelectionChange?.([...base, ...inside])\n }\n\n return {\n update: (next) => {\n opts = { ...opts, ...next }\n },\n begin: (at, { additive = false, selection = [] } = {}) => {\n drag = { from: at, base: additive ? [...selection] : [] }\n setBox(rectOf(at, at))\n // A plain press is a deselect of its own, so that a drag that picks\n // nothing up leaves nothing selected.\n if (!additive) opts.onSelectionChange?.([])\n },\n move: (to) => {\n if (!drag) return\n const rect = rectOf(drag.from, to)\n setBox(rect)\n apply(rect, drag.base)\n },\n end: () => {\n const running = drag !== null\n drag = null\n if (box !== null) setBox(null)\n return running\n },\n box: () => box,\n destroy: () => {\n drag = null\n box = null\n },\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tremolo-ui/dom",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "framework-agnostic DOM layer used in @tremolo-ui/*",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -8,7 +8,8 @@
8
8
  "module": "dist/index.js",
9
9
  "types": "dist/index.d.cts",
10
10
  "scripts": {
11
- "test": "jest",
11
+ "test": "vitest run",
12
+ "test:watch": "vitest",
12
13
  "typecheck": "tsc --noEmit",
13
14
  "build:package": "tsc && tsdown",
14
15
  "watch:package": "tsdown --watch",
@@ -18,7 +19,7 @@
18
19
  "format": "prettier --write ."
19
20
  },
20
21
  "dependencies": {
21
- "@tremolo-ui/functions": "^0.5.0"
22
+ "@tremolo-ui/functions": "^0.6.0"
22
23
  },
23
24
  "files": [
24
25
  "dist",
@@ -17,7 +17,7 @@ export interface AnimationFrame {
17
17
  deltaTime: number
18
18
  /** Milliseconds since the instance was created. */
19
19
  elapsedTime: number
20
- /** Frames per second implied by `deltaTime`. */
20
+ /** Frames per second implied by `deltaTime`, or 0 when no time elapsed. */
21
21
  fps: number
22
22
  }
23
23
 
@@ -47,7 +47,7 @@ export interface AnimationCanvasOptions {
47
47
  animate?: boolean
48
48
 
49
49
  /**
50
- * Size in CSS pixels. Ignored when `relativeSize` is on.
50
+ * Size in CSS pixels. Ignored when `resizable` is on.
51
51
  *
52
52
  * @default { width: 100, height: 100 }
53
53
  */
@@ -61,7 +61,7 @@ export interface AnimationCanvasOptions {
61
61
  *
62
62
  * @default false
63
63
  */
64
- relativeSize?: boolean
64
+ resizable?: boolean
65
65
 
66
66
  /**
67
67
  * Carry the drawing across a resize, so that the canvas does not blank for a
@@ -86,7 +86,7 @@ export interface AnimationCanvasInstance {
86
86
  * render without restarting the animation, so the frame count and the
87
87
  * elapsed time keep running.
88
88
  *
89
- * `relativeSize` and `contextAttributes` are fixed for the lifetime of the
89
+ * `resizable` and `contextAttributes` are fixed for the lifetime of the
90
90
  * instance and are ignored here.
91
91
  *
92
92
  * While `animate` is off this also draws a frame, since nothing else would.
@@ -119,7 +119,7 @@ export function createAnimationCanvas(
119
119
  }
120
120
  const context: CanvasRenderingContext2D = context2d
121
121
 
122
- const relativeSize = opts.relativeSize ?? false
122
+ const resizable = opts.resizable ?? false
123
123
 
124
124
  /** Off-document canvas holding the drawing while the real one is resized. */
125
125
  let memo: HTMLCanvasElement | null = null
@@ -127,6 +127,7 @@ export function createAnimationCanvas(
127
127
 
128
128
  let width = 0
129
129
  let height = 0
130
+ let appliedDpr = 0
130
131
  /** Whether a size has been applied, so a frame can be drawn. */
131
132
  let sized = false
132
133
  let initialized = false
@@ -146,21 +147,20 @@ export function createAnimationCanvas(
146
147
  * Assigning to `memo.width` resets the memo's transform to the identity, so
147
148
  * the copy neither scales nor resamples.
148
149
  */
149
- function takeSnapshot(): DrawingContext | null {
150
- if (!(opts.reduceFlickering ?? true)) return null
150
+ function takeSnapshot() {
151
+ if (!(opts.reduceFlickering ?? true)) return false
151
152
  if (!memo) {
152
153
  memo = globalThis.document?.createElement('canvas') ?? null
153
154
  memoContext = memo?.getContext('2d', opts.contextAttributes) ?? null
154
155
  }
155
- if (!memo || !memoContext) return null
156
+ if (!memo || !memoContext) return false
156
157
 
157
- const state = readDrawingState(context)
158
158
  memo.width = canvas.width
159
159
  memo.height = canvas.height
160
160
  if (canvas.width > 0 && canvas.height > 0) {
161
161
  memoContext.drawImage(canvas, 0, 0)
162
162
  }
163
- return state
163
+ return true
164
164
  }
165
165
 
166
166
  /**
@@ -172,13 +172,12 @@ export function createAnimationCanvas(
172
172
  * snapshot is rescaled once, from its full resolution.
173
173
  */
174
174
  function restoreSnapshot(
175
- state: DrawingContext | null,
175
+ hasSnapshot: boolean,
176
176
  previousWidth: number,
177
177
  previousHeight: number,
178
178
  ) {
179
- if (!state || !memo || !memoContext) return
179
+ if (!hasSnapshot || !memo) return
180
180
  if (memo.width <= 0 || memo.height <= 0) return
181
- writeDrawingState(context, state)
182
181
  context.drawImage(memo, 0, 0, previousWidth, previousHeight)
183
182
  }
184
183
 
@@ -186,60 +185,76 @@ export function createAnimationCanvas(
186
185
  const dpr = devicePixelRatio()
187
186
  const previousWidth = width
188
187
  const previousHeight = height
189
- const state = takeSnapshot()
188
+ const previousDpr = appliedDpr
189
+ const state: DrawingContext | null = sized
190
+ ? readDrawingState(context)
191
+ : null
192
+ const hasSnapshot = sized && takeSnapshot()
190
193
  applyDevicePixelRatio(canvas, context, w, h, dpr)
191
194
  width = w
192
195
  height = h
196
+ appliedDpr = dpr
193
197
  sized = true
194
- restoreSnapshot(state, previousWidth, previousHeight)
198
+ // Restore pixels while the reset context still has neutral alpha,
199
+ // compositing, filter, and shadow settings.
200
+ restoreSnapshot(hasSnapshot, previousWidth, previousHeight)
201
+ if (state) {
202
+ writeDrawingState(context, state, dpr / previousDpr)
203
+ }
195
204
  }
196
205
 
197
- function drawFrame() {
206
+ function applyDevicePixelRatioIfNeeded() {
207
+ if (sized && devicePixelRatio() !== appliedDpr) {
208
+ applySize(width, height)
209
+ }
210
+ }
211
+
212
+ function drawFrame(timestamp = performance.now()) {
198
213
  if (!sized) return
214
+ applyDevicePixelRatioIfNeeded()
199
215
  if (!initialized) {
200
216
  initialized = true
201
217
  opts.init?.(context, { width, height })
202
218
  }
203
- const now = performance.now()
204
- const deltaTime = now - previousTime
205
- previousTime = now
219
+ const deltaTime = timestamp - previousTime
220
+ previousTime = timestamp
206
221
  count += 1
207
222
  opts.draw(context, {
208
223
  width,
209
224
  height,
210
225
  count,
211
226
  deltaTime,
212
- elapsedTime: now - startTime,
213
- fps: 1000 / deltaTime,
227
+ elapsedTime: timestamp - startTime,
228
+ fps: deltaTime > 0 ? 1000 / deltaTime : 0,
214
229
  })
215
230
  }
216
231
 
217
- function tick() {
232
+ function tick(timestamp: DOMHighResTimeStamp) {
218
233
  // Scheduled before drawing so that a slow frame does not delay the next
219
234
  // request, matching how requestAnimationFrame loops are usually written.
220
235
  frameId = requestAnimationFrame(tick)
221
- drawFrame()
236
+ drawFrame(timestamp)
222
237
  }
223
238
 
224
239
  function startLoop() {
225
- if (frameId != null) return
240
+ if (frameId !== null) return
226
241
  previousTime = performance.now()
227
242
  frameId = requestAnimationFrame(tick)
228
243
  }
229
244
 
230
245
  function stopLoop() {
231
- if (frameId == null) return
246
+ if (frameId === null) return
232
247
  cancelAnimationFrame(frameId)
233
248
  frameId = null
234
249
  }
235
250
 
236
251
  let observer: ResizeObserver | null = null
237
252
 
238
- if (relativeSize) {
253
+ if (resizable) {
239
254
  const parent = canvas.parentElement
240
255
  if (!parent) {
241
256
  throw new Error(
242
- 'createAnimationCanvas: relativeSize needs the canvas to have a parent element',
257
+ 'createAnimationCanvas: resizable needs the canvas to have a parent element',
243
258
  )
244
259
  }
245
260
  // The observer reports the current size as soon as it starts, so it is the
@@ -249,7 +264,9 @@ export function createAnimationCanvas(
249
264
  observer = new ResizeObserver((entries) => {
250
265
  for (const entry of entries) {
251
266
  const { width: w, height: h } = entry.contentRect
252
- applySize(w, h)
267
+ if (w !== width || h !== height || devicePixelRatio() !== appliedDpr) {
268
+ applySize(w, h)
269
+ }
253
270
  // With the loop running the next frame covers the new size already.
254
271
  if (!(opts.animate ?? true)) drawFrame()
255
272
  }
@@ -272,14 +289,15 @@ export function createAnimationCanvas(
272
289
  opts = {
273
290
  ...opts,
274
291
  ...next,
275
- relativeSize,
292
+ resizable,
276
293
  contextAttributes: opts.contextAttributes,
277
294
  }
278
295
 
279
- if (!relativeSize) {
296
+ if (!resizable) {
280
297
  const { width: w = 100, height: h = 100 } = opts.size ?? {}
281
- if (w != width || h != height) applySize(w, h)
298
+ if (w !== width || h !== height) applySize(w, h)
282
299
  }
300
+ applyDevicePixelRatioIfNeeded()
283
301
 
284
302
  const animating = opts.animate ?? true
285
303
  if (animating && !wasAnimating) startLoop()