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.
Files changed (48) hide show
  1. package/README.md +735 -10
  2. package/dist/animate.d.ts +2 -2
  3. package/dist/animate.js +2 -2
  4. package/dist/cartoon.d.ts +190 -0
  5. package/dist/cartoon.js +334 -0
  6. package/dist/color.d.ts +53 -0
  7. package/dist/color.js +391 -0
  8. package/dist/directive.d.ts +5 -5
  9. package/dist/directive.js +15 -7
  10. package/dist/easing.d.ts +22 -1
  11. package/dist/easing.js +49 -1
  12. package/dist/flip.d.ts +44 -0
  13. package/dist/flip.js +108 -0
  14. package/dist/horizontal.d.ts +107 -0
  15. package/dist/horizontal.js +208 -0
  16. package/dist/index.d.ts +16 -3
  17. package/dist/index.js +15 -3
  18. package/dist/inview.d.ts +4 -4
  19. package/dist/inview.js +5 -5
  20. package/dist/motion.d.ts +327 -0
  21. package/dist/motion.js +729 -0
  22. package/dist/physics.d.ts +146 -0
  23. package/dist/physics.js +352 -0
  24. package/dist/pointer.d.ts +76 -0
  25. package/dist/pointer.js +123 -0
  26. package/dist/reduced-motion.d.ts +3 -3
  27. package/dist/reduced-motion.js +5 -4
  28. package/dist/scroll.d.ts +3 -3
  29. package/dist/scroll.js +5 -5
  30. package/dist/scrollfx.d.ts +156 -0
  31. package/dist/scrollfx.js +148 -0
  32. package/dist/scrub.d.ts +51 -0
  33. package/dist/scrub.js +67 -0
  34. package/dist/spring.d.ts +39 -4
  35. package/dist/spring.js +26 -7
  36. package/dist/stagger.d.ts +4 -4
  37. package/dist/stagger.js +4 -4
  38. package/dist/timeline.d.ts +45 -0
  39. package/dist/timeline.js +93 -0
  40. package/dist/trail.d.ts +27 -0
  41. package/dist/trail.js +75 -0
  42. package/dist/tween.d.ts +3 -3
  43. package/dist/tween.js +3 -3
  44. package/dist/typography.d.ts +241 -0
  45. package/dist/typography.js +812 -0
  46. package/dist/velocity.d.ts +44 -0
  47. package/dist/velocity.js +88 -0
  48. package/package.json +1 -1
@@ -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 — always `false` on the server. `createSpring`, `createTween`
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>;
@@ -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 — always `false` on the server. `createSpring`, `createTween`
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" && typeof window.matchMedia === "function") {
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 — i.e. the
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(); // page progress
22
- * const bar = createScrollProgress(() => sectionRef); // element progress
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, } from "solid-js";
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 — i.e. the
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(); // page progress
23
- * const bar = createScrollProgress(() => sectionRef); // element progress
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 — report the top of the page.
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>;
@@ -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
+ }
@@ -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 — no jumps,
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); // glides there
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 — no jumps,
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); // glides there
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; step() reads the live target
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 — the
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); // 5 items, 80ms apart
11
- * at(0); // 0
12
- * at(3); // 240
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 — the
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); // 5 items, 80ms apart
11
- * at(0); // 0
12
- * at(3); // 240
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) {