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/README.md +199 -1
- package/dist/ai.d.ts +207 -0
- package/dist/ai.js +570 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.js +3 -1
- package/dist/motion.d.ts +78 -1
- package/dist/motion.js +81 -49
- package/dist/text.d.ts +27 -0
- package/dist/text.js +84 -0
- package/dist/web3.d.ts +213 -0
- package/dist/web3.js +640 -0
- package/package.json +1 -1
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 {};
|