@voila.dev/motion 6.0.121 → 6.0.123
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 +4 -1
- package/dist/animate/js-animation.d.ts.map +1 -1
- package/dist/animate/js-animation.js +13 -11
- package/dist/animate/js-animation.js.map +2 -2
- package/dist/animate/style.d.ts +1 -1
- package/dist/animate/style.d.ts.map +1 -1
- package/dist/animate/style.js +18 -1
- package/dist/animate/style.js.map +2 -2
- package/dist/animate/types.d.ts +2 -2
- package/dist/animate/waapi.d.ts.map +1 -1
- package/dist/animate/waapi.js +5 -2
- package/dist/animate/waapi.js.map +2 -2
- package/dist/generators/spring.d.ts.map +1 -1
- package/dist/generators/spring.js +84 -36
- package/dist/generators/spring.js.map +3 -3
- package/dist/interpolate/color-parse.d.ts +1 -1
- package/dist/interpolate/color-parse.d.ts.map +1 -1
- package/dist/interpolate/color-parse.js +19 -1
- package/dist/interpolate/color-parse.js.map +2 -2
- package/dist/interpolate/mix.d.ts.map +1 -1
- package/dist/interpolate/mix.js.map +2 -2
- package/dist/sequence/sequence.d.ts.map +1 -1
- package/dist/sequence/sequence.js +32 -5
- package/dist/sequence/sequence.js.map +2 -2
- package/package.json +1 -1
- package/src/animate/js-animation.ts +15 -11
- package/src/animate/style.ts +28 -1
- package/src/animate/types.ts +2 -2
- package/src/animate/waapi.ts +7 -2
- package/src/generators/spring.ts +106 -40
- package/src/interpolate/color-parse.ts +31 -2
- package/src/interpolate/mix.ts +3 -2
- package/src/sequence/sequence.ts +61 -15
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mix.d.ts","sourceRoot":"","sources":["../../src/interpolate/mix.ts"],"names":[],"mappings":"AAOA,MAAM,MAAM,KAAK,CAAC,CAAC,IAAI,CAAC,QAAQ,EAAE,MAAM,KAAK,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"mix.d.ts","sourceRoot":"","sources":["../../src/interpolate/mix.ts"],"names":[],"mappings":"AAOA,MAAM,MAAM,KAAK,CAAC,CAAC,IAAI,CAAC,QAAQ,EAAE,MAAM,KAAK,CAAC,CAAC;AAsD/C;;;;GAIG;AACH,wBAAgB,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAQ/C"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/interpolate/mix.ts"],
|
|
4
|
-
"sourcesContent": ["import { mixArray } from \"#/interpolate/array.ts\";\nimport { isColorLike, mixColor, parseColor } from \"#/interpolate/color.ts\";\nimport { mixComplex } from \"#/interpolate/complex.ts\";\nimport { mixNumber } from \"#/interpolate/number.ts\";\nimport { mixObject } from \"#/interpolate/object.ts\";\nimport { mixPath } from \"#/interpolate/path/morph.ts\";\n\nexport type Mixer<T> = (progress: number) => T;\n\ntype Strategy = (from: unknown, to: unknown) => Mixer<unknown> | undefined;\n\nconst PATH = /^\\s*[Mm]/;\n\nfunction bothStrings(from: unknown, to: unknown): [string, string] | undefined {\n\treturn typeof from === \"string\" && typeof to === \"string\"\n\t\t? [from, to]\n\t\t: undefined;\n}\n\n/**\n * A colour pair needs one side
|
|
5
|
-
"mappings": "AAAA,SAAS,gBAAgB;AACzB,SAAS,aAAa,UAAU,kBAAkB;AAClD,SAAS,kBAAkB;AAC3B,SAAS,iBAAiB;AAC1B,SAAS,iBAAiB;AAC1B,SAAS,eAAe;AAMxB,MAAM,OAAO;AAEb,SAAS,YAAY,MAAe,IAA2C;AAC9E,SAAO,OAAO,SAAS,YAAY,OAAO,OAAO,WAC9C,CAAC,MAAM,EAAE,IACT;AACJ;
|
|
4
|
+
"sourcesContent": ["import { mixArray } from \"#/interpolate/array.ts\";\nimport { isColorLike, mixColor, parseColor } from \"#/interpolate/color.ts\";\nimport { mixComplex } from \"#/interpolate/complex.ts\";\nimport { mixNumber } from \"#/interpolate/number.ts\";\nimport { mixObject } from \"#/interpolate/object.ts\";\nimport { mixPath } from \"#/interpolate/path/morph.ts\";\n\nexport type Mixer<T> = (progress: number) => T;\n\ntype Strategy = (from: unknown, to: unknown) => Mixer<unknown> | undefined;\n\nconst PATH = /^\\s*[Mm]/;\n\nfunction bothStrings(from: unknown, to: unknown): [string, string] | undefined {\n\treturn typeof from === \"string\" && typeof to === \"string\"\n\t\t? [from, to]\n\t\t: undefined;\n}\n\n/**\n * A colour pair needs one side spelled out (a name counts where a browser can\n * spell it), or both to be variables: two other bare words are as likely\n * `auto` and `none` as colours.\n */\nfunction colors(from: string, to: string): boolean {\n\tconst resolved =\n\t\tparseColor(from) !== undefined || parseColor(to) !== undefined;\n\tconst variables = /^\\s*var\\(/i.test(from) && /^\\s*var\\(/i.test(to);\n\treturn isColorLike(from) && isColorLike(to) && (resolved || variables);\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/** Tried in order; the first that knows the pair mixes it. */\nconst STRATEGIES: readonly Strategy[] = [\n\t(from, to) =>\n\t\ttypeof from === \"number\" && typeof to === \"number\"\n\t\t\t? (progress) => mixNumber(from, to, progress)\n\t\t\t: undefined,\n\t(from, to) => {\n\t\tconst pair = bothStrings(from, to);\n\t\treturn pair && PATH.test(pair[0]) && PATH.test(pair[1])\n\t\t\t? mixPath(...pair)\n\t\t\t: undefined;\n\t},\n\t(from, to) => {\n\t\tconst pair = bothStrings(from, to);\n\t\treturn pair && colors(...pair) ? mixColor(...pair) : undefined;\n\t},\n\t(from, to) => {\n\t\tconst pair = bothStrings(from, to);\n\t\treturn pair && mixComplex(...pair);\n\t},\n\t(from, to) =>\n\t\tArray.isArray(from) && Array.isArray(to) ? mixArray(from, to) : undefined,\n\t(from, to) =>\n\t\tisRecord(from) && isRecord(to) ? mixObject(from, to) : undefined,\n];\n\n/**\n * The value `progress` of the way from `from` to `to`, whatever they are:\n * numbers, colours (in oklab), paths (morphed), strings of numbers, arrays,\n * objects. What has no midpoint switches halfway.\n */\nexport function mix<T>(from: T, to: T): Mixer<T> {\n\tfor (const strategy of STRATEGIES) {\n\t\tconst mixer = strategy(from, to);\n\t\tif (mixer !== undefined) {\n\t\t\treturn mixer as Mixer<T>;\n\t\t}\n\t}\n\treturn (progress) => (progress < 0.5 ? from : to);\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,gBAAgB;AACzB,SAAS,aAAa,UAAU,kBAAkB;AAClD,SAAS,kBAAkB;AAC3B,SAAS,iBAAiB;AAC1B,SAAS,iBAAiB;AAC1B,SAAS,eAAe;AAMxB,MAAM,OAAO;AAEb,SAAS,YAAY,MAAe,IAA2C;AAC9E,SAAO,OAAO,SAAS,YAAY,OAAO,OAAO,WAC9C,CAAC,MAAM,EAAE,IACT;AACJ;AAOA,SAAS,OAAO,MAAc,IAAqB;AAClD,QAAM,WACL,WAAW,IAAI,MAAM,UAAa,WAAW,EAAE,MAAM;AACtD,QAAM,YAAY,aAAa,KAAK,IAAI,KAAK,aAAa,KAAK,EAAE;AACjE,SAAO,YAAY,IAAI,KAAK,YAAY,EAAE,MAAM,YAAY;AAC7D;AAEA,SAAS,SAAS,OAAkD;AACnE,SAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC3E;AAGA,MAAM,aAAkC;AAAA,EACvC,CAAC,MAAM,OACN,OAAO,SAAS,YAAY,OAAO,OAAO,WACvC,CAAC,aAAa,UAAU,MAAM,IAAI,QAAQ,IAC1C;AAAA,EACJ,CAAC,MAAM,OAAO;AACb,UAAM,OAAO,YAAY,MAAM,EAAE;AACjC,WAAO,QAAQ,KAAK,KAAK,KAAK,CAAC,CAAC,KAAK,KAAK,KAAK,KAAK,CAAC,CAAC,IACnD,QAAQ,GAAG,IAAI,IACf;AAAA,EACJ;AAAA,EACA,CAAC,MAAM,OAAO;AACb,UAAM,OAAO,YAAY,MAAM,EAAE;AACjC,WAAO,QAAQ,OAAO,GAAG,IAAI,IAAI,SAAS,GAAG,IAAI,IAAI;AAAA,EACtD;AAAA,EACA,CAAC,MAAM,OAAO;AACb,UAAM,OAAO,YAAY,MAAM,EAAE;AACjC,WAAO,QAAQ,WAAW,GAAG,IAAI;AAAA,EAClC;AAAA,EACA,CAAC,MAAM,OACN,MAAM,QAAQ,IAAI,KAAK,MAAM,QAAQ,EAAE,IAAI,SAAS,MAAM,EAAE,IAAI;AAAA,EACjE,CAAC,MAAM,OACN,SAAS,IAAI,KAAK,SAAS,EAAE,IAAI,UAAU,MAAM,EAAE,IAAI;AACzD;AAOO,SAAS,IAAO,MAAS,IAAiB;AAChD,aAAW,YAAY,YAAY;AAClC,UAAM,QAAQ,SAAS,MAAM,EAAE;AAC/B,QAAI,UAAU,QAAW;AACxB,aAAO;AAAA,IACR;AAAA,EACD;AACA,SAAO,CAAC,aAAc,WAAW,MAAM,OAAO;AAC/C;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sequence.d.ts","sourceRoot":"","sources":["../../src/sequence/sequence.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"sequence.d.ts","sourceRoot":"","sources":["../../src/sequence/sequence.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACX,iBAAiB,EACjB,gBAAgB,EAChB,YAAY,EACZ,aAAa,EACb,SAAS,EACT,MAAM,oBAAoB,CAAC;AAE5B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAE3D;;;;GAIG;AACH,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,IAAI,MAAM,EAAE,GAAG,IAAI,MAAM,EAAE,GAAG,GAAG,GAAG,MAAM,CAAC;AAE7E,MAAM,WAAW,cAAe,SAAQ,gBAAgB;IACvD,QAAQ,CAAC,EAAE,CAAC,EAAE,UAAU,CAAC;CACzB;AAED,MAAM,MAAM,OAAO,GAChB,SAAS,CAAC,aAAa,EAAE,YAAY,EAAE,cAAc,CAAC,CAAC,GAEvD,SAAS,CAAC,WAAW,CAAC,GAAG,CAAC,EAAE,SAAS,CAAC,GAAG,CAAC,EAAE,cAAc,CAAC,CAAC,GAC5D,MAAM,GACN;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,CAAC,EAAE,UAAU,CAAA;CAAE,CAAC;AAEvD,MAAM,MAAM,UAAU,GAAG,CACxB,MAAM,EAAE,OAAO,EACf,SAAS,EAAE,OAAO,EAClB,OAAO,EAAE,gBAAgB,KACrB,iBAAiB,CAAC;AAEvB,MAAM,MAAM,cAAc,GAAG,CAAC,MAAM,EAAE,OAAO,KAAK,SAAS,MAAM,EAAE,CAAC;AA6CpE;;;;;GAKG;AACH,wBAAgB,eAAe,CAC9B,QAAQ,EAAE,SAAS,OAAO,EAAE,EAC5B,OAAO,EAAE,gBAAgB,EACzB,UAAU,EAAE,UAAU,EACtB,cAAc,EAAE,cAAc,GAC5B,iBAAiB,CAkFnB"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { groupControls, thenable } from "../animate/controls.js";
|
|
2
|
+
import { planGenerator, totalDuration } from "../animate/plan.js";
|
|
2
3
|
import { now } from "../frame/frame.js";
|
|
3
4
|
function startOf(at, cursor, previousStart, labels) {
|
|
4
5
|
if (at === void 0) return cursor;
|
|
@@ -7,12 +8,28 @@ function startOf(at, cursor, previousStart, labels) {
|
|
|
7
8
|
if (/^[+-]/.test(at)) return Math.max(0, cursor + Number(at));
|
|
8
9
|
return labels.get(at) ?? cursor;
|
|
9
10
|
}
|
|
11
|
+
function plannedDuration(keyframes, options) {
|
|
12
|
+
const channels = typeof keyframes === "object" && keyframes !== null && !Array.isArray(keyframes) ? Object.values(keyframes) : [keyframes];
|
|
13
|
+
const { delay } = options;
|
|
14
|
+
return (typeof delay === "number" ? delay : 0) + Math.max(
|
|
15
|
+
0,
|
|
16
|
+
...channels.map((channel) => {
|
|
17
|
+
const frames = Array.isArray(channel) ? channel : [0, channel];
|
|
18
|
+
const { generator } = planGenerator(
|
|
19
|
+
frames.map((_frame, index) => index),
|
|
20
|
+
options
|
|
21
|
+
);
|
|
22
|
+
return totalDuration(generator.duration, options);
|
|
23
|
+
})
|
|
24
|
+
);
|
|
25
|
+
}
|
|
10
26
|
function animateSequence(segments, options, animateOne, resolveTargets) {
|
|
11
27
|
const begun = now();
|
|
12
28
|
const labels = /* @__PURE__ */ new Map();
|
|
13
29
|
const busy = /* @__PURE__ */ new Map();
|
|
14
30
|
const children = [];
|
|
15
31
|
const waits = [];
|
|
32
|
+
let end = 0;
|
|
16
33
|
let cursor = 0;
|
|
17
34
|
let previousStart = 0;
|
|
18
35
|
for (const segment of segments) {
|
|
@@ -31,28 +48,35 @@ function animateSequence(segments, options, animateOne, resolveTargets) {
|
|
|
31
48
|
continue;
|
|
32
49
|
}
|
|
33
50
|
const [target, keyframes, own = {}] = segment;
|
|
34
|
-
const
|
|
35
|
-
const { delay } = own;
|
|
51
|
+
const placed = startOf(own.at, cursor, previousStart, labels);
|
|
36
52
|
const keys = typeof keyframes === "object" && keyframes !== null && !Array.isArray(keyframes) ? Object.keys(keyframes) : ["value"];
|
|
37
53
|
const targets = resolveTargets(target);
|
|
38
54
|
const before = targets.flatMap(
|
|
39
55
|
(one) => keys.flatMap((key) => busy.get(one)?.get(key) ?? [])
|
|
40
56
|
);
|
|
57
|
+
const start = Math.max(placed, ...before.map((earlier) => earlier.end));
|
|
58
|
+
const { delay } = own;
|
|
41
59
|
const launch = () => animateOne(target, keyframes, optionsAt2((now() - begun) / 1e3));
|
|
42
60
|
let finished2;
|
|
61
|
+
let ends;
|
|
43
62
|
if (before.length === 0) {
|
|
44
63
|
const controls = launch();
|
|
45
64
|
children.push(controls);
|
|
46
65
|
finished2 = controls.finished;
|
|
47
|
-
|
|
66
|
+
ends = Math.max(start, controls.duration);
|
|
48
67
|
} else {
|
|
49
|
-
finished2 = Promise.all(before
|
|
68
|
+
finished2 = Promise.all(before.map((earlier) => earlier.finished)).then(
|
|
69
|
+
() => launch().finished
|
|
70
|
+
);
|
|
50
71
|
waits.push(finished2);
|
|
72
|
+
ends = start + plannedDuration(keyframes, { ...options, ...own });
|
|
51
73
|
}
|
|
74
|
+
cursor = ends;
|
|
75
|
+
end = Math.max(end, ends);
|
|
52
76
|
for (const one of targets) {
|
|
53
77
|
const byKey = busy.get(one) ?? /* @__PURE__ */ new Map();
|
|
54
78
|
busy.set(one, byKey);
|
|
55
|
-
for (const key of keys) byKey.set(key, finished2);
|
|
79
|
+
for (const key of keys) byKey.set(key, { finished: finished2, end: ends });
|
|
56
80
|
}
|
|
57
81
|
previousStart = start;
|
|
58
82
|
}
|
|
@@ -60,6 +84,9 @@ function animateSequence(segments, options, animateOne, resolveTargets) {
|
|
|
60
84
|
const finished = Promise.all([group.finished, ...waits]).then(
|
|
61
85
|
() => void 0
|
|
62
86
|
);
|
|
87
|
+
Object.defineProperty(group, "duration", {
|
|
88
|
+
value: Math.max(end, group.duration)
|
|
89
|
+
});
|
|
63
90
|
return Object.assign(group, thenable(finished));
|
|
64
91
|
}
|
|
65
92
|
export {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/sequence/sequence.ts"],
|
|
4
|
-
"sourcesContent": ["import { groupControls, thenable } from \"#/animate/controls.ts\";\nimport type {\n\tAnimationControls,\n\tAnimationOptions,\n\tDOMKeyframes,\n\tElementTarget,\n\tKeyframes,\n} from \"#/animate/types.ts\";\nimport { now } from \"#/frame/frame.ts\";\nimport type { MotionValue } from \"#/value/motion-value.ts\";\n\n/**\n * When a segment starts: seconds from the start of the sequence, `\"+0.1\"` or\n * `\"-0.2\"` from the end of the segment before, `\"<\"` with the segment before,\n * or the name of a label.\n */\nexport type SequenceAt = number | `+${number}` | `-${number}` | \"<\" | string;\n\nexport interface SegmentOptions extends AnimationOptions {\n\treadonly at?: SequenceAt;\n}\n\nexport type Segment =\n\t| readonly [ElementTarget, DOMKeyframes, SegmentOptions?]\n\t// biome-ignore lint/suspicious/noExplicitAny: a sequence mixes values of any type.\n\t| readonly [MotionValue<any>, Keyframes<any>, SegmentOptions?]\n\t| string\n\t| { readonly name: string; readonly at?: SequenceAt };\n\nexport type AnimateOne = (\n\ttarget: unknown,\n\tkeyframes: unknown,\n\toptions: AnimationOptions,\n) => AnimationControls;\n\nexport type ResolveTargets = (target: unknown) => readonly object[];\n\nfunction startOf(\n\tat: SequenceAt | undefined,\n\tcursor: number,\n\tpreviousStart: number,\n\tlabels: ReadonlyMap<string, number>,\n): number {\n\tif (at === undefined) return cursor;\n\tif (typeof at === \"number\") return at;\n\tif (at === \"<\") return previousStart;\n\tif (/^[+-]/.test(at)) return Math.max(0, cursor + Number(at));\n\treturn labels.get(at) ?? cursor;\n}\n\n/**\n * Launches every segment at once, each delayed to its place on the\n * timeline, so the whole sequence shares one clock and can be scrubbed. A\n * segment that animates a property an earlier segment already animates\n * waits for that one to finish, then starts from where it left off.\n */\nexport function animateSequence(\n\tsegments: readonly Segment[],\n\toptions: AnimationOptions,\n\tanimateOne: AnimateOne,\n\tresolveTargets: ResolveTargets,\n): AnimationControls {\n\tconst begun = now();\n\tconst labels = new Map<string, number>();\n\tconst busy = new Map<
|
|
5
|
-
"mappings": "AAAA,SAAS,eAAe,gBAAgB;
|
|
4
|
+
"sourcesContent": ["import { groupControls, thenable } from \"#/animate/controls.ts\";\nimport { planGenerator, totalDuration } from \"#/animate/plan.ts\";\nimport type {\n\tAnimationControls,\n\tAnimationOptions,\n\tDOMKeyframes,\n\tElementTarget,\n\tKeyframes,\n} from \"#/animate/types.ts\";\nimport { now } from \"#/frame/frame.ts\";\nimport type { MotionValue } from \"#/value/motion-value.ts\";\n\n/**\n * When a segment starts: seconds from the start of the sequence, `\"+0.1\"` or\n * `\"-0.2\"` from the end of the segment before, `\"<\"` with the segment before,\n * or the name of a label.\n */\nexport type SequenceAt = number | `+${number}` | `-${number}` | \"<\" | string;\n\nexport interface SegmentOptions extends AnimationOptions {\n\treadonly at?: SequenceAt;\n}\n\nexport type Segment =\n\t| readonly [ElementTarget, DOMKeyframes, SegmentOptions?]\n\t// biome-ignore lint/suspicious/noExplicitAny: a sequence mixes values of any type.\n\t| readonly [MotionValue<any>, Keyframes<any>, SegmentOptions?]\n\t| string\n\t| { readonly name: string; readonly at?: SequenceAt };\n\nexport type AnimateOne = (\n\ttarget: unknown,\n\tkeyframes: unknown,\n\toptions: AnimationOptions,\n) => AnimationControls;\n\nexport type ResolveTargets = (target: unknown) => readonly object[];\n\nfunction startOf(\n\tat: SequenceAt | undefined,\n\tcursor: number,\n\tpreviousStart: number,\n\tlabels: ReadonlyMap<string, number>,\n): number {\n\tif (at === undefined) return cursor;\n\tif (typeof at === \"number\") return at;\n\tif (at === \"<\") return previousStart;\n\tif (/^[+-]/.test(at)) return Math.max(0, cursor + Number(at));\n\treturn labels.get(at) ?? cursor;\n}\n\n/**\n * How long a segment that waits for an earlier one will run once launched.\n * Its first keyframe is only read then, but a tween's length doesn't depend\n * on it, and neither does a default spring's (its rest thresholds scale with\n * the distance).\n */\nfunction plannedDuration(keyframes: unknown, options: AnimationOptions) {\n\tconst channels =\n\t\ttypeof keyframes === \"object\" &&\n\t\tkeyframes !== null &&\n\t\t!Array.isArray(keyframes)\n\t\t\t? Object.values(keyframes)\n\t\t\t: [keyframes];\n\tconst { delay } = options;\n\treturn (\n\t\t(typeof delay === \"number\" ? delay : 0) +\n\t\tMath.max(\n\t\t\t0,\n\t\t\t...channels.map((channel) => {\n\t\t\t\tconst frames = Array.isArray(channel) ? channel : [0, channel];\n\t\t\t\tconst { generator } = planGenerator(\n\t\t\t\t\tframes.map((_frame, index) => index),\n\t\t\t\t\toptions,\n\t\t\t\t);\n\t\t\t\treturn totalDuration(generator.duration, options);\n\t\t\t}),\n\t\t)\n\t);\n}\n\n/**\n * Launches every segment at once, each delayed to its place on the\n * timeline, so the whole sequence shares one clock and can be scrubbed. A\n * segment that animates a property an earlier segment already animates\n * waits for that one to finish, then starts from where it left off.\n */\nexport function animateSequence(\n\tsegments: readonly Segment[],\n\toptions: AnimationOptions,\n\tanimateOne: AnimateOne,\n\tresolveTargets: ResolveTargets,\n): AnimationControls {\n\tconst begun = now();\n\tconst labels = new Map<string, number>();\n\tconst busy = new Map<\n\t\tobject,\n\t\tMap<string, { readonly finished: Promise<void>; readonly end: number }>\n\t>();\n\tconst children: AnimationControls[] = [];\n\tconst waits: Promise<void>[] = [];\n\tlet end = 0;\n\tlet cursor = 0;\n\tlet previousStart = 0;\n\tfor (const segment of segments) {\n\t\tif (typeof segment === \"string\" || \"name\" in segment) {\n\t\t\tconst label = typeof segment === \"string\" ? { name: segment } : segment;\n\t\t\tlabels.set(label.name, startOf(label.at, cursor, previousStart, labels));\n\t\t\tcontinue;\n\t\t}\n\t\tconst [target, keyframes, own = {}] = segment as readonly [\n\t\t\tunknown,\n\t\t\tunknown,\n\t\t\tSegmentOptions?,\n\t\t];\n\t\tconst placed = startOf(own.at, cursor, previousStart, labels);\n\t\tconst keys =\n\t\t\ttypeof keyframes === \"object\" &&\n\t\t\tkeyframes !== null &&\n\t\t\t!Array.isArray(keyframes)\n\t\t\t\t? Object.keys(keyframes)\n\t\t\t\t: [\"value\"];\n\t\tconst targets = resolveTargets(target);\n\t\tconst before = targets.flatMap((one) =>\n\t\t\tkeys.flatMap((key) => busy.get(one)?.get(key) ?? []),\n\t\t);\n\t\tconst start = Math.max(placed, ...before.map((earlier) => earlier.end));\n\t\tconst { delay } = own;\n\t\t/** Launched late, a segment only waits for what is left of its delay. */\n\t\tfunction optionsAt(elapsed: number): AnimationOptions {\n\t\t\tconst offset = start - elapsed;\n\t\t\treturn {\n\t\t\t\t...options,\n\t\t\t\t...own,\n\t\t\t\tdelay:\n\t\t\t\t\ttypeof delay === \"function\"\n\t\t\t\t\t\t? (index, total) => Math.max(0, offset + delay(index, total))\n\t\t\t\t\t\t: Math.max(0, offset + (delay ?? 0)),\n\t\t\t};\n\t\t}\n\t\tconst launch = () =>\n\t\t\tanimateOne(target, keyframes, optionsAt((now() - begun) / 1000));\n\t\tlet finished: Promise<void>;\n\t\tlet ends: number;\n\t\tif (before.length === 0) {\n\t\t\tconst controls = launch();\n\t\t\tchildren.push(controls);\n\t\t\tfinished = controls.finished;\n\t\t\tends = Math.max(start, controls.duration);\n\t\t} else {\n\t\t\tfinished = Promise.all(before.map((earlier) => earlier.finished)).then(\n\t\t\t\t() => launch().finished,\n\t\t\t);\n\t\t\twaits.push(finished);\n\t\t\tends = start + plannedDuration(keyframes, { ...options, ...own });\n\t\t}\n\t\tcursor = ends;\n\t\tend = Math.max(end, ends);\n\t\tfor (const one of targets) {\n\t\t\tconst byKey = busy.get(one) ?? new Map();\n\t\t\tbusy.set(one, byKey);\n\t\t\tfor (const key of keys) byKey.set(key, { finished, end: ends });\n\t\t}\n\t\tpreviousStart = start;\n\t}\n\tconst group = groupControls(children);\n\tconst finished = Promise.all([group.finished, ...waits]).then(\n\t\t() => undefined,\n\t);\n\t// The segments still waiting count too: they end after the ones launched.\n\tObject.defineProperty(group, \"duration\", {\n\t\tvalue: Math.max(end, group.duration),\n\t});\n\treturn Object.assign(group, thenable(finished));\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["optionsAt", "finished"]
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -79,17 +79,25 @@ export function jsAnimation<T>(config: JsAnimationConfig<T>): JsAnimation<T> {
|
|
|
79
79
|
resolve();
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
/** Played backwards, it ends where it started, the way WAAPI does. */
|
|
83
|
+
function finish() {
|
|
84
|
+
time = speed < 0 ? 0 : duration;
|
|
85
|
+
apply(speed < 0 ? (keyframes[0] as T) : last);
|
|
86
|
+
settle("finished");
|
|
87
|
+
config.onComplete?.();
|
|
88
|
+
}
|
|
89
|
+
|
|
82
90
|
function tick(data: FrameData) {
|
|
83
|
-
time = Math.
|
|
91
|
+
time = Math.min(
|
|
92
|
+
duration,
|
|
93
|
+
Math.max(0, time + ((data.timestamp - previous) / 1000) * speed),
|
|
94
|
+
);
|
|
84
95
|
previous = data.timestamp;
|
|
85
|
-
if (time < duration) {
|
|
96
|
+
if (speed < 0 ? time > 0 : time < duration) {
|
|
86
97
|
render();
|
|
87
98
|
return;
|
|
88
99
|
}
|
|
89
|
-
|
|
90
|
-
render();
|
|
91
|
-
settle("finished");
|
|
92
|
-
config.onComplete?.();
|
|
100
|
+
finish();
|
|
93
101
|
}
|
|
94
102
|
|
|
95
103
|
function play() {
|
|
@@ -133,11 +141,7 @@ export function jsAnimation<T>(config: JsAnimationConfig<T>): JsAnimation<T> {
|
|
|
133
141
|
settle("finished");
|
|
134
142
|
},
|
|
135
143
|
complete() {
|
|
136
|
-
if (state
|
|
137
|
-
time = duration;
|
|
138
|
-
render();
|
|
139
|
-
settle("finished");
|
|
140
|
-
config.onComplete?.();
|
|
144
|
+
if (state !== "finished") finish();
|
|
141
145
|
},
|
|
142
146
|
attachTimeline: () => false,
|
|
143
147
|
current() {
|
package/src/animate/style.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { KeyframeValue } from "#/animate/types.ts";
|
|
2
2
|
|
|
3
|
-
type TransformProperty = "translate" | "scale" | "rotate";
|
|
3
|
+
type TransformProperty = "translate" | "scale" | "rotate" | "transform";
|
|
4
4
|
|
|
5
5
|
interface TransformChannel {
|
|
6
6
|
readonly property: TransformProperty;
|
|
@@ -10,6 +10,13 @@ interface TransformChannel {
|
|
|
10
10
|
readonly alone: (value: string) => string;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* The transforms with no CSS property of their own (`rotate` takes one axis,
|
|
15
|
+
* and skew has none): they go through `transform`, in this order, after
|
|
16
|
+
* `translate`, `rotate` and `scale`.
|
|
17
|
+
*/
|
|
18
|
+
const LISTED = ["rotateX", "rotateY", "skew", "skewX", "skewY"];
|
|
19
|
+
|
|
13
20
|
/**
|
|
14
21
|
* The independent transforms. Each animates its CSS property alone with
|
|
15
22
|
* `composite: "add"`, so `x` and `y` run as two compositor animations that
|
|
@@ -38,6 +45,17 @@ export const TRANSFORMS: Readonly<Record<string, TransformChannel>> = {
|
|
|
38
45
|
identity: 0,
|
|
39
46
|
alone: (v) => `${v}`,
|
|
40
47
|
},
|
|
48
|
+
...Object.fromEntries(
|
|
49
|
+
LISTED.map((key) => [
|
|
50
|
+
key,
|
|
51
|
+
{
|
|
52
|
+
property: "transform",
|
|
53
|
+
unit: "deg",
|
|
54
|
+
identity: 0,
|
|
55
|
+
alone: (v: string) => `${key}(${v})`,
|
|
56
|
+
},
|
|
57
|
+
]),
|
|
58
|
+
),
|
|
41
59
|
};
|
|
42
60
|
|
|
43
61
|
const UNITLESS = new Set([
|
|
@@ -102,6 +120,15 @@ export function writeTransforms(element: Element) {
|
|
|
102
120
|
const uniform = Number.parseFloat(at("scale"));
|
|
103
121
|
style.scale = `${uniform * Number.parseFloat(at("scaleX"))} ${uniform * Number.parseFloat(at("scaleY"))}`;
|
|
104
122
|
style.rotate = at("rotate");
|
|
123
|
+
// `transform` stays the page's on an element that never animates these.
|
|
124
|
+
if (
|
|
125
|
+
LISTED.some(
|
|
126
|
+
(key) =>
|
|
127
|
+
committed.get(element)?.has(key) || animating.get(element)?.has(key),
|
|
128
|
+
)
|
|
129
|
+
) {
|
|
130
|
+
style.transform = LISTED.map((key) => `${key}(${at(key)})`).join(" ");
|
|
131
|
+
}
|
|
105
132
|
}
|
|
106
133
|
|
|
107
134
|
export function beginTransform(element: Element, key: string) {
|
package/src/animate/types.ts
CHANGED
|
@@ -59,8 +59,8 @@ export type Keyframes<T = KeyframeValue> = T | readonly T[];
|
|
|
59
59
|
|
|
60
60
|
/**
|
|
61
61
|
* DOM keyframes: any CSS property (camelCase), a CSS variable (`--name`),
|
|
62
|
-
* the
|
|
63
|
-
* `rotate`), or an SVG attribute. A single value animates from the current
|
|
62
|
+
* the transforms (`x`, `y`, `z`, `scale`, `scaleX`, `scaleY`,
|
|
63
|
+
* `rotate`, `rotateX`, `rotateY`, `skew`, `skewX`, `skewY`), or an SVG attribute. A single value animates from the current
|
|
64
64
|
* one; an array lists every keyframe. Numbers take the property's unit (px,
|
|
65
65
|
* deg) when it has one.
|
|
66
66
|
*/
|
package/src/animate/waapi.ts
CHANGED
|
@@ -131,6 +131,11 @@ export function waapiAnimation(
|
|
|
131
131
|
: keyframes.length - 1
|
|
132
132
|
] as KeyframeValue;
|
|
133
133
|
|
|
134
|
+
/** Played backwards, it ends on its first keyframe. */
|
|
135
|
+
function end(): KeyframeValue {
|
|
136
|
+
return animation.playbackRate < 0 ? (keyframes[0] as KeyframeValue) : last;
|
|
137
|
+
}
|
|
138
|
+
|
|
134
139
|
let settled = false;
|
|
135
140
|
/** What stayed on screen when it settled: where a following animation starts. */
|
|
136
141
|
let held: KeyframeValue = keyframes[0] as KeyframeValue;
|
|
@@ -170,7 +175,7 @@ export function waapiAnimation(
|
|
|
170
175
|
animation.finished.then(
|
|
171
176
|
() => {
|
|
172
177
|
if (settled) return;
|
|
173
|
-
settle(
|
|
178
|
+
settle(end());
|
|
174
179
|
config.onComplete?.();
|
|
175
180
|
},
|
|
176
181
|
() => settle(undefined),
|
|
@@ -201,7 +206,7 @@ export function waapiAnimation(
|
|
|
201
206
|
// The browser refuses to finish an endless animation: settle on its last frame instead.
|
|
202
207
|
complete: () =>
|
|
203
208
|
iterations === Number.POSITIVE_INFINITY
|
|
204
|
-
? settle(
|
|
209
|
+
? settle(end())
|
|
205
210
|
: animation.finish(),
|
|
206
211
|
attachTimeline(timeline) {
|
|
207
212
|
animation.timeline = timeline;
|
package/src/generators/spring.ts
CHANGED
|
@@ -10,9 +10,8 @@ export interface SpringPhysics {
|
|
|
10
10
|
readonly mass: number;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
/** A spring that never settles would animate forever: stop
|
|
13
|
+
/** A spring that never settles would animate forever: stop it after this. */
|
|
14
14
|
const MAX_DURATION = 20;
|
|
15
|
-
const SETTLE_STEP = 0.005;
|
|
16
15
|
|
|
17
16
|
/**
|
|
18
17
|
* Duration and bounce to physics, SwiftUI's mapping: the undamped period is
|
|
@@ -38,46 +37,115 @@ export function springPhysics(options: SpringOptions = {}): SpringPhysics {
|
|
|
38
37
|
|
|
39
38
|
type Displacement = (t: number) => readonly [number, number];
|
|
40
39
|
|
|
41
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* When `|p + q·t|·e^(−a·t)`, a bound on a distance or a speed, drops under
|
|
42
|
+
* `threshold` for good. Newton on its logarithm, which is concave past the
|
|
43
|
+
* peak: every step lands at or past the root, so it errs late, never early.
|
|
44
|
+
*/
|
|
45
|
+
function settleTime(p: number, q: number, a: number, threshold: number) {
|
|
46
|
+
if (q < 0) {
|
|
47
|
+
p = -p;
|
|
48
|
+
q = -q;
|
|
49
|
+
}
|
|
50
|
+
const peak = 1 / a - p / q;
|
|
51
|
+
function excess(t: number): number {
|
|
52
|
+
return Math.log(p + q * t) - a * t - Math.log(threshold);
|
|
53
|
+
}
|
|
54
|
+
// No hump above the threshold: only the stretch before it crosses zero, if it does.
|
|
55
|
+
if (q === 0 || excess(Math.max(0, peak)) <= 0) {
|
|
56
|
+
return Math.max(0, Math.log(Math.abs(p) / threshold) / a);
|
|
57
|
+
}
|
|
58
|
+
let t = Math.max(0, peak) + 1 / a;
|
|
59
|
+
for (let step = 0; step < 32; step += 1) {
|
|
60
|
+
const next = t - excess(t) / (q / (p + q * t) - a);
|
|
61
|
+
if (Math.abs(next - t) < 1e-6) return next;
|
|
62
|
+
t = next;
|
|
63
|
+
}
|
|
64
|
+
return t;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The damped oscillator's closed form: displacement from the target and its
|
|
69
|
+
* velocity at `t`, and when both stay under the rest thresholds.
|
|
70
|
+
*/
|
|
42
71
|
function oscillator(
|
|
43
72
|
physics: SpringPhysics,
|
|
44
73
|
x0: number,
|
|
45
74
|
v0: number,
|
|
46
|
-
|
|
75
|
+
restDelta: number,
|
|
76
|
+
restSpeed: number,
|
|
77
|
+
): readonly [Displacement, number] {
|
|
47
78
|
const omega = Math.sqrt(physics.stiffness / physics.mass);
|
|
48
79
|
const zeta =
|
|
49
80
|
physics.damping / (2 * Math.sqrt(physics.stiffness * physics.mass));
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
81
|
+
const critical = Math.abs(zeta - 1) < 1e-6;
|
|
82
|
+
const slow = omega * (zeta < 1 ? zeta : zeta - Math.sqrt(zeta * zeta - 1));
|
|
83
|
+
const b = v0 + zeta * omega * x0;
|
|
84
|
+
/**
|
|
85
|
+
* Exact at critical damping. Elsewhere a bound, from sin(ωt) ≤ ωt and
|
|
86
|
+
* sinh(γt)/γ ≤ t·e^(γt), that stays tight near critical damping, where
|
|
87
|
+
* the amplitudes blow up while the motion doesn't.
|
|
88
|
+
*/
|
|
89
|
+
function near(p: number, q: number, threshold: number): number {
|
|
90
|
+
return critical
|
|
91
|
+
? settleTime(p, q, slow, threshold)
|
|
92
|
+
: settleTime(Math.abs(p), Math.abs(q), slow, threshold);
|
|
93
|
+
}
|
|
94
|
+
const settle = Math.max(
|
|
95
|
+
near(x0, b, restDelta),
|
|
96
|
+
near(v0, -(zeta * omega * v0 + omega * omega * x0), restSpeed),
|
|
97
|
+
);
|
|
98
|
+
if (critical) {
|
|
99
|
+
return [
|
|
100
|
+
(t) => {
|
|
101
|
+
const decay = Math.exp(-omega * t);
|
|
102
|
+
return [decay * (x0 + b * t), decay * (b - omega * (x0 + b * t))];
|
|
103
|
+
},
|
|
104
|
+
settle,
|
|
105
|
+
];
|
|
56
106
|
}
|
|
57
107
|
if (zeta < 1) {
|
|
58
108
|
const omegaD = omega * Math.sqrt(1 - zeta * zeta);
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
decay *
|
|
68
|
-
|
|
69
|
-
|
|
109
|
+
const sine = b / omegaD;
|
|
110
|
+
const amplitude = Math.hypot(x0, sine);
|
|
111
|
+
return [
|
|
112
|
+
(t) => {
|
|
113
|
+
const decay = Math.exp(-slow * t);
|
|
114
|
+
const cos = Math.cos(omegaD * t);
|
|
115
|
+
const sin = Math.sin(omegaD * t);
|
|
116
|
+
const x = x0 * cos + sine * sin;
|
|
117
|
+
return [decay * x, decay * (-slow * x + b * cos - omegaD * x0 * sin)];
|
|
118
|
+
},
|
|
119
|
+
Math.min(
|
|
120
|
+
settle,
|
|
121
|
+
Math.max(
|
|
122
|
+
settleTime(amplitude, 0, slow, restDelta),
|
|
123
|
+
settleTime(amplitude * omega, 0, slow, restSpeed),
|
|
124
|
+
),
|
|
125
|
+
),
|
|
126
|
+
];
|
|
70
127
|
}
|
|
71
|
-
const
|
|
72
|
-
const
|
|
73
|
-
const r2 = -zeta * omega - root;
|
|
128
|
+
const r1 = -slow;
|
|
129
|
+
const r2 = -2 * zeta * omega - r1;
|
|
74
130
|
const c2 = (v0 - r1 * x0) / (r2 - r1);
|
|
75
131
|
const c1 = x0 - c2;
|
|
76
|
-
return
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
132
|
+
return [
|
|
133
|
+
(t) => {
|
|
134
|
+
const e1 = c1 * Math.exp(r1 * t);
|
|
135
|
+
const e2 = c2 * Math.exp(r2 * t);
|
|
136
|
+
return [e1 + e2, r1 * e1 + r2 * e2];
|
|
137
|
+
},
|
|
138
|
+
// Each mode on its own, with half of each threshold.
|
|
139
|
+
Math.min(
|
|
140
|
+
settle,
|
|
141
|
+
Math.max(
|
|
142
|
+
settleTime(c1, 0, -r1, restDelta / 2),
|
|
143
|
+
settleTime(c2, 0, -r2, restDelta / 2),
|
|
144
|
+
settleTime(r1 * c1, 0, -r1, restSpeed / 2),
|
|
145
|
+
settleTime(r2 * c2, 0, -r2, restSpeed / 2),
|
|
146
|
+
),
|
|
147
|
+
),
|
|
148
|
+
];
|
|
81
149
|
}
|
|
82
150
|
|
|
83
151
|
/** Rest thresholds scale with the motion, so 0→1 and 0→1000 both settle to the eye. */
|
|
@@ -91,18 +159,16 @@ export function spring(
|
|
|
91
159
|
): Generator {
|
|
92
160
|
const { from, to } = options;
|
|
93
161
|
const velocity = options.velocity ?? 0;
|
|
94
|
-
const displacement = oscillator(springPhysics(options), from - to, velocity);
|
|
95
162
|
const { restDelta, restSpeed } = restThresholds(options, Math.abs(to - from));
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
163
|
+
const [displacement, settle] = oscillator(
|
|
164
|
+
springPhysics(options),
|
|
165
|
+
from - to,
|
|
166
|
+
velocity,
|
|
167
|
+
restDelta,
|
|
168
|
+
restSpeed,
|
|
169
|
+
);
|
|
170
|
+
// An undamped spring never settles, and degenerate physics yields NaN.
|
|
171
|
+
const duration = settle < MAX_DURATION ? settle : MAX_DURATION;
|
|
106
172
|
|
|
107
173
|
return {
|
|
108
174
|
duration,
|
|
@@ -117,7 +117,36 @@ function parseHex(hex: string): Rgba | undefined {
|
|
|
117
117
|
};
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
|
|
120
|
+
const names = new Map<string, Rgba | undefined>();
|
|
121
|
+
let probe: CanvasRenderingContext2D | null | undefined;
|
|
122
|
+
const SENTINEL = "#010203";
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* A colour name (`red`, `rebeccapurple`) as the browser spells it out: a
|
|
126
|
+
* canvas drops what isn't a colour (`auto`, `none`) and hands back hex for
|
|
127
|
+
* what is. The 148 names would weigh more than this; without a browser a
|
|
128
|
+
* name stays unknown and the page mixes it.
|
|
129
|
+
*/
|
|
130
|
+
function parseName(name: string): Rgba | undefined {
|
|
131
|
+
if (name === "currentcolor" || !/^[a-z]+$/.test(name)) return undefined;
|
|
132
|
+
if (!names.has(name)) {
|
|
133
|
+
probe ??=
|
|
134
|
+
typeof document === "undefined"
|
|
135
|
+
? null
|
|
136
|
+
: document.createElement("canvas").getContext("2d");
|
|
137
|
+
let resolved: Rgba | undefined;
|
|
138
|
+
if (probe) {
|
|
139
|
+
probe.fillStyle = SENTINEL;
|
|
140
|
+
probe.fillStyle = name;
|
|
141
|
+
const spelled = String(probe.fillStyle);
|
|
142
|
+
resolved = spelled === SENTINEL ? undefined : parseColor(spelled);
|
|
143
|
+
}
|
|
144
|
+
names.set(name, resolved);
|
|
145
|
+
}
|
|
146
|
+
return names.get(name);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** A colour written out in full, or a name the browser knows; `undefined` for what only the page can resolve (`var()`, `currentColor`). */
|
|
121
150
|
export function parseColor(text: string): Rgba | undefined {
|
|
122
151
|
const trimmed = text.trim();
|
|
123
152
|
if (trimmed.toLowerCase() === "transparent") {
|
|
@@ -128,7 +157,7 @@ export function parseColor(text: string): Rgba | undefined {
|
|
|
128
157
|
}
|
|
129
158
|
const match = FUNCTION.exec(trimmed);
|
|
130
159
|
if (match === null) {
|
|
131
|
-
return
|
|
160
|
+
return parseName(trimmed.toLowerCase());
|
|
132
161
|
}
|
|
133
162
|
const name = (match[1] as string).toLowerCase().replace(/a$/, "");
|
|
134
163
|
const args = (match[2] as string).split(/[\s,/]+/).filter(Boolean);
|
package/src/interpolate/mix.ts
CHANGED
|
@@ -18,8 +18,9 @@ function bothStrings(from: unknown, to: unknown): [string, string] | undefined {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
|
-
* A colour pair needs one side
|
|
22
|
-
*
|
|
21
|
+
* A colour pair needs one side spelled out (a name counts where a browser can
|
|
22
|
+
* spell it), or both to be variables: two other bare words are as likely
|
|
23
|
+
* `auto` and `none` as colours.
|
|
23
24
|
*/
|
|
24
25
|
function colors(from: string, to: string): boolean {
|
|
25
26
|
const resolved =
|