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/reduced-motion.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { type Accessor } from "solid-js";
|
|
|
2
2
|
/**
|
|
3
3
|
* Non-reactive check: does the user currently prefer reduced motion?
|
|
4
4
|
*
|
|
5
|
-
* SSR-safe
|
|
5
|
+
* SSR-safe: always `false` on the server. `createSpring`, `createTween`
|
|
6
6
|
* and `animate` sample this whenever they (re)start: when reduced motion
|
|
7
7
|
* is preferred they jump straight to the target value instead of animating.
|
|
8
8
|
*/
|
|
@@ -14,8 +14,8 @@ export declare function prefersReducedMotion(): boolean;
|
|
|
14
14
|
* SSR-safe: `false` on the server.
|
|
15
15
|
*
|
|
16
16
|
* ```tsx
|
|
17
|
-
* const reduced = usePrefersReducedMotion()
|
|
18
|
-
* const duration = () => (reduced() ? 0 : 400)
|
|
17
|
+
* const reduced = usePrefersReducedMotion()
|
|
18
|
+
* const duration = () => (reduced() ? 0 : 400)
|
|
19
19
|
* ```
|
|
20
20
|
*/
|
|
21
21
|
export declare function usePrefersReducedMotion(): Accessor<boolean>;
|
package/dist/reduced-motion.js
CHANGED
|
@@ -8,7 +8,7 @@ function queryMatches() {
|
|
|
8
8
|
/**
|
|
9
9
|
* Non-reactive check: does the user currently prefer reduced motion?
|
|
10
10
|
*
|
|
11
|
-
* SSR-safe
|
|
11
|
+
* SSR-safe: always `false` on the server. `createSpring`, `createTween`
|
|
12
12
|
* and `animate` sample this whenever they (re)start: when reduced motion
|
|
13
13
|
* is preferred they jump straight to the target value instead of animating.
|
|
14
14
|
*/
|
|
@@ -22,13 +22,14 @@ export function prefersReducedMotion() {
|
|
|
22
22
|
* SSR-safe: `false` on the server.
|
|
23
23
|
*
|
|
24
24
|
* ```tsx
|
|
25
|
-
* const reduced = usePrefersReducedMotion()
|
|
26
|
-
* const duration = () => (reduced() ? 0 : 400)
|
|
25
|
+
* const reduced = usePrefersReducedMotion()
|
|
26
|
+
* const duration = () => (reduced() ? 0 : 400)
|
|
27
27
|
* ```
|
|
28
28
|
*/
|
|
29
29
|
export function usePrefersReducedMotion() {
|
|
30
30
|
const [reduced, setReduced] = createSignal(queryMatches());
|
|
31
|
-
if (typeof window !== "undefined" &&
|
|
31
|
+
if (typeof window !== "undefined" &&
|
|
32
|
+
typeof window.matchMedia === "function") {
|
|
32
33
|
const mq = window.matchMedia(QUERY);
|
|
33
34
|
const onChange = () => setReduced(mq.matches);
|
|
34
35
|
mq.addEventListener("change", onChange);
|
package/dist/scroll.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export type ScrollTarget = "page" | (() => Element | null | undefined);
|
|
|
7
7
|
* - `"page"` (default): 0 at the very top of the page, 1 when the bottom
|
|
8
8
|
* of the page reaches the bottom of the viewport.
|
|
9
9
|
* - element accessor: 0 when the element's top edge touches the bottom of
|
|
10
|
-
* the viewport, 1 when its bottom edge touches the top
|
|
10
|
+
* the viewport, 1 when its bottom edge touches the top (that is, the
|
|
11
11
|
* element's full traversal through the viewport.
|
|
12
12
|
*
|
|
13
13
|
* Updates are rAF-throttled: no matter how many scroll/resize events fire,
|
|
@@ -18,8 +18,8 @@ export type ScrollTarget = "page" | (() => Element | null | undefined);
|
|
|
18
18
|
* `window` or `document`).
|
|
19
19
|
*
|
|
20
20
|
* ```tsx
|
|
21
|
-
* const progress = createScrollProgress()
|
|
22
|
-
* const bar = createScrollProgress(() => sectionRef)
|
|
21
|
+
* const progress = createScrollProgress() // page progress
|
|
22
|
+
* const bar = createScrollProgress(() => sectionRef) // element progress
|
|
23
23
|
* <div style={{ transform: `scaleX(${progress()})` }} />
|
|
24
24
|
* ```
|
|
25
25
|
*/
|
package/dist/scroll.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createEffect, createSignal, onCleanup
|
|
1
|
+
import { createEffect, createSignal, onCleanup } from "solid-js";
|
|
2
2
|
function clamp01(v) {
|
|
3
3
|
return v < 0 ? 0 : v > 1 ? 1 : v;
|
|
4
4
|
}
|
|
@@ -8,7 +8,7 @@ function clamp01(v) {
|
|
|
8
8
|
* - `"page"` (default): 0 at the very top of the page, 1 when the bottom
|
|
9
9
|
* of the page reaches the bottom of the viewport.
|
|
10
10
|
* - element accessor: 0 when the element's top edge touches the bottom of
|
|
11
|
-
* the viewport, 1 when its bottom edge touches the top
|
|
11
|
+
* the viewport, 1 when its bottom edge touches the top (that is, the
|
|
12
12
|
* element's full traversal through the viewport.
|
|
13
13
|
*
|
|
14
14
|
* Updates are rAF-throttled: no matter how many scroll/resize events fire,
|
|
@@ -19,13 +19,13 @@ function clamp01(v) {
|
|
|
19
19
|
* `window` or `document`).
|
|
20
20
|
*
|
|
21
21
|
* ```tsx
|
|
22
|
-
* const progress = createScrollProgress()
|
|
23
|
-
* const bar = createScrollProgress(() => sectionRef)
|
|
22
|
+
* const progress = createScrollProgress() // page progress
|
|
23
|
+
* const bar = createScrollProgress(() => sectionRef) // element progress
|
|
24
24
|
* <div style={{ transform: `scaleX(${progress()})` }} />
|
|
25
25
|
* ```
|
|
26
26
|
*/
|
|
27
27
|
export function createScrollProgress(target = "page") {
|
|
28
|
-
// SSR: no window, no scrolling
|
|
28
|
+
// SSR: no window, no scrolling, so it reports the top of the page.
|
|
29
29
|
if (typeof window === "undefined")
|
|
30
30
|
return () => 0;
|
|
31
31
|
const [progress, setProgress] = createSignal(0);
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import type { Accessor } from "solid-js";
|
|
2
|
+
import { type Easing, type EasingName } from "./easing.js";
|
|
3
|
+
import { type ColorFormat } from "./color.js";
|
|
4
|
+
/** Output format for an interpolated color string. */
|
|
5
|
+
export type ScrollColorFormat = ColorFormat;
|
|
6
|
+
/** One color stop: progress position and the color to reach there. */
|
|
7
|
+
export interface ScrollColorStop {
|
|
8
|
+
/** Progress position, 0 to 1. */
|
|
9
|
+
at: number;
|
|
10
|
+
/**
|
|
11
|
+
* Color at this position. Accepts hex (#rgb, #rrggbb, with optional
|
|
12
|
+
* alpha), rgb()/rgba(), hsl()/hsla(), and CSS named colors.
|
|
13
|
+
*/
|
|
14
|
+
color: string;
|
|
15
|
+
/**
|
|
16
|
+
* Easing for the segment that ends at this stop, following the CSS
|
|
17
|
+
* keyframe convention. Default "linear".
|
|
18
|
+
*/
|
|
19
|
+
easing?: Easing | EasingName;
|
|
20
|
+
}
|
|
21
|
+
export interface ScrollColorOptions {
|
|
22
|
+
/**
|
|
23
|
+
* Progress signal, 0 to 1. Defaults to whole-page scroll progress.
|
|
24
|
+
* Pass `createScrollProgress(() => section)` for element-scoped color.
|
|
25
|
+
*/
|
|
26
|
+
progress?: Accessor<number>;
|
|
27
|
+
/** Fallback easing for segments without their own. Default "linear". */
|
|
28
|
+
easing?: Easing | EasingName;
|
|
29
|
+
/** Output format. Default "hex". */
|
|
30
|
+
format?: ScrollColorFormat;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Maps a 0-to-1 progress signal through a list of color stops and returns
|
|
34
|
+
* the interpolated color as a string signal.
|
|
35
|
+
*
|
|
36
|
+
* Colors interpolate in linear light, so a midpoint between red and blue
|
|
37
|
+
* is the vivid purple your eyes expect, not the muddy #800080 you get
|
|
38
|
+
* from naive channel math. Alpha channels interpolate too.
|
|
39
|
+
*
|
|
40
|
+
* Stops sort themselves by `at`; progress outside the range clamps to
|
|
41
|
+
* the end colors. Segments default to linear so color tracks scroll 1:1
|
|
42
|
+
* unless you shape them.
|
|
43
|
+
*
|
|
44
|
+
* Pure computation, no listeners, no rAF: SSR-safe by construction.
|
|
45
|
+
* Under reduced motion it holds the final stop's color.
|
|
46
|
+
*
|
|
47
|
+
* ```tsx
|
|
48
|
+
* const progress = createScrollProgress()
|
|
49
|
+
* // The hero tint warms as you scroll through the first chapter.
|
|
50
|
+
* const tint = createScrollColor(
|
|
51
|
+
* [
|
|
52
|
+
* { at: 0, color: "#f4f6f9" },
|
|
53
|
+
* { at: 0.5, color: "#f7e8d0" },
|
|
54
|
+
* { at: 1, color: "#2b5176", easing: "easeInOutQuad" },
|
|
55
|
+
* ],
|
|
56
|
+
* { progress },
|
|
57
|
+
* )
|
|
58
|
+
* <section style={{ "background-color": tint() }} />
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
export declare function createScrollColor(stops: ScrollColorStop[], options?: ScrollColorOptions): Accessor<string>;
|
|
62
|
+
export interface ScrollTrackingOptions {
|
|
63
|
+
/**
|
|
64
|
+
* Progress signal, 0 to 1. Defaults to whole-page scroll progress.
|
|
65
|
+
* Pass `createScrollProgress(() => headline)` for element-scoped tracking.
|
|
66
|
+
*/
|
|
67
|
+
progress?: Accessor<number>;
|
|
68
|
+
/** Letter-spacing at progress 0. Default 0.3. */
|
|
69
|
+
from?: number;
|
|
70
|
+
/** Letter-spacing at progress 1. Default 0. */
|
|
71
|
+
to?: number;
|
|
72
|
+
/** Unit for the returned value. Default "em". */
|
|
73
|
+
unit?: "em" | "px";
|
|
74
|
+
/** Easing applied to the progress before mapping. Default "linear". */
|
|
75
|
+
easing?: Easing | EasingName;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Drives `letter-spacing` from a 0-to-1 progress signal: display words
|
|
79
|
+
* that spread apart or tighten together as you scroll.
|
|
80
|
+
*
|
|
81
|
+
* Returns a string signal like `"0.15em"` or `"6px"`, ready to drop into
|
|
82
|
+
* a style binding. Because the value is a plain string you can also
|
|
83
|
+
* bind it to any CSS property that takes a length.
|
|
84
|
+
*
|
|
85
|
+
* SSR-safe: returns the `from` value on the server. Under reduced motion
|
|
86
|
+
* it holds the `to` value (the settled, readable end state).
|
|
87
|
+
*
|
|
88
|
+
* ```tsx
|
|
89
|
+
* const progress = createScrollProgress(() => chapter)
|
|
90
|
+
* // A chapter title that tightens as it arrives.
|
|
91
|
+
* const tracking = createScrollTracking({
|
|
92
|
+
* progress,
|
|
93
|
+
* from: 0.35,
|
|
94
|
+
* to: 0,
|
|
95
|
+
* unit: "em",
|
|
96
|
+
* easing: "easeOutCubic",
|
|
97
|
+
* })
|
|
98
|
+
* <h2 style={{ "letter-spacing": tracking() }}>Chapter One</h2>
|
|
99
|
+
* ```
|
|
100
|
+
*/
|
|
101
|
+
export declare function createScrollTracking(options?: ScrollTrackingOptions): Accessor<string>;
|
|
102
|
+
/** Axis the divider line grows along. */
|
|
103
|
+
export type ScrollLineAxis = "x" | "y";
|
|
104
|
+
/** Where the line grows from. "start" is left/top in LTR reading order. */
|
|
105
|
+
export type ScrollLineOrigin = "start" | "center" | "end";
|
|
106
|
+
export interface ScrollLineOptions {
|
|
107
|
+
/**
|
|
108
|
+
* Progress signal, 0 to 1. Defaults to whole-page scroll progress.
|
|
109
|
+
* Pass `createScrollProgress(() => divider)` for element-scoped reveal.
|
|
110
|
+
*/
|
|
111
|
+
progress?: Accessor<number>;
|
|
112
|
+
/** Grow along the x (horizontal rule) or y (vertical rule) axis. Default "x". */
|
|
113
|
+
axis?: ScrollLineAxis;
|
|
114
|
+
/** Scale at progress 0. Default 0 (invisible). */
|
|
115
|
+
from?: number;
|
|
116
|
+
/** Scale at progress 1. Default 1 (fully drawn). */
|
|
117
|
+
to?: number;
|
|
118
|
+
/** Which edge the line grows from. Default "start". */
|
|
119
|
+
origin?: ScrollLineOrigin;
|
|
120
|
+
/** Easing applied to the progress before mapping. Default "linear". */
|
|
121
|
+
easing?: Easing | EasingName;
|
|
122
|
+
}
|
|
123
|
+
/** Style pair for a progress-driven divider. Spread into a style binding. */
|
|
124
|
+
export interface ScrollLineStyle {
|
|
125
|
+
/** e.g. "scaleX(0.42)" or "scaleY(1)". */
|
|
126
|
+
transform: string;
|
|
127
|
+
/** e.g. "left", "right", "center", "top", or "bottom". */
|
|
128
|
+
transformOrigin: string;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Drives a divider/rule reveal from a 0-to-1 progress signal: a chapter
|
|
132
|
+
* line that draws itself as you scroll. Uses scale (not width/height) so
|
|
133
|
+
* the reveal stays on the compositor.
|
|
134
|
+
*
|
|
135
|
+
* Returns a signal holding `{ transform, transformOrigin }`, ready to
|
|
136
|
+
* spread into a style binding. The line element itself only needs a
|
|
137
|
+
* background (or border) and a fixed size; the primitive supplies the
|
|
138
|
+
* scale and the origin it grows from.
|
|
139
|
+
*
|
|
140
|
+
* SSR-safe: returns the `from` scale on the server. Under reduced motion
|
|
141
|
+
* it holds the `to` scale, so the line is fully revealed rather than
|
|
142
|
+
* stuck invisible.
|
|
143
|
+
*
|
|
144
|
+
* ```tsx
|
|
145
|
+
* const progress = createScrollProgress(() => chapter)
|
|
146
|
+
* const line = createScrollLine({ progress, axis: "x", origin: "start" })
|
|
147
|
+
* <div
|
|
148
|
+
* style={{
|
|
149
|
+
* height: "2px",
|
|
150
|
+
* "background-color": "#d9a441",
|
|
151
|
+
* ...line(),
|
|
152
|
+
* }}
|
|
153
|
+
* />
|
|
154
|
+
* ```
|
|
155
|
+
*/
|
|
156
|
+
export declare function createScrollLine(options?: ScrollLineOptions): Accessor<ScrollLineStyle>;
|
package/dist/scrollfx.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { resolveEasing } from "./easing.js";
|
|
2
|
+
import { prefersReducedMotion } from "./reduced-motion.js";
|
|
3
|
+
import { createScrollProgress } from "./scroll.js";
|
|
4
|
+
import { parseColorStops, sampleColorStops, } from "./color.js";
|
|
5
|
+
function clamp01(v) {
|
|
6
|
+
return v < 0 ? 0 : v > 1 ? 1 : v;
|
|
7
|
+
}
|
|
8
|
+
/** Round to at most 4 decimals so values like 0.30000000000000004 never leak. */
|
|
9
|
+
function fmtNum(v) {
|
|
10
|
+
return String(Math.round(v * 10000) / 10000);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Maps a 0-to-1 progress signal through a list of color stops and returns
|
|
14
|
+
* the interpolated color as a string signal.
|
|
15
|
+
*
|
|
16
|
+
* Colors interpolate in linear light, so a midpoint between red and blue
|
|
17
|
+
* is the vivid purple your eyes expect, not the muddy #800080 you get
|
|
18
|
+
* from naive channel math. Alpha channels interpolate too.
|
|
19
|
+
*
|
|
20
|
+
* Stops sort themselves by `at`; progress outside the range clamps to
|
|
21
|
+
* the end colors. Segments default to linear so color tracks scroll 1:1
|
|
22
|
+
* unless you shape them.
|
|
23
|
+
*
|
|
24
|
+
* Pure computation, no listeners, no rAF: SSR-safe by construction.
|
|
25
|
+
* Under reduced motion it holds the final stop's color.
|
|
26
|
+
*
|
|
27
|
+
* ```tsx
|
|
28
|
+
* const progress = createScrollProgress()
|
|
29
|
+
* // The hero tint warms as you scroll through the first chapter.
|
|
30
|
+
* const tint = createScrollColor(
|
|
31
|
+
* [
|
|
32
|
+
* { at: 0, color: "#f4f6f9" },
|
|
33
|
+
* { at: 0.5, color: "#f7e8d0" },
|
|
34
|
+
* { at: 1, color: "#2b5176", easing: "easeInOutQuad" },
|
|
35
|
+
* ],
|
|
36
|
+
* { progress },
|
|
37
|
+
* )
|
|
38
|
+
* <section style={{ "background-color": tint() }} />
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export function createScrollColor(stops, options = {}) {
|
|
42
|
+
if (stops.length === 0) {
|
|
43
|
+
throw new Error("createScrollColor needs at least one color stop.");
|
|
44
|
+
}
|
|
45
|
+
const parsed = parseColorStops(stops, options.easing);
|
|
46
|
+
const format = options.format ?? "hex";
|
|
47
|
+
if (prefersReducedMotion()) {
|
|
48
|
+
// Reduced motion: hold the final stop's color instead of tracking
|
|
49
|
+
// progress, matching the library's immediate-target behavior.
|
|
50
|
+
const endColor = sampleColorStops(parsed, 1, format);
|
|
51
|
+
return () => endColor;
|
|
52
|
+
}
|
|
53
|
+
const progress = options.progress ?? createScrollProgress();
|
|
54
|
+
return () => sampleColorStops(parsed, progress(), format);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Drives `letter-spacing` from a 0-to-1 progress signal: display words
|
|
58
|
+
* that spread apart or tighten together as you scroll.
|
|
59
|
+
*
|
|
60
|
+
* Returns a string signal like `"0.15em"` or `"6px"`, ready to drop into
|
|
61
|
+
* a style binding. Because the value is a plain string you can also
|
|
62
|
+
* bind it to any CSS property that takes a length.
|
|
63
|
+
*
|
|
64
|
+
* SSR-safe: returns the `from` value on the server. Under reduced motion
|
|
65
|
+
* it holds the `to` value (the settled, readable end state).
|
|
66
|
+
*
|
|
67
|
+
* ```tsx
|
|
68
|
+
* const progress = createScrollProgress(() => chapter)
|
|
69
|
+
* // A chapter title that tightens as it arrives.
|
|
70
|
+
* const tracking = createScrollTracking({
|
|
71
|
+
* progress,
|
|
72
|
+
* from: 0.35,
|
|
73
|
+
* to: 0,
|
|
74
|
+
* unit: "em",
|
|
75
|
+
* easing: "easeOutCubic",
|
|
76
|
+
* })
|
|
77
|
+
* <h2 style={{ "letter-spacing": tracking() }}>Chapter One</h2>
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
80
|
+
export function createScrollTracking(options = {}) {
|
|
81
|
+
const from = options.from ?? 0.3;
|
|
82
|
+
const to = options.to ?? 0;
|
|
83
|
+
const unit = options.unit ?? "em";
|
|
84
|
+
const easing = resolveEasing(options.easing ?? "linear");
|
|
85
|
+
if (prefersReducedMotion()) {
|
|
86
|
+
return () => `${fmtNum(to)}${unit}`;
|
|
87
|
+
}
|
|
88
|
+
const progress = options.progress ?? createScrollProgress();
|
|
89
|
+
return () => {
|
|
90
|
+
const t = easing(clamp01(progress()));
|
|
91
|
+
return `${fmtNum(from + (to - from) * t)}${unit}`;
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function originWord(axis, origin) {
|
|
95
|
+
if (origin === "center")
|
|
96
|
+
return "center";
|
|
97
|
+
if (axis === "x")
|
|
98
|
+
return origin === "start" ? "left" : "right";
|
|
99
|
+
return origin === "start" ? "top" : "bottom";
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Drives a divider/rule reveal from a 0-to-1 progress signal: a chapter
|
|
103
|
+
* line that draws itself as you scroll. Uses scale (not width/height) so
|
|
104
|
+
* the reveal stays on the compositor.
|
|
105
|
+
*
|
|
106
|
+
* Returns a signal holding `{ transform, transformOrigin }`, ready to
|
|
107
|
+
* spread into a style binding. The line element itself only needs a
|
|
108
|
+
* background (or border) and a fixed size; the primitive supplies the
|
|
109
|
+
* scale and the origin it grows from.
|
|
110
|
+
*
|
|
111
|
+
* SSR-safe: returns the `from` scale on the server. Under reduced motion
|
|
112
|
+
* it holds the `to` scale, so the line is fully revealed rather than
|
|
113
|
+
* stuck invisible.
|
|
114
|
+
*
|
|
115
|
+
* ```tsx
|
|
116
|
+
* const progress = createScrollProgress(() => chapter)
|
|
117
|
+
* const line = createScrollLine({ progress, axis: "x", origin: "start" })
|
|
118
|
+
* <div
|
|
119
|
+
* style={{
|
|
120
|
+
* height: "2px",
|
|
121
|
+
* "background-color": "#d9a441",
|
|
122
|
+
* ...line(),
|
|
123
|
+
* }}
|
|
124
|
+
* />
|
|
125
|
+
* ```
|
|
126
|
+
*/
|
|
127
|
+
export function createScrollLine(options = {}) {
|
|
128
|
+
const axis = options.axis ?? "x";
|
|
129
|
+
const from = options.from ?? 0;
|
|
130
|
+
const to = options.to ?? 1;
|
|
131
|
+
const easing = resolveEasing(options.easing ?? "linear");
|
|
132
|
+
const transform = axis === "x" ? "scaleX" : "scaleY";
|
|
133
|
+
const transformOrigin = originWord(axis, options.origin ?? "start");
|
|
134
|
+
if (prefersReducedMotion()) {
|
|
135
|
+
return () => ({
|
|
136
|
+
transform: `${transform}(${fmtNum(to)})`,
|
|
137
|
+
transformOrigin,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
const progress = options.progress ?? createScrollProgress();
|
|
141
|
+
return () => {
|
|
142
|
+
const t = easing(clamp01(progress()));
|
|
143
|
+
return {
|
|
144
|
+
transform: `${transform}(${fmtNum(from + (to - from) * t)})`,
|
|
145
|
+
transformOrigin,
|
|
146
|
+
};
|
|
147
|
+
};
|
|
148
|
+
}
|
package/dist/scrub.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { Accessor } from "solid-js";
|
|
2
|
+
import { type Easing, type EasingName } from "./easing.js";
|
|
3
|
+
export interface ScrubKeyframe {
|
|
4
|
+
/** Progress position, 0 to 1. */
|
|
5
|
+
at: number;
|
|
6
|
+
/** Value at this position. */
|
|
7
|
+
value: number;
|
|
8
|
+
/**
|
|
9
|
+
* Easing for the segment that ends at this keyframe, following the
|
|
10
|
+
* CSS keyframe convention. Default "linear".
|
|
11
|
+
*/
|
|
12
|
+
easing?: Easing | EasingName;
|
|
13
|
+
}
|
|
14
|
+
export interface ScrubOptions {
|
|
15
|
+
/** Fallback easing for segments without their own. Default "linear". */
|
|
16
|
+
easing?: Easing | EasingName;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Maps a 0-to-1 progress signal through an array of keyframes, returning
|
|
20
|
+
* the interpolated value as a signal.
|
|
21
|
+
*
|
|
22
|
+
* This is the scroll-choreography primitive: pair it with
|
|
23
|
+
* `createScrollProgress` and any numeric style becomes a scrubbed
|
|
24
|
+
* sequence. Parallax is the two-keyframe case, longer keyframe lists
|
|
25
|
+
* build full scenes (fade, rise, scale, hold, exit) driven by one scroll.
|
|
26
|
+
*
|
|
27
|
+
* Keyframes are sorted by `at` automatically. Progress outside the first
|
|
28
|
+
* and last keyframe clamps to their values. Segments default to linear
|
|
29
|
+
* easing so motion tracks scroll 1:1 unless you ask for shaping.
|
|
30
|
+
*
|
|
31
|
+
* Pure computation, no listeners, no rAF: SSR-safe by construction.
|
|
32
|
+
* Under reduced motion it holds the final keyframe value instead of
|
|
33
|
+
* tracking progress, matching the library's immediate-target behavior.
|
|
34
|
+
*
|
|
35
|
+
* ```tsx
|
|
36
|
+
* const progress = createScrollProgress(() => sectionRef)
|
|
37
|
+
* // Parallax: background drifts slower than the scroll.
|
|
38
|
+
* const y = createScrub(progress, [
|
|
39
|
+
* { at: 0, value: 60 },
|
|
40
|
+
* { at: 1, value: -60 },
|
|
41
|
+
* ])
|
|
42
|
+
* // Choreography: fade in, hold, fade out.
|
|
43
|
+
* const opacity = createScrub(progress, [
|
|
44
|
+
* { at: 0, value: 0 },
|
|
45
|
+
* { at: 0.3, value: 1, easing: "easeOutCubic" },
|
|
46
|
+
* { at: 0.7, value: 1 },
|
|
47
|
+
* { at: 1, value: 0 },
|
|
48
|
+
* ])
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export declare function createScrub(progress: Accessor<number>, keyframes: ScrubKeyframe[], options?: ScrubOptions): Accessor<number>;
|
package/dist/scrub.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { resolveEasing } from "./easing.js";
|
|
2
|
+
import { prefersReducedMotion } from "./reduced-motion.js";
|
|
3
|
+
/**
|
|
4
|
+
* Maps a 0-to-1 progress signal through an array of keyframes, returning
|
|
5
|
+
* the interpolated value as a signal.
|
|
6
|
+
*
|
|
7
|
+
* This is the scroll-choreography primitive: pair it with
|
|
8
|
+
* `createScrollProgress` and any numeric style becomes a scrubbed
|
|
9
|
+
* sequence. Parallax is the two-keyframe case, longer keyframe lists
|
|
10
|
+
* build full scenes (fade, rise, scale, hold, exit) driven by one scroll.
|
|
11
|
+
*
|
|
12
|
+
* Keyframes are sorted by `at` automatically. Progress outside the first
|
|
13
|
+
* and last keyframe clamps to their values. Segments default to linear
|
|
14
|
+
* easing so motion tracks scroll 1:1 unless you ask for shaping.
|
|
15
|
+
*
|
|
16
|
+
* Pure computation, no listeners, no rAF: SSR-safe by construction.
|
|
17
|
+
* Under reduced motion it holds the final keyframe value instead of
|
|
18
|
+
* tracking progress, matching the library's immediate-target behavior.
|
|
19
|
+
*
|
|
20
|
+
* ```tsx
|
|
21
|
+
* const progress = createScrollProgress(() => sectionRef)
|
|
22
|
+
* // Parallax: background drifts slower than the scroll.
|
|
23
|
+
* const y = createScrub(progress, [
|
|
24
|
+
* { at: 0, value: 60 },
|
|
25
|
+
* { at: 1, value: -60 },
|
|
26
|
+
* ])
|
|
27
|
+
* // Choreography: fade in, hold, fade out.
|
|
28
|
+
* const opacity = createScrub(progress, [
|
|
29
|
+
* { at: 0, value: 0 },
|
|
30
|
+
* { at: 0.3, value: 1, easing: "easeOutCubic" },
|
|
31
|
+
* { at: 0.7, value: 1 },
|
|
32
|
+
* { at: 1, value: 0 },
|
|
33
|
+
* ])
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export function createScrub(progress, keyframes, options = {}) {
|
|
37
|
+
if (keyframes.length === 0) {
|
|
38
|
+
throw new Error("createScrub needs at least one keyframe.");
|
|
39
|
+
}
|
|
40
|
+
const fallback = resolveEasing(options.easing ?? "linear");
|
|
41
|
+
const sorted = [...keyframes].sort((a, b) => a.at - b.at);
|
|
42
|
+
const segmentEasing = sorted.map((keyframe, index) => index === 0
|
|
43
|
+
? fallback
|
|
44
|
+
: resolveEasing(keyframe.easing ?? options.easing ?? "linear"));
|
|
45
|
+
if (prefersReducedMotion()) {
|
|
46
|
+
// Reduced motion: hold the final keyframe value instead of tracking
|
|
47
|
+
// progress, matching the library's immediate-target behavior.
|
|
48
|
+
const endValue = sorted[sorted.length - 1].value;
|
|
49
|
+
return () => endValue;
|
|
50
|
+
}
|
|
51
|
+
return () => {
|
|
52
|
+
const p = Math.min(Math.max(progress(), 0), 1);
|
|
53
|
+
if (p <= sorted[0].at)
|
|
54
|
+
return sorted[0].value;
|
|
55
|
+
const last = sorted[sorted.length - 1];
|
|
56
|
+
if (p >= last.at)
|
|
57
|
+
return last.value;
|
|
58
|
+
let i = 0;
|
|
59
|
+
while (i < sorted.length - 2 && sorted[i + 1].at <= p)
|
|
60
|
+
i++;
|
|
61
|
+
const a = sorted[i];
|
|
62
|
+
const b = sorted[i + 1];
|
|
63
|
+
const span = b.at - a.at;
|
|
64
|
+
const t = span <= 0 ? 0 : (p - a.at) / span;
|
|
65
|
+
return a.value + (b.value - a.value) * segmentEasing[i + 1](t);
|
|
66
|
+
};
|
|
67
|
+
}
|
package/dist/spring.d.ts
CHANGED
|
@@ -15,14 +15,49 @@ export interface SpringOptions {
|
|
|
15
15
|
* A signal that smoothly follows a source signal with spring physics.
|
|
16
16
|
*
|
|
17
17
|
* Retargeting is seamless: if the source changes mid-flight, the spring
|
|
18
|
-
* keeps its current velocity and bends toward the new target
|
|
18
|
+
* keeps its current velocity and bends toward the new target, no jumps,
|
|
19
19
|
* no restarts.
|
|
20
20
|
*
|
|
21
21
|
* ```tsx
|
|
22
|
-
* const [target, setTarget] = createSignal(0)
|
|
23
|
-
* const x = createSpring(target, { stiffness: 170, damping: 26 })
|
|
22
|
+
* const [target, setTarget] = createSignal(0)
|
|
23
|
+
* const x = createSpring(target, { stiffness: 170, damping: 26 })
|
|
24
24
|
* <div style={{ transform: `translateX(${x()}px)` }} />
|
|
25
|
-
* setTarget(200)
|
|
25
|
+
* setTarget(200) // glides there
|
|
26
26
|
* ```
|
|
27
27
|
*/
|
|
28
28
|
export declare function createSpring(source: Accessor<number>, options?: SpringOptions): Accessor<number>;
|
|
29
|
+
/**
|
|
30
|
+
* Named spring configurations for common feels. Spread into
|
|
31
|
+
* `createSpring`, `createMagnetic`, or `createTilt` options:
|
|
32
|
+
*
|
|
33
|
+
* ```ts
|
|
34
|
+
* const x = createSpring(target, { ...springPresets.wobbly })
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export declare const springPresets: {
|
|
38
|
+
/** Soft and calm. Default-like, a touch slower. */
|
|
39
|
+
gentle: {
|
|
40
|
+
stiffness: number;
|
|
41
|
+
damping: number;
|
|
42
|
+
};
|
|
43
|
+
/** The library default balance. */
|
|
44
|
+
default: {
|
|
45
|
+
stiffness: number;
|
|
46
|
+
damping: number;
|
|
47
|
+
};
|
|
48
|
+
/** Tight and responsive, for UI that must keep up. */
|
|
49
|
+
snappy: {
|
|
50
|
+
stiffness: number;
|
|
51
|
+
damping: number;
|
|
52
|
+
};
|
|
53
|
+
/** Loose and playful, with a visible overshoot. */
|
|
54
|
+
wobbly: {
|
|
55
|
+
stiffness: number;
|
|
56
|
+
damping: number;
|
|
57
|
+
};
|
|
58
|
+
/** Heavy and deliberate, like moving through syrup. */
|
|
59
|
+
molasses: {
|
|
60
|
+
stiffness: number;
|
|
61
|
+
damping: number;
|
|
62
|
+
};
|
|
63
|
+
};
|
package/dist/spring.js
CHANGED
|
@@ -5,14 +5,14 @@ import { prefersReducedMotion } from "./reduced-motion.js";
|
|
|
5
5
|
* A signal that smoothly follows a source signal with spring physics.
|
|
6
6
|
*
|
|
7
7
|
* Retargeting is seamless: if the source changes mid-flight, the spring
|
|
8
|
-
* keeps its current velocity and bends toward the new target
|
|
8
|
+
* keeps its current velocity and bends toward the new target, no jumps,
|
|
9
9
|
* no restarts.
|
|
10
10
|
*
|
|
11
11
|
* ```tsx
|
|
12
|
-
* const [target, setTarget] = createSignal(0)
|
|
13
|
-
* const x = createSpring(target, { stiffness: 170, damping: 26 })
|
|
12
|
+
* const [target, setTarget] = createSignal(0)
|
|
13
|
+
* const x = createSpring(target, { stiffness: 170, damping: 26 })
|
|
14
14
|
* <div style={{ transform: `translateX(${x()}px)` }} />
|
|
15
|
-
* setTarget(200)
|
|
15
|
+
* setTarget(200) // glides there
|
|
16
16
|
* ```
|
|
17
17
|
*/
|
|
18
18
|
export function createSpring(source, options = {}) {
|
|
@@ -31,8 +31,7 @@ export function createSpring(source, options = {}) {
|
|
|
31
31
|
velocity += (force / mass) * dt;
|
|
32
32
|
current += velocity * dt;
|
|
33
33
|
setValue(current);
|
|
34
|
-
const settled = Math.abs(current - target) < precision &&
|
|
35
|
-
Math.abs(velocity) < precision;
|
|
34
|
+
const settled = Math.abs(current - target) < precision && Math.abs(velocity) < precision;
|
|
36
35
|
if (settled) {
|
|
37
36
|
current = target;
|
|
38
37
|
velocity = 0;
|
|
@@ -45,7 +44,7 @@ export function createSpring(source, options = {}) {
|
|
|
45
44
|
};
|
|
46
45
|
const kick = () => {
|
|
47
46
|
if (cancel)
|
|
48
|
-
return; // already running
|
|
47
|
+
return; // already running, step() reads the live target
|
|
49
48
|
lastTime = now();
|
|
50
49
|
cancel = schedule(step);
|
|
51
50
|
};
|
|
@@ -66,3 +65,23 @@ export function createSpring(source, options = {}) {
|
|
|
66
65
|
onCleanup(() => cancel?.());
|
|
67
66
|
return value;
|
|
68
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Named spring configurations for common feels. Spread into
|
|
70
|
+
* `createSpring`, `createMagnetic`, or `createTilt` options:
|
|
71
|
+
*
|
|
72
|
+
* ```ts
|
|
73
|
+
* const x = createSpring(target, { ...springPresets.wobbly })
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
export const springPresets = {
|
|
77
|
+
/** Soft and calm. Default-like, a touch slower. */
|
|
78
|
+
gentle: { stiffness: 120, damping: 20 },
|
|
79
|
+
/** The library default balance. */
|
|
80
|
+
default: { stiffness: 170, damping: 26 },
|
|
81
|
+
/** Tight and responsive, for UI that must keep up. */
|
|
82
|
+
snappy: { stiffness: 260, damping: 30 },
|
|
83
|
+
/** Loose and playful, with a visible overshoot. */
|
|
84
|
+
wobbly: { stiffness: 180, damping: 11 },
|
|
85
|
+
/** Heavy and deliberate, like moving through syrup. */
|
|
86
|
+
molasses: { stiffness: 55, damping: 16 },
|
|
87
|
+
};
|
package/dist/stagger.d.ts
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Given an item index, returns its delay in milliseconds (`index * delayMs`).
|
|
5
5
|
* Pair with `createTween`'s `delay` option (or `animate`) so items entrance
|
|
6
|
-
* one after another instead of all at once. `count` is informational
|
|
6
|
+
* one after another instead of all at once. `count` is informational: the
|
|
7
7
|
* number of items being staggered.
|
|
8
8
|
*
|
|
9
9
|
* ```ts
|
|
10
|
-
* const at = createStagger(5, 80)
|
|
11
|
-
* at(0)
|
|
12
|
-
* at(3)
|
|
10
|
+
* const at = createStagger(5, 80) // 5 items, 80ms apart
|
|
11
|
+
* at(0) // 0
|
|
12
|
+
* at(3) // 240
|
|
13
13
|
* ```
|
|
14
14
|
*/
|
|
15
15
|
export declare function createStagger(count: number, delayMs: number): (index: number) => number;
|
package/dist/stagger.js
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Given an item index, returns its delay in milliseconds (`index * delayMs`).
|
|
5
5
|
* Pair with `createTween`'s `delay` option (or `animate`) so items entrance
|
|
6
|
-
* one after another instead of all at once. `count` is informational
|
|
6
|
+
* one after another instead of all at once. `count` is informational: the
|
|
7
7
|
* number of items being staggered.
|
|
8
8
|
*
|
|
9
9
|
* ```ts
|
|
10
|
-
* const at = createStagger(5, 80)
|
|
11
|
-
* at(0)
|
|
12
|
-
* at(3)
|
|
10
|
+
* const at = createStagger(5, 80) // 5 items, 80ms apart
|
|
11
|
+
* at(0) // 0
|
|
12
|
+
* at(3) // 240
|
|
13
13
|
* ```
|
|
14
14
|
*/
|
|
15
15
|
export function createStagger(count, delayMs) {
|