@humanspeak/svelte-motion 0.4.9 → 0.5.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/dist/index.d.ts +14 -7
- package/dist/index.js +7 -6
- package/dist/utils/attachable.js +14 -9
- package/dist/utils/augmentMotionValue.svelte.d.ts +156 -0
- package/dist/utils/augmentMotionValue.svelte.js +193 -0
- package/dist/utils/dom.d.ts +13 -0
- package/dist/utils/dom.js +19 -0
- package/dist/utils/followValue.svelte.d.ts +84 -0
- package/dist/utils/followValue.svelte.js +51 -0
- package/dist/utils/motionTemplate.svelte.d.ts +53 -0
- package/dist/utils/motionTemplate.svelte.js +78 -0
- package/dist/utils/motionValue.svelte.d.ts +61 -0
- package/dist/utils/motionValue.svelte.js +49 -0
- package/dist/utils/scroll.svelte.d.ts +91 -0
- package/dist/utils/scroll.svelte.js +259 -0
- package/dist/utils/spring.svelte.d.ts +26 -31
- package/dist/utils/spring.svelte.js +8 -116
- package/dist/utils/time.svelte.d.ts +47 -0
- package/dist/utils/time.svelte.js +128 -0
- package/dist/utils/transform.svelte.d.ts +170 -0
- package/dist/utils/transform.svelte.js +189 -0
- package/dist/utils/velocity.svelte.d.ts +61 -0
- package/dist/utils/velocity.svelte.js +132 -0
- package/package.json +1 -1
- package/dist/utils/motionTemplate.d.ts +0 -21
- package/dist/utils/motionTemplate.js +0 -33
- package/dist/utils/motionValue.d.ts +0 -6
- package/dist/utils/motionValue.js +0 -13
- package/dist/utils/scroll.d.ts +0 -63
- package/dist/utils/scroll.js +0 -79
- package/dist/utils/time.d.ts +0 -14
- package/dist/utils/time.js +0 -68
- package/dist/utils/transform.d.ts +0 -74
- package/dist/utils/transform.js +0 -211
- package/dist/utils/velocity.d.ts +0 -15
- package/dist/utils/velocity.js +0 -62
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type Readable } from 'svelte/store';
|
|
2
|
+
import { type AugmentedMotionValue } from './augmentMotionValue.svelte.js';
|
|
3
|
+
/**
|
|
4
|
+
* Input to {@link useMotionTemplate}'s interpolation slots: a motion-dom
|
|
5
|
+
* `MotionValue`, a Svelte readable, or a plain `number` / `string` literal.
|
|
6
|
+
* Mirrors framer-motion's `useMotionTemplate` signature.
|
|
7
|
+
*/
|
|
8
|
+
export type MotionTemplateInput = AugmentedMotionValue<number | string> | Readable<number | string> | number | string;
|
|
9
|
+
/**
|
|
10
|
+
* Tagged template literal that builds an augmented `MotionValue<string>`
|
|
11
|
+
* from interpolated motion values, Svelte readables, and plain literals.
|
|
12
|
+
*
|
|
13
|
+
* Mirrors framer-motion 1:1: returns a real motion-dom `MotionValue<string>`
|
|
14
|
+
* (via `transformValue`) that auto-tracks every `MotionValue.get()` called
|
|
15
|
+
* during template composition. Whenever any tracked input emits, the
|
|
16
|
+
* composer reruns and writes the new string into the result value.
|
|
17
|
+
*
|
|
18
|
+
* Svelte-readable slots are sampled via `svelte/store`'s `get()` inside the
|
|
19
|
+
* composer so they re-emit when adjacent motion values trigger a recompute.
|
|
20
|
+
* Plain `number` / `string` slots are stringified inline.
|
|
21
|
+
*
|
|
22
|
+
* The result is augmented with a `$state`-backed `.current` getter and a
|
|
23
|
+
* Svelte readable `.subscribe` shim so it composes with the rest of the
|
|
24
|
+
* Tier 2 surface and reads reactively in Svelte 5 scopes.
|
|
25
|
+
*
|
|
26
|
+
* Lifecycle: must be called during component initialization. motion-dom
|
|
27
|
+
* cleans up the change-subscriptions when the result `MotionValue` is
|
|
28
|
+
* destroyed; we wire that destroy to the surrounding `$effect`.
|
|
29
|
+
*
|
|
30
|
+
* SSR-safe: motion-dom's `transformValue` works without DOM access (no
|
|
31
|
+
* timers, no listeners). On the server the result is a static augmented
|
|
32
|
+
* motion value with no `$effect` registered.
|
|
33
|
+
*
|
|
34
|
+
* @param strings Static template string parts (supplied by the tagged-template syntax).
|
|
35
|
+
* @param values Interpolated motion values, Svelte readables, or literals.
|
|
36
|
+
* @returns An `AugmentedMotionValue<string>` with the composed template.
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```svelte
|
|
40
|
+
* <script>
|
|
41
|
+
* import { useSpring, useTransform, useMotionTemplate } from '@humanspeak/svelte-motion'
|
|
42
|
+
*
|
|
43
|
+
* const x = useSpring(0)
|
|
44
|
+
* const blur = useTransform(x, [-100, 0, 100], [10, 0, 10])
|
|
45
|
+
* const filter = useMotionTemplate`blur(${blur}px)`
|
|
46
|
+
* </script>
|
|
47
|
+
*
|
|
48
|
+
* <div style="filter: {filter.current}">Animated blur</div>
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* @see https://motion.dev/docs/react-use-motion-template
|
|
52
|
+
*/
|
|
53
|
+
export declare const useMotionTemplate: (strings: TemplateStringsArray, ...values: MotionTemplateInput[]) => AugmentedMotionValue<string>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { isMotionValue, transformValue } from 'motion-dom';
|
|
2
|
+
import {} from 'svelte/store';
|
|
3
|
+
import { augmentMotionValue, sampleSource } from './augmentMotionValue.svelte.js';
|
|
4
|
+
/**
|
|
5
|
+
* Tagged template literal that builds an augmented `MotionValue<string>`
|
|
6
|
+
* from interpolated motion values, Svelte readables, and plain literals.
|
|
7
|
+
*
|
|
8
|
+
* Mirrors framer-motion 1:1: returns a real motion-dom `MotionValue<string>`
|
|
9
|
+
* (via `transformValue`) that auto-tracks every `MotionValue.get()` called
|
|
10
|
+
* during template composition. Whenever any tracked input emits, the
|
|
11
|
+
* composer reruns and writes the new string into the result value.
|
|
12
|
+
*
|
|
13
|
+
* Svelte-readable slots are sampled via `svelte/store`'s `get()` inside the
|
|
14
|
+
* composer so they re-emit when adjacent motion values trigger a recompute.
|
|
15
|
+
* Plain `number` / `string` slots are stringified inline.
|
|
16
|
+
*
|
|
17
|
+
* The result is augmented with a `$state`-backed `.current` getter and a
|
|
18
|
+
* Svelte readable `.subscribe` shim so it composes with the rest of the
|
|
19
|
+
* Tier 2 surface and reads reactively in Svelte 5 scopes.
|
|
20
|
+
*
|
|
21
|
+
* Lifecycle: must be called during component initialization. motion-dom
|
|
22
|
+
* cleans up the change-subscriptions when the result `MotionValue` is
|
|
23
|
+
* destroyed; we wire that destroy to the surrounding `$effect`.
|
|
24
|
+
*
|
|
25
|
+
* SSR-safe: motion-dom's `transformValue` works without DOM access (no
|
|
26
|
+
* timers, no listeners). On the server the result is a static augmented
|
|
27
|
+
* motion value with no `$effect` registered.
|
|
28
|
+
*
|
|
29
|
+
* @param strings Static template string parts (supplied by the tagged-template syntax).
|
|
30
|
+
* @param values Interpolated motion values, Svelte readables, or literals.
|
|
31
|
+
* @returns An `AugmentedMotionValue<string>` with the composed template.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```svelte
|
|
35
|
+
* <script>
|
|
36
|
+
* import { useSpring, useTransform, useMotionTemplate } from '@humanspeak/svelte-motion'
|
|
37
|
+
*
|
|
38
|
+
* const x = useSpring(0)
|
|
39
|
+
* const blur = useTransform(x, [-100, 0, 100], [10, 0, 10])
|
|
40
|
+
* const filter = useMotionTemplate`blur(${blur}px)`
|
|
41
|
+
* </script>
|
|
42
|
+
*
|
|
43
|
+
* <div style="filter: {filter.current}">Animated blur</div>
|
|
44
|
+
* ```
|
|
45
|
+
*
|
|
46
|
+
* @see https://motion.dev/docs/react-use-motion-template
|
|
47
|
+
*/
|
|
48
|
+
export const useMotionTemplate = (strings, ...values) => {
|
|
49
|
+
const numFragments = strings.length;
|
|
50
|
+
const buildValue = () => {
|
|
51
|
+
let output = '';
|
|
52
|
+
for (let i = 0; i < numFragments; i++) {
|
|
53
|
+
output += strings[i] ?? '';
|
|
54
|
+
if (i >= values.length)
|
|
55
|
+
continue;
|
|
56
|
+
const value = values[i];
|
|
57
|
+
// motion-dom's collectMotionValues session inside transformValue
|
|
58
|
+
// auto-discovers MotionValue deps via `.get()`. Readables don't
|
|
59
|
+
// participate — they're sampled inline via get(); they only
|
|
60
|
+
// re-emit if some adjacent motion value triggers a recompute.
|
|
61
|
+
if (isMotionValue(value)) {
|
|
62
|
+
output += String(value.get());
|
|
63
|
+
}
|
|
64
|
+
else if (value && typeof value === 'object' && 'subscribe' in value) {
|
|
65
|
+
output += String(sampleSource(value));
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
output += String(value);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return output;
|
|
72
|
+
};
|
|
73
|
+
const result = transformValue(buildValue);
|
|
74
|
+
if (typeof window !== 'undefined') {
|
|
75
|
+
$effect(() => () => result.destroy());
|
|
76
|
+
}
|
|
77
|
+
return augmentMotionValue(result);
|
|
78
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { type MotionValue as MotionDomMotionValue } from 'motion-dom';
|
|
2
|
+
import { type AugmentedMotionValue } from './augmentMotionValue.svelte.js';
|
|
3
|
+
/**
|
|
4
|
+
* The shape returned by {@link useMotionValue}: a real motion-dom
|
|
5
|
+
* `MotionValue<T>` (so it composes with `animate()`, `useTransform`,
|
|
6
|
+
* `useSpring`, etc. and passes `isMotionValue`) plus a Svelte 5 reactive
|
|
7
|
+
* `.current` getter and a Svelte readable store `.subscribe` shim.
|
|
8
|
+
*
|
|
9
|
+
* @template T The value type — typically `number` or `string`.
|
|
10
|
+
* @see {@link AugmentedMotionValue}
|
|
11
|
+
*/
|
|
12
|
+
export type MotionValue<T = number> = AugmentedMotionValue<T>;
|
|
13
|
+
/**
|
|
14
|
+
* Re-export of motion-dom's raw `MotionValue` type for cases where the
|
|
15
|
+
* un-augmented shape is needed (e.g. typing follow sources, function
|
|
16
|
+
* dependencies). Most call sites should prefer {@link MotionValue}.
|
|
17
|
+
*/
|
|
18
|
+
export type RawMotionValue<T = number> = MotionDomMotionValue<T>;
|
|
19
|
+
/**
|
|
20
|
+
* Creates a tracked, mutable value backed by motion-dom's `MotionValue`.
|
|
21
|
+
*
|
|
22
|
+
* Use `.set(v)` to write the value imperatively, `.get()` to sample it
|
|
23
|
+
* imperatively, and `.current` to read it inside Svelte 5 reactive scopes
|
|
24
|
+
* (templates, `$derived`, `$effect`). The same value also implements the
|
|
25
|
+
* Svelte readable store contract via `.subscribe(run)`, so legacy `$mv`
|
|
26
|
+
* template syntax and `svelte/store`'s `get()` keep working.
|
|
27
|
+
*
|
|
28
|
+
* Returned object is a real motion-dom `MotionValue` — it composes with
|
|
29
|
+
* `useTransform`, `useSpring`, `useVelocity`, the `animate()` driver, and
|
|
30
|
+
* passes `isMotionValue`. Unlike `useSpring`, writes are immediate — there
|
|
31
|
+
* is no follow source and no animation.
|
|
32
|
+
*
|
|
33
|
+
* Lifecycle: must be called during component initialization. Cleanup is
|
|
34
|
+
* registered via `$effect`; motion-dom's internal listeners and animation
|
|
35
|
+
* subscriptions are released when the surrounding component / effect tears
|
|
36
|
+
* down. Call `.destroy()` to clean up early.
|
|
37
|
+
*
|
|
38
|
+
* SSR-safe: motion-dom's `motionValue` runs without DOM access; on the
|
|
39
|
+
* server reads return the initial value and writes still work, with no
|
|
40
|
+
* timers or listeners attached.
|
|
41
|
+
*
|
|
42
|
+
* @template T The value type — typically `number` or `string`.
|
|
43
|
+
* @param initial The starting value.
|
|
44
|
+
* @returns A `MotionValue<T>` augmented with `.current` and `.subscribe`.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```svelte
|
|
48
|
+
* <script lang="ts">
|
|
49
|
+
* import { useMotionValue, useTransform } from '@humanspeak/svelte-motion'
|
|
50
|
+
*
|
|
51
|
+
* const x = useMotionValue(0)
|
|
52
|
+
* const opacity = useTransform(x, [0, 200], [0, 1])
|
|
53
|
+
* </script>
|
|
54
|
+
*
|
|
55
|
+
* <input type="range" min="0" max="200" oninput={(e) => x.set(+e.currentTarget.value)} />
|
|
56
|
+
* <div style="opacity: {opacity.current}">x = {x.current}</div>
|
|
57
|
+
* ```
|
|
58
|
+
*
|
|
59
|
+
* @see https://motion.dev/docs/react-motion-value
|
|
60
|
+
*/
|
|
61
|
+
export declare const useMotionValue: <T = number>(initial: T) => MotionValue<T>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { motionValue } from 'motion-dom';
|
|
2
|
+
import { augmentMotionValue } from './augmentMotionValue.svelte.js';
|
|
3
|
+
/**
|
|
4
|
+
* Creates a tracked, mutable value backed by motion-dom's `MotionValue`.
|
|
5
|
+
*
|
|
6
|
+
* Use `.set(v)` to write the value imperatively, `.get()` to sample it
|
|
7
|
+
* imperatively, and `.current` to read it inside Svelte 5 reactive scopes
|
|
8
|
+
* (templates, `$derived`, `$effect`). The same value also implements the
|
|
9
|
+
* Svelte readable store contract via `.subscribe(run)`, so legacy `$mv`
|
|
10
|
+
* template syntax and `svelte/store`'s `get()` keep working.
|
|
11
|
+
*
|
|
12
|
+
* Returned object is a real motion-dom `MotionValue` — it composes with
|
|
13
|
+
* `useTransform`, `useSpring`, `useVelocity`, the `animate()` driver, and
|
|
14
|
+
* passes `isMotionValue`. Unlike `useSpring`, writes are immediate — there
|
|
15
|
+
* is no follow source and no animation.
|
|
16
|
+
*
|
|
17
|
+
* Lifecycle: must be called during component initialization. Cleanup is
|
|
18
|
+
* registered via `$effect`; motion-dom's internal listeners and animation
|
|
19
|
+
* subscriptions are released when the surrounding component / effect tears
|
|
20
|
+
* down. Call `.destroy()` to clean up early.
|
|
21
|
+
*
|
|
22
|
+
* SSR-safe: motion-dom's `motionValue` runs without DOM access; on the
|
|
23
|
+
* server reads return the initial value and writes still work, with no
|
|
24
|
+
* timers or listeners attached.
|
|
25
|
+
*
|
|
26
|
+
* @template T The value type — typically `number` or `string`.
|
|
27
|
+
* @param initial The starting value.
|
|
28
|
+
* @returns A `MotionValue<T>` augmented with `.current` and `.subscribe`.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```svelte
|
|
32
|
+
* <script lang="ts">
|
|
33
|
+
* import { useMotionValue, useTransform } from '@humanspeak/svelte-motion'
|
|
34
|
+
*
|
|
35
|
+
* const x = useMotionValue(0)
|
|
36
|
+
* const opacity = useTransform(x, [0, 200], [0, 1])
|
|
37
|
+
* </script>
|
|
38
|
+
*
|
|
39
|
+
* <input type="range" min="0" max="200" oninput={(e) => x.set(+e.currentTarget.value)} />
|
|
40
|
+
* <div style="opacity: {opacity.current}">x = {x.current}</div>
|
|
41
|
+
* ```
|
|
42
|
+
*
|
|
43
|
+
* @see https://motion.dev/docs/react-motion-value
|
|
44
|
+
*/
|
|
45
|
+
export const useMotionValue = (initial) => {
|
|
46
|
+
const value = motionValue(initial);
|
|
47
|
+
$effect(() => () => value.destroy());
|
|
48
|
+
return augmentMotionValue(value);
|
|
49
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { type AugmentedMotionValue } from './augmentMotionValue.svelte.js';
|
|
2
|
+
import { type ElementOrGetter } from './dom.js';
|
|
3
|
+
/**
|
|
4
|
+
* A scroll offset edge defined as a string (e.g. `"start"`, `"end"`,
|
|
5
|
+
* `"center"`) or a number (0–1 progress). Each offset entry is a pair of
|
|
6
|
+
* `[target, container]`.
|
|
7
|
+
*
|
|
8
|
+
* Intentionally looser than motion's internal `ScrollOffset` type — that
|
|
9
|
+
* uses template-literal types (`` `${Edge} ${Edge}` ``) we don't want to
|
|
10
|
+
* surface to consumers. The runtime call paths through `scroll(...)` cast
|
|
11
|
+
* `options.offset as never` to bridge the two; safe because every shape our
|
|
12
|
+
* type allows is structurally a member of motion's union.
|
|
13
|
+
*/
|
|
14
|
+
type ScrollOffset = Array<[number | string, number | string]> | string[];
|
|
15
|
+
/**
|
|
16
|
+
* Options accepted by {@link useScroll}.
|
|
17
|
+
*/
|
|
18
|
+
export type UseScrollOptions = {
|
|
19
|
+
/** Scrollable container to track. Defaults to the page. Accepts an element or a getter function. */
|
|
20
|
+
container?: ElementOrGetter;
|
|
21
|
+
/** Target element to track position of within the container. Accepts an element or a getter function. */
|
|
22
|
+
target?: ElementOrGetter;
|
|
23
|
+
/** Scroll offset configuration for element position tracking. */
|
|
24
|
+
offset?: ScrollOffset;
|
|
25
|
+
/** Which axis to use for the single-axis `progress` value supplied to `scroll()`. */
|
|
26
|
+
axis?: 'x' | 'y';
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Return type of {@link useScroll} — four motion-dom `MotionValue<number>`s
|
|
30
|
+
* representing scroll position and normalised progress for both axes, each
|
|
31
|
+
* augmented with `.current` + `.subscribe`.
|
|
32
|
+
*/
|
|
33
|
+
export type UseScrollReturn = {
|
|
34
|
+
scrollX: AugmentedMotionValue<number>;
|
|
35
|
+
scrollY: AugmentedMotionValue<number>;
|
|
36
|
+
scrollXProgress: AugmentedMotionValue<number>;
|
|
37
|
+
scrollYProgress: AugmentedMotionValue<number>;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Creates scroll-linked motion values for building scroll-driven animations
|
|
41
|
+
* such as progress indicators and parallax effects.
|
|
42
|
+
*
|
|
43
|
+
* Returns four `MotionValue<number>`s: `scrollX` / `scrollY` (current
|
|
44
|
+
* position in px) and `scrollXProgress` / `scrollYProgress` (0–1
|
|
45
|
+
* normalised). Each is a real motion-dom `MotionValue` augmented with a
|
|
46
|
+
* `$state`-backed `.current` getter and a `.subscribe` shim, so they
|
|
47
|
+
* compose with `useTransform`, `useSpring`, and the rest of the Tier 2
|
|
48
|
+
* surface, and they read reactively in Svelte 5 templates.
|
|
49
|
+
*
|
|
50
|
+
* **GPU-accelerated when supported.** On browsers that implement CSS
|
|
51
|
+
* scroll-timeline (no `target`) or view-timeline (with a `target` and a
|
|
52
|
+
* preset `offset`), the *Progress motion values run on the compositor
|
|
53
|
+
* thread via WAAPI — no per-frame JS callback. The non-progress
|
|
54
|
+
* `scrollX` / `scrollY` motion values always use the JS-driven `scroll()`
|
|
55
|
+
* primitive since the absolute pixel offset isn't directly available from
|
|
56
|
+
* native timelines.
|
|
57
|
+
*
|
|
58
|
+
* `container` and `target` accept either an `HTMLElement` directly or a
|
|
59
|
+
* getter `() => HTMLElement | undefined`. The getter form is the right
|
|
60
|
+
* choice with `bind:this`. Element resolution is deferred to a microtask
|
|
61
|
+
* (matches React framer-motion 1:1, faster than rAF polling), so a getter
|
|
62
|
+
* that hasn't hydrated yet is retried as soon as Svelte's mount tick
|
|
63
|
+
* settles it.
|
|
64
|
+
*
|
|
65
|
+
* Lifecycle: the underlying `scroll()` observer and any accelerate factory
|
|
66
|
+
* attach at mount via `$effect` and detach at unmount, regardless of how
|
|
67
|
+
* many consumers are reading the values. The four motion values are torn
|
|
68
|
+
* down at the same time.
|
|
69
|
+
*
|
|
70
|
+
* SSR-safe: returns four static `motionValue(0)`s with no scroll observer
|
|
71
|
+
* on the server.
|
|
72
|
+
*
|
|
73
|
+
* @param options Optional scroll tracking configuration.
|
|
74
|
+
* @returns Four `MotionValue<number>`s — `scrollX`, `scrollY`, `scrollXProgress`, `scrollYProgress`.
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```svelte
|
|
78
|
+
* <script>
|
|
79
|
+
* import { useScroll, useSpring } from '@humanspeak/svelte-motion'
|
|
80
|
+
*
|
|
81
|
+
* const { scrollYProgress } = useScroll()
|
|
82
|
+
* const scaleX = useSpring(scrollYProgress)
|
|
83
|
+
* </script>
|
|
84
|
+
*
|
|
85
|
+
* <div style="transform: scaleX({scaleX.current}); transform-origin: left;" />
|
|
86
|
+
* ```
|
|
87
|
+
*
|
|
88
|
+
* @see https://motion.dev/docs/react-use-scroll
|
|
89
|
+
*/
|
|
90
|
+
export declare const useScroll: (options?: UseScrollOptions) => UseScrollReturn;
|
|
91
|
+
export {};
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { scroll } from 'motion';
|
|
2
|
+
import { cancelMicrotask, microtask, motionValue, supportsScrollTimeline, supportsViewTimeline } from 'motion-dom';
|
|
3
|
+
import { augmentMotionValue } from './augmentMotionValue.svelte.js';
|
|
4
|
+
import { isRefPending, resolveElement } from './dom.js';
|
|
5
|
+
/**
|
|
6
|
+
* Named view-timeline presets — mirror framer-motion's ScrollOffset table.
|
|
7
|
+
* Each entry is a pair of `[target, container]` intersection points that
|
|
8
|
+
* defines a phase of the scroll relationship.
|
|
9
|
+
*/
|
|
10
|
+
const VIEW_TIMELINE_PRESETS = [
|
|
11
|
+
[
|
|
12
|
+
[
|
|
13
|
+
[0, 1],
|
|
14
|
+
[1, 1]
|
|
15
|
+
],
|
|
16
|
+
'entry'
|
|
17
|
+
],
|
|
18
|
+
[
|
|
19
|
+
[
|
|
20
|
+
[0, 0],
|
|
21
|
+
[1, 0]
|
|
22
|
+
],
|
|
23
|
+
'exit'
|
|
24
|
+
],
|
|
25
|
+
[
|
|
26
|
+
[
|
|
27
|
+
[1, 0],
|
|
28
|
+
[0, 1]
|
|
29
|
+
],
|
|
30
|
+
'cover'
|
|
31
|
+
],
|
|
32
|
+
[
|
|
33
|
+
[
|
|
34
|
+
[0, 0],
|
|
35
|
+
[1, 1]
|
|
36
|
+
],
|
|
37
|
+
'contain'
|
|
38
|
+
]
|
|
39
|
+
];
|
|
40
|
+
const PROGRESS_BY_STRING = { start: 0, end: 1 };
|
|
41
|
+
/**
|
|
42
|
+
* Parse `"start end"` / `"end start"` / etc. into an explicit
|
|
43
|
+
* `[target, container]` intersection pair.
|
|
44
|
+
*/
|
|
45
|
+
const parseStringOffset = (s) => {
|
|
46
|
+
const parts = s.trim().split(/\s+/);
|
|
47
|
+
if (parts.length !== 2)
|
|
48
|
+
return undefined;
|
|
49
|
+
const a = PROGRESS_BY_STRING[parts[0]];
|
|
50
|
+
const b = PROGRESS_BY_STRING[parts[1]];
|
|
51
|
+
if (a === undefined || b === undefined)
|
|
52
|
+
return undefined;
|
|
53
|
+
return [a, b];
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Normalize a {@link ScrollOffset} into an array of explicit intersection
|
|
57
|
+
* pairs, or `undefined` if the shape doesn't match the simple two-element
|
|
58
|
+
* preset form.
|
|
59
|
+
*/
|
|
60
|
+
const normaliseOffset = (offset) => {
|
|
61
|
+
if (offset.length !== 2)
|
|
62
|
+
return undefined;
|
|
63
|
+
const result = [];
|
|
64
|
+
for (const item of offset) {
|
|
65
|
+
if (Array.isArray(item)) {
|
|
66
|
+
result.push(item);
|
|
67
|
+
}
|
|
68
|
+
else if (typeof item === 'string') {
|
|
69
|
+
const parsed = parseStringOffset(item);
|
|
70
|
+
if (!parsed)
|
|
71
|
+
return undefined;
|
|
72
|
+
result.push(parsed);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return result;
|
|
79
|
+
};
|
|
80
|
+
const matchesPreset = (offset, preset) => {
|
|
81
|
+
const normalised = normaliseOffset(offset);
|
|
82
|
+
if (!normalised)
|
|
83
|
+
return false;
|
|
84
|
+
for (let i = 0; i < 2; i++) {
|
|
85
|
+
const o = normalised[i];
|
|
86
|
+
const p = preset[i];
|
|
87
|
+
if (o[0] !== p[0] || o[1] !== p[1])
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
return true;
|
|
91
|
+
};
|
|
92
|
+
/**
|
|
93
|
+
* Map a {@link ScrollOffset} to its corresponding CSS view-timeline range, if
|
|
94
|
+
* any. Returning `undefined` signals the caller to fall back to JS-driven
|
|
95
|
+
* scroll tracking. Mirrors framer-motion's `offsetToViewTimelineRange`.
|
|
96
|
+
*/
|
|
97
|
+
const offsetToViewTimelineRange = (offset) => {
|
|
98
|
+
if (!offset)
|
|
99
|
+
return { rangeStart: 'contain 0%', rangeEnd: 'contain 100%' };
|
|
100
|
+
for (const [preset, name] of VIEW_TIMELINE_PRESETS) {
|
|
101
|
+
if (matchesPreset(offset, preset)) {
|
|
102
|
+
return { rangeStart: `${name} 0%`, rangeEnd: `${name} 100%` };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return undefined;
|
|
106
|
+
};
|
|
107
|
+
/**
|
|
108
|
+
* Whether this scroll configuration can be driven by a native CSS
|
|
109
|
+
* scroll-timeline / view-timeline. When `true`, the resulting motion value
|
|
110
|
+
* runs on the compositor thread without per-frame JS callbacks.
|
|
111
|
+
*/
|
|
112
|
+
const canAccelerateScroll = (target, offset) => {
|
|
113
|
+
if (typeof window === 'undefined')
|
|
114
|
+
return false;
|
|
115
|
+
return target
|
|
116
|
+
? supportsViewTimeline() && !!offsetToViewTimelineRange(offset)
|
|
117
|
+
: supportsScrollTimeline();
|
|
118
|
+
};
|
|
119
|
+
/**
|
|
120
|
+
* Build the AccelerateConfig for a progress motion value driven by a native
|
|
121
|
+
* scroll-timeline / view-timeline animation. Mirrors framer-motion's
|
|
122
|
+
* `makeAccelerateConfig` 1:1: the `factory` defers `scroll()` until refs
|
|
123
|
+
* hydrate via `microtask.read`, then attaches the animation; the `times` /
|
|
124
|
+
* `keyframes` / `ease` / `duration` describe the 0→1 linear mapping.
|
|
125
|
+
*/
|
|
126
|
+
const makeAccelerateConfig = (axis, options) => ({
|
|
127
|
+
factory: (animation) => {
|
|
128
|
+
let cleanup;
|
|
129
|
+
const start = () => {
|
|
130
|
+
if (isRefPending(options.container) || isRefPending(options.target)) {
|
|
131
|
+
microtask.read(start);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
cleanup = scroll(animation, {
|
|
135
|
+
offset: options.offset,
|
|
136
|
+
axis,
|
|
137
|
+
container: resolveElement(options.container),
|
|
138
|
+
target: resolveElement(options.target)
|
|
139
|
+
});
|
|
140
|
+
};
|
|
141
|
+
microtask.read(start);
|
|
142
|
+
return () => {
|
|
143
|
+
cancelMicrotask(start);
|
|
144
|
+
cleanup?.();
|
|
145
|
+
};
|
|
146
|
+
},
|
|
147
|
+
times: [0, 1],
|
|
148
|
+
keyframes: [0, 1],
|
|
149
|
+
ease: (v) => v,
|
|
150
|
+
duration: 1
|
|
151
|
+
});
|
|
152
|
+
/**
|
|
153
|
+
* Creates scroll-linked motion values for building scroll-driven animations
|
|
154
|
+
* such as progress indicators and parallax effects.
|
|
155
|
+
*
|
|
156
|
+
* Returns four `MotionValue<number>`s: `scrollX` / `scrollY` (current
|
|
157
|
+
* position in px) and `scrollXProgress` / `scrollYProgress` (0–1
|
|
158
|
+
* normalised). Each is a real motion-dom `MotionValue` augmented with a
|
|
159
|
+
* `$state`-backed `.current` getter and a `.subscribe` shim, so they
|
|
160
|
+
* compose with `useTransform`, `useSpring`, and the rest of the Tier 2
|
|
161
|
+
* surface, and they read reactively in Svelte 5 templates.
|
|
162
|
+
*
|
|
163
|
+
* **GPU-accelerated when supported.** On browsers that implement CSS
|
|
164
|
+
* scroll-timeline (no `target`) or view-timeline (with a `target` and a
|
|
165
|
+
* preset `offset`), the *Progress motion values run on the compositor
|
|
166
|
+
* thread via WAAPI — no per-frame JS callback. The non-progress
|
|
167
|
+
* `scrollX` / `scrollY` motion values always use the JS-driven `scroll()`
|
|
168
|
+
* primitive since the absolute pixel offset isn't directly available from
|
|
169
|
+
* native timelines.
|
|
170
|
+
*
|
|
171
|
+
* `container` and `target` accept either an `HTMLElement` directly or a
|
|
172
|
+
* getter `() => HTMLElement | undefined`. The getter form is the right
|
|
173
|
+
* choice with `bind:this`. Element resolution is deferred to a microtask
|
|
174
|
+
* (matches React framer-motion 1:1, faster than rAF polling), so a getter
|
|
175
|
+
* that hasn't hydrated yet is retried as soon as Svelte's mount tick
|
|
176
|
+
* settles it.
|
|
177
|
+
*
|
|
178
|
+
* Lifecycle: the underlying `scroll()` observer and any accelerate factory
|
|
179
|
+
* attach at mount via `$effect` and detach at unmount, regardless of how
|
|
180
|
+
* many consumers are reading the values. The four motion values are torn
|
|
181
|
+
* down at the same time.
|
|
182
|
+
*
|
|
183
|
+
* SSR-safe: returns four static `motionValue(0)`s with no scroll observer
|
|
184
|
+
* on the server.
|
|
185
|
+
*
|
|
186
|
+
* @param options Optional scroll tracking configuration.
|
|
187
|
+
* @returns Four `MotionValue<number>`s — `scrollX`, `scrollY`, `scrollXProgress`, `scrollYProgress`.
|
|
188
|
+
*
|
|
189
|
+
* @example
|
|
190
|
+
* ```svelte
|
|
191
|
+
* <script>
|
|
192
|
+
* import { useScroll, useSpring } from '@humanspeak/svelte-motion'
|
|
193
|
+
*
|
|
194
|
+
* const { scrollYProgress } = useScroll()
|
|
195
|
+
* const scaleX = useSpring(scrollYProgress)
|
|
196
|
+
* </script>
|
|
197
|
+
*
|
|
198
|
+
* <div style="transform: scaleX({scaleX.current}); transform-origin: left;" />
|
|
199
|
+
* ```
|
|
200
|
+
*
|
|
201
|
+
* @see https://motion.dev/docs/react-use-scroll
|
|
202
|
+
*/
|
|
203
|
+
export const useScroll = (options = {}) => {
|
|
204
|
+
const scrollX = motionValue(0);
|
|
205
|
+
const scrollY = motionValue(0);
|
|
206
|
+
const scrollXProgress = motionValue(0);
|
|
207
|
+
const scrollYProgress = motionValue(0);
|
|
208
|
+
// SSR: return static motion values with no observer and no $effect.
|
|
209
|
+
if (typeof window === 'undefined') {
|
|
210
|
+
return {
|
|
211
|
+
scrollX: augmentMotionValue(scrollX),
|
|
212
|
+
scrollY: augmentMotionValue(scrollY),
|
|
213
|
+
scrollXProgress: augmentMotionValue(scrollXProgress),
|
|
214
|
+
scrollYProgress: augmentMotionValue(scrollYProgress)
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
// The *Progress MVs accelerate to the compositor thread when the browser
|
|
218
|
+
// supports CSS scroll/view-timelines; the non-progress scrollX/scrollY
|
|
219
|
+
// MVs always need the JS callback for absolute pixel offsets.
|
|
220
|
+
if (canAccelerateScroll(options.target, options.offset)) {
|
|
221
|
+
scrollXProgress.accelerate = makeAccelerateConfig('x', options);
|
|
222
|
+
scrollYProgress.accelerate = makeAccelerateConfig('y', options);
|
|
223
|
+
}
|
|
224
|
+
let cleanup;
|
|
225
|
+
const start = () => {
|
|
226
|
+
if (isRefPending(options.container) || isRefPending(options.target)) {
|
|
227
|
+
microtask.read(start);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
cleanup = scroll((_progress, info) => {
|
|
231
|
+
scrollX.set(info.x.current);
|
|
232
|
+
scrollY.set(info.y.current);
|
|
233
|
+
scrollXProgress.set(info.x.progress);
|
|
234
|
+
scrollYProgress.set(info.y.progress);
|
|
235
|
+
}, {
|
|
236
|
+
container: resolveElement(options.container),
|
|
237
|
+
target: resolveElement(options.target),
|
|
238
|
+
offset: options.offset,
|
|
239
|
+
axis: options.axis
|
|
240
|
+
});
|
|
241
|
+
};
|
|
242
|
+
$effect(() => {
|
|
243
|
+
microtask.read(start);
|
|
244
|
+
return () => {
|
|
245
|
+
cancelMicrotask(start);
|
|
246
|
+
cleanup?.();
|
|
247
|
+
scrollX.destroy();
|
|
248
|
+
scrollY.destroy();
|
|
249
|
+
scrollXProgress.destroy();
|
|
250
|
+
scrollYProgress.destroy();
|
|
251
|
+
};
|
|
252
|
+
});
|
|
253
|
+
return {
|
|
254
|
+
scrollX: augmentMotionValue(scrollX),
|
|
255
|
+
scrollY: augmentMotionValue(scrollY),
|
|
256
|
+
scrollXProgress: augmentMotionValue(scrollXProgress),
|
|
257
|
+
scrollYProgress: augmentMotionValue(scrollYProgress)
|
|
258
|
+
};
|
|
259
|
+
};
|