solid-drift 0.7.1 → 0.8.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/ai.js ADDED
@@ -0,0 +1,570 @@
1
+ /**
2
+ * Streaming-text and agent-state family: motion primitives for
3
+ * conversational interfaces.
4
+ *
5
+ * Token streams arrive in bursts, so `createStreamReveal` batches them
6
+ * into a readable cadence before animating each unit's entrance.
7
+ * `createAgentState` is a pure-signal state machine for agent
8
+ * status, kept free of motion so each state can pair with any
9
+ * primitive. `parseDriftSpec` and `createSpecPlayer` let generated
10
+ * JSON choreography be validated and rendered deterministically.
11
+ *
12
+ * All primitives are signal-native, SSR-safe, dependency-free, and
13
+ * define sensible static behavior under `prefers-reduced-motion`.
14
+ */
15
+ import { createSignal, onCleanup, } from "solid-js";
16
+ import { animate } from "./animate.js";
17
+ import { resolveEasing } from "./easing.js";
18
+ import { now, schedule } from "./engine.js";
19
+ import { createBeat, createCamera, createColorShift, createKineticType, createTransition, } from "./motion.js";
20
+ import { prefersReducedMotion } from "./reduced-motion.js";
21
+ import { appendUnits } from "./text.js";
22
+ function clamp01(v) {
23
+ return v < 0 ? 0 : v > 1 ? 1 : v;
24
+ }
25
+ /** Round to 2 decimals for style values. */
26
+ function fmt(v) {
27
+ return String(Math.round(v * 100) / 100);
28
+ }
29
+ /**
30
+ * Smooth irregular token cadence into readable animated text.
31
+ *
32
+ * Stream tokens arrive in bursts, a flood, then silence. `push()`
33
+ * only appends to an internal queue; one rAF task flushes the queue
34
+ * on a steady cadence (or early when it overflows) and drives every
35
+ * unit's entrance, so a 40-token burst reads as a calm typed line
36
+ * instead of a flicker.
37
+ *
38
+ * SSR-safe: `push` is a no-op on the server, `status()` is "done",
39
+ * and the host renders the full text statically. Under reduced motion
40
+ * batching and entrances are skipped: text appears as it is pushed.
41
+ *
42
+ * ```tsx
43
+ * let out!: HTMLDivElement
44
+ * const stream = createStreamReveal(() => out, { unit: "chars" })
45
+ * const reader = response.body.getReader()
46
+ * onMount(async () => {
47
+ * for (;;) {
48
+ * const { done, value } = await reader.read()
49
+ * if (done) break
50
+ * stream.push(decoder.decode(value))
51
+ * }
52
+ * stream.complete()
53
+ * })
54
+ * <div ref={out} aria-live="polite" />
55
+ * ```
56
+ */
57
+ export function createStreamReveal(ref, options = {}) {
58
+ const { unit = "chars", batchMs = 120, maxBatch = 24, duration = 450, stagger = 30, easing: easingOpt = "easeOutExpo", } = options;
59
+ const from = {
60
+ y: options.from?.y ?? 28,
61
+ blur: options.from?.blur ?? 10,
62
+ scale: options.from?.scale ?? 0.85,
63
+ opacity: options.from?.opacity ?? 0,
64
+ rotate: options.from?.rotate ?? 0,
65
+ };
66
+ const easing = resolveEasing(easingOpt);
67
+ const server = typeof window === "undefined";
68
+ const [status, setStatus] = createSignal(server ? "done" : "idle");
69
+ const [pending, setPending] = createSignal(0);
70
+ let queue = "";
71
+ let units = [];
72
+ let unitIndex = 0;
73
+ let cancel = null;
74
+ let lastFlush = 0;
75
+ let settling = false;
76
+ const applyEntrance = (el, e) => {
77
+ const t = 1 - e;
78
+ el.style.transform =
79
+ `translateY(${fmt(from.y * t)}px) scale(${fmt(from.scale + (1 - from.scale) * e)}) rotate(${fmt(from.rotate * t)}deg)`;
80
+ const blur = from.blur * t;
81
+ el.style.filter = blur > 0.05 ? `blur(${fmt(blur)}px)` : "none";
82
+ el.style.opacity = fmt(from.opacity + (1 - from.opacity) * e);
83
+ };
84
+ const flush = (t) => {
85
+ const el = ref();
86
+ if (!el || queue.length === 0) {
87
+ queue = "";
88
+ setPending(0);
89
+ return;
90
+ }
91
+ const batch = queue;
92
+ queue = "";
93
+ setPending(0);
94
+ for (const span of appendUnits(el, batch, unit)) {
95
+ units.push({ el: span, startAt: t + unitIndex * stagger });
96
+ unitIndex++;
97
+ }
98
+ };
99
+ const tick = (t) => {
100
+ if (queue.length > 0 &&
101
+ (queue.length >= maxBatch || t - lastFlush >= batchMs)) {
102
+ flush(t);
103
+ lastFlush = t;
104
+ }
105
+ let alive = false;
106
+ units = units.filter((u) => {
107
+ const local = clamp01((t - u.startAt) / duration);
108
+ applyEntrance(u.el, easing(local));
109
+ if (local < 1)
110
+ alive = true;
111
+ return local < 1;
112
+ });
113
+ if (queue.length > 0)
114
+ alive = true;
115
+ if (!alive) {
116
+ cancel = null;
117
+ if (settling) {
118
+ settling = false;
119
+ setStatus("done");
120
+ }
121
+ }
122
+ return alive;
123
+ };
124
+ const ensureLoop = () => {
125
+ if (cancel || server)
126
+ return;
127
+ lastFlush = now();
128
+ cancel = schedule(tick);
129
+ };
130
+ const push = (chunk) => {
131
+ if (server || chunk.length === 0)
132
+ return;
133
+ if (prefersReducedMotion()) {
134
+ // Accessibility: skip batching and entrances, show text at once.
135
+ const el = ref();
136
+ if (el) {
137
+ for (const span of appendUnits(el, chunk, unit)) {
138
+ void span;
139
+ }
140
+ }
141
+ setPending(0);
142
+ setStatus("streaming");
143
+ return;
144
+ }
145
+ queue += chunk;
146
+ setPending(queue.length);
147
+ setStatus("streaming");
148
+ ensureLoop();
149
+ };
150
+ const complete = () => {
151
+ if (server) {
152
+ setStatus("done");
153
+ return;
154
+ }
155
+ if (prefersReducedMotion()) {
156
+ queue = "";
157
+ setPending(0);
158
+ setStatus("done");
159
+ return;
160
+ }
161
+ flush(now());
162
+ if (units.length === 0 && queue.length === 0) {
163
+ setStatus("done");
164
+ return;
165
+ }
166
+ settling = true;
167
+ ensureLoop();
168
+ };
169
+ const reset = () => {
170
+ cancel?.();
171
+ cancel = null;
172
+ queue = "";
173
+ units = [];
174
+ unitIndex = 0;
175
+ settling = false;
176
+ setPending(0);
177
+ if (!server) {
178
+ const el = ref();
179
+ el?.removeAttribute?.("aria-label");
180
+ if (el)
181
+ el.textContent = "";
182
+ setStatus("idle");
183
+ }
184
+ };
185
+ onCleanup(() => {
186
+ cancel?.();
187
+ cancel = null;
188
+ });
189
+ return { push, complete, reset, status, pending };
190
+ }
191
+ /**
192
+ * A pure-signal state machine for agent UI.
193
+ *
194
+ * Motion is deliberately not built in: pair each state with a recipe
195
+ * instead, so the machine stays transparent, testable, and SSR-safe.
196
+ * A typical pairing is `createWobble` on typing dots while "thinking",
197
+ * `createStreamReveal` while "streaming", `createTransition` for the
198
+ * "tool-call" overlay, and `createColorShift` on the status pill for
199
+ * "done"/"error". Each of those degrades on its own under reduced
200
+ * motion.
201
+ *
202
+ * SSR-safe by construction: signals only, no DOM, no clock.
203
+ *
204
+ * ```ts
205
+ * const agent = createAgentState()
206
+ * agent.set("thinking")
207
+ * agent.state() // "thinking"
208
+ * agent.is("streaming") // false
209
+ * ```
210
+ */
211
+ export function createAgentState(options = {}) {
212
+ const { initial = "idle", allowed, onEnter, onExit } = options;
213
+ const [state, setState] = createSignal(initial);
214
+ const [prev, setPrev] = createSignal(initial);
215
+ const legal = (from, to) => {
216
+ if (!allowed)
217
+ return true;
218
+ return allowed.some((t) => t.from === from && t.to === to);
219
+ };
220
+ const move = (next) => {
221
+ const current = state();
222
+ if (next === current || !legal(current, next))
223
+ return;
224
+ onExit?.(current, next);
225
+ setPrev(current);
226
+ setState(next);
227
+ onEnter?.(next, current);
228
+ };
229
+ const reset = () => {
230
+ move(initial);
231
+ };
232
+ return {
233
+ state,
234
+ prev,
235
+ set: move,
236
+ reset,
237
+ is: (s) => state() === s,
238
+ };
239
+ }
240
+ /**
241
+ * Thrown by `parseDriftSpec`. `path` pinpoints the invalid value
242
+ * (for example "scenes[2].options.duration") so a generator can
243
+ * repair the spec without guessing.
244
+ */
245
+ export class DriftSpecError extends Error {
246
+ constructor(path, message) {
247
+ super(path
248
+ ? `Invalid drift spec at "${path}": ${message}`
249
+ : `Invalid drift spec: ${message}`);
250
+ this.name = "DriftSpecError";
251
+ this.path = path;
252
+ }
253
+ }
254
+ const PRIMITIVES = [
255
+ "kineticType",
256
+ "camera",
257
+ "colorShift",
258
+ "transition",
259
+ "beat",
260
+ "streamReveal",
261
+ ];
262
+ /** Primitives that render into an element and need a target key. */
263
+ const DOM_PRIMITIVES = [
264
+ "kineticType",
265
+ "streamReveal",
266
+ ];
267
+ function isRecord(v) {
268
+ return typeof v === "object" && v !== null && !Array.isArray(v);
269
+ }
270
+ function assertPositiveNumber(value, path) {
271
+ if (typeof value !== "number" || !(value > 0)) {
272
+ throw new DriftSpecError(path, "expected a positive number");
273
+ }
274
+ }
275
+ function checkOptions(primitive, options, base) {
276
+ const at = (key) => `${base}.options.${key}`;
277
+ switch (primitive) {
278
+ case "kineticType":
279
+ if (options.duration !== undefined) {
280
+ assertPositiveNumber(options.duration, at("duration"));
281
+ }
282
+ if (options.stagger !== undefined) {
283
+ assertPositiveNumber(options.stagger, at("stagger"));
284
+ }
285
+ break;
286
+ case "camera":
287
+ if (options.keyframes !== undefined &&
288
+ !Array.isArray(options.keyframes)) {
289
+ throw new DriftSpecError(at("keyframes"), "expected an array");
290
+ }
291
+ break;
292
+ case "colorShift":
293
+ if (options.stops !== undefined && !Array.isArray(options.stops)) {
294
+ throw new DriftSpecError(at("stops"), "expected an array");
295
+ }
296
+ if (options.duration !== undefined) {
297
+ assertPositiveNumber(options.duration, at("duration"));
298
+ }
299
+ break;
300
+ case "transition":
301
+ if (options.type !== undefined &&
302
+ !["cut", "fade", "slide", "wipe"].includes(options.type)) {
303
+ throw new DriftSpecError(at("type"), 'expected one of "cut", "fade", "slide", "wipe"');
304
+ }
305
+ if (options.duration !== undefined) {
306
+ assertPositiveNumber(options.duration, at("duration"));
307
+ }
308
+ break;
309
+ case "beat":
310
+ if (options.bpm !== undefined) {
311
+ assertPositiveNumber(options.bpm, at("bpm"));
312
+ }
313
+ break;
314
+ case "streamReveal":
315
+ if (options.text !== undefined && typeof options.text !== "string") {
316
+ throw new DriftSpecError(at("text"), "expected a string");
317
+ }
318
+ break;
319
+ }
320
+ }
321
+ /**
322
+ * Validate unknown input against the DriftSpec schema and return the
323
+ * typed spec. Throws `DriftSpecError` with a precise `path` on the
324
+ * first invalid value.
325
+ *
326
+ * Pure validation, no DOM: safe to run on the server.
327
+ *
328
+ * ```ts
329
+ * const spec = parseDriftSpec(JSON.parse(raw))
330
+ * const player = createSpecPlayer(spec, { title: () => titleEl })
331
+ * await player.play()
332
+ * ```
333
+ */
334
+ export function parseDriftSpec(input) {
335
+ if (!isRecord(input)) {
336
+ throw new DriftSpecError("", "expected an object");
337
+ }
338
+ if (input.version !== 1) {
339
+ throw new DriftSpecError("version", "expected 1");
340
+ }
341
+ if (!Array.isArray(input.scenes) || input.scenes.length === 0) {
342
+ throw new DriftSpecError("scenes", "expected a non-empty array");
343
+ }
344
+ const scenes = input.scenes.map((raw, i) => {
345
+ const base = `scenes[${i}]`;
346
+ if (!isRecord(raw)) {
347
+ throw new DriftSpecError(base, "expected an object");
348
+ }
349
+ if (typeof raw.primitive !== "string" ||
350
+ !PRIMITIVES.includes(raw.primitive)) {
351
+ throw new DriftSpecError(`${base}.primitive`, `expected one of ${PRIMITIVES.join(", ")}`);
352
+ }
353
+ const primitive = raw.primitive;
354
+ let target;
355
+ if (raw.target !== undefined) {
356
+ if (typeof raw.target !== "string" || raw.target.length === 0) {
357
+ throw new DriftSpecError(`${base}.target`, "expected a non-empty string key");
358
+ }
359
+ target = raw.target;
360
+ }
361
+ else if (DOM_PRIMITIVES.includes(primitive)) {
362
+ throw new DriftSpecError(`${base}.target`, `"${primitive}" needs a target key into the refs record`);
363
+ }
364
+ let specOptions;
365
+ if (raw.options !== undefined) {
366
+ if (!isRecord(raw.options)) {
367
+ throw new DriftSpecError(`${base}.options`, "expected an object");
368
+ }
369
+ specOptions = raw.options;
370
+ checkOptions(primitive, specOptions, base);
371
+ }
372
+ let duration;
373
+ if (raw.duration !== undefined) {
374
+ assertPositiveNumber(raw.duration, `${base}.duration`);
375
+ duration = raw.duration;
376
+ }
377
+ return { primitive, target, options: specOptions, duration };
378
+ });
379
+ return { version: 1, scenes };
380
+ }
381
+ const noopHandle = () => ({
382
+ promise: Promise.resolve(),
383
+ stop: () => { },
384
+ });
385
+ /** Resolve when `isDone()` turns true, on the shared clock. */
386
+ function whenDone(isDone) {
387
+ let cancel = null;
388
+ let resolveFn;
389
+ const promise = new Promise((resolve) => {
390
+ resolveFn = resolve;
391
+ if (isDone()) {
392
+ resolve();
393
+ return;
394
+ }
395
+ cancel = schedule(() => {
396
+ if (isDone()) {
397
+ resolve();
398
+ return false;
399
+ }
400
+ return true;
401
+ });
402
+ });
403
+ return {
404
+ promise,
405
+ stop: () => {
406
+ cancel?.();
407
+ cancel = null;
408
+ resolveFn();
409
+ },
410
+ };
411
+ }
412
+ const stepPlayers = {
413
+ kineticType: (target, options) => {
414
+ const c = createKineticType(target ?? (() => null), options);
415
+ return { promise: c.play(), stop: c.stop };
416
+ },
417
+ streamReveal: (target, options) => {
418
+ const c = createStreamReveal(target ?? (() => null), options);
419
+ const text = typeof options.text === "string" ? options.text : "";
420
+ c.push(text);
421
+ c.complete();
422
+ return whenDone(() => c.status() === "done");
423
+ },
424
+ camera: (target, options, budget) => {
425
+ const el = target?.();
426
+ const keyframes = options.keyframes ?? [];
427
+ if (!el || keyframes.length === 0)
428
+ return noopHandle();
429
+ const [p, setP] = createSignal(0);
430
+ const cam = createCamera(keyframes, { progress: p });
431
+ const ctl = animate(0, 1, {
432
+ duration: Math.max(budget ?? 1200, 1),
433
+ easing: "linear",
434
+ onUpdate: (v) => {
435
+ setP(v);
436
+ el.style.transform = cam();
437
+ },
438
+ });
439
+ return { promise: ctl.finished, stop: ctl.stop };
440
+ },
441
+ colorShift: (target, options, budget) => {
442
+ const el = target?.();
443
+ const stops = options.stops ?? [];
444
+ if (!el || stops.length === 0)
445
+ return noopHandle();
446
+ const c = createColorShift(stops, {
447
+ ...options,
448
+ duration: Math.max(budget ?? 1200, 1),
449
+ });
450
+ const playPromise = c.play();
451
+ const apply = schedule(() => {
452
+ el.style.color = c.color();
453
+ return c.status() === "running";
454
+ });
455
+ const settle = () => {
456
+ apply();
457
+ el.style.color = c.color();
458
+ };
459
+ return {
460
+ promise: playPromise.then(settle),
461
+ stop: () => {
462
+ c.stop();
463
+ settle();
464
+ },
465
+ };
466
+ },
467
+ transition: (_target, options) => {
468
+ const c = createTransition(options);
469
+ return { promise: c.play(), stop: c.stop };
470
+ },
471
+ beat: (_target, options, budget) => {
472
+ const c = createBeat(options);
473
+ c.start();
474
+ const timer = animate(0, 1, { duration: Math.max(budget ?? 2000, 1) });
475
+ const stopAll = () => {
476
+ timer.stop();
477
+ c.stop();
478
+ };
479
+ return { promise: timer.finished.then(() => c.stop()), stop: stopAll };
480
+ },
481
+ };
482
+ /**
483
+ * Render a validated DriftSpec: each scene's primitive plays in
484
+ * order against the element refs the host supplies. `duration` on a
485
+ * step caps that step's budget.
486
+ *
487
+ * SSR-safe: `play()` is a no-op on the server. Under reduced motion
488
+ * `play()` jumps straight to the last scene (the clean final frame),
489
+ * matching `createScenePlayer`.
490
+ *
491
+ * ```ts
492
+ * const player = createSpecPlayer(spec, {
493
+ * title: () => titleEl,
494
+ * body: () => bodyEl,
495
+ * })
496
+ * player.scene() // 0, 1, ... as the spec plays
497
+ * await player.play()
498
+ * ```
499
+ */
500
+ export function createSpecPlayer(spec, refs) {
501
+ const [status, setStatus] = createSignal("idle");
502
+ const [scene, setScene] = createSignal(-1);
503
+ let currentStop = null;
504
+ let resolvePlay = null;
505
+ let stopped = false;
506
+ const runStep = async (step) => {
507
+ const target = step.target === undefined ? undefined : refs[step.target];
508
+ const handle = stepPlayers[step.primitive](target, step.options ?? {}, step.duration);
509
+ currentStop = handle.stop;
510
+ if (step.duration === undefined) {
511
+ await handle.promise;
512
+ }
513
+ else {
514
+ // The duration is a budget: the step ends when the primitive
515
+ // finishes or the budget runs out, whichever comes first.
516
+ const timer = animate(0, 1, { duration: Math.max(step.duration, 0.001) });
517
+ const stopStep = currentStop;
518
+ currentStop = () => {
519
+ stopStep();
520
+ timer.stop();
521
+ };
522
+ await Promise.race([handle.promise, timer.finished]);
523
+ timer.stop();
524
+ }
525
+ currentStop = null;
526
+ };
527
+ const play = () => {
528
+ if (spec.scenes.length === 0) {
529
+ setScene(-1);
530
+ setStatus("done");
531
+ return Promise.resolve();
532
+ }
533
+ if (prefersReducedMotion() || typeof window === "undefined") {
534
+ setScene(spec.scenes.length - 1);
535
+ setStatus("done");
536
+ return Promise.resolve();
537
+ }
538
+ // A superseded play() must not leave its caller hanging.
539
+ resolvePlay?.();
540
+ resolvePlay = null;
541
+ stopped = false;
542
+ setStatus("running");
543
+ return new Promise((resolve) => {
544
+ resolvePlay = resolve;
545
+ void (async () => {
546
+ for (let i = 0; i < spec.scenes.length; i++) {
547
+ if (stopped)
548
+ break;
549
+ setScene(i);
550
+ await runStep(spec.scenes[i]);
551
+ }
552
+ currentStop = null;
553
+ const done = resolvePlay;
554
+ resolvePlay = null;
555
+ setStatus(stopped ? "idle" : "done");
556
+ done?.();
557
+ })();
558
+ });
559
+ };
560
+ const stop = () => {
561
+ if (status() !== "running")
562
+ return;
563
+ stopped = true;
564
+ currentStop?.();
565
+ currentStop = null;
566
+ // The async loop observes `stopped`, breaks, and resolves play().
567
+ };
568
+ onCleanup(stop);
569
+ return { play, stop, status, scene };
570
+ }
package/dist/index.d.ts CHANGED
@@ -24,5 +24,9 @@ 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 { createStreamReveal, createAgentState, parseDriftSpec, createSpecPlayer, DriftSpecError, } from "./ai.js";
30
+ export type { StreamRevealStatus, StreamRevealOptions, StreamRevealControls, AgentState, AgentStateTransition, AgentStateOptions, AgentStateControls, DriftSpecPrimitive, DriftSpecStep, DriftSpec, SpecPlayerStatus, SpecPlayerControls, } from "./ai.js";
31
+ export { createTxLifecycle, createTicker, createMintReveal, createConnectButton, } from "./web3.js";
32
+ 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,6 @@ 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 { createStreamReveal, createAgentState, parseDriftSpec, createSpecPlayer, DriftSpecError, } from "./ai.js";
29
+ 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 {};