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/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
+ }
package/dist/web3.d.ts ADDED
@@ -0,0 +1,213 @@
1
+ /**
2
+ * web3 family: motion primitives for onchain product UI.
3
+ *
4
+ * Transaction lifecycle, price tickers, mint reveals, and wallet
5
+ * button micro-interactions, without depending on any wallet or chain
6
+ * library. State arrives through accessors in a wagmi/viem-style
7
+ * shape (the adapter pattern), so the primitives stay zero-dependency
8
+ * while your app keeps its own stack.
9
+ *
10
+ * All primitives are signal-native, SSR-safe, dependency-free, and
11
+ * define sensible static behavior under `prefers-reduced-motion`.
12
+ */
13
+ import { type Accessor } from "solid-js";
14
+ import { type Easing, type EasingName } from "./easing.js";
15
+ type MaybeElement = () => Element | null | undefined;
16
+ /** Stages of a transaction's life, from wallet prompt to finality. */
17
+ export type TxState = "idle" | "signing" | "pending" | "confirming" | "success" | "failed";
18
+ /** wagmi/viem-style transaction status fed through `source`. */
19
+ export interface TxStatusInput {
20
+ /** Transaction status from the wallet/chain adapter. */
21
+ status?: "pending" | "success" | "error" | "idle";
22
+ /** Confirmations seen so far, once a hash exists. */
23
+ confirmations?: number;
24
+ }
25
+ export interface TxLifecycleOptions {
26
+ /**
27
+ * wagmi/viem-style state accessor. Omit for fully manual control
28
+ * through `set()`.
29
+ */
30
+ source?: Accessor<TxStatusInput>;
31
+ /** Confirmations that promote "pending" to "confirming". Default 1. */
32
+ requiredConfirmations?: number;
33
+ /** Spring stiffness for the progress ring. Default 170. */
34
+ stiffness?: number;
35
+ /** Spring damping for the progress ring. Default 26. */
36
+ damping?: number;
37
+ /** Called after entering a state, with the previous state. */
38
+ onEnter?: (state: TxState, prev: TxState) => void;
39
+ }
40
+ export interface TxLifecycleControls {
41
+ /** Current lifecycle state. */
42
+ state: Accessor<TxState>;
43
+ /**
44
+ * Set the state manually, for stages the chain never reports
45
+ * (for example "signing" when the wallet prompt opens).
46
+ */
47
+ set: (next: TxState) => void;
48
+ /** Return to "idle". */
49
+ reset: () => void;
50
+ /**
51
+ * 0 to 1 across the lifecycle, spring-smoothed for progress rings
52
+ * and bars: idle 0, signing 0.25, pending 0.5, confirming 0.75,
53
+ * success/failed 1.
54
+ */
55
+ progress: Accessor<number>;
56
+ }
57
+ /**
58
+ * Signal-native transaction lifecycle.
59
+ *
60
+ * Feed it wagmi/viem-style state through `source` and it derives the
61
+ * stage: a reported hash with too few confirmations is "pending",
62
+ * enough confirmations is "confirming", success/error map to
63
+ * "success"/"failed". Stages the chain never reports, like "signing"
64
+ * while the wallet prompt is open, are set manually.
65
+ *
66
+ * ```ts
67
+ * const tx = createTxLifecycle({
68
+ * source: () => ({
69
+ * status: receiptQuery.status, // "pending" | "success" | "error" | "idle"
70
+ * confirmations: receiptQuery.confirmations,
71
+ * }),
72
+ * requiredConfirmations: 2,
73
+ * })
74
+ * const openWallet = () => {
75
+ * tx.set("signing")
76
+ * sendTransaction()
77
+ * }
78
+ * // <ProgressRing value={tx.progress()} state={tx.state()} />
79
+ * ```
80
+ *
81
+ * SSR-safe: "idle" with `progress()` 0. Under reduced motion state
82
+ * changes apply instantly and `progress()` jumps to its target.
83
+ */
84
+ export declare function createTxLifecycle(options?: TxLifecycleOptions): TxLifecycleControls;
85
+ export interface TickerOptions {
86
+ /** Decimals in the formatted output. Default 2. */
87
+ decimals?: number;
88
+ /** Roll duration per digit, in ms. Default 400. */
89
+ duration?: number;
90
+ /** Flash color when the value rises. Default "#16a34a". */
91
+ upColor?: string;
92
+ /** Flash color when the value falls. Default "#dc2626". */
93
+ downColor?: string;
94
+ /** How long the flash color holds, in ms. Default 600. */
95
+ flashMs?: number;
96
+ /** Locale for grouping separators. Default "en-US". */
97
+ locale?: string;
98
+ }
99
+ export interface TickerControls {
100
+ /** Formatted value, SSR-safe. */
101
+ display: Accessor<string>;
102
+ /** Direction of the last update: "up" | "down" | "flat". */
103
+ direction: Accessor<"up" | "down" | "flat">;
104
+ }
105
+ /**
106
+ * Animated price/balance ticker: per-digit roll, direction flash.
107
+ *
108
+ * Each digit is a 0-9 strip in an overflow-hidden column, driven by a
109
+ * single rAF task for the whole ticker. Rapid source updates batch to
110
+ * one render per frame, keeping only the latest value, so a hot price
111
+ * feed never queues animation debt. Non-digit characters (decimal
112
+ * point, grouping separators, minus sign) render statically.
113
+ *
114
+ * ```tsx
115
+ * let el!: HTMLSpanElement
116
+ * const [price, setPrice] = createSignal(48210.5)
117
+ * const ticker = createTicker(price, () => el, { decimals: 2 })
118
+ * <span ref={el} style={{ color: ticker.direction() === "up" ? "green" : "red" }}>
119
+ * {ticker.display()}
120
+ * </span>
121
+ * ```
122
+ *
123
+ * SSR-safe: `display()` returns the formatted string with no DOM.
124
+ * Under reduced motion the text swaps instantly: no roll, no flash.
125
+ */
126
+ export declare function createTicker(source: Accessor<number>, ref: MaybeElement, options?: TickerOptions): TickerControls;
127
+ export type MintRevealStatus = "idle" | "anticipating" | "flipping" | "revealed";
128
+ export interface MintRevealOptions {
129
+ /** Anticipation shake duration, in ms. Default 500. */
130
+ shakeDuration?: number;
131
+ /** rotateY flip duration, in ms. Default 700. */
132
+ flipDuration?: number;
133
+ /** Squash and stretch on landing. Default true. */
134
+ squash?: boolean;
135
+ /** Easing for the flip. Default "easeOutCubic". */
136
+ easing?: Easing | EasingName;
137
+ /** Called at the flip midpoint: swap the card faces here. */
138
+ onFlip?: () => void;
139
+ }
140
+ export interface MintRevealControls {
141
+ /** Play the full reveal choreography. Resolves when revealed. */
142
+ play: () => Promise<void>;
143
+ /** Return to "idle". */
144
+ reset: () => void;
145
+ /** Reactive status. */
146
+ status: Accessor<MintRevealStatus>;
147
+ }
148
+ /**
149
+ * Pack-open / card-reveal choreography, built on the cartoon family:
150
+ * an anticipation shake, a rotateY flip with a face swap at the
151
+ * midpoint, and squash and stretch driven by the flip's own velocity
152
+ * as the card lands. Everything stays on compositor-friendly
153
+ * properties (transform, and the CSS `scale` property for the deform).
154
+ *
155
+ * ```tsx
156
+ * let card!: HTMLDivElement
157
+ * const [face, setFace] = createSignal<"back" | "front">("back")
158
+ * const reveal = createMintReveal(() => card, {
159
+ * onFlip: () => setFace("front"), // swap faces mid-flip
160
+ * })
161
+ * <button onClick={() => reveal.play()}>Reveal</button>
162
+ * <div ref={card} style={{ "backface-visibility": "hidden" }}>
163
+ * {face() === "back" ? <CardBack /> : <NftFront />}
164
+ * </div>
165
+ * ```
166
+ *
167
+ * SSR-safe: no-op on the server, `status()` is "revealed" so the face
168
+ * renders statically. Under reduced motion the shake and flip are
169
+ * skipped and the final face shows at once.
170
+ */
171
+ export declare function createMintReveal(ref: MaybeElement, options?: MintRevealOptions): MintRevealControls;
172
+ export interface ConnectButtonOptions {
173
+ /** Magnetic pull strength, 0 to 1. Default 0.35. */
174
+ strength?: number;
175
+ /** Scale while pressed. Default 0.96. */
176
+ pressScale?: number;
177
+ }
178
+ export type ConnectButtonStatus = "idle" | "ticking" | "pulsing";
179
+ export interface ConnectButtonControls {
180
+ /** Checkmark tick for copy-address feedback. */
181
+ copyTick: () => void;
182
+ /** Expanding ring pulse for chain switches. */
183
+ chainPulse: () => void;
184
+ /** Reactive status: "idle" | "ticking" | "pulsing". */
185
+ status: Accessor<ConnectButtonStatus>;
186
+ }
187
+ /**
188
+ * Wallet connect-button micro-interactions: magnetic hover pull,
189
+ * press scale, a checkmark tick for copy-address feedback, and an
190
+ * expanding ring pulse for chain switches.
191
+ *
192
+ * The button's transform is owned by the primitive (magnetic pull
193
+ * plus press scale); the tick and pulse animate overlay spans so
194
+ * they never fight the hover motion. The button is given
195
+ * `position: relative` the first time a tick or pulse runs, to stage
196
+ * the overlays.
197
+ *
198
+ * ```tsx
199
+ * let btn!: HTMLButtonElement
200
+ * const connect = createConnectButton(() => btn)
201
+ * const copy = async () => {
202
+ * await navigator.clipboard.writeText(address())
203
+ * connect.copyTick()
204
+ * }
205
+ * <button ref={btn} onClick={copy}>0x7a…f3c2</button>
206
+ * ```
207
+ *
208
+ * SSR-safe: no-op on the server. Under reduced motion the magnetic
209
+ * pull is off and the tick/pulse become instant state changes (the
210
+ * check shows statically, then hides).
211
+ */
212
+ export declare function createConnectButton(ref: MaybeElement, options?: ConnectButtonOptions): ConnectButtonControls;
213
+ export {};