solid-drift 0.2.0 → 0.7.1
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 +735 -10
- package/dist/animate.d.ts +2 -2
- package/dist/animate.js +2 -2
- package/dist/cartoon.d.ts +190 -0
- package/dist/cartoon.js +334 -0
- package/dist/color.d.ts +53 -0
- package/dist/color.js +391 -0
- package/dist/directive.d.ts +5 -5
- package/dist/directive.js +15 -7
- package/dist/easing.d.ts +22 -1
- package/dist/easing.js +49 -1
- package/dist/flip.d.ts +44 -0
- package/dist/flip.js +108 -0
- package/dist/horizontal.d.ts +107 -0
- package/dist/horizontal.js +208 -0
- package/dist/index.d.ts +16 -3
- package/dist/index.js +15 -3
- package/dist/inview.d.ts +4 -4
- package/dist/inview.js +5 -5
- package/dist/motion.d.ts +327 -0
- package/dist/motion.js +729 -0
- package/dist/physics.d.ts +146 -0
- package/dist/physics.js +352 -0
- package/dist/pointer.d.ts +76 -0
- package/dist/pointer.js +123 -0
- package/dist/reduced-motion.d.ts +3 -3
- package/dist/reduced-motion.js +5 -4
- package/dist/scroll.d.ts +3 -3
- package/dist/scroll.js +5 -5
- package/dist/scrollfx.d.ts +156 -0
- package/dist/scrollfx.js +148 -0
- package/dist/scrub.d.ts +51 -0
- package/dist/scrub.js +67 -0
- package/dist/spring.d.ts +39 -4
- package/dist/spring.js +26 -7
- package/dist/stagger.d.ts +4 -4
- package/dist/stagger.js +4 -4
- package/dist/timeline.d.ts +45 -0
- package/dist/timeline.js +93 -0
- package/dist/trail.d.ts +27 -0
- package/dist/trail.js +75 -0
- package/dist/tween.d.ts +3 -3
- package/dist/tween.js +3 -3
- package/dist/typography.d.ts +241 -0
- package/dist/typography.js +812 -0
- package/dist/velocity.d.ts +44 -0
- package/dist/velocity.js +88 -0
- package/package.json +1 -1
package/dist/animate.d.ts
CHANGED
|
@@ -25,8 +25,8 @@ export interface AnimationControls {
|
|
|
25
25
|
* duration: 500,
|
|
26
26
|
* easing: "easeOutExpo",
|
|
27
27
|
* onUpdate: (v) => el.style.opacity = String(v / 100),
|
|
28
|
-
* })
|
|
29
|
-
* await ctl.finished
|
|
28
|
+
* })
|
|
29
|
+
* await ctl.finished
|
|
30
30
|
* ```
|
|
31
31
|
*/
|
|
32
32
|
export declare function animate(from: number, to: number, options?: AnimateOptions): AnimationControls;
|
package/dist/animate.js
CHANGED
|
@@ -9,8 +9,8 @@ import { prefersReducedMotion } from "./reduced-motion.js";
|
|
|
9
9
|
* duration: 500,
|
|
10
10
|
* easing: "easeOutExpo",
|
|
11
11
|
* onUpdate: (v) => el.style.opacity = String(v / 100),
|
|
12
|
-
* })
|
|
13
|
-
* await ctl.finished
|
|
12
|
+
* })
|
|
13
|
+
* await ctl.finished
|
|
14
14
|
* ```
|
|
15
15
|
*/
|
|
16
16
|
export function animate(from, to, options = {}) {
|
|
@@ -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;
|
package/dist/cartoon.js
ADDED
|
@@ -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
|
+
}
|
package/dist/color.d.ts
ADDED
|
@@ -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;
|