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.
- package/README.md +236 -1
- package/dist/ai.d.ts +207 -0
- package/dist/ai.js +570 -0
- package/dist/gesture.d.ts +108 -0
- package/dist/gesture.js +207 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.js +4 -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/gesture.js
ADDED
|
@@ -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
|
-
|
|
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),
|
|
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
|
+
}
|