@voila.dev/motion 0.0.0-stage → 5.0.113
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 +105 -2
- package/dist/accessibility/reduced-motion.d.ts +13 -0
- package/dist/accessibility/reduced-motion.d.ts.map +1 -0
- package/dist/accessibility/reduced-motion.js +56 -0
- package/dist/accessibility/reduced-motion.js.map +7 -0
- package/dist/animate/animate.d.ts +21 -0
- package/dist/animate/animate.d.ts.map +1 -0
- package/dist/animate/animate.js +76 -0
- package/dist/animate/animate.js.map +7 -0
- package/dist/animate/controls.d.ts +16 -0
- package/dist/animate/controls.d.ts.map +1 -0
- package/dist/animate/controls.js +62 -0
- package/dist/animate/controls.js.map +7 -0
- package/dist/animate/dom.d.ts +11 -0
- package/dist/animate/dom.d.ts.map +1 -0
- package/dist/animate/dom.js +112 -0
- package/dist/animate/dom.js.map +7 -0
- package/dist/animate/js-animation.d.ts +25 -0
- package/dist/animate/js-animation.d.ts.map +1 -0
- package/dist/animate/js-animation.js +117 -0
- package/dist/animate/js-animation.js.map +7 -0
- package/dist/animate/plan.d.ts +17 -0
- package/dist/animate/plan.d.ts.map +1 -0
- package/dist/animate/plan.js +48 -0
- package/dist/animate/plan.js.map +7 -0
- package/dist/animate/style.d.ts +27 -0
- package/dist/animate/style.d.ts.map +1 -0
- package/dist/animate/style.js +96 -0
- package/dist/animate/style.js.map +7 -0
- package/dist/animate/types.d.ts +62 -0
- package/dist/animate/types.d.ts.map +1 -0
- package/dist/animate/types.js +1 -0
- package/dist/animate/types.js.map +7 -0
- package/dist/animate/value.d.ts +13 -0
- package/dist/animate/value.d.ts.map +1 -0
- package/dist/animate/value.js +71 -0
- package/dist/animate/value.js.map +7 -0
- package/dist/animate/waapi.d.ts +20 -0
- package/dist/animate/waapi.d.ts.map +1 -0
- package/dist/animate/waapi.js +152 -0
- package/dist/animate/waapi.js.map +7 -0
- package/dist/dom.d.ts +12 -0
- package/dist/dom.d.ts.map +1 -0
- package/dist/dom.js +17 -0
- package/dist/dom.js.map +7 -0
- package/dist/easing/cubic-bezier.d.ts +4 -0
- package/dist/easing/cubic-bezier.d.ts.map +1 -0
- package/dist/easing/cubic-bezier.js +54 -0
- package/dist/easing/cubic-bezier.js.map +7 -0
- package/dist/easing/linear-css.d.ts +9 -0
- package/dist/easing/linear-css.d.ts.map +1 -0
- package/dist/easing/linear-css.js +30 -0
- package/dist/easing/linear-css.js.map +7 -0
- package/dist/easing/named.d.ts +16 -0
- package/dist/easing/named.d.ts.map +1 -0
- package/dist/easing/named.js +82 -0
- package/dist/easing/named.js.map +7 -0
- package/dist/easing/spring.d.ts +11 -0
- package/dist/easing/spring.d.ts.map +1 -0
- package/dist/easing/spring.js +18 -0
- package/dist/easing/spring.js.map +7 -0
- package/dist/easing/steps.d.ts +4 -0
- package/dist/easing/steps.d.ts.map +1 -0
- package/dist/easing/steps.js +14 -0
- package/dist/easing/steps.js.map +7 -0
- package/dist/easing/types.d.ts +5 -0
- package/dist/easing/types.d.ts.map +1 -0
- package/dist/easing/types.js +1 -0
- package/dist/easing/types.js.map +7 -0
- package/dist/frame/driver.d.ts +13 -0
- package/dist/frame/driver.d.ts.map +1 -0
- package/dist/frame/driver.js +42 -0
- package/dist/frame/driver.js.map +7 -0
- package/dist/frame/frame.d.ts +20 -0
- package/dist/frame/frame.d.ts.map +1 -0
- package/dist/frame/frame.js +77 -0
- package/dist/frame/frame.js.map +7 -0
- package/dist/generators/inertia.d.ts +20 -0
- package/dist/generators/inertia.d.ts.map +1 -0
- package/dist/generators/inertia.js +59 -0
- package/dist/generators/inertia.js.map +7 -0
- package/dist/generators/spring.d.ts +17 -0
- package/dist/generators/spring.d.ts.map +1 -0
- package/dist/generators/spring.js +87 -0
- package/dist/generators/spring.js.map +7 -0
- package/dist/generators/tween.d.ts +15 -0
- package/dist/generators/tween.d.ts.map +1 -0
- package/dist/generators/tween.js +51 -0
- package/dist/generators/tween.js.map +7 -0
- package/dist/generators/types.d.ts +30 -0
- package/dist/generators/types.d.ts.map +1 -0
- package/dist/generators/types.js +1 -0
- package/dist/generators/types.js.map +7 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +97 -0
- package/dist/index.js.map +7 -0
- package/dist/interpolate/array.d.ts +3 -0
- package/dist/interpolate/array.d.ts.map +1 -0
- package/dist/interpolate/array.js +12 -0
- package/dist/interpolate/array.js.map +7 -0
- package/dist/interpolate/color-parse.d.ts +14 -0
- package/dist/interpolate/color-parse.d.ts.map +1 -0
- package/dist/interpolate/color-parse.js +111 -0
- package/dist/interpolate/color-parse.js.map +7 -0
- package/dist/interpolate/color.d.ts +11 -0
- package/dist/interpolate/color.d.ts.map +1 -0
- package/dist/interpolate/color.js +74 -0
- package/dist/interpolate/color.js.map +7 -0
- package/dist/interpolate/complex.d.ts +7 -0
- package/dist/interpolate/complex.d.ts.map +1 -0
- package/dist/interpolate/complex.js +56 -0
- package/dist/interpolate/complex.js.map +7 -0
- package/dist/interpolate/interpolate.d.ts +16 -0
- package/dist/interpolate/interpolate.d.ts.map +1 -0
- package/dist/interpolate/interpolate.js +39 -0
- package/dist/interpolate/interpolate.js.map +7 -0
- package/dist/interpolate/mix.d.ts +8 -0
- package/dist/interpolate/mix.d.ts.map +1 -0
- package/dist/interpolate/mix.js +48 -0
- package/dist/interpolate/mix.js.map +7 -0
- package/dist/interpolate/number.d.ts +2 -0
- package/dist/interpolate/number.d.ts.map +1 -0
- package/dist/interpolate/number.js +7 -0
- package/dist/interpolate/number.js.map +7 -0
- package/dist/interpolate/object.d.ts +5 -0
- package/dist/interpolate/object.d.ts.map +1 -0
- package/dist/interpolate/object.js +17 -0
- package/dist/interpolate/object.js.map +7 -0
- package/dist/interpolate/path/flatten.d.ts +10 -0
- package/dist/interpolate/path/flatten.d.ts.map +1 -0
- package/dist/interpolate/path/flatten.js +135 -0
- package/dist/interpolate/path/flatten.js.map +7 -0
- package/dist/interpolate/path/morph.d.ts +9 -0
- package/dist/interpolate/path/morph.d.ts.map +1 -0
- package/dist/interpolate/path/morph.js +72 -0
- package/dist/interpolate/path/morph.js.map +7 -0
- package/dist/interpolate/path/parse.d.ts +24 -0
- package/dist/interpolate/path/parse.d.ts.map +1 -0
- package/dist/interpolate/path/parse.js +125 -0
- package/dist/interpolate/path/parse.js.map +7 -0
- package/dist/interpolate/path/rings.d.ts +13 -0
- package/dist/interpolate/path/rings.d.ts.map +1 -0
- package/dist/interpolate/path/rings.js +117 -0
- package/dist/interpolate/path/rings.js.map +7 -0
- package/dist/react/index.d.ts +7 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +19 -0
- package/dist/react/index.js.map +7 -0
- package/dist/react/presence.d.ts +27 -0
- package/dist/react/presence.d.ts.map +1 -0
- package/dist/react/presence.js +135 -0
- package/dist/react/presence.js.map +7 -0
- package/dist/react/use-animate.d.ts +17 -0
- package/dist/react/use-animate.d.ts.map +1 -0
- package/dist/react/use-animate.js +34 -0
- package/dist/react/use-animate.js.map +7 -0
- package/dist/react/use-motion-value.d.ts +4 -0
- package/dist/react/use-motion-value.d.ts.map +1 -0
- package/dist/react/use-motion-value.js +11 -0
- package/dist/react/use-motion-value.js.map +7 -0
- package/dist/react/use-reduced-motion.d.ts +7 -0
- package/dist/react/use-reduced-motion.d.ts.map +1 -0
- package/dist/react/use-reduced-motion.js +19 -0
- package/dist/react/use-reduced-motion.js.map +7 -0
- package/dist/react/use-spring.d.ts +9 -0
- package/dist/react/use-spring.d.ts.map +1 -0
- package/dist/react/use-spring.js +27 -0
- package/dist/react/use-spring.js.map +7 -0
- package/dist/react/use-transform.d.ts +13 -0
- package/dist/react/use-transform.d.ts.map +1 -0
- package/dist/react/use-transform.js +44 -0
- package/dist/react/use-transform.js.map +7 -0
- package/dist/sequence/sequence.d.ts +25 -0
- package/dist/sequence/sequence.d.ts.map +1 -0
- package/dist/sequence/sequence.js +68 -0
- package/dist/sequence/sequence.js.map +7 -0
- package/dist/stagger/stagger.d.ts +12 -0
- package/dist/stagger/stagger.d.ts.map +1 -0
- package/dist/stagger/stagger.js +21 -0
- package/dist/stagger/stagger.js.map +7 -0
- package/dist/testing/manual-frames.d.ts +11 -0
- package/dist/testing/manual-frames.d.ts.map +1 -0
- package/dist/testing/manual-frames.js +40 -0
- package/dist/testing/manual-frames.js.map +7 -0
- package/dist/value/motion-value.d.ts +27 -0
- package/dist/value/motion-value.d.ts.map +1 -0
- package/dist/value/motion-value.js +88 -0
- package/dist/value/motion-value.js.map +7 -0
- package/dist/value/transform.d.ts +16 -0
- package/dist/value/transform.d.ts.map +1 -0
- package/dist/value/transform.js +28 -0
- package/dist/value/transform.js.map +7 -0
- package/dist/view/in-view.d.ts +19 -0
- package/dist/view/in-view.d.ts.map +1 -0
- package/dist/view/in-view.js +46 -0
- package/dist/view/in-view.js.map +7 -0
- package/dist/view/resize.d.ts +13 -0
- package/dist/view/resize.d.ts.map +1 -0
- package/dist/view/resize.js +72 -0
- package/dist/view/resize.js.map +7 -0
- package/dist/view/resolve-elements.d.ts +3 -0
- package/dist/view/resolve-elements.d.ts.map +1 -0
- package/dist/view/resolve-elements.js +10 -0
- package/dist/view/resolve-elements.js.map +7 -0
- package/dist/view/scroll-measure.d.ts +16 -0
- package/dist/view/scroll-measure.d.ts.map +1 -0
- package/dist/view/scroll-measure.js +51 -0
- package/dist/view/scroll-measure.js.map +7 -0
- package/dist/view/scroll-offset.d.ts +28 -0
- package/dist/view/scroll-offset.d.ts.map +1 -0
- package/dist/view/scroll-offset.js +55 -0
- package/dist/view/scroll-offset.js.map +7 -0
- package/dist/view/scroll.d.ts +34 -0
- package/dist/view/scroll.d.ts.map +1 -0
- package/dist/view/scroll.js +83 -0
- package/dist/view/scroll.js.map +7 -0
- package/package.json +57 -5
- package/src/accessibility/reduced-motion.ts +75 -0
- package/src/animate/animate.ts +155 -0
- package/src/animate/controls.ts +76 -0
- package/src/animate/dom.ts +166 -0
- package/src/animate/js-animation.ts +151 -0
- package/src/animate/plan.ts +74 -0
- package/src/animate/style.ts +131 -0
- package/src/animate/types.ts +69 -0
- package/src/animate/value.ts +93 -0
- package/src/animate/waapi.ts +212 -0
- package/src/dom.ts +35 -0
- package/src/easing/cubic-bezier.ts +64 -0
- package/src/easing/linear-css.ts +41 -0
- package/src/easing/named.ts +88 -0
- package/src/easing/spring.ts +25 -0
- package/src/easing/steps.ts +16 -0
- package/src/easing/types.ts +18 -0
- package/src/frame/driver.ts +56 -0
- package/src/frame/frame.ts +102 -0
- package/src/generators/inertia.ts +88 -0
- package/src/generators/spring.ts +117 -0
- package/src/generators/tween.ts +74 -0
- package/src/generators/types.ts +31 -0
- package/src/index.ts +91 -0
- package/src/interpolate/array.ts +13 -0
- package/src/interpolate/color-parse.ts +136 -0
- package/src/interpolate/color.ts +97 -0
- package/src/interpolate/complex.ts +80 -0
- package/src/interpolate/interpolate.ts +60 -0
- package/src/interpolate/mix.ts +74 -0
- package/src/interpolate/number.ts +3 -0
- package/src/interpolate/object.ts +20 -0
- package/src/interpolate/path/flatten.ts +185 -0
- package/src/interpolate/path/morph.ts +100 -0
- package/src/interpolate/path/parse.ts +162 -0
- package/src/interpolate/path/rings.ts +151 -0
- package/src/react/index.ts +11 -0
- package/src/react/presence.tsx +194 -0
- package/src/react/use-animate.ts +46 -0
- package/src/react/use-motion-value.ts +9 -0
- package/src/react/use-reduced-motion.ts +22 -0
- package/src/react/use-spring.ts +35 -0
- package/src/react/use-transform.ts +73 -0
- package/src/sequence/sequence.ts +129 -0
- package/src/stagger/stagger.ts +36 -0
- package/src/testing/browser-commands.d.ts +11 -0
- package/src/testing/manual-frames.ts +49 -0
- package/src/value/motion-value.ts +131 -0
- package/src/value/transform.ts +61 -0
- package/src/view/in-view.ts +74 -0
- package/src/view/resize.ts +101 -0
- package/src/view/resolve-elements.ts +11 -0
- package/src/view/scroll-measure.ts +83 -0
- package/src/view/scroll-offset.ts +100 -0
- package/src/view/scroll.ts +143 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import {
|
|
2
|
+
oklabToRgba,
|
|
3
|
+
parseColor,
|
|
4
|
+
type Rgba,
|
|
5
|
+
} from "#/interpolate/color-parse.ts";
|
|
6
|
+
import { mixNumber } from "#/interpolate/number.ts";
|
|
7
|
+
|
|
8
|
+
export { parseColor, type Rgba } from "#/interpolate/color-parse.ts";
|
|
9
|
+
|
|
10
|
+
type Oklab = readonly [number, number, number, number];
|
|
11
|
+
|
|
12
|
+
const VARIABLE = /^var\(/i;
|
|
13
|
+
const IDENTIFIER = /^[a-z]+$/i;
|
|
14
|
+
|
|
15
|
+
function srgbToLinear(value: number): number {
|
|
16
|
+
return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function toOklab({ r, g, b, alpha }: Rgba): Oklab {
|
|
20
|
+
const [lr, lg, lb] = [srgbToLinear(r), srgbToLinear(g), srgbToLinear(b)];
|
|
21
|
+
const l = Math.cbrt(
|
|
22
|
+
0.4122214708 * lr + 0.5363325363 * lg + 0.0514459929 * lb,
|
|
23
|
+
);
|
|
24
|
+
const m = Math.cbrt(
|
|
25
|
+
0.2119034982 * lr + 0.6806995451 * lg + 0.1073969566 * lb,
|
|
26
|
+
);
|
|
27
|
+
const s = Math.cbrt(
|
|
28
|
+
0.0883024619 * lr + 0.2817188376 * lg + 0.6299787005 * lb,
|
|
29
|
+
);
|
|
30
|
+
return [
|
|
31
|
+
0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s,
|
|
32
|
+
1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s,
|
|
33
|
+
0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s,
|
|
34
|
+
alpha,
|
|
35
|
+
];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function byte(value: number): number {
|
|
39
|
+
return Math.round(Math.min(1, Math.max(0, value)) * 255);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function format({ r, g, b, alpha }: Rgba): string {
|
|
43
|
+
const a = Math.round(Math.min(1, Math.max(0, alpha)) * 1000) / 1000;
|
|
44
|
+
return `rgba(${byte(r)}, ${byte(g)}, ${byte(b)}, ${a})`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** A string the page resolves to a colour: written out, a `var()`, a name, `currentColor`. */
|
|
48
|
+
export function isColorLike(text: string): boolean {
|
|
49
|
+
const trimmed = text.trim();
|
|
50
|
+
return (
|
|
51
|
+
parseColor(trimmed) !== undefined ||
|
|
52
|
+
VARIABLE.test(trimmed) ||
|
|
53
|
+
IDENTIFIER.test(trimmed)
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Two written-out colours mix in oklab here, premultiplied so a fade from
|
|
59
|
+
* `transparent` does not pass through grey. A `var()` or a name is only known
|
|
60
|
+
* to the page, so it mixes there: `color-mix()` with the share quantised to
|
|
61
|
+
* 2 %, which caps the strings a long animation mints at 51.
|
|
62
|
+
*/
|
|
63
|
+
export function mixColor(
|
|
64
|
+
from: string,
|
|
65
|
+
to: string,
|
|
66
|
+
): (progress: number) => string {
|
|
67
|
+
const start = parseColor(from);
|
|
68
|
+
const end = parseColor(to);
|
|
69
|
+
if (start === undefined || end === undefined) {
|
|
70
|
+
return (progress) => {
|
|
71
|
+
const share = Math.round(progress * 50) * 2;
|
|
72
|
+
if (share <= 0) {
|
|
73
|
+
return from;
|
|
74
|
+
}
|
|
75
|
+
return share >= 100
|
|
76
|
+
? to
|
|
77
|
+
: `color-mix(in oklab, ${to} ${share}%, ${from})`;
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const a = toOklab(start);
|
|
81
|
+
const b = toOklab(end);
|
|
82
|
+
return (progress) => {
|
|
83
|
+
if (progress === 0) {
|
|
84
|
+
return from;
|
|
85
|
+
}
|
|
86
|
+
if (progress === 1) {
|
|
87
|
+
return to;
|
|
88
|
+
}
|
|
89
|
+
const alpha = mixNumber(a[3], b[3], progress);
|
|
90
|
+
function channel(index: 0 | 1 | 2) {
|
|
91
|
+
return alpha === 0
|
|
92
|
+
? 0
|
|
93
|
+
: mixNumber(a[index] * a[3], b[index] * b[3], progress) / alpha;
|
|
94
|
+
}
|
|
95
|
+
return format(oklabToRgba([channel(0), channel(1), channel(2), alpha]));
|
|
96
|
+
};
|
|
97
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { mixColor } from "#/interpolate/color.ts";
|
|
2
|
+
import { mixNumber } from "#/interpolate/number.ts";
|
|
3
|
+
|
|
4
|
+
type Token =
|
|
5
|
+
| { readonly kind: "number"; readonly value: number }
|
|
6
|
+
| { readonly kind: "color"; readonly value: string };
|
|
7
|
+
|
|
8
|
+
interface Parsed {
|
|
9
|
+
readonly skeleton: readonly string[];
|
|
10
|
+
readonly tokens: readonly Token[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Colours first, so a hex or an `rgb()` is one token rather than its digits. */
|
|
14
|
+
const TOKEN =
|
|
15
|
+
/(#[0-9a-f]{3,8}\b|(?:rgba?|hsla?|oklch|oklab)\([^()]*\))|(-?(?:\d+\.?\d*|\.\d+)(?:e[-+]?\d+)?)/gi;
|
|
16
|
+
|
|
17
|
+
function parse(text: string): Parsed {
|
|
18
|
+
const skeleton: string[] = [];
|
|
19
|
+
const tokens: Token[] = [];
|
|
20
|
+
let last = 0;
|
|
21
|
+
for (const match of text.matchAll(TOKEN)) {
|
|
22
|
+
skeleton.push(text.slice(last, match.index));
|
|
23
|
+
tokens.push(
|
|
24
|
+
match[1] === undefined
|
|
25
|
+
? { kind: "number", value: Number(match[2]) }
|
|
26
|
+
: { kind: "color", value: match[1] },
|
|
27
|
+
);
|
|
28
|
+
last = match.index + match[0].length;
|
|
29
|
+
}
|
|
30
|
+
skeleton.push(text.slice(last));
|
|
31
|
+
return { skeleton, tokens };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function round(value: number): number {
|
|
35
|
+
return Math.round(value * 1000) / 1000;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Strings that differ only in their numbers and colours: `10px 20px`, a
|
|
40
|
+
* transform list, two paths with the same commands. Anything else has no
|
|
41
|
+
* midpoint here, and `undefined` says so.
|
|
42
|
+
*/
|
|
43
|
+
export function mixComplex(
|
|
44
|
+
from: string,
|
|
45
|
+
to: string,
|
|
46
|
+
): ((progress: number) => string) | undefined {
|
|
47
|
+
const start = parse(from);
|
|
48
|
+
const end = parse(to);
|
|
49
|
+
const sameShape =
|
|
50
|
+
start.tokens.length === end.tokens.length &&
|
|
51
|
+
start.skeleton.every((part, index) => part === end.skeleton[index]) &&
|
|
52
|
+
start.tokens.every(
|
|
53
|
+
(token, index) => token.kind === end.tokens[index]?.kind,
|
|
54
|
+
);
|
|
55
|
+
if (!sameShape || (start.tokens.length === 0 && from !== to)) {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
const mixers = start.tokens.map((token, index) => {
|
|
59
|
+
const target = end.tokens[index] as Token;
|
|
60
|
+
if (token.kind === "color") {
|
|
61
|
+
return mixColor(token.value, target.value as string);
|
|
62
|
+
}
|
|
63
|
+
const a = token.value;
|
|
64
|
+
const b = target.value as number;
|
|
65
|
+
return (progress: number) => String(round(mixNumber(a, b, progress)));
|
|
66
|
+
});
|
|
67
|
+
return (progress) => {
|
|
68
|
+
if (progress === 0) {
|
|
69
|
+
return from;
|
|
70
|
+
}
|
|
71
|
+
if (progress === 1) {
|
|
72
|
+
return to;
|
|
73
|
+
}
|
|
74
|
+
let text = start.skeleton[0] as string;
|
|
75
|
+
mixers.forEach((mixer, index) => {
|
|
76
|
+
text += mixer(progress) + (start.skeleton[index + 1] as string);
|
|
77
|
+
});
|
|
78
|
+
return text;
|
|
79
|
+
};
|
|
80
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { resolveEasing } from "#/easing/named.ts";
|
|
2
|
+
import type { EasingDefinition } from "#/easing/types.ts";
|
|
3
|
+
import { type Mixer, mix } from "#/interpolate/mix.ts";
|
|
4
|
+
|
|
5
|
+
export interface InterpolateOptions<T> {
|
|
6
|
+
/** Hold the output at its ends outside the input range. Default true. */
|
|
7
|
+
readonly clamp?: boolean;
|
|
8
|
+
/** One easing for every segment, or one per segment. */
|
|
9
|
+
readonly ease?: EasingDefinition | readonly EasingDefinition[];
|
|
10
|
+
readonly mixer?: (from: T, to: T) => Mixer<T>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Maps a number through a piecewise range: `interpolate([0, 100], [1, 0])`
|
|
15
|
+
* turns a scroll offset into an opacity, `[0, 50, 100]` with three colours a
|
|
16
|
+
* progress into a gradient. The input may run either way.
|
|
17
|
+
*/
|
|
18
|
+
export function interpolate<T>(
|
|
19
|
+
input: readonly number[],
|
|
20
|
+
output: readonly T[],
|
|
21
|
+
options: InterpolateOptions<T> = {},
|
|
22
|
+
): (value: number) => T {
|
|
23
|
+
if (input.length !== output.length || input.length === 0) {
|
|
24
|
+
throw new RangeError(
|
|
25
|
+
"interpolate: input and output need the same, non-zero length",
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
if (input.length === 1) {
|
|
29
|
+
return () => output[0] as T;
|
|
30
|
+
}
|
|
31
|
+
const direction =
|
|
32
|
+
(input[0] as number) > (input[input.length - 1] as number) ? -1 : 1;
|
|
33
|
+
const makeMixer = options.mixer ?? mix;
|
|
34
|
+
const mixers = output
|
|
35
|
+
.slice(1)
|
|
36
|
+
.map((value, index) => makeMixer(output[index] as T, value));
|
|
37
|
+
const eases = mixers.map((_mixer, index) => {
|
|
38
|
+
const ease = options.ease;
|
|
39
|
+
const definition = Array.isArray(ease) ? ease[index] : ease;
|
|
40
|
+
return resolveEasing((definition ?? "linear") as EasingDefinition);
|
|
41
|
+
});
|
|
42
|
+
const clamp = options.clamp ?? true;
|
|
43
|
+
const low = Math.min(input[0] as number, input[input.length - 1] as number);
|
|
44
|
+
const high = Math.max(input[0] as number, input[input.length - 1] as number);
|
|
45
|
+
return (value) => {
|
|
46
|
+
const at = clamp ? Math.min(high, Math.max(low, value)) : value;
|
|
47
|
+
let segment = 0;
|
|
48
|
+
while (
|
|
49
|
+
segment < mixers.length - 1 &&
|
|
50
|
+
(at - (input[segment + 1] as number)) * direction > 0
|
|
51
|
+
) {
|
|
52
|
+
segment += 1;
|
|
53
|
+
}
|
|
54
|
+
const start = input[segment] as number;
|
|
55
|
+
const span = (input[segment + 1] as number) - start;
|
|
56
|
+
const progress = span === 0 ? 1 : (at - start) / span;
|
|
57
|
+
const eased = (eases[segment] as (progress: number) => number)(progress);
|
|
58
|
+
return (mixers[segment] as Mixer<T>)(eased);
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { mixArray } from "#/interpolate/array.ts";
|
|
2
|
+
import { isColorLike, mixColor, parseColor } from "#/interpolate/color.ts";
|
|
3
|
+
import { mixComplex } from "#/interpolate/complex.ts";
|
|
4
|
+
import { mixNumber } from "#/interpolate/number.ts";
|
|
5
|
+
import { mixObject } from "#/interpolate/object.ts";
|
|
6
|
+
import { mixPath } from "#/interpolate/path/morph.ts";
|
|
7
|
+
|
|
8
|
+
export type Mixer<T> = (progress: number) => T;
|
|
9
|
+
|
|
10
|
+
type Strategy = (from: unknown, to: unknown) => Mixer<unknown> | undefined;
|
|
11
|
+
|
|
12
|
+
const PATH = /^\s*[Mm]/;
|
|
13
|
+
|
|
14
|
+
function bothStrings(from: unknown, to: unknown): [string, string] | undefined {
|
|
15
|
+
return typeof from === "string" && typeof to === "string"
|
|
16
|
+
? [from, to]
|
|
17
|
+
: undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A colour pair needs one side written out, or both to be variables: two bare
|
|
22
|
+
* words are as likely `auto` and `none` as `red` and `blue`.
|
|
23
|
+
*/
|
|
24
|
+
function colors(from: string, to: string): boolean {
|
|
25
|
+
const resolved =
|
|
26
|
+
parseColor(from) !== undefined || parseColor(to) !== undefined;
|
|
27
|
+
const variables = /^\s*var\(/i.test(from) && /^\s*var\(/i.test(to);
|
|
28
|
+
return isColorLike(from) && isColorLike(to) && (resolved || variables);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
32
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Tried in order; the first that knows the pair mixes it. */
|
|
36
|
+
const STRATEGIES: readonly Strategy[] = [
|
|
37
|
+
(from, to) =>
|
|
38
|
+
typeof from === "number" && typeof to === "number"
|
|
39
|
+
? (progress) => mixNumber(from, to, progress)
|
|
40
|
+
: undefined,
|
|
41
|
+
(from, to) => {
|
|
42
|
+
const pair = bothStrings(from, to);
|
|
43
|
+
return pair && PATH.test(pair[0]) && PATH.test(pair[1])
|
|
44
|
+
? mixPath(...pair)
|
|
45
|
+
: undefined;
|
|
46
|
+
},
|
|
47
|
+
(from, to) => {
|
|
48
|
+
const pair = bothStrings(from, to);
|
|
49
|
+
return pair && colors(...pair) ? mixColor(...pair) : undefined;
|
|
50
|
+
},
|
|
51
|
+
(from, to) => {
|
|
52
|
+
const pair = bothStrings(from, to);
|
|
53
|
+
return pair && mixComplex(...pair);
|
|
54
|
+
},
|
|
55
|
+
(from, to) =>
|
|
56
|
+
Array.isArray(from) && Array.isArray(to) ? mixArray(from, to) : undefined,
|
|
57
|
+
(from, to) =>
|
|
58
|
+
isRecord(from) && isRecord(to) ? mixObject(from, to) : undefined,
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The value `progress` of the way from `from` to `to`, whatever they are:
|
|
63
|
+
* numbers, colours (in oklab), paths (morphed), strings of numbers, arrays,
|
|
64
|
+
* objects. What has no midpoint switches halfway.
|
|
65
|
+
*/
|
|
66
|
+
export function mix<T>(from: T, to: T): Mixer<T> {
|
|
67
|
+
for (const strategy of STRATEGIES) {
|
|
68
|
+
const mixer = strategy(from, to);
|
|
69
|
+
if (mixer !== undefined) {
|
|
70
|
+
return mixer as Mixer<T>;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return (progress) => (progress < 0.5 ? from : to);
|
|
74
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { mix } from "#/interpolate/mix.ts";
|
|
2
|
+
|
|
3
|
+
type Record_ = Readonly<Record<string, unknown>>;
|
|
4
|
+
|
|
5
|
+
/** Key by key over the target's keys; a key the start lacks starts at its target. */
|
|
6
|
+
export function mixObject<T extends Record_>(
|
|
7
|
+
from: T,
|
|
8
|
+
to: T,
|
|
9
|
+
): (progress: number) => T {
|
|
10
|
+
const mixers = Object.keys(to).map(
|
|
11
|
+
(key) => [key, mix(key in from ? from[key] : to[key], to[key])] as const,
|
|
12
|
+
);
|
|
13
|
+
return (progress) => {
|
|
14
|
+
const result: Record<string, unknown> = {};
|
|
15
|
+
for (const [key, mixer] of mixers) {
|
|
16
|
+
result[key] = mixer(progress);
|
|
17
|
+
}
|
|
18
|
+
return result as T;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import type { PathCommand } from "#/interpolate/path/parse.ts";
|
|
2
|
+
|
|
3
|
+
export type Point = readonly [number, number];
|
|
4
|
+
|
|
5
|
+
/** One subpath as a polyline. */
|
|
6
|
+
export interface Ring {
|
|
7
|
+
readonly points: readonly Point[];
|
|
8
|
+
readonly closed: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const MAX_SEGMENTS = 256;
|
|
12
|
+
|
|
13
|
+
/** Enough segments that no chord strays more than `tolerance` from its curve. */
|
|
14
|
+
function segmentsFor(bend: number, tolerance: number): number {
|
|
15
|
+
return Math.min(
|
|
16
|
+
MAX_SEGMENTS,
|
|
17
|
+
Math.max(1, Math.ceil(Math.sqrt((0.75 * bend) / tolerance))),
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function cubic(
|
|
22
|
+
from: Point,
|
|
23
|
+
values: readonly number[],
|
|
24
|
+
tolerance: number,
|
|
25
|
+
): Point[] {
|
|
26
|
+
const [x1, y1, x2, y2, x, y] = values as [
|
|
27
|
+
number,
|
|
28
|
+
number,
|
|
29
|
+
number,
|
|
30
|
+
number,
|
|
31
|
+
number,
|
|
32
|
+
number,
|
|
33
|
+
];
|
|
34
|
+
const [x0, y0] = from;
|
|
35
|
+
const bend = Math.max(
|
|
36
|
+
Math.hypot(x0 - 2 * x1 + x2, y0 - 2 * y1 + y2),
|
|
37
|
+
Math.hypot(x1 - 2 * x2 + x, y1 - 2 * y2 + y),
|
|
38
|
+
);
|
|
39
|
+
const count = segmentsFor(bend, tolerance);
|
|
40
|
+
return Array.from({ length: count }, (_unused, index) => {
|
|
41
|
+
const t = (index + 1) / count;
|
|
42
|
+
const u = 1 - t;
|
|
43
|
+
const [a, b, c, d] = [u * u * u, 3 * u * u * t, 3 * u * t * t, t * t * t];
|
|
44
|
+
return [a * x0 + b * x1 + c * x2 + d * x, a * y0 + b * y1 + c * y2 + d * y];
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function quadratic(
|
|
49
|
+
from: Point,
|
|
50
|
+
values: readonly number[],
|
|
51
|
+
tolerance: number,
|
|
52
|
+
): Point[] {
|
|
53
|
+
const [x1, y1, x, y] = values as [number, number, number, number];
|
|
54
|
+
const [x0, y0] = from;
|
|
55
|
+
const count = segmentsFor(
|
|
56
|
+
Math.hypot(x0 - 2 * x1 + x, y0 - 2 * y1 + y) / 3,
|
|
57
|
+
tolerance,
|
|
58
|
+
);
|
|
59
|
+
return Array.from({ length: count }, (_unused, index) => {
|
|
60
|
+
const t = (index + 1) / count;
|
|
61
|
+
const u = 1 - t;
|
|
62
|
+
return [
|
|
63
|
+
u * u * x0 + 2 * u * t * x1 + t * t * x,
|
|
64
|
+
u * u * y0 + 2 * u * t * y1 + t * t * y,
|
|
65
|
+
];
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function angleBetween(ux: number, uy: number, vx: number, vy: number): number {
|
|
70
|
+
return Math.atan2(ux * vy - uy * vx, ux * vx + uy * vy);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** SVG's endpoint arc, converted to its centre form (SVG 1.1, appendix F.6.5). */
|
|
74
|
+
function arc(
|
|
75
|
+
from: Point,
|
|
76
|
+
values: readonly number[],
|
|
77
|
+
tolerance: number,
|
|
78
|
+
): Point[] {
|
|
79
|
+
const [rxIn, ryIn, degrees, large, sweep, x2, y2] = values as [
|
|
80
|
+
number,
|
|
81
|
+
number,
|
|
82
|
+
number,
|
|
83
|
+
number,
|
|
84
|
+
number,
|
|
85
|
+
number,
|
|
86
|
+
number,
|
|
87
|
+
];
|
|
88
|
+
const [x1, y1] = from;
|
|
89
|
+
if (rxIn === 0 || ryIn === 0) {
|
|
90
|
+
return [[x2, y2]];
|
|
91
|
+
}
|
|
92
|
+
const phi = (degrees * Math.PI) / 180;
|
|
93
|
+
const [cos, sin] = [Math.cos(phi), Math.sin(phi)];
|
|
94
|
+
const dx = (x1 - x2) / 2;
|
|
95
|
+
const dy = (y1 - y2) / 2;
|
|
96
|
+
const x1p = cos * dx + sin * dy;
|
|
97
|
+
const y1p = -sin * dx + cos * dy;
|
|
98
|
+
const scale = Math.max(
|
|
99
|
+
1,
|
|
100
|
+
Math.sqrt((x1p * x1p) / (rxIn * rxIn) + (y1p * y1p) / (ryIn * ryIn)),
|
|
101
|
+
);
|
|
102
|
+
const rx = Math.abs(rxIn) * scale;
|
|
103
|
+
const ry = Math.abs(ryIn) * scale;
|
|
104
|
+
const numerator =
|
|
105
|
+
rx * rx * ry * ry - rx * rx * y1p * y1p - ry * ry * x1p * x1p;
|
|
106
|
+
const denominator = rx * rx * y1p * y1p + ry * ry * x1p * x1p;
|
|
107
|
+
const coefficient =
|
|
108
|
+
(large === sweep ? -1 : 1) *
|
|
109
|
+
Math.sqrt(Math.max(0, numerator / (denominator || 1)));
|
|
110
|
+
const cxp = (coefficient * rx * y1p) / ry;
|
|
111
|
+
const cyp = (-coefficient * ry * x1p) / rx;
|
|
112
|
+
const cx = cos * cxp - sin * cyp + (x1 + x2) / 2;
|
|
113
|
+
const cy = sin * cxp + cos * cyp + (y1 + y2) / 2;
|
|
114
|
+
const start = angleBetween(1, 0, (x1p - cxp) / rx, (y1p - cyp) / ry);
|
|
115
|
+
let delta = angleBetween(
|
|
116
|
+
(x1p - cxp) / rx,
|
|
117
|
+
(y1p - cyp) / ry,
|
|
118
|
+
(-x1p - cxp) / rx,
|
|
119
|
+
(-y1p - cyp) / ry,
|
|
120
|
+
);
|
|
121
|
+
if (sweep === 0 && delta > 0) {
|
|
122
|
+
delta -= 2 * Math.PI;
|
|
123
|
+
}
|
|
124
|
+
if (sweep === 1 && delta < 0) {
|
|
125
|
+
delta += 2 * Math.PI;
|
|
126
|
+
}
|
|
127
|
+
const radius = Math.max(rx, ry);
|
|
128
|
+
const step = 2 * Math.acos(Math.max(-1, 1 - tolerance / radius));
|
|
129
|
+
const count = Math.min(
|
|
130
|
+
MAX_SEGMENTS,
|
|
131
|
+
Math.max(1, Math.ceil(Math.abs(delta) / step)),
|
|
132
|
+
);
|
|
133
|
+
return Array.from({ length: count }, (_unused, index): Point => {
|
|
134
|
+
if (index === count - 1) {
|
|
135
|
+
return [x2, y2];
|
|
136
|
+
}
|
|
137
|
+
const theta = start + (delta * (index + 1)) / count;
|
|
138
|
+
const [c, s] = [Math.cos(theta), Math.sin(theta)];
|
|
139
|
+
return [cx + rx * c * cos - ry * s * sin, cy + rx * c * sin + ry * s * cos];
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const CURVES = { C: cubic, Q: quadratic, A: arc } as const;
|
|
144
|
+
|
|
145
|
+
/** Every subpath of a parsed path as a polyline; `tolerance` in path units. */
|
|
146
|
+
export function flattenPath(
|
|
147
|
+
commands: readonly PathCommand[],
|
|
148
|
+
tolerance = 0.25,
|
|
149
|
+
): Ring[] {
|
|
150
|
+
const rings: { points: Point[]; closed: boolean }[] = [];
|
|
151
|
+
let current: { points: Point[]; closed: boolean } = {
|
|
152
|
+
points: [],
|
|
153
|
+
closed: false,
|
|
154
|
+
};
|
|
155
|
+
let start: Point = [0, 0];
|
|
156
|
+
for (const command of commands) {
|
|
157
|
+
if (command.type === "M" || current.closed) {
|
|
158
|
+
start = command.type === "M" ? command.values : start;
|
|
159
|
+
current = { points: [start], closed: false };
|
|
160
|
+
rings.push(current);
|
|
161
|
+
}
|
|
162
|
+
const last = current.points[current.points.length - 1] as Point;
|
|
163
|
+
switch (command.type) {
|
|
164
|
+
case "M":
|
|
165
|
+
break;
|
|
166
|
+
case "L":
|
|
167
|
+
current.points.push(command.values);
|
|
168
|
+
break;
|
|
169
|
+
case "Z":
|
|
170
|
+
current.closed = true;
|
|
171
|
+
break;
|
|
172
|
+
default:
|
|
173
|
+
current.points.push(
|
|
174
|
+
...CURVES[command.type](last, command.values, tolerance),
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return rings.map(({ points, closed }) => {
|
|
179
|
+
const first = points[0] as Point;
|
|
180
|
+
const end = points[points.length - 1] as Point;
|
|
181
|
+
const repeats =
|
|
182
|
+
closed && points.length > 1 && first[0] === end[0] && first[1] === end[1];
|
|
183
|
+
return { points: repeats ? points.slice(0, -1) : points, closed };
|
|
184
|
+
});
|
|
185
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { mixComplex } from "#/interpolate/complex.ts";
|
|
2
|
+
import {
|
|
3
|
+
flattenPath,
|
|
4
|
+
type Point,
|
|
5
|
+
type Ring,
|
|
6
|
+
} from "#/interpolate/path/flatten.ts";
|
|
7
|
+
import { parsePath } from "#/interpolate/path/parse.ts";
|
|
8
|
+
import {
|
|
9
|
+
alignRing,
|
|
10
|
+
centroid,
|
|
11
|
+
matchRings,
|
|
12
|
+
resample,
|
|
13
|
+
} from "#/interpolate/path/rings.ts";
|
|
14
|
+
|
|
15
|
+
const MIN_POINTS = 16;
|
|
16
|
+
const MAX_POINTS = 512;
|
|
17
|
+
|
|
18
|
+
interface Track {
|
|
19
|
+
readonly from: readonly Point[];
|
|
20
|
+
readonly to: readonly Point[];
|
|
21
|
+
readonly closedFrom: boolean;
|
|
22
|
+
readonly closedTo: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function round(value: number): number {
|
|
26
|
+
return Math.round(value * 100) / 100;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** A ring that does not exist on one side is that side's centroid, repeated. */
|
|
30
|
+
function collapsed(ring: Ring, count: number): Point[] {
|
|
31
|
+
return Array.from({ length: count }, () => centroid(ring.points));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function track(from: Ring | undefined, to: Ring | undefined): Track {
|
|
35
|
+
const reference = (from ?? to) as Ring;
|
|
36
|
+
const count = Math.min(
|
|
37
|
+
MAX_POINTS,
|
|
38
|
+
Math.max(MIN_POINTS, from?.points.length ?? 0, to?.points.length ?? 0),
|
|
39
|
+
);
|
|
40
|
+
const start =
|
|
41
|
+
from === undefined ? collapsed(reference, count) : resample(from, count);
|
|
42
|
+
const end =
|
|
43
|
+
to === undefined ? collapsed(reference, count) : resample(to, count);
|
|
44
|
+
const closed = (from?.closed ?? true) && (to?.closed ?? true);
|
|
45
|
+
return {
|
|
46
|
+
from: start,
|
|
47
|
+
to: alignRing(start, end, closed),
|
|
48
|
+
closedFrom: from?.closed ?? reference.closed,
|
|
49
|
+
closedTo: to?.closed ?? reference.closed,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function draw(tracks: readonly Track[], progress: number): string {
|
|
54
|
+
return tracks
|
|
55
|
+
.map(({ from, to, closedFrom, closedTo }) => {
|
|
56
|
+
const points = from.map((point, index) => {
|
|
57
|
+
const other = to[index] as Point;
|
|
58
|
+
const x = round(point[0] + (other[0] - point[0]) * progress);
|
|
59
|
+
const y = round(point[1] + (other[1] - point[1]) * progress);
|
|
60
|
+
return `${x},${y}`;
|
|
61
|
+
});
|
|
62
|
+
const closed = progress < 0.5 ? closedFrom : closedTo;
|
|
63
|
+
return `M${points.join("L")}${closed ? "Z" : ""}`;
|
|
64
|
+
})
|
|
65
|
+
.join("");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* A path between two paths. Same commands: the numbers mix, exactly. Any
|
|
70
|
+
* other pair is flattened to polylines, each subpath resampled to a shared
|
|
71
|
+
* point count by arc length, paired with its nearest counterpart and turned
|
|
72
|
+
* to line up with it, so every frame is a valid path that moves as little as
|
|
73
|
+
* it can. The ends are the input strings themselves.
|
|
74
|
+
*/
|
|
75
|
+
export function mixPath(
|
|
76
|
+
from: string,
|
|
77
|
+
to: string,
|
|
78
|
+
): (progress: number) => string {
|
|
79
|
+
// Arc flags are 0 or 1: mixed as numbers, they would pass through 0.5.
|
|
80
|
+
const hasArcs = /a/i.test(from) || /a/i.test(to);
|
|
81
|
+
const same =
|
|
82
|
+
from === to ? () => from : hasArcs ? undefined : mixComplex(from, to);
|
|
83
|
+
if (same !== undefined) {
|
|
84
|
+
return same;
|
|
85
|
+
}
|
|
86
|
+
const start = parsePath(from);
|
|
87
|
+
const end = parsePath(to);
|
|
88
|
+
if (start === undefined || end === undefined) {
|
|
89
|
+
return (progress) => (progress < 0.5 ? from : to);
|
|
90
|
+
}
|
|
91
|
+
const tracks = matchRings(flattenPath(start), flattenPath(end)).map(
|
|
92
|
+
([a, b]) => track(a, b),
|
|
93
|
+
);
|
|
94
|
+
return (progress) => {
|
|
95
|
+
if (progress === 0) {
|
|
96
|
+
return from;
|
|
97
|
+
}
|
|
98
|
+
return progress === 1 ? to : draw(tracks, progress);
|
|
99
|
+
};
|
|
100
|
+
}
|