solid-drift 0.2.0 → 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.
Files changed (54) hide show
  1. package/README.md +933 -10
  2. package/dist/ai.d.ts +207 -0
  3. package/dist/ai.js +570 -0
  4. package/dist/animate.d.ts +2 -2
  5. package/dist/animate.js +2 -2
  6. package/dist/cartoon.d.ts +190 -0
  7. package/dist/cartoon.js +334 -0
  8. package/dist/color.d.ts +53 -0
  9. package/dist/color.js +391 -0
  10. package/dist/directive.d.ts +5 -5
  11. package/dist/directive.js +15 -7
  12. package/dist/easing.d.ts +22 -1
  13. package/dist/easing.js +49 -1
  14. package/dist/flip.d.ts +44 -0
  15. package/dist/flip.js +108 -0
  16. package/dist/horizontal.d.ts +107 -0
  17. package/dist/horizontal.js +208 -0
  18. package/dist/index.d.ts +20 -3
  19. package/dist/index.js +17 -3
  20. package/dist/inview.d.ts +4 -4
  21. package/dist/inview.js +5 -5
  22. package/dist/motion.d.ts +404 -0
  23. package/dist/motion.js +761 -0
  24. package/dist/physics.d.ts +146 -0
  25. package/dist/physics.js +352 -0
  26. package/dist/pointer.d.ts +76 -0
  27. package/dist/pointer.js +123 -0
  28. package/dist/reduced-motion.d.ts +3 -3
  29. package/dist/reduced-motion.js +5 -4
  30. package/dist/scroll.d.ts +3 -3
  31. package/dist/scroll.js +5 -5
  32. package/dist/scrollfx.d.ts +156 -0
  33. package/dist/scrollfx.js +148 -0
  34. package/dist/scrub.d.ts +51 -0
  35. package/dist/scrub.js +67 -0
  36. package/dist/spring.d.ts +39 -4
  37. package/dist/spring.js +26 -7
  38. package/dist/stagger.d.ts +4 -4
  39. package/dist/stagger.js +4 -4
  40. package/dist/text.d.ts +27 -0
  41. package/dist/text.js +84 -0
  42. package/dist/timeline.d.ts +45 -0
  43. package/dist/timeline.js +93 -0
  44. package/dist/trail.d.ts +27 -0
  45. package/dist/trail.js +75 -0
  46. package/dist/tween.d.ts +3 -3
  47. package/dist/tween.js +3 -3
  48. package/dist/typography.d.ts +241 -0
  49. package/dist/typography.js +812 -0
  50. package/dist/velocity.d.ts +44 -0
  51. package/dist/velocity.js +88 -0
  52. package/dist/web3.d.ts +213 -0
  53. package/dist/web3.js +640 -0
  54. package/package.json +1 -1
@@ -0,0 +1,190 @@
1
+ import { type Accessor } from "solid-js";
2
+ import { type SpringOptions } from "./spring.js";
3
+ import { type AnimateOptions, type AnimationControls } from "./animate.js";
4
+ import { type Easing, type EasingName } from "./easing.js";
5
+ export interface SquashStretchOptions {
6
+ /**
7
+ * The motion to deform around: a single signal (horizontal motion in
8
+ * its own units) or an `{ x, y }` pair for two-dimensional motion.
9
+ */
10
+ source: Accessor<number> | {
11
+ x: Accessor<number>;
12
+ y: Accessor<number>;
13
+ };
14
+ /** Peak stretch scale at full speed. Default 1.3. */
15
+ maxStretch?: number;
16
+ /** Deepest squash scale on a hard stop. Default 0.7. */
17
+ maxSquash?: number;
18
+ /** Speed (units per second) that maps to full stretch. Default 2400. */
19
+ fullSpeed?: number;
20
+ /**
21
+ * How much the cross axis compensates the stretch, 0 to 1.
22
+ * 1 keeps the volume constant, 0 leaves the cross axis alone.
23
+ * Default 0.8.
24
+ */
25
+ preserveVolume?: number;
26
+ /**
27
+ * Deceleration (units per second squared) that counts as an impact
28
+ * and triggers the squash. Default 9000.
29
+ */
30
+ impactThreshold?: number;
31
+ /**
32
+ * Spring smoothing for the deform. Defaults to a snappy spring with
33
+ * a little bounce, so arrivals feel jelly.
34
+ */
35
+ spring?: SpringOptions;
36
+ }
37
+ export interface SquashStretchResult {
38
+ /** Scale along the dominant motion axis. */
39
+ scaleX: Accessor<number>;
40
+ /** Scale along the cross axis. */
41
+ scaleY: Accessor<number>;
42
+ }
43
+ /**
44
+ * Cartoon squash and stretch driven by a motion signal.
45
+ *
46
+ * The element stretches along its direction of travel as it speeds up
47
+ * (with the cross axis shrinking to preserve volume, like a water
48
+ * balloon in flight) and squashes flat for a beat when it stops hard,
49
+ * exactly the way a cartoon character pancakes on landing. The deform
50
+ * is applied through the CSS `scale` property, so it composes with any
51
+ * `transform` you set yourself, and the smoothed `scaleX`/`scaleY`
52
+ * accessors are returned for custom composition.
53
+ *
54
+ * SSR-safe and reduced-motion safe: no deform is applied and both
55
+ * accessors return constant `1`.
56
+ *
57
+ * ```tsx
58
+ * let ball!: HTMLDivElement
59
+ * const [target, setTarget] = createSignal(0)
60
+ * const x = createSpring(target, { stiffness: 120, damping: 14 })
61
+ * createSquashStretch(() => ball, { source: x })
62
+ * <div ref={ball} style={{ transform: `translateX(${x()}px)` }} />
63
+ * setTarget(400) // the ball stretches mid-flight, squashes on arrival
64
+ * ```
65
+ */
66
+ export declare function createSquashStretch(ref: () => Element | null | undefined, options: SquashStretchOptions): SquashStretchResult;
67
+ export interface FollowThroughOptions {
68
+ /** Number of followers chained behind the leader. Default 3. */
69
+ links?: number;
70
+ /** Extra lag added per link, in milliseconds. Default 70. */
71
+ delayPerLink?: number;
72
+ /**
73
+ * Spring physics per link. A single config applies to every link;
74
+ * an array configures each link (shorter arrays reuse the last entry).
75
+ * Defaults to a loose spring with visible overshoot.
76
+ */
77
+ spring?: SpringOptions | SpringOptions[];
78
+ }
79
+ /**
80
+ * Follow-through and overlapping action: a chain of followers that lag
81
+ * behind a leader signal, then catch up with an overshoot.
82
+ *
83
+ * This is the Tom and Jerry shadow: the leader darts away, each follower
84
+ * stays behind for a beat, then runs after it and overshoots before
85
+ * settling. Chain the returned accessors to trailing elements (a comet
86
+ * tail, a drag ghost, cascading highlights) and the whole chain ripples
87
+ * with one signal change.
88
+ *
89
+ * Each link trails the previous link's output and then springs toward it
90
+ * with a loose spring, so the lag compounds down the chain. Under
91
+ * reduced motion (or on the server) every follower returns the source
92
+ * itself: no lag, no overshoot.
93
+ *
94
+ * ```tsx
95
+ * const [tab, setTab] = createSignal(0)
96
+ * const x = createSpring(tab, { stiffness: 200, damping: 26 })
97
+ * // Three ghosts chase the tab indicator with cartoon lag.
98
+ * const [g1, g2, g3] = createFollowThrough(x, { links: 3 })
99
+ * ```
100
+ */
101
+ export declare function createFollowThrough(source: Accessor<number>, options?: FollowThroughOptions): Accessor<number>[];
102
+ export interface AnticipationOptions {
103
+ /**
104
+ * Windup distance in the animation's units, applied opposite to the
105
+ * direction of travel. Default: 10% of the travel distance.
106
+ */
107
+ windup?: number;
108
+ /** Windup duration in milliseconds. Default 140. */
109
+ windupDuration?: number;
110
+ /** Hold at full windup before firing, in milliseconds. Default 50. */
111
+ holdDuration?: number;
112
+ /** Easing for the windup move. Default "easeInQuad". */
113
+ windupEasing?: Easing | EasingName;
114
+ }
115
+ /**
116
+ * Anticipation: wind up in the opposite direction before the main
117
+ * motion fires.
118
+ *
119
+ * A composable wrapper around `animate()`: it plays a short windup
120
+ * (from `from` to slightly before `from`), holds for a beat, then runs
121
+ * the main animation to `to` with all of `animate()`'s options. The
122
+ * windup sells the weight of what follows, a crouch before the jump,
123
+ * a pull-back before the punch.
124
+ *
125
+ * Returns `AnimationControls` just like `animate()`: `stop()` halts
126
+ * mid-windup or mid-flight, `finished` resolves either way. Under
127
+ * reduced motion the windup is skipped and it behaves exactly like
128
+ * `animate(from, to, options)`.
129
+ *
130
+ * ```ts
131
+ * // The button crouches 12px, holds, then springs 200px right.
132
+ * const ctl = createAnticipation(0, 200, {
133
+ * windup: 12,
134
+ * windupDuration: 120,
135
+ * duration: 450,
136
+ * easing: "easeOutBack",
137
+ * onUpdate: (v) => (el.style.transform = `translateX(${v}px)`),
138
+ * })
139
+ * await ctl.finished
140
+ * ```
141
+ */
142
+ export declare function createAnticipation(from: number, to: number, options?: AnticipationOptions & AnimateOptions): AnimationControls;
143
+ export interface WobbleOptions {
144
+ /** Peak rotation in degrees. Default 7. */
145
+ rotation?: number;
146
+ /** Peak scale deviation, 0.08 is 8%. Default 0.08. */
147
+ scale?: number;
148
+ /** Oscillation frequency in Hertz. Default 5. */
149
+ frequency?: number;
150
+ /** Decay time constant in seconds: smaller dies faster. Default 0.45. */
151
+ decay?: number;
152
+ }
153
+ export interface WobbleResult {
154
+ /** Rotation in degrees, a decaying oscillation around 0. */
155
+ rotate: Accessor<number>;
156
+ /** Horizontal scale, oscillating around 1. */
157
+ scaleX: Accessor<number>;
158
+ /** Vertical scale, oscillating around 1 in counter-phase. */
159
+ scaleY: Accessor<number>;
160
+ /**
161
+ * Trigger a wobble. Strength scales the initial kick and may be
162
+ * negative to wobble the other way. Default 1.
163
+ */
164
+ wobble: (strength?: number) => void;
165
+ }
166
+ /**
167
+ * Jelly wobble: a decaying rotational and scale oscillation after a
168
+ * disturbance, like poking a bowl of jelly.
169
+ *
170
+ * Call `wobble()` to kick it (or poke the element directly when a `ref`
171
+ * is given: pointerdown on the element triggers a wobble). Rotation and
172
+ * scale oscillate in counter-phase inside an exponential envelope, then
173
+ * park exactly at rest. Re-triggering mid-wobble restarts the kick.
174
+ *
175
+ * SSR-safe and reduced-motion safe: `wobble()` is a no-op and the
176
+ * accessors rest at 0, 1, 1.
177
+ *
178
+ * ```tsx
179
+ * let jelly!: HTMLDivElement
180
+ * const { rotate, scaleX, scaleY, wobble } = createWobble(() => jelly)
181
+ * <div
182
+ * ref={jelly}
183
+ * onClick={() => wobble()}
184
+ * style={{
185
+ * transform: `rotate(${rotate()}deg) scale(${scaleX()}, ${scaleY()})`,
186
+ * }}
187
+ * />
188
+ * ```
189
+ */
190
+ export declare function createWobble(ref?: () => Element | null | undefined, options?: WobbleOptions): WobbleResult;
@@ -0,0 +1,334 @@
1
+ import { createEffect, createSignal, onCleanup, untrack, } from "solid-js";
2
+ import { now, schedule } from "./engine.js";
3
+ import { prefersReducedMotion } from "./reduced-motion.js";
4
+ import { createSpring } from "./spring.js";
5
+ import { createTrail } from "./trail.js";
6
+ import { animate, } from "./animate.js";
7
+ /**
8
+ * Cartoon squash and stretch driven by a motion signal.
9
+ *
10
+ * The element stretches along its direction of travel as it speeds up
11
+ * (with the cross axis shrinking to preserve volume, like a water
12
+ * balloon in flight) and squashes flat for a beat when it stops hard,
13
+ * exactly the way a cartoon character pancakes on landing. The deform
14
+ * is applied through the CSS `scale` property, so it composes with any
15
+ * `transform` you set yourself, and the smoothed `scaleX`/`scaleY`
16
+ * accessors are returned for custom composition.
17
+ *
18
+ * SSR-safe and reduced-motion safe: no deform is applied and both
19
+ * accessors return constant `1`.
20
+ *
21
+ * ```tsx
22
+ * let ball!: HTMLDivElement
23
+ * const [target, setTarget] = createSignal(0)
24
+ * const x = createSpring(target, { stiffness: 120, damping: 14 })
25
+ * createSquashStretch(() => ball, { source: x })
26
+ * <div ref={ball} style={{ transform: `translateX(${x()}px)` }} />
27
+ * setTarget(400) // the ball stretches mid-flight, squashes on arrival
28
+ * ```
29
+ */
30
+ export function createSquashStretch(ref, options) {
31
+ const { source, maxStretch = 1.3, maxSquash = 0.7, fullSpeed = 2400, preserveVolume = 0.8, impactThreshold = 9000, spring = { stiffness: 260, damping: 16 }, } = options;
32
+ if (typeof window === "undefined" || prefersReducedMotion()) {
33
+ const one = () => 1;
34
+ return { scaleX: one, scaleY: one };
35
+ }
36
+ const readX = typeof source === "function" ? source : source.x;
37
+ const readY = typeof source === "function" ? null : source.y;
38
+ const [targetSX, setTargetSX] = createSignal(1);
39
+ const [targetSY, setTargetSY] = createSignal(1);
40
+ const scaleX = createSpring(targetSX, spring);
41
+ const scaleY = createSpring(targetSY, spring);
42
+ // Apply the deform through the CSS `scale` property so it composes
43
+ // with whatever `transform` the user sets on the element.
44
+ createEffect(() => {
45
+ const el = ref();
46
+ el?.style?.setProperty?.("scale", `${scaleX().toFixed(4)} ${scaleY().toFixed(4)}`);
47
+ });
48
+ let cancel = null;
49
+ let lastT = 0;
50
+ let lastX = untrack(readX);
51
+ let lastY = readY ? untrack(readY) : 0;
52
+ let vx = 0;
53
+ let vy = 0;
54
+ let prevSpeed = 0;
55
+ let impact = 0;
56
+ const loop = (t) => {
57
+ const dt = Math.min(Math.max((t - lastT) / 1000, 0.0001), 0.064);
58
+ lastT = t;
59
+ const x = untrack(readX);
60
+ const y = readY ? untrack(readY) : 0;
61
+ // Exponential smoothing keeps the velocity estimate stable across
62
+ // uneven frame times without lagging behind real motion.
63
+ const blend = 1 - Math.exp(-dt * 18);
64
+ vx += ((x - lastX) / dt - vx) * blend;
65
+ vy += ((y - lastY) / dt - vy) * blend;
66
+ lastX = x;
67
+ lastY = y;
68
+ const speed = Math.hypot(vx, vy);
69
+ const decel = (prevSpeed - speed) / dt;
70
+ if (prevSpeed > fullSpeed * 0.2 && decel > impactThreshold)
71
+ impact = 1;
72
+ prevSpeed = speed;
73
+ impact = Math.max(0, impact - dt * 3.5);
74
+ const speedNorm = Math.min(speed / fullSpeed, 1);
75
+ const stretch = speedNorm * (maxStretch - 1);
76
+ const squash = impact * (1 - maxSquash);
77
+ // On impact the stretch collapses and the squash takes over: pancake.
78
+ const along = 1 + stretch * (1 - impact) - squash;
79
+ const cross = 1 - stretch * (1 - impact) * preserveVolume + squash * preserveVolume;
80
+ if (Math.abs(vx) >= Math.abs(vy)) {
81
+ setTargetSX(along);
82
+ setTargetSY(cross);
83
+ }
84
+ else {
85
+ setTargetSX(cross);
86
+ setTargetSY(along);
87
+ }
88
+ const idle = speed < 1 &&
89
+ impact <= 0 &&
90
+ Math.abs(untrack(scaleX) - 1) < 0.002 &&
91
+ Math.abs(untrack(scaleY) - 1) < 0.002;
92
+ if (idle) {
93
+ setTargetSX(1);
94
+ setTargetSY(1);
95
+ cancel = null;
96
+ return false;
97
+ }
98
+ return true;
99
+ };
100
+ const kick = () => {
101
+ if (cancel)
102
+ return;
103
+ // Re-seed from the live values so a stale sample never reads as a
104
+ // phantom burst of speed on the first frame.
105
+ lastT = now();
106
+ lastX = untrack(readX);
107
+ lastY = readY ? untrack(readY) : 0;
108
+ vx = 0;
109
+ vy = 0;
110
+ prevSpeed = 0;
111
+ cancel = schedule(loop);
112
+ };
113
+ createEffect(() => {
114
+ readX();
115
+ readY?.();
116
+ kick();
117
+ });
118
+ onCleanup(() => cancel?.());
119
+ return { scaleX, scaleY };
120
+ }
121
+ /**
122
+ * Follow-through and overlapping action: a chain of followers that lag
123
+ * behind a leader signal, then catch up with an overshoot.
124
+ *
125
+ * This is the Tom and Jerry shadow: the leader darts away, each follower
126
+ * stays behind for a beat, then runs after it and overshoots before
127
+ * settling. Chain the returned accessors to trailing elements (a comet
128
+ * tail, a drag ghost, cascading highlights) and the whole chain ripples
129
+ * with one signal change.
130
+ *
131
+ * Each link trails the previous link's output and then springs toward it
132
+ * with a loose spring, so the lag compounds down the chain. Under
133
+ * reduced motion (or on the server) every follower returns the source
134
+ * itself: no lag, no overshoot.
135
+ *
136
+ * ```tsx
137
+ * const [tab, setTab] = createSignal(0)
138
+ * const x = createSpring(tab, { stiffness: 200, damping: 26 })
139
+ * // Three ghosts chase the tab indicator with cartoon lag.
140
+ * const [g1, g2, g3] = createFollowThrough(x, { links: 3 })
141
+ * ```
142
+ */
143
+ export function createFollowThrough(source, options = {}) {
144
+ const { links = 3, delayPerLink = 70 } = options;
145
+ if (typeof window === "undefined" || prefersReducedMotion() || links <= 0) {
146
+ return Array.from({ length: Math.max(links, 0) }, () => source);
147
+ }
148
+ const springFor = (i) => {
149
+ const s = options.spring;
150
+ if (Array.isArray(s))
151
+ return s[Math.min(i, s.length - 1)] ?? {};
152
+ return s ?? { stiffness: 170, damping: 11 };
153
+ };
154
+ const followers = [];
155
+ let prev = source;
156
+ for (let i = 0; i < links; i++) {
157
+ const lagged = createTrail(prev, { delay: delayPerLink });
158
+ const springy = createSpring(lagged, springFor(i));
159
+ followers.push(springy);
160
+ prev = springy;
161
+ }
162
+ return followers;
163
+ }
164
+ /**
165
+ * Anticipation: wind up in the opposite direction before the main
166
+ * motion fires.
167
+ *
168
+ * A composable wrapper around `animate()`: it plays a short windup
169
+ * (from `from` to slightly before `from`), holds for a beat, then runs
170
+ * the main animation to `to` with all of `animate()`'s options. The
171
+ * windup sells the weight of what follows, a crouch before the jump,
172
+ * a pull-back before the punch.
173
+ *
174
+ * Returns `AnimationControls` just like `animate()`: `stop()` halts
175
+ * mid-windup or mid-flight, `finished` resolves either way. Under
176
+ * reduced motion the windup is skipped and it behaves exactly like
177
+ * `animate(from, to, options)`.
178
+ *
179
+ * ```ts
180
+ * // The button crouches 12px, holds, then springs 200px right.
181
+ * const ctl = createAnticipation(0, 200, {
182
+ * windup: 12,
183
+ * windupDuration: 120,
184
+ * duration: 450,
185
+ * easing: "easeOutBack",
186
+ * onUpdate: (v) => (el.style.transform = `translateX(${v}px)`),
187
+ * })
188
+ * await ctl.finished
189
+ * ```
190
+ */
191
+ export function createAnticipation(from, to, options = {}) {
192
+ const { windup, windupDuration = 140, holdDuration = 50, windupEasing = "easeInQuad", ...main } = options;
193
+ if (prefersReducedMotion()) {
194
+ return animate(from, to, main);
195
+ }
196
+ const distance = to - from;
197
+ const windupAmount = windup ?? Math.abs(distance) * 0.1;
198
+ const windupTarget = from - Math.sign(distance === 0 ? 1 : distance) * windupAmount;
199
+ let resolveFinished;
200
+ const finished = new Promise((resolve) => {
201
+ resolveFinished = resolve;
202
+ });
203
+ let done = false;
204
+ let stopCurrent = () => { };
205
+ const finish = (completed) => {
206
+ if (done)
207
+ return;
208
+ done = true;
209
+ if (completed)
210
+ main.onComplete?.();
211
+ resolveFinished();
212
+ };
213
+ const playMain = () => {
214
+ stopCurrent = animate(windupTarget, to, {
215
+ ...main,
216
+ onComplete: () => finish(true),
217
+ }).stop;
218
+ };
219
+ stopCurrent = animate(from, windupTarget, {
220
+ duration: windupDuration,
221
+ easing: windupEasing,
222
+ onUpdate: main.onUpdate,
223
+ onComplete: () => {
224
+ if (done)
225
+ return;
226
+ if (holdDuration > 0) {
227
+ // Hold at full windup on the shared clock: no setTimeout, so
228
+ // the pause stays in sync with the animation engine.
229
+ const startAt = now() + holdDuration;
230
+ const cancelHold = schedule((t) => {
231
+ if (t < startAt)
232
+ return true;
233
+ if (!done)
234
+ playMain();
235
+ return false;
236
+ });
237
+ const stopWindup = stopCurrent;
238
+ stopCurrent = () => {
239
+ cancelHold();
240
+ stopWindup();
241
+ };
242
+ }
243
+ else {
244
+ playMain();
245
+ }
246
+ },
247
+ }).stop;
248
+ return {
249
+ stop: () => {
250
+ stopCurrent();
251
+ finish(false);
252
+ },
253
+ finished,
254
+ };
255
+ }
256
+ /**
257
+ * Jelly wobble: a decaying rotational and scale oscillation after a
258
+ * disturbance, like poking a bowl of jelly.
259
+ *
260
+ * Call `wobble()` to kick it (or poke the element directly when a `ref`
261
+ * is given: pointerdown on the element triggers a wobble). Rotation and
262
+ * scale oscillate in counter-phase inside an exponential envelope, then
263
+ * park exactly at rest. Re-triggering mid-wobble restarts the kick.
264
+ *
265
+ * SSR-safe and reduced-motion safe: `wobble()` is a no-op and the
266
+ * accessors rest at 0, 1, 1.
267
+ *
268
+ * ```tsx
269
+ * let jelly!: HTMLDivElement
270
+ * const { rotate, scaleX, scaleY, wobble } = createWobble(() => jelly)
271
+ * <div
272
+ * ref={jelly}
273
+ * onClick={() => wobble()}
274
+ * style={{
275
+ * transform: `rotate(${rotate()}deg) scale(${scaleX()}, ${scaleY()})`,
276
+ * }}
277
+ * />
278
+ * ```
279
+ */
280
+ export function createWobble(ref, options = {}) {
281
+ const { rotation = 7, scale = 0.08, frequency = 5, decay = 0.45 } = options;
282
+ if (typeof window === "undefined" || prefersReducedMotion()) {
283
+ return {
284
+ rotate: () => 0,
285
+ scaleX: () => 1,
286
+ scaleY: () => 1,
287
+ wobble: () => { },
288
+ };
289
+ }
290
+ const [rotate, setRotate] = createSignal(0);
291
+ const [scaleX, setScaleX] = createSignal(1);
292
+ const [scaleY, setScaleY] = createSignal(1);
293
+ let cancel = null;
294
+ let startT = 0;
295
+ let strength = 0;
296
+ const loop = (t) => {
297
+ const elapsed = (t - startT) / 1000;
298
+ const envelope = Math.exp(-elapsed / decay) * strength;
299
+ if (Math.abs(envelope) < 0.001) {
300
+ setRotate(0);
301
+ setScaleX(1);
302
+ setScaleY(1);
303
+ cancel = null;
304
+ return false;
305
+ }
306
+ const phase = 2 * Math.PI * frequency * elapsed;
307
+ const swing = Math.sin(phase) * envelope;
308
+ setRotate(rotation * swing);
309
+ // Counter-phase scale on the two axes reads as jelly, not a spin.
310
+ setScaleX(1 + scale * swing);
311
+ setScaleY(1 - scale * swing);
312
+ return true;
313
+ };
314
+ const wobble = (s = 1) => {
315
+ if (s === 0)
316
+ return;
317
+ strength = s;
318
+ startT = now();
319
+ if (!cancel)
320
+ cancel = schedule(loop);
321
+ };
322
+ if (ref) {
323
+ const onDown = () => wobble(1);
324
+ createEffect(() => {
325
+ const el = ref();
326
+ if (!el)
327
+ return;
328
+ el.addEventListener("pointerdown", onDown);
329
+ onCleanup(() => el.removeEventListener("pointerdown", onDown));
330
+ });
331
+ }
332
+ onCleanup(() => cancel?.());
333
+ return { rotate, scaleX, scaleY, wobble };
334
+ }
@@ -0,0 +1,53 @@
1
+ import { type Easing, type EasingName } from "./easing.js";
2
+ /**
3
+ * Shared color engine for solid-drift.
4
+ *
5
+ * `createScrollColor` (scroll-driven) and `createColorShift`
6
+ * (time-driven) both interpolate through CSS color stops in linear
7
+ * light. This module holds the parser, the interpolator, and the
8
+ * stop-sampling helper so neither primitive duplicates them.
9
+ */
10
+ /** A color in linear RGB (0 to 1 per channel) plus alpha (0 to 1). */
11
+ export interface LinearColor {
12
+ r: number;
13
+ g: number;
14
+ b: number;
15
+ a: number;
16
+ }
17
+ /** Output format for a computed color string. */
18
+ export type ColorFormat = "hex" | "rgb" | "hsl";
19
+ /** Parse any supported CSS color into linear RGB plus alpha. */
20
+ export declare function parseColor(input: string): LinearColor;
21
+ export declare function lerpColor(a: LinearColor, b: LinearColor, t: number): LinearColor;
22
+ export declare function formatColor(color: LinearColor, format: ColorFormat): string;
23
+ /** One color stop: a progress position and the color to reach there. */
24
+ export interface ColorStop {
25
+ /** Progress position, 0 to 1. */
26
+ at: number;
27
+ /**
28
+ * Color at this position. Accepts hex (#rgb, #rrggbb, with optional
29
+ * alpha), rgb()/rgba(), hsl()/hsla(), and CSS named colors.
30
+ */
31
+ color: string;
32
+ /**
33
+ * Easing for the segment that ends at this stop, following the CSS
34
+ * keyframe convention. Default "linear".
35
+ */
36
+ easing?: Easing | EasingName;
37
+ }
38
+ /** A color stop with its color parsed and easing resolved. */
39
+ export interface ParsedColorStop {
40
+ at: number;
41
+ color: LinearColor;
42
+ easing: Easing;
43
+ }
44
+ /**
45
+ * Sort stops by `at`, parse every color, and resolve each segment's
46
+ * easing. Shared by the scroll-driven and time-driven color primitives.
47
+ */
48
+ export declare function parseColorStops(stops: ColorStop[], fallbackEasing?: Easing | EasingName): ParsedColorStop[];
49
+ /**
50
+ * Sample a parsed stop list at progress `p` (clamped 0 to 1) and return
51
+ * the interpolated color string in the requested format.
52
+ */
53
+ export declare function sampleColorStops(stops: ParsedColorStop[], p: number, format: ColorFormat): string;