@voila.dev/chart 5.0.113 → 5.0.117
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/canvas/paint-scene.d.ts.map +1 -1
- package/dist/canvas/paint-scene.js +3 -1
- package/dist/canvas/paint-scene.js.map +2 -2
- package/dist/contour/density.d.ts.map +1 -1
- package/dist/contour/density.js +2 -0
- package/dist/contour/density.js.map +2 -2
- package/dist/core/marks/area.d.ts.map +1 -1
- package/dist/core/marks/area.js +27 -11
- package/dist/core/marks/area.js.map +2 -2
- package/dist/core/marks/bar.d.ts.map +1 -1
- package/dist/core/marks/bar.js +2 -0
- package/dist/core/marks/bar.js.map +2 -2
- package/dist/core/marks/line.d.ts.map +1 -1
- package/dist/core/marks/line.js +13 -4
- package/dist/core/marks/line.js.map +2 -2
- package/dist/core/marks/polar.d.ts.map +1 -1
- package/dist/core/marks/polar.js +22 -5
- package/dist/core/marks/polar.js.map +2 -2
- package/dist/core/marks/shared.d.ts +8 -1
- package/dist/core/marks/shared.d.ts.map +1 -1
- package/dist/core/marks/shared.js.map +2 -2
- package/dist/core/marks/stats/difference.d.ts.map +1 -1
- package/dist/core/marks/stats/difference.js +20 -7
- package/dist/core/marks/stats/difference.js.map +2 -2
- package/dist/core/motion/arc-motion.d.ts +9 -0
- package/dist/core/motion/arc-motion.d.ts.map +1 -0
- package/dist/core/motion/arc-motion.js +67 -0
- package/dist/core/motion/arc-motion.js.map +7 -0
- package/dist/core/motion/enter-exit.d.ts +6 -0
- package/dist/core/motion/enter-exit.d.ts.map +1 -0
- package/dist/core/motion/enter-exit.js +20 -0
- package/dist/core/motion/enter-exit.js.map +7 -0
- package/dist/core/motion/leaving.d.ts +12 -0
- package/dist/core/motion/leaving.d.ts.map +1 -0
- package/dist/core/motion/leaving.js +33 -0
- package/dist/core/motion/leaving.js.map +7 -0
- package/dist/core/motion/motion-store.d.ts +22 -0
- package/dist/core/motion/motion-store.d.ts.map +1 -0
- package/dist/core/motion/motion-store.js +325 -0
- package/dist/core/motion/motion-store.js.map +7 -0
- package/dist/core/motion/points-motion.d.ts +6 -0
- package/dist/core/motion/points-motion.d.ts.map +1 -0
- package/dist/core/motion/points-motion.js +140 -0
- package/dist/core/motion/points-motion.js.map +7 -0
- package/dist/core/motion/strings.d.ts +18 -0
- package/dist/core/motion/strings.d.ts.map +1 -0
- package/dist/core/motion/strings.js +34 -0
- package/dist/core/motion/strings.js.map +7 -0
- package/dist/core/motion/timing.d.ts +30 -0
- package/dist/core/motion/timing.d.ts.map +1 -0
- package/dist/core/motion/timing.js +25 -0
- package/dist/core/motion/timing.js.map +7 -0
- package/dist/core/motion/tween.d.ts +2 -0
- package/dist/core/motion/tween.d.ts.map +1 -1
- package/dist/core/motion/tween.js +1 -0
- package/dist/core/motion/tween.js.map +2 -2
- package/dist/core/types.d.ts +85 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/geo/geo.d.ts.map +1 -1
- package/dist/geo/geo.js +2 -0
- package/dist/geo/geo.js.map +2 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/index.js.map +2 -2
- package/dist/react/chart.d.ts +7 -2
- package/dist/react/chart.d.ts.map +1 -1
- package/dist/react/chart.js +12 -7
- package/dist/react/chart.js.map +2 -2
- package/dist/react/svg-scene.d.ts.map +1 -1
- package/dist/react/svg-scene.js +3 -1
- package/dist/react/svg-scene.js.map +2 -2
- package/dist/react/use-animated-scene.d.ts +6 -5
- package/dist/react/use-animated-scene.d.ts.map +1 -1
- package/dist/react/use-animated-scene.js +36 -24
- package/dist/react/use-animated-scene.js.map +2 -2
- package/dist/voronoi/voronoi.d.ts.map +1 -1
- package/dist/voronoi/voronoi.js +2 -0
- package/dist/voronoi/voronoi.js.map +2 -2
- package/package.json +2 -1
- package/src/canvas/paint-scene.ts +5 -1
- package/src/contour/density.ts +2 -0
- package/src/core/marks/area.ts +28 -13
- package/src/core/marks/bar.ts +2 -0
- package/src/core/marks/line.ts +15 -8
- package/src/core/marks/polar.ts +22 -5
- package/src/core/marks/shared.ts +8 -0
- package/src/core/marks/stats/difference.ts +28 -8
- package/src/core/motion/arc-motion.ts +113 -0
- package/src/core/motion/enter-exit.ts +44 -0
- package/src/core/motion/leaving.ts +55 -0
- package/src/core/motion/motion-store.ts +449 -0
- package/src/core/motion/points-motion.ts +190 -0
- package/src/core/motion/strings.ts +68 -0
- package/src/core/motion/timing.ts +63 -0
- package/src/core/motion/tween.ts +1 -1
- package/src/core/types.ts +98 -0
- package/src/geo/geo.ts +2 -0
- package/src/index.ts +9 -0
- package/src/react/chart.tsx +21 -9
- package/src/react/svg-scene.tsx +5 -1
- package/src/react/use-animated-scene.ts +49 -33
- package/src/voronoi/voronoi.ts +2 -0
package/src/core/marks/polar.ts
CHANGED
|
@@ -12,6 +12,8 @@ import {
|
|
|
12
12
|
seriesResolver,
|
|
13
13
|
tips,
|
|
14
14
|
} from "#/core/marks/shared.ts";
|
|
15
|
+
import { ARC_MOTION } from "#/core/motion/arc-motion.ts";
|
|
16
|
+
import { POINTS_MOTION } from "#/core/motion/points-motion.ts";
|
|
15
17
|
import {
|
|
16
18
|
arcPath,
|
|
17
19
|
type ChartXY,
|
|
@@ -181,6 +183,9 @@ export function arc<TDatum>(
|
|
|
181
183
|
series: series.key,
|
|
182
184
|
role: "mark",
|
|
183
185
|
d: arcPath(shape),
|
|
186
|
+
geometry: { kind: "arc", ...shape },
|
|
187
|
+
motion: ARC_MOTION,
|
|
188
|
+
enter: options.enter ?? "grow",
|
|
184
189
|
paint: paint({
|
|
185
190
|
fill: series.color,
|
|
186
191
|
stroke: options.stroke ?? context.theme.background,
|
|
@@ -306,6 +311,9 @@ export function radialBar<TDatum>(
|
|
|
306
311
|
series: series.key,
|
|
307
312
|
role: "mark",
|
|
308
313
|
d: arcPath(shape),
|
|
314
|
+
geometry: { kind: "arc", ...shape },
|
|
315
|
+
motion: ARC_MOTION,
|
|
316
|
+
enter: options.enter ?? "grow",
|
|
309
317
|
paint: paint({ fill: series.color, opacity: options.opacity }),
|
|
310
318
|
});
|
|
311
319
|
if (!tips(options.tip, data[index], index)) {
|
|
@@ -532,16 +540,25 @@ export function radar<TDatum>(
|
|
|
532
540
|
}
|
|
533
541
|
}
|
|
534
542
|
for (const [key, polygon] of polygons) {
|
|
543
|
+
const geometry = {
|
|
544
|
+
kind: "points" as const,
|
|
545
|
+
shape: "polygon" as const,
|
|
546
|
+
runs: [
|
|
547
|
+
polygon.vertices.map((vertex, spoke) => ({
|
|
548
|
+
key: categoryKey(axes[spoke] as ChartValue),
|
|
549
|
+
...(vertex ?? { x: frame.cx, y: frame.cy }),
|
|
550
|
+
})),
|
|
551
|
+
],
|
|
552
|
+
};
|
|
535
553
|
nodes.push({
|
|
536
554
|
kind: "path",
|
|
537
555
|
key: `${id}:shape:${key}`,
|
|
538
556
|
series: key,
|
|
539
557
|
role: "mark",
|
|
540
|
-
d: polygonPath(
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
),
|
|
558
|
+
d: polygonPath(geometry.runs[0]),
|
|
559
|
+
geometry,
|
|
560
|
+
motion: POINTS_MOTION,
|
|
561
|
+
enter: options.enter,
|
|
545
562
|
paint: paint({
|
|
546
563
|
fill: polygon.color,
|
|
547
564
|
fillOpacity: options.fillOpacity ?? 0.2,
|
package/src/core/marks/shared.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { categoryKey } from "#/core/scales/discrete.ts";
|
|
|
8
8
|
import type {
|
|
9
9
|
ChartAccessor,
|
|
10
10
|
ChartChannel,
|
|
11
|
+
ChartEnter,
|
|
11
12
|
ChartMarkContext,
|
|
12
13
|
ChartPaint,
|
|
13
14
|
ChartPositionScale,
|
|
@@ -29,6 +30,13 @@ export interface ChartMarkOptions<TDatum> {
|
|
|
29
30
|
*/
|
|
30
31
|
readonly tip?: boolean | ((datum: TDatum, index: number) => boolean);
|
|
31
32
|
readonly opacity?: number;
|
|
33
|
+
/**
|
|
34
|
+
* How the mark appears when an update adds it: "grow" from its baseline
|
|
35
|
+
* or neighbour (the default for bars, areas and slices), "fade" (the
|
|
36
|
+
* default otherwise), "none". A line can also "draw": trace itself in on
|
|
37
|
+
* the first render.
|
|
38
|
+
*/
|
|
39
|
+
readonly enter?: ChartEnter;
|
|
32
40
|
}
|
|
33
41
|
|
|
34
42
|
export function markId(
|
|
@@ -5,12 +5,16 @@ import {
|
|
|
5
5
|
readChannel,
|
|
6
6
|
} from "#/core/channel.ts";
|
|
7
7
|
import { markId, paint, valueOn } from "#/core/marks/shared.ts";
|
|
8
|
-
import {
|
|
8
|
+
import { POINTS_MOTION, pointsPath } from "#/core/motion/points-motion.ts";
|
|
9
|
+
import { areaPath, type ChartXY } from "#/core/paths.ts";
|
|
10
|
+
import { categoryKey } from "#/core/scales/discrete.ts";
|
|
9
11
|
import type {
|
|
10
12
|
ChartAccessor,
|
|
11
13
|
ChartMark,
|
|
12
14
|
ChartPoint,
|
|
13
15
|
ChartValue,
|
|
16
|
+
GeometryMotion,
|
|
17
|
+
SceneGeometry,
|
|
14
18
|
SceneNode,
|
|
15
19
|
} from "#/core/types.ts";
|
|
16
20
|
|
|
@@ -34,6 +38,25 @@ interface Sample {
|
|
|
34
38
|
readonly bottom: number;
|
|
35
39
|
}
|
|
36
40
|
|
|
41
|
+
/** One of the two lines, keyed by x so an update slides it like any line. */
|
|
42
|
+
function keyedLine(
|
|
43
|
+
samples: readonly (Sample & { readonly key: string })[],
|
|
44
|
+
edge: "top" | "bottom",
|
|
45
|
+
): { d: string; geometry: SceneGeometry; motion: GeometryMotion } {
|
|
46
|
+
const geometry: SceneGeometry = {
|
|
47
|
+
kind: "points",
|
|
48
|
+
shape: "line",
|
|
49
|
+
runs: [
|
|
50
|
+
samples.map((sample) => ({
|
|
51
|
+
key: sample.key,
|
|
52
|
+
x: sample.x,
|
|
53
|
+
y: sample[edge],
|
|
54
|
+
})),
|
|
55
|
+
],
|
|
56
|
+
};
|
|
57
|
+
return { d: pointsPath(geometry), geometry, motion: POINTS_MOTION };
|
|
58
|
+
}
|
|
59
|
+
|
|
37
60
|
/** Splits the band between two lines into runs of one sign, cut where they cross. */
|
|
38
61
|
function signRuns(
|
|
39
62
|
samples: ReadonlyArray<Sample>,
|
|
@@ -99,7 +122,7 @@ export function differenceY<TDatum>(
|
|
|
99
122
|
}
|
|
100
123
|
const positive = options.positiveFill ?? context.paletteColor(1);
|
|
101
124
|
const negative = options.negativeFill ?? context.paletteColor(0);
|
|
102
|
-
const samples: Sample
|
|
125
|
+
const samples: Array<Sample & { readonly key: string }> = [];
|
|
103
126
|
const points: ChartPoint[] = [];
|
|
104
127
|
for (const [index, raw] of xs.entries()) {
|
|
105
128
|
const x = valueOn(xScale, raw);
|
|
@@ -107,6 +130,7 @@ export function differenceY<TDatum>(
|
|
|
107
130
|
const b = second[index];
|
|
108
131
|
if (x === undefined || a === undefined || b === undefined) continue;
|
|
109
132
|
const sample = {
|
|
133
|
+
key: categoryKey(x),
|
|
110
134
|
x: xScale.center(x),
|
|
111
135
|
top: yScale.map(a),
|
|
112
136
|
bottom: yScale.map(b),
|
|
@@ -157,9 +181,7 @@ export function differenceY<TDatum>(
|
|
|
157
181
|
key: `${id}:line:y1`,
|
|
158
182
|
series: `${id}:y1`,
|
|
159
183
|
role: "mark",
|
|
160
|
-
|
|
161
|
-
samples.map((sample) => ({ x: sample.x, y: sample.top })),
|
|
162
|
-
),
|
|
184
|
+
...keyedLine(samples, "top"),
|
|
163
185
|
paint: {
|
|
164
186
|
fill: "none",
|
|
165
187
|
stroke: context.theme.foreground,
|
|
@@ -171,9 +193,7 @@ export function differenceY<TDatum>(
|
|
|
171
193
|
key: `${id}:line:y2`,
|
|
172
194
|
series: `${id}:y2`,
|
|
173
195
|
role: "mark",
|
|
174
|
-
|
|
175
|
-
samples.map((sample) => ({ x: sample.x, y: sample.bottom })),
|
|
176
|
-
),
|
|
196
|
+
...keyedLine(samples, "bottom"),
|
|
177
197
|
paint: {
|
|
178
198
|
fill: "none",
|
|
179
199
|
stroke: context.theme.muted,
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { arcPath } from "#/core/paths.ts";
|
|
2
|
+
import type {
|
|
3
|
+
GeometryMotion,
|
|
4
|
+
GeometryPlan,
|
|
5
|
+
SceneGeometry,
|
|
6
|
+
SceneNode,
|
|
7
|
+
ScenePath,
|
|
8
|
+
} from "#/core/types.ts";
|
|
9
|
+
|
|
10
|
+
type Arc = Extract<SceneGeometry, { kind: "arc" }>;
|
|
11
|
+
|
|
12
|
+
const ARC_FIELDS = [
|
|
13
|
+
"cx",
|
|
14
|
+
"cy",
|
|
15
|
+
"innerRadius",
|
|
16
|
+
"outerRadius",
|
|
17
|
+
"startAngle",
|
|
18
|
+
"endAngle",
|
|
19
|
+
] as const;
|
|
20
|
+
|
|
21
|
+
function arcOf(node: SceneNode | undefined): Arc | undefined {
|
|
22
|
+
return node?.kind === "path" && node.geometry?.kind === "arc"
|
|
23
|
+
? node.geometry
|
|
24
|
+
: undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Slices of one ring tile it: those are the neighbours a slice opens from and closes onto. */
|
|
28
|
+
function sameRing(a: Arc, b: Arc): boolean {
|
|
29
|
+
return (
|
|
30
|
+
a.cx === b.cx &&
|
|
31
|
+
a.cy === b.cy &&
|
|
32
|
+
a.innerRadius === b.innerRadius &&
|
|
33
|
+
a.outerRadius === b.outerRadius
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function withAngle(node: ScenePath, arc: Arc, angle: number): ScenePath {
|
|
38
|
+
return { ...node, geometry: { ...arc, startAngle: angle, endAngle: angle } };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The boundary angle next to `node` among the slices of its ring in `order`,
|
|
43
|
+
* read from `known`: the end of the nearest one before it, or the start of the
|
|
44
|
+
* nearest one after it, whichever side `afterFirst` looks at first.
|
|
45
|
+
*/
|
|
46
|
+
function boundary(
|
|
47
|
+
node: SceneNode,
|
|
48
|
+
order: readonly SceneNode[],
|
|
49
|
+
known: ReadonlyMap<string, SceneNode>,
|
|
50
|
+
arc: Arc,
|
|
51
|
+
afterFirst: boolean,
|
|
52
|
+
): number {
|
|
53
|
+
const index = order.findIndex((other) => other.key === node.key);
|
|
54
|
+
function scan(step: -1 | 1): number | undefined {
|
|
55
|
+
for (let at = index + step; at >= 0 && at < order.length; at += step) {
|
|
56
|
+
const other = arcOf(known.get((order[at] as SceneNode).key));
|
|
57
|
+
if (other && sameRing(other, arc)) {
|
|
58
|
+
return step === 1 ? other.startAngle : other.endAngle;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
const [first, second] = afterFirst ? ([1, -1] as const) : ([-1, 1] as const);
|
|
64
|
+
return scan(first) ?? scan(second) ?? arc.startAngle;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function arcPlan(from: Arc, to: Arc): GeometryPlan {
|
|
68
|
+
function record(arc: Arc) {
|
|
69
|
+
return new Map<string, number>(
|
|
70
|
+
ARC_FIELDS.map((field) => [field, arc[field]]),
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
starts: record(from),
|
|
75
|
+
targets: record(to),
|
|
76
|
+
build(value) {
|
|
77
|
+
const arc = { kind: "arc" } as Record<string, unknown>;
|
|
78
|
+
for (const field of ARC_FIELDS) arc[field] = value(field);
|
|
79
|
+
return arc as Arc;
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Slices and radial bars: their angles and radii move, and the arc is
|
|
86
|
+
* rebuilt each frame, so its large-arc flag is never wrong. A new slice opens
|
|
87
|
+
* from the boundary where it will sit; a removed one closes onto the slice
|
|
88
|
+
* after it.
|
|
89
|
+
*/
|
|
90
|
+
export const ARC_MOTION: GeometryMotion = {
|
|
91
|
+
plan: (from, to) =>
|
|
92
|
+
from.kind === "arc" && to.kind === "arc" ? arcPlan(from, to) : undefined,
|
|
93
|
+
draw: (geometry) =>
|
|
94
|
+
geometry.kind === "arc"
|
|
95
|
+
? arcPath({
|
|
96
|
+
...geometry,
|
|
97
|
+
endAngle: Math.max(geometry.startAngle, geometry.endAngle),
|
|
98
|
+
})
|
|
99
|
+
: "",
|
|
100
|
+
enter(node, siblings, before) {
|
|
101
|
+
const arc = arcOf(node);
|
|
102
|
+
return node.enter === "grow" && arc
|
|
103
|
+
? withAngle(node, arc, boundary(node, siblings, before, arc, false))
|
|
104
|
+
: undefined;
|
|
105
|
+
},
|
|
106
|
+
exit(node, siblings, after) {
|
|
107
|
+
const arc = arcOf(node);
|
|
108
|
+
// A slice closes onto the one after it, so it takes that one's new start.
|
|
109
|
+
return node.enter === "grow" && arc
|
|
110
|
+
? withAngle(node, arc, boundary(node, siblings, after, arc, true))
|
|
111
|
+
: undefined;
|
|
112
|
+
},
|
|
113
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { SceneNode, SceneRect } from "#/core/types.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The shape a data mark grows from when an update adds it, and collapses to
|
|
5
|
+
* when an update removes it. Always a node of the same kind, so the store
|
|
6
|
+
* springs it like any other change. Undefined when the mark has no such
|
|
7
|
+
* shape: it fades in, and is gone at once when it leaves. A bar has its
|
|
8
|
+
* baseline; a path asks the motion its mark brought (a slice opens from its
|
|
9
|
+
* neighbour, an area grows from its lower edge).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
function collapsedBar(node: SceneRect): SceneRect | undefined {
|
|
13
|
+
const { baseline } = node;
|
|
14
|
+
if (!baseline) return undefined;
|
|
15
|
+
return baseline.axis === "y"
|
|
16
|
+
? { ...node, y: baseline.at, height: 0 }
|
|
17
|
+
: { ...node, x: baseline.at, width: 0 };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Where a node an update adds starts from. `siblings` is the list it sits in, `before` the old scene. */
|
|
21
|
+
export function enterFrom(
|
|
22
|
+
node: SceneNode,
|
|
23
|
+
siblings: readonly SceneNode[],
|
|
24
|
+
before: ReadonlyMap<string, SceneNode>,
|
|
25
|
+
): SceneNode | undefined {
|
|
26
|
+
if (node.enter !== "grow") return undefined;
|
|
27
|
+
if (node.kind === "rect") return collapsedBar(node);
|
|
28
|
+
return node.kind === "path"
|
|
29
|
+
? node.motion?.enter?.(node, siblings, before)
|
|
30
|
+
: undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Where a node an update removes collapses to. `siblings` is the old list it sat in, `after` the new scene. */
|
|
34
|
+
export function exitTo(
|
|
35
|
+
node: SceneNode,
|
|
36
|
+
siblings: readonly SceneNode[],
|
|
37
|
+
after: ReadonlyMap<string, SceneNode>,
|
|
38
|
+
): SceneNode | undefined {
|
|
39
|
+
if (node.enter !== "grow") return undefined;
|
|
40
|
+
if (node.kind === "rect") return collapsedBar(node);
|
|
41
|
+
return node.kind === "path"
|
|
42
|
+
? node.motion?.exit?.(node, siblings, after)
|
|
43
|
+
: undefined;
|
|
44
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { SceneNode } from "#/core/types.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Guides that left the scene still fade out where they stood: a tick label
|
|
5
|
+
* or a grid line vanishing at once reads as a glitch. Data marks never
|
|
6
|
+
* linger — a bar the data no longer has would show a value that isn't true.
|
|
7
|
+
*/
|
|
8
|
+
const GUIDE_ROLES = new Set(["grid", "axis"]);
|
|
9
|
+
|
|
10
|
+
export interface LeavingGuide {
|
|
11
|
+
readonly node: SceneNode;
|
|
12
|
+
/** The group it sat in, or null at the root. */
|
|
13
|
+
readonly parent: string | null;
|
|
14
|
+
readonly index: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** The guides of `nodes` whose keys `stays` no longer has. */
|
|
18
|
+
export function leavingGuides(
|
|
19
|
+
nodes: readonly SceneNode[],
|
|
20
|
+
stays: (key: string) => boolean,
|
|
21
|
+
parent: string | null = null,
|
|
22
|
+
into: LeavingGuide[] = [],
|
|
23
|
+
): LeavingGuide[] {
|
|
24
|
+
for (const [index, node] of nodes.entries()) {
|
|
25
|
+
if (node.kind === "group") {
|
|
26
|
+
leavingGuides(node.children, stays, node.key, into);
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (!stays(node.key) && GUIDE_ROLES.has(node.role ?? "")) {
|
|
30
|
+
into.push({ node, parent, index });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return into;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** `children` with the leaving guides of `parent` back at their places, faded to `opacity`. */
|
|
37
|
+
export function withLeaving(
|
|
38
|
+
children: readonly SceneNode[],
|
|
39
|
+
parent: string | null,
|
|
40
|
+
leaving: ReadonlyArray<readonly [LeavingGuide, number]>,
|
|
41
|
+
): SceneNode[] {
|
|
42
|
+
const placed = [...children];
|
|
43
|
+
for (const [guide, opacity] of leaving) {
|
|
44
|
+
if (guide.parent !== parent || guide.node.kind === "group") continue;
|
|
45
|
+
const faded = {
|
|
46
|
+
...guide.node,
|
|
47
|
+
paint: {
|
|
48
|
+
...guide.node.paint,
|
|
49
|
+
opacity: (guide.node.paint.opacity ?? 1) * opacity,
|
|
50
|
+
},
|
|
51
|
+
} as SceneNode;
|
|
52
|
+
placed.splice(Math.min(guide.index, placed.length), 0, faded);
|
|
53
|
+
}
|
|
54
|
+
return placed;
|
|
55
|
+
}
|