See More

// src/pointer/index.js var pathOf = (el) => { const path = []; for (let cur = el; cur; cur = cur.parentElement) path.push(cur); return path.reverse(); }; var prefixLen = (a, b) => { const n = Math.min(a.length, b.length); let i = 0; while (i < n && a[i] === b[i]) i++; return i; }; var eventDefaults = (type) => ({ pointerover: { bubbles: true, cancelable: true, composed: true }, pointerenter: { bubbles: false, cancelable: false, composed: false }, pointerdown: { bubbles: true, cancelable: true, composed: true }, pointermove: { bubbles: true, cancelable: true, composed: true }, pointerup: { bubbles: true, cancelable: true, composed: true }, pointerout: { bubbles: true, cancelable: true, composed: true }, pointerleave: { bubbles: false, cancelable: false, composed: false }, pointercancel: { bubbles: true, cancelable: true, composed: true }, gotpointercapture: { bubbles: true, cancelable: false, composed: true }, lostpointercapture: { bubbles: true, cancelable: false, composed: true } })[type] ?? { bubbles: true, cancelable: true, composed: true }; var mouseCompat = { pointerover: "mouseover", pointerenter: "mouseenter", pointerdown: "mousedown", pointermove: "mousemove", pointerup: "mouseup", pointerout: "mouseout", pointerleave: "mouseleave" }; var clamp = (min, max, n) => Math.min(max, Math.max(min, n)); var rand01 = (seed) => { let t = seed + 1831565813 >>> 0; t = Math.imul(t ^ t >>> 15, t | 1); t ^= t + Math.imul(t ^ t >>> 7, t | 61); return ((t ^ t >>> 14) >>> 0) / 4294967296; }; var phase = (seed) => rand01(seed) * Math.PI * 2; var drift = (min, max, t, phi) => min + (max - min) * ((Math.sin(Math.PI * 2 * t + phi) + 1) / 2); var Pointer = class _Pointer { static id = () => { const { crypto } = globalThis; if (!crypto?.getRandomValues) throw new Error("crypto.getRandomValues is required"); return crypto.getRandomValues(new Uint32Array(1))[0]; }; #id; #primary; #pressed = false; #path = []; #target = null; #captureTarget = null; #lastPoint = null; #lastMovePoint = null; constructor({ id = _Pointer.id(), primary } = {}) { if (typeof primary !== "boolean") throw new TypeError("Pointer.primary must be boolean"); this.#id = id; this.#primary = primary; } get id() { return this.#id; } get target() { return this.#target; } get type() { throw new Error("Pointer.type must be implemented"); } get emitsTouch() { return false; } get implicitCapture() { return false; } props(i, total) { return {}; } enter(target, point) { const next = pathOf(target); this.#target = target; this.#path = next; this.#lastMovePoint = point; this.#dispatch("pointerover", target, point, { bubbles: true }); for (const el of next) this.#dispatch("pointerenter", el, point, { bubbles: false }); return this; } down(target, point, i, total) { this.#pressed = true; this.#target = target; this.#lastPoint = point; this.#lastMovePoint = point; if (this.implicitCapture) this.#captureTarget = target; this.#dispatch("pointerdown", target, point, { bubbles: true, button: 0, buttons: 1 }, { i, total }); return this; } move(target, point, i, total) { const nextTarget = this.#captureTarget ?? target; if (!this.#captureTarget) this.#transition(nextTarget, point, i, total); this.#dispatch("pointermove", this.#captureTarget ?? this.#target, point, { bubbles: true, buttons: this.#pressed ? 1 : 0 }, { i, total }); return this; } up(target, point, i, total) { this.#pressed = false; this.#target = this.#captureTarget ?? target; this.#dispatch("pointerup", this.#target, point, { bubbles: true, button: 0, buttons: 0 }, { i, total }); return this; } leave(target, point, i, total) { if (!this.#target) return this; this.release(this.#captureTarget, i, total); this.#dispatch( "pointerout", this.#target, point, { bubbles: true }, { i, total } ); for (const el of [...this.#path].reverse()) this.#dispatch( "pointerleave", el, point, { bubbles: false }, { i, total } ); this.#pressed = false; this.#path = []; this.#target = null; this.#captureTarget = null; this.#lastPoint = null; this.#lastMovePoint = null; return this; } cancel(target, point, i, total) { if (!this.#target) return this; this.#dispatch("pointercancel", this.#target, point, { bubbles: true, buttons: 0 }, { i, total }); return this.leave(target, point, i, total); } capture(target, i, total) { this.#captureTarget = target; if (target !== this.#target) { this.#target = target; this.#path = pathOf(target); } this.#dispatch("gotpointercapture", target, this.#lastPoint ?? null, { bubbles: true }, { i, total }); return this; } release(target, i, total) { if (!this.#captureTarget) return this; this.#captureTarget = null; this.#dispatch("lostpointercapture", target, this.#lastPoint ?? null, { bubbles: true }, { i, total }); return this; } touch(target, point) { if (!this.emitsTouch) return null; const Touch = globalThis.Touch; if (typeof Touch !== "function") throw new Error("Touch is not available in this environment"); const { width, height } = this.props(0, 1); const win = target.ownerDocument?.defaultView ?? globalThis.window; const scrollX = typeof win?.scrollX === "number" ? win.scrollX : 0; const scrollY = typeof win?.scrollY === "number" ? win.scrollY : 0; const clientX = point.x; const clientY = point.y; return new Touch({ identifier: this.id, target, clientX, clientY, pageX: clientX + scrollX, pageY: clientY + scrollY, screenX: clientX, screenY: clientY, radiusX: width / 2, radiusY: height / 2, rotationAngle: 0, force: 0 }); } #transition(nextTarget, point, i, total) { if (nextTarget === this.#target) return; const prev = this.#path; const next = pathOf(nextTarget); const common = prefixLen(prev, next); this.#dispatch( "pointerout", this.#target, point, { bubbles: true }, { i, total } ); for (const el of prev.slice(common).reverse()) this.#dispatch( "pointerleave", el, point, { bubbles: false }, { i, total } ); this.#dispatch( "pointerover", nextTarget, point, { bubbles: true }, { i, total } ); for (const el of next.slice(common)) this.#dispatch( "pointerenter", el, point, { bubbles: false }, { i, total } ); this.#path = next; this.#target = nextTarget; } #dispatch(type, target, point, extra, { i = 0, total = 1 } = {}) { const Event2 = globalThis.PointerEvent; if (typeof Event2 !== "function") throw new Error("PointerEvent is not available in this environment"); const defaults = eventDefaults(type); const base = { pointerId: this.#id, pointerType: this.type, isPrimary: this.#primary, hasCapture: Boolean(this.#captureTarget) }; const coords = point ? { clientX: point.x, clientY: point.y, pageX: point.x + (target.ownerDocument?.defaultView?.scrollX ?? 0), pageY: point.y + (target.ownerDocument?.defaultView?.scrollY ?? 0), screenX: point.x, screenY: point.y } : {}; const props = this.props(i, total); const movement = type === "pointermove" ? this.#movement(point) : {}; const init = { ...defaults, ...base, ...coords, ...props, ...movement, ...extra }; target.dispatchEvent(new Event2(type, init)); const mtype = mouseCompat[type]; if (this.type === "mouse" && mtype) target.dispatchEvent(new MouseEvent(mtype, init)); if (point) this.#lastPoint = point; } #movement(point) { if (!point || !this.#lastMovePoint) return { movementX: 0, movementY: 0 }; const movementX = point.x - this.#lastMovePoint.x; const movementY = point.y - this.#lastMovePoint.y; this.#lastMovePoint = point; return { movementX, movementY }; } }; var PenPointer = class extends Pointer { get type() { return "pen"; } get emitsTouch() { return true; } props(i, total) { return { width: 0.5, height: 0.5, pressure: 0.5, tangentialPressure: 0, tiltX: 0, tiltY: 0, twist: 0, altitudeAngle: 1, azimuthAngle: 0.6 }; } }; var IosPenPointer = class extends PenPointer { props(i, total) { const t = total <= 1 ? 0 : i / (total - 1); const seed = this.id; const pressureBase = 0.08 + 0.52 * Math.sin(Math.PI * t) ** 8; const noise = (rand01(seed + 1e4 + i * 101) - 0.5) * 0.04; const pressure = clamp(0.08, 0.6, pressureBase + noise); return { width: 0.5, height: 0.5, pressure, tiltX: drift(22, 35, t, phase(seed + 1)), tiltY: drift(20, 30, t, phase(seed + 2)), tangentialPressure: 0, twist: 0, altitudeAngle: drift(0.86, 1.04, t, phase(seed + 3)), azimuthAngle: drift(0.47, 0.83, t, phase(seed + 4)) }; } }; var MousePointer = class extends Pointer { get type() { return "mouse"; } props(i, total) { return { width: 1, height: 1, pressure: 0, tiltX: 0, tiltY: 0 }; } }; var IosMousePointer = class extends MousePointer { }; var TouchPointer = class extends Pointer { get type() { return "touch"; } get emitsTouch() { return true; } get implicitCapture() { return true; } props(i, total) { return { width: 42, height: 42, pressure: 0, tangentialPressure: 0, tiltX: 0, tiltY: 0, twist: 0, altitudeAngle: Math.PI / 2, azimuthAngle: 0 }; } }; var IosTouchPointer = class extends TouchPointer { props(i, total) { return { ...super.props(i, total), width: 41.72413777559996, height: 41.72413777559996 }; } }; var webkit = { pen: IosPenPointer, mouse: IosMousePointer, touch: IosTouchPointer }; // src/motion/index.js var Motion = class { #el; #platform; #touches = /* @__PURE__ */ new Map(); #lastTarget = null; #hitWarned = false; constructor(el, opts = {}) { if (!(el instanceof Element)) throw new TypeError("Motion.el must be an Element"); if (opts === null || typeof opts !== "object") throw new TypeError("Motion.opts must be an object"); const { platform = webkit } = opts; this.#el = el; this.#platform = platform; } get el() { return this.#el; } get platform() { return this.#platform; } get device() { throw new Error("Motion.device must be implemented"); } static normalizePoints(name, raw) { const pointsKey = `${name}.points`; if (!Array.isArray(raw)) throw new TypeError(`${pointsKey} must be [x, y, ms][]`); const points = raw.map((p, i) => { if (!Array.isArray(p) || p.length !== 3) throw new TypeError(`${pointsKey}[${i}] must be [x, y, ms]`); const [x, y, ms] = p; if (![x, y, ms].every((n) => typeof n === "number" && Number.isFinite(n))) throw new TypeError(`${pointsKey}[${i}] must contain finite numbers`); if (ms < 0) throw new RangeError(`${pointsKey}[${i}][2] must be >= 0`); return { x, y, ms }; }); for (let i = 1; i < points.length; i++) { if (points[i].ms < points[i - 1].ms) throw new RangeError( `${pointsKey}[${i}][2] must be >= ${pointsKey}[${i - 1}][2]` ); } return points; } pointer(opts) { const Ctor = this.#platform?.[this.device]; if (!Ctor) throw new Error(`Platform missing device: ${this.device}`); return new Ctor(opts); } hit(point) { const target = document.elementFromPoint(point.x, point.y); if (target && this.#el.contains(target)) return this.#lastTarget = target; if (!this.#hitWarned) { console.warn(`hit-test missed: (${point.x},${point.y}) outside element`); this.#hitWarned = true; } return this.#lastTarget ?? this.#el; } delay(ms) { if (!ms) return Promise.resolve(); return new Promise( (resolve) => setTimeout(resolve, ms) ); } touchstart(pointer, point, gesture = { scale: 1, rotation: 0 }) { const target = pointer.target; if (!pointer.emitsTouch || !target) return; this.#touches.set(pointer.id, { pointer, target, point }); const changed = pointer.touch(target, point); this.#dispatchTouch("touchstart", target, changed, gesture); } touchsync(pointer, point) { if (!pointer.emitsTouch) return; const entry = this.#touches.get(pointer.id); if (!entry) return; entry.point = point; } touchmove(pointer, point, gesture = { scale: 1, rotation: 0 }) { if (!pointer.emitsTouch) return; const entry = this.#touches.get(pointer.id); if (!entry) return; entry.point = point; const changed = pointer.touch(entry.target, point); this.#dispatchTouch("touchmove", entry.target, changed, gesture); } touchend(pointer, point, gesture = { scale: 1, rotation: 0 }) { if (!pointer.emitsTouch) return; const entry = this.#touches.get(pointer.id); if (!entry) return; const changed = pointer.touch(entry.target, point); this.#touches.delete(pointer.id); this.#dispatchTouch("touchend", entry.target, changed, gesture); } touchcancel(pointer, point, gesture = { scale: 1, rotation: 0 }) { if (!pointer.emitsTouch) return; const entry = this.#touches.get(pointer.id); if (!entry) return; const changed = pointer.touch(entry.target, point); this.#touches.delete(pointer.id); this.#dispatchTouch("touchcancel", entry.target, changed, gesture); } gesturestart(target, gesture = { scale: 1, rotation: 0 }) { this.#dispatchGesture("gesturestart", target, gesture); } gesturechange(target, gesture) { this.#dispatchGesture("gesturechange", target, gesture); } gestureend(target, gesture) { this.#dispatchGesture("gestureend", target, gesture); } #dispatchTouch(type, target, changed, gesture) { const Event2 = globalThis.TouchEvent; if (typeof Event2 !== "function") throw new Error("TouchEvent is not available in this environment"); const touches = [...this.#touches.values()].map( (entry) => entry.target === target && entry.pointer.id === changed.identifier ? changed : entry.pointer.touch(entry.target, entry.point) ); const init = { bubbles: true, cancelable: true, composed: true, touches, targetTouches: touches.filter((t) => t.target === target), changedTouches: [changed], altKey: false, ctrlKey: false, metaKey: false, shiftKey: false, scale: gesture.scale, rotation: gesture.rotation }; target.dispatchEvent(new Event2(type, init)); } #dispatchGesture(type, target, { scale = 1, rotation = 0 } = {}) { const Event2 = globalThis.GestureEvent; if (typeof Event2 !== "function") return; const points = [...this.#touches.values()].map((entry) => entry.point).filter(Boolean); const coords = points.length ? (() => { const clientX = points.reduce((sum, p) => sum + p.x, 0) / points.length; const clientY = points.reduce((sum, p) => sum + p.y, 0) / points.length; const win = target.ownerDocument?.defaultView ?? globalThis.window; const scrollX = typeof win?.scrollX === "number" ? win.scrollX : 0; const scrollY = typeof win?.scrollY === "number" ? win.scrollY : 0; return { clientX, clientY, pageX: clientX + scrollX, pageY: clientY + scrollY }; })() : {}; target.dispatchEvent(new Event2(type, { bubbles: true, cancelable: true, composed: true, ...coords, scale, rotation })); } }; // src/font/index.js var attr = (tag, name) => { const match = tag.match(new RegExp(`${name}="([^"]*)"`, "i")); return match?.[1] ?? null; }; var parseD = (d) => { if (!d) return []; const tokens = d.trim().split(/\s+/); const strokes = []; let current = null; for (let i = 0; i < tokens.length; i++) { const t = tokens[i]; if (t === "M") { current = [[+tokens[i + 1], +tokens[i + 2]]]; strokes.push(current); i += 2; } else if (t === "L") { current?.push([+tokens[i + 1], +tokens[i + 2]]); i += 2; } } return strokes.filter((s) => s.length > 1); }; var parseSvg = (svg) => { const fontTag = svg.match(/]*>/i)?.[0]; if (!fontTag) throw new TypeError("missing element"); const faceTag = svg.match(/]*>/i)?.[0]; if (!faceTag) throw new TypeError("missing element"); const defaultAdvance = +(attr(fontTag, "horiz-adv-x") || 0); const unitsPerEm = +(attr(faceTag, "units-per-em") || 1e3); const ascent = +(attr(faceTag, "ascent") || 800); const glyphs = /* @__PURE__ */ new Map(); const re = /]*>/gi; let m; while (m = re.exec(svg)) { const tag = m[0]; const unicode = attr(tag, "unicode"); if (unicode == null) continue; glyphs.set(unicode, { advance: +(attr(tag, "horiz-adv-x") || defaultAdvance), strokes: parseD(attr(tag, "d")) }); } return { unitsPerEm, ascent, defaultAdvance, glyphs }; }; var Font = class _Font { #meta; constructor(meta) { this.#meta = meta; } static from(svg) { return new _Font(parseSvg(svg)); } static async load(url) { const res = await fetch(url); if (!res.ok) throw new Error(`Font fetch failed: ${res.status}`); return _Font.from(await res.text()); } layout(text, { size }) { const { unitsPerEm, ascent, glyphs } = this.#meta; const scale = size / unitsPerEm; const result = []; let cursor = 0; for (const ch of text) { const glyph = glyphs.get(ch); if (!glyph) throw new TypeError(`Unknown glyph: "${ch}"`); for (const stroke of glyph.strokes) result.push( stroke.map(([x, y]) => [ (x + cursor) * scale, (ascent - y) * scale ]) ); cursor += glyph.advance; } return result; } }; // src/path/index.js var FREQ = 60; var cached = null; var defaultFont = () => cached ??= Font.load( new URL("../../fonts/hershey-script.svg", import.meta.url).href ); var segmentLengths = (points) => points.slice(1).map( (p, i) => Math.hypot(p[0] - points[i][0], p[1] - points[i][1]) ); var interpolate = (waypoints, offsetX, offsetY, velocity) => { const lengths = segmentLengths(waypoints); const total = lengths.reduce((a, b) => a + b, 0); if (total === 0) return [{ x: waypoints[0][0] + offsetX, y: waypoints[0][1] + offsetY, ms: 0 }]; const duration = total / velocity; const frames = Math.max(1, Math.round(duration * FREQ)); return Array.from({ length: frames + 1 }, (_, f) => { const t = f / frames; let dist = t * total; let seg = 0; while (seg < lengths.length - 1 && dist > lengths[seg]) { dist -= lengths[seg]; seg++; } const ratio = lengths[seg] > 0 ? dist / lengths[seg] : 0; const [ax, ay] = waypoints[seg]; const [bx, by] = waypoints[seg + 1]; return { x: ax + (bx - ax) * ratio + offsetX, y: ay + (by - ay) * ratio + offsetY, ms: f * (1e3 / FREQ) }; }); }; var PathMotion = class extends Motion { #strokes; #pending; constructor(el, input, opts = {}) { const name = new.target.name; if (typeof input === "string") { const { font, size, x, y, ...rest } = opts; super(el, rest); if (typeof size !== "number" || !Number.isFinite(size) || size <= 0) throw new TypeError(`${name}.size must be a finite number > 0`); if (typeof x !== "number" || !Number.isFinite(x)) throw new TypeError(`${name}.x must be a finite number`); if (typeof y !== "number" || !Number.isFinite(y)) throw new TypeError(`${name}.y must be a finite number`); this.#pending = { text: input, font, size, x, y }; this.#strokes = null; } else { super(el, opts); this.#strokes = [Motion.normalizePoints(name, input)]; this.#pending = null; } } get strokes() { return this.#strokes; } async resolve() { if (this.#strokes) return; const { text, font: fontOpt, size, x, y } = this.#pending; const font = fontOpt instanceof Font ? fontOpt : typeof fontOpt === "string" ? await Font.load(fontOpt) : await defaultFont(); const velocity = size * 4; this.#strokes = font.layout(text, { size }).map((waypoints) => interpolate(waypoints, x, y, velocity)); } }; // motions/drag/index.js var DragMotion = class extends PathMotion { get device() { return "mouse"; } async perform() { await this.resolve(); for (const points of this.strokes) { if (!points.length) continue; const pointer = this.pointer({ primary: true }); const n = points.length; let point = points[0]; let target = this.hit(point); pointer.enter(target, point).down(target, point, 0, n); try { for (let i = 1; i < n; i++) { await this.delay(points[i].ms - points[i - 1].ms); target = this.hit(points[i]); point = points[i]; pointer.move(target, point, i, n); } pointer.up(target, point, n - 1, n).leave(target, point, n - 1, n); } catch (err) { pointer.cancel(target, point); throw new Error(`drag aborted: ${err?.message ?? String(err)}`, { cause: err }); } } } }; // motions/glide/index.js var GlideMotion = class extends PathMotion { get device() { return "touch"; } async perform() { await this.resolve(); for (const points of this.strokes) { if (!points.length) continue; const pointer = this.pointer({ primary: true }); const n = points.length; const target = this.hit(points[0]); let point = points[0]; pointer.enter(target, point).down(target, point, 0, n); try { pointer.capture(target); this.touchstart(pointer, point); for (let i = 1; i < n; i++) { await this.delay(points[i].ms - points[i - 1].ms); this.hit(points[i]); point = points[i]; pointer.move(target, point, i, n); this.touchmove(pointer, point); } pointer.up(target, point, n - 1, n).release(target, n - 1, n).leave(target, point, n - 1, n); this.touchend(pointer, point); } catch (err) { pointer.cancel(target, point); this.touchcancel(pointer, point); throw new Error(`glide aborted: ${err?.message ?? String(err)}`, { cause: err }); } } } }; // motions/stroke/index.js var StrokeMotion = class extends PathMotion { get device() { return "pen"; } async perform() { await this.resolve(); for (const points of this.strokes) { if (!points.length) continue; const pointer = this.pointer({ primary: true }); const n = points.length; let point = points[0]; let target = this.hit(point); let i = 0; pointer.enter(target, point).down(target, point, 0, n); try { for (let next = 1; next < n; next++) { await this.delay(points[next].ms - points[next - 1].ms); target = this.hit(points[next]); point = points[next]; pointer.move(target, point, next, n); i = next; } pointer.up(target, point, n - 1, n).leave(target, point, n - 1, n); } catch (err) { pointer.cancel(target, point, i, n); throw new Error(`stroke aborted: ${err?.message ?? String(err)}`, { cause: err }); } } } }; // src/gesture/index.js var frameDelay = () => { const raf = globalThis.requestAnimationFrame; if (typeof raf !== "function") throw new Error("requestAnimationFrame is required"); return new Promise((resolve) => raf(() => resolve())); }; var GestureMotion = class extends Motion { #center; #steps; constructor(el, opts = {}) { const name = new.target.name; if (opts === null || typeof opts !== "object") throw new TypeError(`${name}.opts must be an object`); const { x, y, steps = 20, ...rest } = opts; super(el, rest); if (typeof x !== "number" || !Number.isFinite(x)) throw new TypeError(`${name}.x must be a finite number`); if (typeof y !== "number" || !Number.isFinite(y)) throw new TypeError(`${name}.y must be a finite number`); if (!Number.isInteger(steps) || steps <= 0) throw new RangeError(`${name}.steps must be positive integer`); this.#center = { x, y }; this.#steps = steps; } get device() { return "touch"; } get center() { return this.#center; } get steps() { return this.#steps; } positions() { throw new Error("GestureMotion.positions must be implemented"); } gesture(a, b, base) { const dx = b.x - a.x; const dy = b.y - a.y; const dist = Math.hypot(dx, dy); const scale = dist / base.dist; const rotation = (Math.atan2(dy, dx) - base.angle) * (180 / Math.PI); return { scale, rotation }; } async perform() { const pos = this.positions(); const n = pos.length; const a0 = this.hit(pos[0][0]); const b0 = this.hit(pos[0][1]); const a = this.pointer({ primary: true }); const b = this.pointer({ primary: false }); const base = { dist: Math.hypot( pos[0][1].x - pos[0][0].x, pos[0][1].y - pos[0][0].y ), angle: Math.atan2( pos[0][1].y - pos[0][0].y, pos[0][1].x - pos[0][0].x ) }; let gesture = { scale: 1, rotation: 0 }; let lastPos = pos[0]; let gestureStarted = false; let gestureEnded = false; try { a.enter(a0, pos[0][0]); a.down(a0, pos[0][0], 0, n); a.capture(a0); this.touchstart(a, pos[0][0], { scale: 1, rotation: 0 }); b.enter(b0, pos[0][1]); b.down(b0, pos[0][1], 0, n); b.capture(b0); this.touchstart(b, pos[0][1], { scale: 1, rotation: 0 }); this.gesturestart(a0, { scale: 1, rotation: 0 }); this.gesturestart(b0, { scale: 1, rotation: 0 }); gestureStarted = true; for (let i = 1; i < n; i++) { await frameDelay(); this.hit(pos[i][0]); this.hit(pos[i][1]); lastPos = pos[i]; gesture = this.gesture(pos[i][0], pos[i][1], base); a.move(a0, pos[i][0], i, n); b.move(b0, pos[i][1], i, n); this.touchsync(a, pos[i][0]); this.touchsync(b, pos[i][1]); this.touchmove(a, pos[i][0], gesture); this.touchmove(b, pos[i][1], gesture); this.gesturechange(a0, gesture); this.gesturechange(b0, gesture); } this.gestureend(a0, gesture); this.gestureend(b0, gesture); gestureEnded = true; a.up(a0, pos[n - 1][0], n - 1, n); a.release(a0, n - 1, n); a.leave(a0, pos[n - 1][0], n - 1, n); this.touchend(a, pos[n - 1][0], gesture); b.up(b0, pos[n - 1][1], n - 1, n); b.release(b0, n - 1, n); b.leave(b0, pos[n - 1][1], n - 1, n); this.touchend(b, pos[n - 1][1], { scale: 1, rotation: 0 }); } catch (err) { if (gestureStarted && !gestureEnded) { this.gestureend(a0, gesture); this.gestureend(b0, gesture); } a.cancel(a0, lastPos[0]); this.touchcancel(a, lastPos[0]); b.cancel(b0, lastPos[1]); this.touchcancel(b, lastPos[1]); const verb = this.constructor.name.replace(/Motion$/, "").toLowerCase(); throw new Error( `${verb} aborted: ${err?.message ?? String(err)}`, { cause: err } ); } } }; // motions/pinch/index.js var lerp = (a, b, t) => a + (b - a) * t; var PinchMotion = class extends GestureMotion { #scale; #distance; constructor(el, scale, opts = {}) { const name = new.target.name; if (opts === null || typeof opts !== "object") throw new TypeError(`${name}.opts must be an object`); const { distance = 100, ...rest } = opts; super(el, rest); if (typeof scale !== "number" || !Number.isFinite(scale)) throw new TypeError(`${name}.scale must be a finite number`); if (scale <= 0) throw new RangeError(`${name}.scale must be > 0`); if (typeof distance !== "number" || !Number.isFinite(distance)) throw new TypeError(`${name}.distance must be a finite number`); if (distance <= 0) throw new RangeError(`${name}.distance must be > 0`); this.#scale = scale; this.#distance = distance; } positions() { const { x, y } = this.center; const steps = this.steps; const scale = this.#scale; const distance = this.#distance; return Array.from({ length: steps + 1 }, (_, i) => { const t = i / steps; const d = distance * lerp(1, scale, t) / 2; return [ { x: x - d, y }, { x: x + d, y } ]; }); } }; // motions/twist/index.js var wrap180 = (deg) => ((deg + 180) % 360 + 360) % 360 - 180; var TwistMotion = class extends GestureMotion { #degrees; #radius; constructor(el, degrees = 45, opts = {}) { const name = new.target.name; if (opts === null || typeof opts !== "object") throw new TypeError(`${name}.opts must be an object`); const { radius = 80, ...rest } = opts; super(el, rest); if (typeof degrees !== "number" || !Number.isFinite(degrees)) throw new TypeError(`${name}.degrees must be a finite number`); if (typeof radius !== "number" || !Number.isFinite(radius)) throw new TypeError(`${name}.radius must be a finite number`); if (radius <= 0) throw new RangeError(`${name}.radius must be > 0`); this.#degrees = degrees; this.#radius = radius; } positions() { const { x, y } = this.center; const steps = this.steps; const degrees = this.#degrees; const radius = this.#radius; return Array.from({ length: steps + 1 }, (_, i) => { const t = i / steps; const angle = degrees * Math.PI * t / 180; return [ { x: x + Math.cos(angle) * radius, y: y + Math.sin(angle) * radius }, { x: x + Math.cos(angle + Math.PI) * radius, y: y + Math.sin(angle + Math.PI) * radius } ]; }); } gesture(a, b, base) { const raw = super.gesture(a, b, base); return { scale: raw.scale, rotation: wrap180(raw.rotation) }; } }; // motions/swipe/index.js var SwipeMotion = class extends GestureMotion { #distance; #angle; #separation; constructor(el, distance, opts = {}) { const name = new.target.name; if (opts === null || typeof opts !== "object") throw new TypeError(`${name}.opts must be an object`); const { angle = 0, separation = 40, ...rest } = opts; super(el, rest); if (typeof distance !== "number" || !Number.isFinite(distance)) throw new TypeError( `${name}.distance must be a finite number` ); if (distance <= 0) throw new RangeError(`${name}.distance must be > 0`); if (typeof angle !== "number" || !Number.isFinite(angle)) throw new TypeError( `${name}.angle must be a finite number` ); if (typeof separation !== "number" || !Number.isFinite(separation)) throw new TypeError( `${name}.separation must be a finite number` ); if (separation <= 0) throw new RangeError(`${name}.separation must be > 0`); this.#distance = distance; this.#angle = angle; this.#separation = separation; } positions() { const { x, y } = this.center; const steps = this.steps; const distance = this.#distance; const rad = this.#angle * Math.PI / 180; const sep = this.#separation / 2; const px = -Math.sin(rad) * sep; const py = Math.cos(rad) * sep; return Array.from({ length: steps + 1 }, (_, i) => { const t = i / steps; const dx = Math.cos(rad) * distance * t; const dy = Math.sin(rad) * distance * t; return [ { x: x + px + dx, y: y + py + dy }, { x: x - px + dx, y: y - py + dy } ]; }); } }; // src/glass/index.js var css = ` [data-glass] { position: fixed; inset: 0; z-index: 2147483647; pointer-events: none; color-scheme: light dark; canvas { display: block; width: 100%; height: 100%; background: light-dark( rgba(0, 0, 0, 0.15), rgba(255, 255, 255, 0.15) ); } table { position: absolute; bottom: calc(12px + env(safe-area-inset-bottom, 0px)); left: calc(12px + env(safe-area-inset-left, 0px)); border: none; border-collapse: collapse; font: 11px/1.4 system-ui, -apple-system, sans-serif; color: light-dark(#1d1d1f, #f5f5f7); background: none; } td { padding: 4px 10px; border: none; } td + td { border-left: 0.5px solid light-dark( rgba(0, 0, 0, 0.06), rgba(255, 255, 255, 0.06) ); } tr + tr td { border-top: 0.5px solid light-dark( rgba(0, 0, 0, 0.06), rgba(255, 255, 255, 0.06) ); } button { position: absolute; top: calc(1rem + env(safe-area-inset-top, 0px)); right: calc(1rem + env(safe-area-inset-right, 0px)); pointer-events: auto; cursor: pointer; background: none; color: light-dark( rgba(0, 0, 0, 0.25), rgba(255, 255, 255, 0.25) ); border: 0.5px solid currentColor; border-radius: 5px; font: 200 11px/1 system-ui, -apple-system, sans-serif; padding: 6px 10px; display: inline-flex; align-items: baseline; justify-content: center; gap: 3px; span { translate: 0 0.5px; } &:hover { color: light-dark( rgba(0, 0, 0, 0.45), rgba(255, 255, 255, 0.45) ); } } &[data-minimized] { canvas { display: none; } table { display: none; } button { opacity: 0.5; } } } `; var js = `;(() => { const el = document.querySelector('[data-glass]') const canvas = el.querySelector('canvas') const tbody = el.querySelector('tbody') const btn = el.querySelector('button') const ctx = canvas.getContext('2d') const dpr = devicePixelRatio || 1 canvas.width = innerWidth * dpr canvas.height = innerHeight * dpr ctx.scale(dpr, dpr) const families = [ { name: 'pointer', color: '#0A84FF', types: [ 'pointerdown', 'pointermove', 'pointerup', 'pointerover', 'pointerout', 'pointerenter', 'pointerleave', 'pointercancel', 'gotpointercapture', 'lostpointercapture' ]}, { name: 'touch', color: '#30D158', types: [ 'touchstart', 'touchmove', 'touchend', 'touchcancel' ]}, { name: 'gesture', color: '#BF5AF2', types: [ 'gesturestart', 'gesturechange', 'gestureend' ]}, { name: 'mouse', color: '#FF9F0A', types: [ 'mousedown', 'mousemove', 'mouseup', 'mouseover', 'mouseout', 'mouseenter', 'mouseleave' ]}, { name: 'wheel', color: '#64D2FF', types: [ 'wheel' ]}, ] const lookup = new Map() for (const f of families) for (const t of f.types) lookup.set(t, f) const counts = new Map() const dot = e => { if (e.isTrusted) return const f = lookup.get(e.type) if (!f) return if (f.name === 'pointer' && e.pointerType !== 'pen') return const x = e.changedTouches?.[0]?.clientX ?? e.clientX const y = e.changedTouches?.[0]?.clientY ?? e.clientY if (x == null || y == null) return if (!el.hasAttribute('data-minimized')) { ctx.fillStyle = f.color ctx.globalAlpha = 0.8 ctx.beginPath() ctx.arc(x, y, 1, 0, Math.PI * 2) ctx.fill() ctx.globalAlpha = 1 } const entry = counts.get(f.name) if (entry) { entry.n++ entry.cell.textContent = entry.n return } const row = tbody.insertRow() const dc = row.insertCell() dc.textContent = '\\u25cf' dc.style.color = f.color row.insertCell().textContent = f.name const countCell = row.insertCell() countCell.textContent = 1 counts.set(f.name, { n: 1, cell: countCell }) } btn.addEventListener('click', () => { el.toggleAttribute('data-minimized') btn.innerHTML = el.hasAttribute('data-minimized') ? '\\u25cf glass' : '\\u2715 close' }) for (const [type] of lookup) document.addEventListener(type, dot, true) el.addEventListener('glass:remove', () => { for (const [type] of lookup) document.removeEventListener(type, dot, true) }) })()`; var Glass = class { #el; constructor(fn) { this.#el = document.createElement("div"); this.#el.setAttribute("data-glass", ""); const style = document.createElement("style"); style.textContent = css; const canvas = document.createElement("canvas"); const table = document.createElement("table"); table.innerHTML = ""; const btn = document.createElement("button"); btn.innerHTML = "\u2715 close"; const script = document.createElement("script"); script.textContent = js; this.#el.append(style, canvas, table, btn, script); document.body.appendChild(this.#el); if (typeof fn === "function") fn(); } remove() { this.#el.dispatchEvent(new Event("glass:remove")); this.#el.remove(); } }; export { DragMotion, Font, Glass, GlideMotion, PinchMotion, StrokeMotion, SwipeMotion, TwistMotion };