solid-drift 0.7.1 → 0.9.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.
@@ -0,0 +1,207 @@
1
+ import { createEffect, createSignal, onCleanup, untrack, } from "solid-js";
2
+ import { now, schedule } from "./engine.js";
3
+ import { prefersReducedMotion } from "./reduced-motion.js";
4
+ const zero = () => 0;
5
+ const idle = () => "idle";
6
+ /**
7
+ * Pointer drag with spring physics, constraints, and momentum.
8
+ *
9
+ * The gesture workhorse: draggable cards, sliders, bottom-sheet handles,
10
+ * sortable rows. While the pointer is down the element tracks it 1:1;
11
+ * on release it glides with inertia and springs into its constraints,
12
+ * stretching elastically past the edges while dragged.
13
+ *
14
+ * This is the framer-motion `drag` vocabulary rebuilt signal-native:
15
+ * `axis` locks direction, `constraints` bounds the travel, `elastic`
16
+ * controls the overshoot, and `momentum` toggles the release glide.
17
+ * For the physics-toy flavor (friction plus bouncing off walls), see
18
+ * `createFling` instead.
19
+ *
20
+ * Set `touch-action: none` on the draggable element so touch drags do
21
+ * not fight the page scroll.
22
+ *
23
+ * SSR-safe and reduced-motion aware: on the server everything rests at
24
+ * 0; under reduced motion the drag still tracks the pointer (direct
25
+ * manipulation is not animation) but release snaps instantly to the
26
+ * constrained target with no glide.
27
+ *
28
+ * ```tsx
29
+ * let card!: HTMLDivElement
30
+ * const { x, y, status } = createDrag(() => card, {
31
+ * constraints: { left: 0, right: 300, top: 0, bottom: 0 },
32
+ * elastic: 0.4,
33
+ * })
34
+ * <div
35
+ * ref={card}
36
+ * style={{
37
+ * transform: `translate(${x()}px, ${y()}px)`,
38
+ * "touch-action": "none",
39
+ * cursor: status() === "dragging" ? "grabbing" : "grab",
40
+ * }}
41
+ * >
42
+ * Drag me
43
+ * </div>
44
+ * ```
45
+ */
46
+ export function createDrag(ref, options = {}) {
47
+ if (typeof window === "undefined") {
48
+ return { x: zero, y: zero, status: idle };
49
+ }
50
+ const { axis = "both", constraints, elastic = 0.35, momentum = true, inertia = 0.2, spring = {}, onDragStart, onDragEnd, } = options;
51
+ const [x, setX] = createSignal(0);
52
+ const [y, setY] = createSignal(0);
53
+ const [status, setStatus] = createSignal("idle");
54
+ let cancelSettle = null;
55
+ const clampAxis = (value, min, max) => {
56
+ if (min !== undefined && value < min)
57
+ return min;
58
+ if (max !== undefined && value > max)
59
+ return max;
60
+ return value;
61
+ };
62
+ /** Elastic resistance past the constraints while dragging. */
63
+ const applyConstraints = (px, py) => {
64
+ if (!constraints)
65
+ return [px, py];
66
+ const { left, top, right, bottom } = constraints;
67
+ let ox = px;
68
+ let oy = py;
69
+ if (left !== undefined && ox < left)
70
+ ox = left + (ox - left) * elastic;
71
+ if (right !== undefined && ox > right)
72
+ ox = right + (ox - right) * elastic;
73
+ if (top !== undefined && oy < top)
74
+ oy = top + (oy - top) * elastic;
75
+ if (bottom !== undefined && oy > bottom)
76
+ oy = bottom + (oy - bottom) * elastic;
77
+ return [ox, oy];
78
+ };
79
+ const hardClamp = (px, py) => {
80
+ if (!constraints)
81
+ return [px, py];
82
+ const { left, top, right, bottom } = constraints;
83
+ return [clampAxis(px, left, right), clampAxis(py, top, bottom)];
84
+ };
85
+ const stopSettle = () => {
86
+ cancelSettle?.();
87
+ cancelSettle = null;
88
+ };
89
+ /** Spring glide toward the target, seeded with the release velocity. */
90
+ const settleTo = (tx, ty, vx0, vy0) => {
91
+ const { stiffness = 300, damping = 32 } = spring;
92
+ let cx = untrack(x);
93
+ let cy = untrack(y);
94
+ let vx = vx0;
95
+ let vy = vy0;
96
+ let last = now();
97
+ setStatus("settling");
98
+ stopSettle();
99
+ const task = (t) => {
100
+ const dt = Math.min(Math.max((t - last) / 1000, 0), 0.064);
101
+ last = t;
102
+ // Semi-implicit Euler toward the target, same integrator as createSpring.
103
+ vx += (-stiffness * (cx - tx) - damping * vx) * dt;
104
+ vy += (-stiffness * (cy - ty) - damping * vy) * dt;
105
+ cx += vx * dt;
106
+ cy += vy * dt;
107
+ setX(cx);
108
+ setY(cy);
109
+ const settled = Math.hypot(cx - tx, cy - ty) < 0.5 && Math.hypot(vx, vy) < 20;
110
+ if (settled) {
111
+ setX(tx);
112
+ setY(ty);
113
+ setStatus("idle");
114
+ cancelSettle = null;
115
+ return false;
116
+ }
117
+ return true;
118
+ };
119
+ cancelSettle = schedule(task);
120
+ };
121
+ const onDown = (event) => {
122
+ if (event.isPrimary === false)
123
+ return;
124
+ const el = ref();
125
+ if (!el)
126
+ return;
127
+ stopSettle();
128
+ const startPX = event.clientX;
129
+ const startPY = event.clientY;
130
+ const startX = untrack(x);
131
+ const startY = untrack(y);
132
+ const samples = [
133
+ [now(), startX, startY],
134
+ ];
135
+ setStatus("dragging");
136
+ onDragStart?.();
137
+ const move = (ev) => {
138
+ if (ev.isPrimary === false)
139
+ return;
140
+ let nx = startX + (ev.clientX - startPX);
141
+ let ny = startY + (ev.clientY - startPY);
142
+ if (axis === "x")
143
+ ny = startY;
144
+ if (axis === "y")
145
+ nx = startX;
146
+ [nx, ny] = applyConstraints(nx, ny);
147
+ setX(nx);
148
+ setY(ny);
149
+ samples.push([now(), nx, ny]);
150
+ if (samples.length > 8)
151
+ samples.shift();
152
+ };
153
+ const up = () => {
154
+ window.removeEventListener("pointermove", move);
155
+ window.removeEventListener("pointerup", up);
156
+ window.removeEventListener("pointercancel", up);
157
+ // Release velocity from the last ~120ms of the drag.
158
+ const t = now();
159
+ let oi = 0;
160
+ while (oi < samples.length - 1 && t - samples[oi][0] > 120)
161
+ oi++;
162
+ const [ot, ox, oy] = samples[oi];
163
+ const [lt, lx, ly] = samples[samples.length - 1];
164
+ const dt = Math.max(lt - ot, 1) / 1000;
165
+ const velocityX = ((lx - ox) / dt) * (axis === "y" ? 0 : 1);
166
+ const velocityY = ((ly - oy) / dt) * (axis === "x" ? 0 : 1);
167
+ const releaseX = untrack(x);
168
+ const releaseY = untrack(y);
169
+ onDragEnd?.({ x: releaseX, y: releaseY, velocityX, velocityY });
170
+ if (prefersReducedMotion()) {
171
+ // Direct manipulation still works, but the release snaps
172
+ // instantly to the constrained target: no glide, no spring.
173
+ const [cx, cy] = hardClamp(releaseX, releaseY);
174
+ setX(cx);
175
+ setY(cy);
176
+ setStatus("idle");
177
+ return;
178
+ }
179
+ // Project the release velocity forward, then spring to the
180
+ // clamped target. Overshoot past the constraints snaps back
181
+ // elastically because the target sits on the edge.
182
+ let tx = releaseX;
183
+ let ty = releaseY;
184
+ if (momentum) {
185
+ tx += velocityX * inertia;
186
+ ty += velocityY * inertia;
187
+ }
188
+ [tx, ty] = hardClamp(tx, ty);
189
+ const vx0 = momentum ? velocityX : 0;
190
+ const vy0 = momentum ? velocityY : 0;
191
+ settleTo(tx, ty, vx0, vy0);
192
+ };
193
+ window.addEventListener("pointermove", move, { passive: true });
194
+ window.addEventListener("pointerup", up);
195
+ window.addEventListener("pointercancel", up);
196
+ };
197
+ // Late-bound refs (Solid assigns `ref` after mount) still get the grab.
198
+ createEffect(() => {
199
+ const el = ref();
200
+ if (!el)
201
+ return;
202
+ el.addEventListener("pointerdown", onDown);
203
+ onCleanup(() => el.removeEventListener("pointerdown", onDown));
204
+ });
205
+ onCleanup(() => stopSettle());
206
+ return { x, y, status };
207
+ }
package/dist/index.d.ts CHANGED
@@ -24,5 +24,10 @@ export { createSquashStretch, type SquashStretchOptions, type SquashStretchResul
24
24
  export { createGravity, type GravityOptions, type GravityResult, createPendulum, type PendulumOptions, type PendulumResult, createFling, type FlingOptions, type FlingResult, } from "./physics.js";
25
25
  export { createFontSwap, type FontSwapOptions, type FontSwapResult, createTyping, type TypingOptions, type TypingResult, createTextPhysics, type TextPhysicsOptions, type TextPhysicsResult, createTextTunnel, type TextTunnelOptions, type TextTunnelResult, createTextCutout, type TextCutoutOptions, createTextGradient, type TextGradientOptions, createTextScramble, type TextScrambleOptions, type TextScrambleResult, createTextWave, type TextWaveOptions, } from "./typography.js";
26
26
  export { easings, cubicBezier, linear, easeInQuad, easeOutQuad, easeInOutQuad, easeInCubic, easeOutCubic, easeInOutCubic, easeInQuart, easeOutQuart, easeInOutQuart, easeOutExpo, easeOutBack, easeInBack, easeInOutBack, easeOutElastic, easeOutBounce, resolveEasing, type Easing, type EasingName, } from "./easing.js";
27
- export { createKineticType, createScenePlayer, createCamera, createColorShift, createTransition, createBeat, } from "./motion.js";
28
- export type { KineticTypeFrom, KineticTypeOptions, KineticTypeStatus, KineticTypeControls, MotionScene, ScenePlayerStatus, ScenePlayerControls, CameraKeyframe, CameraOptions, ColorShiftOptions, ColorShiftStatus, ColorShiftControls, TransitionType, TransitionDirection, TransitionOptions, TransitionLayerStyle, TransitionStatus, TransitionControls, BeatOptions, BeatStatus, BeatControls, } from "./motion.js";
27
+ export { createKineticType, createScenePlayer, createShowreel, createCamera, createColorShift, createTransition, createBeat, createBeatCuts, } from "./motion.js";
28
+ export type { KineticTypeFrom, KineticTypeOptions, KineticTypeStatus, KineticTypeControls, MotionScene, ScenePlayerStatus, ScenePlayerControls, ShowreelScene, ShowreelSceneKind, CameraKeyframe, CameraOptions, ColorShiftOptions, ColorShiftStatus, ColorShiftControls, TransitionType, TransitionDirection, TransitionOptions, TransitionLayerStyle, TransitionStatus, TransitionControls, BeatOptions, BeatStatus, BeatControls, BeatCutOptions, } from "./motion.js";
29
+ export { createDrag, type DragStatus, type DragAxis, type DragConstraints, type DragEndInfo, type DragOptions, type DragControls, } from "./gesture.js";
30
+ export { createStreamReveal, createAgentState, parseDriftSpec, createSpecPlayer, DriftSpecError, } from "./ai.js";
31
+ export type { StreamRevealStatus, StreamRevealOptions, StreamRevealControls, AgentState, AgentStateTransition, AgentStateOptions, AgentStateControls, DriftSpecPrimitive, DriftSpecStep, DriftSpec, SpecPlayerStatus, SpecPlayerControls, } from "./ai.js";
32
+ export { createTxLifecycle, createTicker, createMintReveal, createConnectButton, } from "./web3.js";
33
+ export type { TxState, TxStatusInput, TxLifecycleOptions, TxLifecycleControls, TickerOptions, TickerControls, MintRevealStatus, MintRevealOptions, MintRevealControls, ConnectButtonOptions, ConnectButtonStatus, ConnectButtonControls, } from "./web3.js";
package/dist/index.js CHANGED
@@ -24,4 +24,7 @@ export { createSquashStretch, createFollowThrough, createAnticipation, createWob
24
24
  export { createGravity, createPendulum, createFling, } from "./physics.js";
25
25
  export { createFontSwap, createTyping, createTextPhysics, createTextTunnel, createTextCutout, createTextGradient, createTextScramble, createTextWave, } from "./typography.js";
26
26
  export { easings, cubicBezier, linear, easeInQuad, easeOutQuad, easeInOutQuad, easeInCubic, easeOutCubic, easeInOutCubic, easeInQuart, easeOutQuart, easeInOutQuart, easeOutExpo, easeOutBack, easeInBack, easeInOutBack, easeOutElastic, easeOutBounce, resolveEasing, } from "./easing.js";
27
- export { createKineticType, createScenePlayer, createCamera, createColorShift, createTransition, createBeat, } from "./motion.js";
27
+ export { createKineticType, createScenePlayer, createShowreel, createCamera, createColorShift, createTransition, createBeat, createBeatCuts, } from "./motion.js";
28
+ export { createDrag, } from "./gesture.js";
29
+ export { createStreamReveal, createAgentState, parseDriftSpec, createSpecPlayer, DriftSpecError, } from "./ai.js";
30
+ export { createTxLifecycle, createTicker, createMintReveal, createConnectButton, } from "./web3.js";
package/dist/motion.d.ts CHANGED
@@ -25,6 +25,18 @@ export interface KineticTypeFrom {
25
25
  opacity?: number;
26
26
  /** Rotation in degrees where each unit starts. Default 0. */
27
27
  rotate?: number;
28
+ /**
29
+ * Per-unit jitter around the `from` values, 0 to 1. Default 0.
30
+ * At 0 every unit shares the exact `from` state; above 0 each unit
31
+ * gets a seeded random offset so the entrance feels hand-set
32
+ * instead of mechanical.
33
+ */
34
+ variance?: number;
35
+ /**
36
+ * Seed for the per-unit jitter. Same seed renders the same jitter
37
+ * on every run. Default 0.
38
+ */
39
+ seed?: number;
28
40
  }
29
41
  export interface KineticTypeOptions {
30
42
  /** Split into "chars" or "words". Default "chars". */
@@ -55,7 +67,8 @@ export interface KineticTypeControls {
55
67
  *
56
68
  * One master clock drives every unit, so a headline with 40 characters
57
69
  * costs a single rAF task, not 40 timers. Units animate through the
58
- * same `from` state with per-unit easing.
70
+ * same `from` state with per-unit easing; set `from.variance` above 0
71
+ * for seeded per-unit jitter around those values.
59
72
  *
60
73
  * SSR-safe: no-op on the server. Under reduced motion every unit jumps
61
74
  * to its final state when `play()` runs, so the text is fully readable.
@@ -293,6 +306,8 @@ export interface BeatControls {
293
306
  bar: Accessor<number>;
294
307
  /** Fractional position within the current beat, 0 to 1. */
295
308
  phase: Accessor<number>;
309
+ /** Beats per bar, from the options. Used as the default cut interval. */
310
+ beatsPerBar: number;
296
311
  /**
297
312
  * Register a callback fired on every beat with the beat index.
298
313
  * Returns an unsubscribe function.
@@ -324,4 +339,66 @@ export interface BeatControls {
324
339
  * ```
325
340
  */
326
341
  export declare function createBeat(options?: BeatOptions): BeatControls;
342
+ /** Named role of a showreel scene, for readability. */
343
+ export type ShowreelSceneKind = "title" | "camera" | "color" | "cut" | "custom";
344
+ /**
345
+ * One scene in a guided showreel: a `MotionScene` with an optional
346
+ * named kind describing what the scene does.
347
+ */
348
+ export interface ShowreelScene extends MotionScene {
349
+ /**
350
+ * Named kind for readability: "title" for kinetic-type title cards,
351
+ * "camera" for camera-move scenes, "color" for color-shift scenes,
352
+ * "cut" for transition handoffs, "custom" for anything else.
353
+ * Informational only; it does not change playback.
354
+ */
355
+ kind?: ShowreelSceneKind;
356
+ }
357
+ /**
358
+ * Guided showreel recipe: a thin typed wrapper over
359
+ * `createScenePlayer` for showreels and launch films. Scenes carry a
360
+ * named `kind` so the reel reads like a shot list, and each scene's
361
+ * `onEnter` wires one of the motion-graphics primitives
362
+ * (`createKineticType`, `createCamera`, `createColorShift`,
363
+ * `createTransition`, `createBeat`).
364
+ *
365
+ * Same controls, status values, and reduced-motion behavior as
366
+ * `createScenePlayer`: `play()` jumps to the final frame under reduced
367
+ * motion or on the server.
368
+ *
369
+ * ```ts
370
+ * const reel = createShowreel([
371
+ * { kind: "title", duration: 1200, onEnter: () => titleCard.play() },
372
+ * { kind: "camera", duration: 2000, onEnter: () => dolly.play() },
373
+ * { kind: "color", duration: 1500, onEnter: () => finale.play() },
374
+ * ])
375
+ * beatCuts = createBeatCuts(beat, reel, { every: 8 })
376
+ * await reel.play()
377
+ * ```
378
+ */
379
+ export declare function createShowreel(scenes: ShowreelScene[]): ScenePlayerControls;
380
+ export interface BeatCutOptions {
381
+ /**
382
+ * Cut every N beats. Default: the beat clock's `beatsPerBar`, so a
383
+ * cut lands on every downbeat.
384
+ */
385
+ every?: number;
386
+ }
387
+ /**
388
+ * Beat-synced scene cuts: advance the player every N beats through
389
+ * the beat clock's `onBeat`. Returns a cleanup function that
390
+ * unsubscribes the cut listener.
391
+ *
392
+ * Cuts only fire while the player is running, so pausing the reel
393
+ * pauses the cuts too.
394
+ *
395
+ * ```ts
396
+ * const beat = createBeat({ bpm: 128, beatsPerBar: 4 })
397
+ * const stopCuts = createBeatCuts(beat, player) // cut every bar
398
+ * beat.start()
399
+ * await player.play()
400
+ * stopCuts()
401
+ * ```
402
+ */
403
+ export declare function createBeatCuts(beat: BeatControls, player: Pick<ScenePlayerControls, "next" | "status">, options?: BeatCutOptions): () => void;
327
404
  export {};
package/dist/motion.js CHANGED
@@ -16,53 +16,7 @@ import { parseColorStops, sampleColorStops, } from "./color.js";
16
16
  import { resolveEasing } from "./easing.js";
17
17
  import { now, schedule } from "./engine.js";
18
18
  import { prefersReducedMotion } from "./reduced-motion.js";
19
- function ownerDoc(el) {
20
- const od = el
21
- .ownerDocument;
22
- if (od)
23
- return od ?? undefined;
24
- return typeof document !== "undefined" ? document : undefined;
25
- }
26
- /**
27
- * Split an element's text into per-unit inline-block spans so each
28
- * letter (or word) can be transformed independently. The original text
29
- * is preserved as an aria-label for screen readers.
30
- */
31
- function splitUnits(el, unit) {
32
- const doc = ownerDoc(el);
33
- if (!doc)
34
- return [];
35
- const text = el.textContent ?? "";
36
- el.textContent = "";
37
- el.setAttribute("aria-label", text);
38
- const spans = [];
39
- const push = (content) => {
40
- const s = doc.createElement("span");
41
- s.textContent = content;
42
- s.setAttribute("aria-hidden", "true");
43
- s.style.display = "inline-block";
44
- s.style.willChange = "transform, opacity, filter";
45
- el.appendChild(s);
46
- spans.push(s);
47
- };
48
- if (unit === "words") {
49
- for (const word of text.split(/(\s+)/)) {
50
- if (word.length === 0)
51
- continue;
52
- if (/^\s+$/.test(word)) {
53
- el.appendChild(doc.createTextNode(word));
54
- }
55
- else {
56
- push(word);
57
- }
58
- }
59
- }
60
- else {
61
- for (const ch of text)
62
- push(ch === " " ? " " : ch);
63
- }
64
- return spans;
65
- }
19
+ import { mulberry32, splitUnits } from "./text.js";
66
20
  function clamp01(v) {
67
21
  return v < 0 ? 0 : v > 1 ? 1 : v;
68
22
  }
@@ -88,7 +42,8 @@ function applyKineticStyle(el, e, from) {
88
42
  *
89
43
  * One master clock drives every unit, so a headline with 40 characters
90
44
  * costs a single rAF task, not 40 timers. Units animate through the
91
- * same `from` state with per-unit easing.
45
+ * same `from` state with per-unit easing; set `from.variance` above 0
46
+ * for seeded per-unit jitter around those values.
92
47
  *
93
48
  * SSR-safe: no-op on the server. Under reduced motion every unit jumps
94
49
  * to its final state when `play()` runs, so the text is fully readable.
@@ -113,10 +68,13 @@ export function createKineticType(ref, options = {}) {
113
68
  scale: options.from?.scale ?? 0.85,
114
69
  opacity: options.from?.opacity ?? 0,
115
70
  rotate: options.from?.rotate ?? 0,
71
+ variance: options.from?.variance ?? 0,
72
+ seed: options.from?.seed ?? 0,
116
73
  };
117
74
  const easing = resolveEasing(easingOpt);
118
75
  const [status, setStatus] = createSignal("idle");
119
76
  let units = [];
77
+ let unitFrom = [];
120
78
  let controls = null;
121
79
  let runToken = 0;
122
80
  const play = () => {
@@ -124,6 +82,7 @@ export function createKineticType(ref, options = {}) {
124
82
  controls?.stop();
125
83
  controls = null;
126
84
  units = [];
85
+ unitFrom = [];
127
86
  if (typeof window !== "undefined") {
128
87
  const el = ref();
129
88
  if (el)
@@ -133,6 +92,29 @@ export function createKineticType(ref, options = {}) {
133
92
  setStatus("done");
134
93
  return Promise.resolve();
135
94
  }
95
+ // Seeded per-unit jitter around the `from` values. Deterministic
96
+ // for a given seed, so the same headline renders the same way on
97
+ // every run. Variance 0 keeps the exact legacy behavior.
98
+ const variance = clamp01(from.variance);
99
+ if (variance > 0) {
100
+ const rand = mulberry32(from.seed);
101
+ unitFrom = units.map(() => ({
102
+ y: from.y + variance * (rand() * 2 - 1) * 20,
103
+ blur: Math.max(0, from.blur + variance * (rand() * 2 - 1) * 8),
104
+ scale: from.scale + variance * (rand() * 2 - 1) * 0.15,
105
+ opacity: from.opacity,
106
+ rotate: from.rotate + variance * (rand() * 2 - 1) * 12,
107
+ }));
108
+ }
109
+ else {
110
+ unitFrom = units.map(() => ({
111
+ y: from.y,
112
+ blur: from.blur,
113
+ scale: from.scale,
114
+ opacity: from.opacity,
115
+ rotate: from.rotate,
116
+ }));
117
+ }
136
118
  setStatus("running");
137
119
  const total = duration + stagger * (units.length - 1);
138
120
  return new Promise((resolve) => {
@@ -146,7 +128,7 @@ export function createKineticType(ref, options = {}) {
146
128
  onUpdate: (elapsed) => {
147
129
  for (let i = 0; i < units.length; i++) {
148
130
  const local = clamp01((elapsed - i * stagger) / duration);
149
- applyKineticStyle(units[i], easing(local), from);
131
+ applyKineticStyle(units[i], easing(local), unitFrom[i]);
150
132
  }
151
133
  },
152
134
  onComplete: () => {
@@ -716,6 +698,7 @@ export function createBeat(options = {}) {
716
698
  beat,
717
699
  bar,
718
700
  phase,
701
+ beatsPerBar: perBar,
719
702
  onBeat: (cb) => {
720
703
  listeners.add(cb);
721
704
  return () => {
@@ -727,3 +710,52 @@ export function createBeat(options = {}) {
727
710
  status,
728
711
  };
729
712
  }
713
+ /**
714
+ * Guided showreel recipe: a thin typed wrapper over
715
+ * `createScenePlayer` for showreels and launch films. Scenes carry a
716
+ * named `kind` so the reel reads like a shot list, and each scene's
717
+ * `onEnter` wires one of the motion-graphics primitives
718
+ * (`createKineticType`, `createCamera`, `createColorShift`,
719
+ * `createTransition`, `createBeat`).
720
+ *
721
+ * Same controls, status values, and reduced-motion behavior as
722
+ * `createScenePlayer`: `play()` jumps to the final frame under reduced
723
+ * motion or on the server.
724
+ *
725
+ * ```ts
726
+ * const reel = createShowreel([
727
+ * { kind: "title", duration: 1200, onEnter: () => titleCard.play() },
728
+ * { kind: "camera", duration: 2000, onEnter: () => dolly.play() },
729
+ * { kind: "color", duration: 1500, onEnter: () => finale.play() },
730
+ * ])
731
+ * beatCuts = createBeatCuts(beat, reel, { every: 8 })
732
+ * await reel.play()
733
+ * ```
734
+ */
735
+ export function createShowreel(scenes) {
736
+ return createScenePlayer(scenes);
737
+ }
738
+ /**
739
+ * Beat-synced scene cuts: advance the player every N beats through
740
+ * the beat clock's `onBeat`. Returns a cleanup function that
741
+ * unsubscribes the cut listener.
742
+ *
743
+ * Cuts only fire while the player is running, so pausing the reel
744
+ * pauses the cuts too.
745
+ *
746
+ * ```ts
747
+ * const beat = createBeat({ bpm: 128, beatsPerBar: 4 })
748
+ * const stopCuts = createBeatCuts(beat, player) // cut every bar
749
+ * beat.start()
750
+ * await player.play()
751
+ * stopCuts()
752
+ * ```
753
+ */
754
+ export function createBeatCuts(beat, player, options = {}) {
755
+ const every = Math.max(1, Math.floor(options.every ?? beat.beatsPerBar));
756
+ return beat.onBeat((b) => {
757
+ if (b % every === 0 && player.status() === "running") {
758
+ player.next();
759
+ }
760
+ });
761
+ }
package/dist/text.d.ts ADDED
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Internal text utilities shared by the motion-graphics primitives.
3
+ *
4
+ * Not part of the public API: family modules import from here, but
5
+ * nothing in this file is re-exported from the package index.
6
+ */
7
+ /** Document owning the element, with an SSR-safe fallback. */
8
+ export declare function ownerDoc(el: Element): Document | undefined;
9
+ /**
10
+ * Split an element's text into per-unit inline-block spans so each
11
+ * letter (or word) can be transformed independently. The original text
12
+ * is preserved as an aria-label for screen readers. Any previous
13
+ * content is replaced.
14
+ */
15
+ export declare function splitUnits(el: Element, unit: "chars" | "words"): HTMLElement[];
16
+ /**
17
+ * Append per-unit inline-block spans for `text` to the element's
18
+ * existing content, without clearing it. Used by streaming text, where
19
+ * each flushed batch adds new units while earlier units keep playing.
20
+ */
21
+ export declare function appendUnits(el: Element, text: string, unit: "chars" | "words"): HTMLElement[];
22
+ /**
23
+ * Deterministic pseudo-random generator (mulberry32). Used for
24
+ * per-unit variance so seeded jitter renders identically on every
25
+ * run, which keeps tests stable and output reproducible.
26
+ */
27
+ export declare function mulberry32(seed: number): () => number;
package/dist/text.js ADDED
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Internal text utilities shared by the motion-graphics primitives.
3
+ *
4
+ * Not part of the public API: family modules import from here, but
5
+ * nothing in this file is re-exported from the package index.
6
+ */
7
+ /** Document owning the element, with an SSR-safe fallback. */
8
+ export function ownerDoc(el) {
9
+ const od = el
10
+ .ownerDocument;
11
+ if (od)
12
+ return od ?? undefined;
13
+ return typeof document !== "undefined" ? document : undefined;
14
+ }
15
+ function pushUnit(doc, parent, content) {
16
+ const s = doc.createElement("span");
17
+ s.textContent = content;
18
+ s.setAttribute("aria-hidden", "true");
19
+ s.style.display = "inline-block";
20
+ s.style.willChange = "transform, opacity, filter";
21
+ parent.appendChild(s);
22
+ return s;
23
+ }
24
+ function appendTokens(doc, parent, text, unit) {
25
+ const spans = [];
26
+ if (unit === "words") {
27
+ for (const word of text.split(/(\s+)/)) {
28
+ if (word.length === 0)
29
+ continue;
30
+ if (/^\s+$/.test(word)) {
31
+ parent.appendChild(doc.createTextNode(word));
32
+ }
33
+ else {
34
+ spans.push(pushUnit(doc, parent, word));
35
+ }
36
+ }
37
+ }
38
+ else {
39
+ for (const ch of text)
40
+ spans.push(pushUnit(doc, parent, ch === " " ? " " : ch));
41
+ }
42
+ return spans;
43
+ }
44
+ /**
45
+ * Split an element's text into per-unit inline-block spans so each
46
+ * letter (or word) can be transformed independently. The original text
47
+ * is preserved as an aria-label for screen readers. Any previous
48
+ * content is replaced.
49
+ */
50
+ export function splitUnits(el, unit) {
51
+ const doc = ownerDoc(el);
52
+ if (!doc)
53
+ return [];
54
+ const text = el.textContent ?? "";
55
+ el.textContent = "";
56
+ el.setAttribute("aria-label", text);
57
+ return appendTokens(doc, el, text, unit);
58
+ }
59
+ /**
60
+ * Append per-unit inline-block spans for `text` to the element's
61
+ * existing content, without clearing it. Used by streaming text, where
62
+ * each flushed batch adds new units while earlier units keep playing.
63
+ */
64
+ export function appendUnits(el, text, unit) {
65
+ const doc = ownerDoc(el);
66
+ if (!doc)
67
+ return [];
68
+ return appendTokens(doc, el, text, unit);
69
+ }
70
+ /**
71
+ * Deterministic pseudo-random generator (mulberry32). Used for
72
+ * per-unit variance so seeded jitter renders identically on every
73
+ * run, which keeps tests stable and output reproducible.
74
+ */
75
+ export function mulberry32(seed) {
76
+ let a = seed >>> 0;
77
+ return () => {
78
+ a |= 0;
79
+ a = (a + 0x6d2b79f5) | 0;
80
+ let t = Math.imul(a ^ (a >>> 15), 1 | a);
81
+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
82
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
83
+ };
84
+ }