@voila.dev/chart 5.0.114 → 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.
Files changed (86) hide show
  1. package/dist/canvas/paint-scene.d.ts.map +1 -1
  2. package/dist/canvas/paint-scene.js +3 -1
  3. package/dist/canvas/paint-scene.js.map +2 -2
  4. package/dist/contour/density.d.ts.map +1 -1
  5. package/dist/contour/density.js +2 -0
  6. package/dist/contour/density.js.map +2 -2
  7. package/dist/core/marks/area.d.ts.map +1 -1
  8. package/dist/core/marks/area.js +27 -11
  9. package/dist/core/marks/area.js.map +2 -2
  10. package/dist/core/marks/bar.d.ts.map +1 -1
  11. package/dist/core/marks/bar.js +2 -0
  12. package/dist/core/marks/bar.js.map +2 -2
  13. package/dist/core/marks/line.d.ts.map +1 -1
  14. package/dist/core/marks/line.js +13 -4
  15. package/dist/core/marks/line.js.map +2 -2
  16. package/dist/core/marks/polar.d.ts.map +1 -1
  17. package/dist/core/marks/polar.js +22 -5
  18. package/dist/core/marks/polar.js.map +2 -2
  19. package/dist/core/marks/shared.d.ts +8 -1
  20. package/dist/core/marks/shared.d.ts.map +1 -1
  21. package/dist/core/marks/shared.js.map +2 -2
  22. package/dist/core/marks/stats/difference.d.ts.map +1 -1
  23. package/dist/core/marks/stats/difference.js +20 -7
  24. package/dist/core/marks/stats/difference.js.map +2 -2
  25. package/dist/core/motion/arc-motion.d.ts +9 -0
  26. package/dist/core/motion/arc-motion.d.ts.map +1 -0
  27. package/dist/core/motion/arc-motion.js +67 -0
  28. package/dist/core/motion/arc-motion.js.map +7 -0
  29. package/dist/core/motion/enter-exit.d.ts +6 -0
  30. package/dist/core/motion/enter-exit.d.ts.map +1 -0
  31. package/dist/core/motion/enter-exit.js +20 -0
  32. package/dist/core/motion/enter-exit.js.map +7 -0
  33. package/dist/core/motion/leaving.d.ts +12 -0
  34. package/dist/core/motion/leaving.d.ts.map +1 -0
  35. package/dist/core/motion/leaving.js +33 -0
  36. package/dist/core/motion/leaving.js.map +7 -0
  37. package/dist/core/motion/motion-store.d.ts +4 -1
  38. package/dist/core/motion/motion-store.d.ts.map +1 -1
  39. package/dist/core/motion/motion-store.js +251 -100
  40. package/dist/core/motion/motion-store.js.map +3 -3
  41. package/dist/core/motion/points-motion.d.ts +6 -0
  42. package/dist/core/motion/points-motion.d.ts.map +1 -0
  43. package/dist/core/motion/points-motion.js +140 -0
  44. package/dist/core/motion/points-motion.js.map +7 -0
  45. package/dist/core/motion/strings.d.ts +18 -0
  46. package/dist/core/motion/strings.d.ts.map +1 -0
  47. package/dist/core/motion/strings.js +34 -0
  48. package/dist/core/motion/strings.js.map +7 -0
  49. package/dist/core/types.d.ts +85 -0
  50. package/dist/core/types.d.ts.map +1 -1
  51. package/dist/geo/geo.d.ts.map +1 -1
  52. package/dist/geo/geo.js +2 -0
  53. package/dist/geo/geo.js.map +2 -2
  54. package/dist/react/chart.d.ts.map +1 -1
  55. package/dist/react/chart.js +6 -2
  56. package/dist/react/chart.js.map +2 -2
  57. package/dist/react/svg-scene.d.ts.map +1 -1
  58. package/dist/react/svg-scene.js +3 -1
  59. package/dist/react/svg-scene.js.map +2 -2
  60. package/dist/react/use-animated-scene.d.ts.map +1 -1
  61. package/dist/react/use-animated-scene.js +14 -2
  62. package/dist/react/use-animated-scene.js.map +2 -2
  63. package/dist/voronoi/voronoi.d.ts.map +1 -1
  64. package/dist/voronoi/voronoi.js +2 -0
  65. package/dist/voronoi/voronoi.js.map +2 -2
  66. package/package.json +2 -2
  67. package/src/canvas/paint-scene.ts +5 -1
  68. package/src/contour/density.ts +2 -0
  69. package/src/core/marks/area.ts +28 -13
  70. package/src/core/marks/bar.ts +2 -0
  71. package/src/core/marks/line.ts +15 -8
  72. package/src/core/marks/polar.ts +22 -5
  73. package/src/core/marks/shared.ts +8 -0
  74. package/src/core/marks/stats/difference.ts +28 -8
  75. package/src/core/motion/arc-motion.ts +113 -0
  76. package/src/core/motion/enter-exit.ts +44 -0
  77. package/src/core/motion/leaving.ts +55 -0
  78. package/src/core/motion/motion-store.ts +332 -109
  79. package/src/core/motion/points-motion.ts +190 -0
  80. package/src/core/motion/strings.ts +68 -0
  81. package/src/core/types.ts +98 -0
  82. package/src/geo/geo.ts +2 -0
  83. package/src/react/chart.tsx +8 -2
  84. package/src/react/svg-scene.tsx +5 -1
  85. package/src/react/use-animated-scene.ts +18 -2
  86. package/src/voronoi/voronoi.ts +2 -0
@@ -1,7 +1,24 @@
1
1
  import { type Generator, spring, tween } from "@voila.dev/motion";
2
+ import { enterFrom, exitTo } from "#/core/motion/enter-exit.ts";
3
+ import {
4
+ type LeavingGuide,
5
+ leavingGuides,
6
+ withLeaving,
7
+ } from "#/core/motion/leaving.ts";
8
+ import {
9
+ STRING_CHANNELS,
10
+ type StringChannel,
11
+ stringMixer,
12
+ stringOf,
13
+ withStrings,
14
+ } from "#/core/motion/strings.ts";
2
15
  import { type ChartTiming, staggerDelay } from "#/core/motion/timing.ts";
3
- import { tweenPath } from "#/core/motion/tween.ts";
4
- import type { ChartScene, SceneNode } from "#/core/types.ts";
16
+ import type {
17
+ ChartScene,
18
+ GeometryMotion,
19
+ GeometryPlan,
20
+ SceneNode,
21
+ } from "#/core/types.ts";
5
22
 
6
23
  /**
7
24
  * Every moving number of a chart, as springs keyed by node and channel. A
@@ -9,10 +26,13 @@ import type { ChartScene, SceneNode } from "#/core/types.ts";
9
26
  * the speed it has, so an update landing mid-flight bends the motion instead
10
27
  * of kinking it. Pure and clocked from outside (milliseconds), so the SVG and
11
28
  * Canvas renderers move identically and tests step it by hand. Hit-testing
12
- * and focus never read it: they read the scene it moves towards.
29
+ * and focus never read it: they read the scene it moves towards, which never
30
+ * holds the marks collapsing or the guides fading on their way out.
13
31
  */
14
32
  export interface MotionStore {
15
33
  retarget(scene: ChartScene, now: number): void;
34
+ /** The first render: lines that `enter: "draw"` trace themselves in. False when none does. */
35
+ introduce(now: number): boolean;
16
36
  /** Jumps to `scene`: a resize, or reduced motion. */
17
37
  snap(scene: ChartScene): void;
18
38
  frame(now: number): ChartScene;
@@ -25,14 +45,26 @@ interface Motion {
25
45
  readonly start: number;
26
46
  }
27
47
 
28
- interface PathMotion extends Motion {
29
- readonly from: string;
30
- readonly to: string;
48
+ /** A path or a colour: a progress from 0 to 1, and the mix it reads. */
49
+ interface StringMotion extends Motion {
50
+ readonly mix: (progress: number) => string;
51
+ }
52
+
53
+ /** A guide fading out where it stood. */
54
+ interface GuideMotion extends Motion {
55
+ readonly guide: LeavingGuide;
56
+ }
57
+
58
+ /** A data mark collapsing on its way out: drawn after its old sibling until its springs settle. */
59
+ interface Leaving {
60
+ readonly node: SceneNode;
61
+ readonly after: string | undefined;
62
+ readonly siblings: readonly SceneNode[];
31
63
  }
32
64
 
33
65
  type Numeric = Record<string, number>;
34
66
 
35
- const CHANNELS: Record<SceneNode["kind"], readonly string[]> = {
67
+ const FIELDS: Record<SceneNode["kind"], readonly string[]> = {
36
68
  rect: ["x", "y", "width", "height"],
37
69
  circle: ["cx", "cy", "r"],
38
70
  line: ["x1", "y1", "x2", "y2"],
@@ -42,20 +74,31 @@ const CHANNELS: Record<SceneNode["kind"], readonly string[]> = {
42
74
  };
43
75
 
44
76
  const OPACITY = "opacity";
77
+ const DRAWN = "drawn";
78
+ const SEPARATOR = "\u0000";
45
79
 
46
- function flatten(
80
+ function walk(
47
81
  nodes: readonly SceneNode[],
48
- into = new Map<string, SceneNode>(),
82
+ visit: (node: SceneNode, siblings: readonly SceneNode[]) => void,
49
83
  ) {
50
84
  for (const node of nodes) {
51
- into.set(node.key, node);
52
- if (node.kind === "group") flatten(node.children, into);
85
+ visit(node, nodes);
86
+ if (node.kind === "group") walk(node.children, visit);
53
87
  }
54
- return into;
88
+ }
89
+
90
+ function index(nodes: readonly SceneNode[]) {
91
+ const byKey = new Map<string, SceneNode>();
92
+ const siblings = new Map<string, readonly SceneNode[]>();
93
+ walk(nodes, (node, list) => {
94
+ byKey.set(node.key, node);
95
+ siblings.set(node.key, list);
96
+ });
97
+ return { byKey, siblings };
55
98
  }
56
99
 
57
100
  function channelKey(node: string, channel: string): string {
58
- return `${node}\u0000${channel}`;
101
+ return `${node}${SEPARATOR}${channel}`;
59
102
  }
60
103
 
61
104
  function elapsed(motion: Motion, now: number): number {
@@ -66,13 +109,23 @@ function done(motion: Motion, now: number): boolean {
66
109
  return elapsed(motion, now) >= motion.generator.duration;
67
110
  }
68
111
 
112
+ function clamp(value: number): number {
113
+ return Math.min(1, Math.max(0, value));
114
+ }
115
+
69
116
  export function createMotionStore(
70
117
  initial: ChartScene,
71
118
  timing: ChartTiming,
72
119
  ): MotionStore {
73
120
  let target = initial;
74
121
  const numbers = new Map<string, Motion>();
75
- const paths = new Map<string, PathMotion>();
122
+ const strings = new Map<string, StringMotion>();
123
+ const guides = new Map<string, GuideMotion>();
124
+ const shapes = new Map<
125
+ string,
126
+ { readonly plan: GeometryPlan; readonly motion: GeometryMotion }
127
+ >();
128
+ const leaving = new Map<string, Leaving>();
76
129
 
77
130
  function generator(from: number, to: number, velocity: number): Generator {
78
131
  return timing.type === "spring"
@@ -90,137 +143,307 @@ export function createMotionStore(
90
143
  });
91
144
  }
92
145
 
93
- function pathNow(key: string, fallback: string | undefined, now: number) {
94
- const motion = paths.get(key);
95
- if (!motion) return fallback;
96
- return tweenPath(
97
- motion.from,
98
- motion.to,
99
- motion.generator.at(elapsed(motion, now)).value,
100
- );
146
+ function value(node: string, channel: string, fallback: number, now: number) {
147
+ const motion = numbers.get(channelKey(node, channel));
148
+ return motion ? motion.generator.at(elapsed(motion, now)).value : fallback;
101
149
  }
102
150
 
103
- function moveNode(
104
- node: SceneNode,
105
- before: SceneNode | undefined,
151
+ /** Springs one channel from `from` to `to`, at the speed it already has. */
152
+ function spring1(
153
+ node: string,
154
+ channel: string,
155
+ from: number,
156
+ to: number,
106
157
  start: number,
107
158
  now: number,
108
159
  ) {
109
- for (const channel of CHANNELS[node.kind]) {
110
- const key = channelKey(node.key, channel);
111
- const running = numbers.get(key);
112
- const state = running?.generator.at(elapsed(running, now));
113
- const from =
114
- state?.value ?? (before as unknown as Numeric | undefined)?.[channel];
115
- const to = (node as unknown as Numeric)[channel] as number;
116
- numbers.delete(key);
117
- if (from === undefined || (from === to && !state?.velocity)) continue;
118
- numbers.set(key, {
119
- generator: generator(from, to, state?.velocity ?? 0),
120
- start,
121
- });
160
+ const key = channelKey(node, channel);
161
+ const running = numbers.get(key);
162
+ const velocity = running
163
+ ? running.generator.at(elapsed(running, now)).velocity
164
+ : 0;
165
+ numbers.delete(key);
166
+ if (from === to && velocity === 0) return;
167
+ numbers.set(key, { generator: generator(from, to, velocity), start });
168
+ }
169
+
170
+ /** Mixes one string channel from what is painted to what `to` asks for. */
171
+ function mixString(
172
+ from: SceneNode,
173
+ to: SceneNode,
174
+ channel: StringChannel,
175
+ start: number,
176
+ ) {
177
+ const key = channelKey(to.key, channel);
178
+ strings.delete(key);
179
+ const was = stringOf(from, channel);
180
+ const goal = stringOf(to, channel);
181
+ if (was === undefined || goal === undefined || was === goal) return;
182
+ strings.set(key, {
183
+ generator: generator(0, 1, 0),
184
+ start,
185
+ mix: stringMixer(to, channel, was, goal),
186
+ });
187
+ }
188
+
189
+ function drop(node: string) {
190
+ for (const motions of [numbers, strings]) {
191
+ for (const key of motions.keys()) {
192
+ if (key.startsWith(node + SEPARATOR)) motions.delete(key);
193
+ }
122
194
  }
123
- if (node.kind === "path") {
124
- const from = pathNow(
125
- node.key,
126
- before?.kind === "path" ? before.d : undefined,
195
+ shapes.delete(node);
196
+ leaving.delete(node);
197
+ }
198
+
199
+ /** `from` is the node as painted now (or the shape it enters from), `to` where it goes. */
200
+ function move(from: SceneNode, to: SceneNode, start: number, now: number) {
201
+ const key = to.key;
202
+ for (const field of FIELDS[to.kind]) {
203
+ const was = (from as unknown as Numeric)[field] as number;
204
+ spring1(
205
+ key,
206
+ field,
207
+ was,
208
+ (to as unknown as Numeric)[field] as number,
209
+ start,
127
210
  now,
128
211
  );
129
- paths.delete(node.key);
130
- if (from !== undefined && from !== node.d) {
131
- paths.set(node.key, {
132
- generator: generator(0, 1, 0),
133
- start,
134
- from,
135
- to: node.d,
136
- });
212
+ }
213
+ mixString(from, to, "fill", start);
214
+ mixString(from, to, "stroke", start);
215
+ if (to.kind !== "path" || from.kind !== "path") return;
216
+ const previous = shapes.get(key);
217
+ shapes.delete(key);
218
+ strings.delete(channelKey(key, "d"));
219
+ const motion = to.motion;
220
+ const plan =
221
+ motion && from.geometry && to.geometry
222
+ ? motion.plan(from.geometry, to.geometry)
223
+ : undefined;
224
+ if (!plan) {
225
+ // No meaning to move: the node's own morph, else the numbers of `d`.
226
+ mixString(from, to, "d", start);
227
+ return;
228
+ }
229
+ for (const channel of previous?.plan.targets.keys() ?? []) {
230
+ if (!plan.targets.has(channel)) {
231
+ numbers.delete(channelKey(key, `g${channel}`));
137
232
  }
138
233
  }
139
- const fading = channelKey(node.key, OPACITY);
140
- const fade = numbers.get(fading);
141
- if (before === undefined || fade) {
142
- const from = fade ? fade.generator.at(elapsed(fade, now)).value : 0;
143
- numbers.set(fading, { generator: generator(from, 1, 0), start });
234
+ for (const [channel, goal] of plan.targets) {
235
+ const was = plan.starts.get(channel) ?? goal;
236
+ spring1(key, `g${channel}`, was, goal, start, now);
144
237
  }
238
+ shapes.set(key, { plan, motion: motion as GeometryMotion });
145
239
  }
146
240
 
147
- function paint(node: SceneNode, now: number): SceneNode {
241
+ function paintNode(node: SceneNode, now: number): SceneNode {
148
242
  if (node.kind === "group") {
149
- return {
150
- ...node,
151
- children: node.children.map((child) => paint(child, now)),
152
- };
243
+ return { ...node, children: paintList(node.children, node.key, now) };
153
244
  }
245
+ const key = node.key;
154
246
  const moved: Numeric = {};
155
- for (const channel of [...CHANNELS[node.kind], OPACITY]) {
156
- const motion = numbers.get(channelKey(node.key, channel));
157
- if (!motion) continue;
158
- moved[channel] = motion.generator.at(elapsed(motion, now)).value;
247
+ for (const field of FIELDS[node.kind]) {
248
+ moved[field] = value(
249
+ key,
250
+ field,
251
+ (node as unknown as Numeric)[field] as number,
252
+ now,
253
+ );
159
254
  }
160
- const d = node.kind === "path" ? pathNow(node.key, node.d, now) : undefined;
161
- const { [OPACITY]: opacity, ...geometry } = moved;
162
- const next = {
163
- ...node,
164
- ...geometry,
165
- ...(d === undefined ? {} : { d }),
166
- } as SceneNode;
167
- if (opacity === undefined) return next;
168
- const shown = Math.min(1, Math.max(0, opacity));
169
- const painted = next as Exclude<SceneNode, { kind: "group" }>;
255
+ let next = { ...node, ...moved } as Exclude<SceneNode, { kind: "group" }>;
256
+ const shape = shapes.get(key);
257
+ if (next.kind === "path" && shape) {
258
+ const geometry = shape.plan.build((channel) =>
259
+ value(key, `g${channel}`, shape.plan.targets.get(channel) ?? 0, now),
260
+ );
261
+ next = { ...next, geometry, d: shape.motion.draw(geometry) };
262
+ }
263
+ const mixed: Partial<Record<StringChannel, string>> = {};
264
+ for (const channel of STRING_CHANNELS) {
265
+ const motion = strings.get(channelKey(key, channel));
266
+ if (motion) {
267
+ mixed[channel] = motion.mix(
268
+ motion.generator.at(elapsed(motion, now)).value,
269
+ );
270
+ }
271
+ }
272
+ next = withStrings(next, mixed) as typeof next;
273
+ const opacity = value(key, OPACITY, 1, now);
274
+ const drawn = value(key, DRAWN, 1, now);
275
+ if (opacity === 1 && drawn === 1) return next;
276
+ const geometry = next.kind === "path" ? next.geometry : undefined;
277
+ const length =
278
+ geometry && next.kind === "path" ? next.motion?.length?.(geometry) : 0;
170
279
  return {
171
- ...painted,
280
+ ...next,
172
281
  paint: {
173
- ...painted.paint,
174
- opacity: (painted.paint.opacity ?? 1) * shown,
282
+ ...next.paint,
283
+ opacity: (next.paint.opacity ?? 1) * clamp(opacity),
284
+ ...(drawn < 1 && geometry
285
+ ? {
286
+ drawn: {
287
+ fraction: clamp(drawn),
288
+ length: length ?? 0,
289
+ },
290
+ }
291
+ : {}),
175
292
  },
176
293
  } as SceneNode;
177
294
  }
178
295
 
296
+ /**
297
+ * The painted list, with the marks still collapsing after the sibling they
298
+ * followed, and the guides still fading back at their places.
299
+ */
300
+ function paintList(
301
+ list: readonly SceneNode[],
302
+ parent: string | null,
303
+ now: number,
304
+ ): SceneNode[] {
305
+ const out: SceneNode[] = [];
306
+ function place(after: string | undefined) {
307
+ for (const [key, exit] of leaving) {
308
+ if (exit.after !== after) continue;
309
+ out.push(paintNode(exit.node, now));
310
+ place(key);
311
+ }
312
+ }
313
+ if (parent === null) place(undefined);
314
+ for (const node of list) {
315
+ out.push(paintNode(node, now));
316
+ place(node.key);
317
+ }
318
+ if (guides.size === 0) return out;
319
+ const fading = [...guides.values()].map(
320
+ (motion) =>
321
+ [
322
+ motion.guide,
323
+ Math.max(0, motion.generator.at(elapsed(motion, now)).value),
324
+ ] as const,
325
+ );
326
+ return withLeaving(out, parent, fading);
327
+ }
328
+
329
+ function prune(now: number) {
330
+ for (const motions of [numbers, strings, guides]) {
331
+ for (const [key, motion] of motions) {
332
+ if (done(motion, now)) motions.delete(key);
333
+ }
334
+ }
335
+ const moving = new Set(
336
+ [...numbers.keys(), ...strings.keys()].map(
337
+ (key) => key.split(SEPARATOR)[0],
338
+ ),
339
+ );
340
+ for (const key of shapes.keys()) {
341
+ if (!moving.has(key)) shapes.delete(key);
342
+ }
343
+ for (const key of leaving.keys()) {
344
+ if (!moving.has(key)) leaving.delete(key);
345
+ }
346
+ }
347
+
179
348
  return {
180
349
  retarget(scene, now) {
181
- const before = flatten(target.nodes);
182
- const after = flatten(scene.nodes);
183
- const marks = [...after.values()].filter((node) => node.role === "mark");
184
- const order = new Map(marks.map((node, index) => [node.key, index]));
185
- for (const key of before.keys()) {
186
- if (after.has(key)) continue;
187
- paths.delete(key);
188
- for (const channel of [
189
- ...(CHANNELS[before.get(key)?.kind ?? "group"] ?? []),
190
- OPACITY,
191
- ]) {
192
- numbers.delete(channelKey(key, channel));
350
+ const before = index(target.nodes);
351
+ const after = index(scene.nodes);
352
+ function painted(key: string): SceneNode | undefined {
353
+ const node =
354
+ before.byKey.get(key) ??
355
+ leaving.get(key)?.node ??
356
+ guides.get(key)?.guide.node;
357
+ return node && paintNode(node, now);
358
+ }
359
+ // Read before the exits below drop what the guides were showing.
360
+ for (const guide of leavingGuides(target.nodes, (key) =>
361
+ after.byKey.has(key),
362
+ )) {
363
+ guides.set(guide.node.key, {
364
+ generator: generator(value(guide.node.key, OPACITY, 1, now), 0, 0),
365
+ start: now,
366
+ guide,
367
+ });
368
+ }
369
+ for (const key of new Set([...before.byKey.keys(), ...leaving.keys()])) {
370
+ if (after.byKey.has(key)) continue;
371
+ const base = (before.byKey.get(key) ??
372
+ leaving.get(key)?.node) as SceneNode;
373
+ const siblings =
374
+ before.siblings.get(key) ?? leaving.get(key)?.siblings ?? [];
375
+ const goal = exitTo(base, siblings, after.byKey);
376
+ const from = painted(key);
377
+ if (!goal || !from) {
378
+ drop(key);
379
+ continue;
193
380
  }
381
+ move(from, goal, now, now);
382
+ const at = siblings.findIndex((node) => node.key === key);
383
+ leaving.set(key, {
384
+ node: goal,
385
+ after: siblings[at - 1]?.key,
386
+ siblings,
387
+ });
194
388
  }
195
- for (const node of after.values()) {
196
- const delay = staggerDelay(
197
- timing,
198
- order.get(node.key) ?? 0,
199
- marks.length,
200
- );
201
- moveNode(node, before.get(node.key), now + delay * 1000, now);
389
+ const marks = [...after.byKey.values()].filter(
390
+ (node) => node.role === "mark",
391
+ );
392
+ const order = new Map(marks.map((node, at) => [node.key, at]));
393
+ for (const node of after.byKey.values()) {
394
+ if (node.kind === "group") continue;
395
+ const start =
396
+ now +
397
+ staggerDelay(timing, order.get(node.key) ?? 0, marks.length) * 1000;
398
+ const old = painted(node.key);
399
+ leaving.delete(node.key);
400
+ const from =
401
+ old ??
402
+ enterFrom(node, after.siblings.get(node.key) ?? [], before.byKey);
403
+ if (from) move(from, node, start, now);
404
+ const fading = numbers.get(channelKey(node.key, OPACITY));
405
+ const returning = guides.get(node.key);
406
+ if ((!from && node.enter !== "none") || fading || returning) {
407
+ const shown = returning
408
+ ? Math.max(0, returning.generator.at(elapsed(returning, now)).value)
409
+ : fading
410
+ ? value(node.key, OPACITY, 1, now)
411
+ : 0;
412
+ spring1(node.key, OPACITY, shown, 1, start, now);
413
+ }
202
414
  }
415
+ for (const key of after.byKey.keys()) guides.delete(key);
203
416
  target = scene;
204
417
  },
418
+ introduce(now) {
419
+ let any = false;
420
+ walk(target.nodes, (node) => {
421
+ if (node.kind !== "path" || node.enter !== "draw") return;
422
+ if (node.geometry?.kind !== "points" || node.paint.strokeDasharray) {
423
+ return;
424
+ }
425
+ spring1(node.key, DRAWN, 0, 1, now, now);
426
+ any = true;
427
+ });
428
+ return any;
429
+ },
205
430
  snap(scene) {
206
- numbers.clear();
207
- paths.clear();
431
+ for (const motions of [numbers, strings, guides, shapes, leaving]) {
432
+ motions.clear();
433
+ }
208
434
  target = scene;
209
435
  },
210
436
  frame(now) {
211
- for (const [key, motion] of numbers) {
212
- if (done(motion, now)) numbers.delete(key);
213
- }
214
- for (const [key, motion] of paths) {
215
- if (done(motion, now)) paths.delete(key);
216
- }
217
- if (numbers.size === 0 && paths.size === 0) return target;
218
- return { ...target, nodes: target.nodes.map((node) => paint(node, now)) };
437
+ prune(now);
438
+ if (numbers.size + strings.size + guides.size === 0) return target;
439
+ return { ...target, nodes: paintList(target.nodes, null, now) };
219
440
  },
220
441
  settled(now) {
221
- return [...numbers.values(), ...paths.values()].every((motion) =>
222
- done(motion, now),
223
- );
442
+ return [
443
+ ...numbers.values(),
444
+ ...strings.values(),
445
+ ...guides.values(),
446
+ ].every((motion) => done(motion, now));
224
447
  },
225
448
  };
226
449
  }