@voila.dev/chart 5.0.118 → 6.0.121
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/core/motion/motion-store.d.ts.map +1 -1
- package/dist/core/motion/motion-store.js +65 -105
- package/dist/core/motion/motion-store.js.map +3 -3
- package/dist/core/motion/points-motion.d.ts.map +1 -1
- package/dist/core/motion/points-motion.js +9 -22
- package/dist/core/motion/points-motion.js.map +2 -2
- package/dist/core/motion/strings.d.ts +4 -2
- package/dist/core/motion/strings.d.ts.map +1 -1
- package/dist/core/motion/strings.js +5 -6
- package/dist/core/motion/strings.js.map +2 -2
- package/dist/core/motion/timing.d.ts +16 -18
- package/dist/core/motion/timing.d.ts.map +1 -1
- package/dist/core/motion/timing.js +9 -8
- package/dist/core/motion/timing.js.map +2 -2
- package/dist/core/motion/tween-timing.d.ts +17 -0
- package/dist/core/motion/tween-timing.d.ts.map +1 -0
- package/dist/core/motion/tween-timing.js +22 -0
- package/dist/core/motion/tween-timing.js.map +7 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +2 -2
- package/dist/react/chart.d.ts +3 -2
- package/dist/react/chart.d.ts.map +1 -1
- package/dist/react/chart.js +3 -1
- package/dist/react/chart.js.map +2 -2
- package/dist/react/use-animated-scene.d.ts.map +1 -1
- package/dist/react/use-animated-scene.js +4 -4
- package/dist/react/use-animated-scene.js.map +2 -2
- package/package.json +2 -2
- package/src/core/motion/motion-store.ts +93 -139
- package/src/core/motion/points-motion.ts +12 -24
- package/src/core/motion/strings.ts +9 -12
- package/src/core/motion/timing.ts +28 -33
- package/src/core/motion/tween-timing.ts +35 -0
- package/src/index.ts +2 -0
- package/src/react/chart.tsx +9 -4
- package/src/react/use-animated-scene.ts +6 -4
- package/dist/core/motion/leaving.d.ts +0 -12
- package/dist/core/motion/leaving.d.ts.map +0 -1
- package/dist/core/motion/leaving.js +0 -33
- package/dist/core/motion/leaving.js.map +0 -7
- package/src/core/motion/leaving.ts +0 -55
|
@@ -1,16 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { Generator } from "@voila.dev/motion";
|
|
2
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
3
|
import {
|
|
9
4
|
STRING_CHANNELS,
|
|
10
5
|
type StringChannel,
|
|
11
6
|
stringMixer,
|
|
12
7
|
stringOf,
|
|
13
|
-
|
|
8
|
+
withString,
|
|
14
9
|
} from "#/core/motion/strings.ts";
|
|
15
10
|
import { type ChartTiming, staggerDelay } from "#/core/motion/timing.ts";
|
|
16
11
|
import type {
|
|
@@ -43,21 +38,17 @@ interface Motion {
|
|
|
43
38
|
readonly generator: Generator;
|
|
44
39
|
/** Milliseconds, the delay included. */
|
|
45
40
|
readonly start: number;
|
|
41
|
+
/** A path or a colour: the generator runs a progress from 0 to 1, and this reads it. */
|
|
42
|
+
readonly mix?: (progress: number) => string;
|
|
46
43
|
}
|
|
47
44
|
|
|
48
|
-
/**
|
|
49
|
-
|
|
50
|
-
|
|
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. */
|
|
45
|
+
/**
|
|
46
|
+
* A node on its way out, drawn after its old sibling until its springs
|
|
47
|
+
* settle: a data mark collapsing, or a guide fading where it stood.
|
|
48
|
+
*/
|
|
59
49
|
interface Leaving {
|
|
60
50
|
readonly node: SceneNode;
|
|
51
|
+
/** Its old previous sibling's key, else its group's `first` marker. */
|
|
61
52
|
readonly after: string | undefined;
|
|
62
53
|
readonly siblings: readonly SceneNode[];
|
|
63
54
|
}
|
|
@@ -76,25 +67,46 @@ const FIELDS: Record<SceneNode["kind"], readonly string[]> = {
|
|
|
76
67
|
const OPACITY = "opacity";
|
|
77
68
|
const DRAWN = "drawn";
|
|
78
69
|
const SEPARATOR = "\u0000";
|
|
70
|
+
/**
|
|
71
|
+
* A tick label or a grid line vanishing at once reads as a glitch, so they
|
|
72
|
+
* fade out. Data marks never linger: a bar the data no longer has would show
|
|
73
|
+
* a value that isn't true.
|
|
74
|
+
*/
|
|
75
|
+
const GUIDE_ROLES = new Set(["grid", "axis"]);
|
|
79
76
|
|
|
77
|
+
/** Where the leaving first children of the group `parent` are drawn. */
|
|
78
|
+
function first(parent: string | null): string | undefined {
|
|
79
|
+
return parent === null ? undefined : parent + SEPARATOR;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Visits every node with its list and the key it is drawn after when it leaves (see `Leaving`). */
|
|
80
83
|
function walk(
|
|
81
84
|
nodes: readonly SceneNode[],
|
|
82
|
-
visit: (
|
|
85
|
+
visit: (
|
|
86
|
+
node: SceneNode,
|
|
87
|
+
siblings: readonly SceneNode[],
|
|
88
|
+
after: string | undefined,
|
|
89
|
+
) => void,
|
|
90
|
+
parent: string | null = null,
|
|
83
91
|
) {
|
|
92
|
+
let after = first(parent);
|
|
84
93
|
for (const node of nodes) {
|
|
85
|
-
visit(node, nodes);
|
|
86
|
-
if (node.kind === "group") walk(node.children, visit);
|
|
94
|
+
visit(node, nodes, after);
|
|
95
|
+
if (node.kind === "group") walk(node.children, visit, node.key);
|
|
96
|
+
after = node.key;
|
|
87
97
|
}
|
|
88
98
|
}
|
|
89
99
|
|
|
90
100
|
function index(nodes: readonly SceneNode[]) {
|
|
91
101
|
const byKey = new Map<string, SceneNode>();
|
|
92
102
|
const siblings = new Map<string, readonly SceneNode[]>();
|
|
93
|
-
|
|
103
|
+
const afters = new Map<string, string | undefined>();
|
|
104
|
+
walk(nodes, (node, list, after) => {
|
|
94
105
|
byKey.set(node.key, node);
|
|
95
106
|
siblings.set(node.key, list);
|
|
107
|
+
afters.set(node.key, after);
|
|
96
108
|
});
|
|
97
|
-
return { byKey, siblings };
|
|
109
|
+
return { byKey, siblings, afters };
|
|
98
110
|
}
|
|
99
111
|
|
|
100
112
|
function channelKey(node: string, channel: string): string {
|
|
@@ -105,6 +117,10 @@ function elapsed(motion: Motion, now: number): number {
|
|
|
105
117
|
return Math.max(0, (now - motion.start) / 1000);
|
|
106
118
|
}
|
|
107
119
|
|
|
120
|
+
function sample(motion: Motion, now: number) {
|
|
121
|
+
return motion.generator.at(elapsed(motion, now));
|
|
122
|
+
}
|
|
123
|
+
|
|
108
124
|
function done(motion: Motion, now: number): boolean {
|
|
109
125
|
return elapsed(motion, now) >= motion.generator.duration;
|
|
110
126
|
}
|
|
@@ -118,34 +134,16 @@ export function createMotionStore(
|
|
|
118
134
|
timing: ChartTiming,
|
|
119
135
|
): MotionStore {
|
|
120
136
|
let target = initial;
|
|
121
|
-
const
|
|
122
|
-
const strings = new Map<string, StringMotion>();
|
|
123
|
-
const guides = new Map<string, GuideMotion>();
|
|
137
|
+
const channels = new Map<string, Motion>();
|
|
124
138
|
const shapes = new Map<
|
|
125
139
|
string,
|
|
126
140
|
{ readonly plan: GeometryPlan; readonly motion: GeometryMotion }
|
|
127
141
|
>();
|
|
128
142
|
const leaving = new Map<string, Leaving>();
|
|
129
143
|
|
|
130
|
-
function generator(from: number, to: number, velocity: number): Generator {
|
|
131
|
-
return timing.type === "spring"
|
|
132
|
-
? spring({
|
|
133
|
-
from,
|
|
134
|
-
to,
|
|
135
|
-
velocity,
|
|
136
|
-
duration: timing.duration,
|
|
137
|
-
bounce: timing.bounce,
|
|
138
|
-
})
|
|
139
|
-
: tween({
|
|
140
|
-
keyframes: [from, to],
|
|
141
|
-
duration: timing.duration,
|
|
142
|
-
ease: timing.easing,
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
|
|
146
144
|
function value(node: string, channel: string, fallback: number, now: number) {
|
|
147
|
-
const motion =
|
|
148
|
-
return motion ?
|
|
145
|
+
const motion = channels.get(channelKey(node, channel));
|
|
146
|
+
return motion ? sample(motion, now).value : fallback;
|
|
149
147
|
}
|
|
150
148
|
|
|
151
149
|
/** Springs one channel from `from` to `to`, at the speed it already has. */
|
|
@@ -158,13 +156,11 @@ export function createMotionStore(
|
|
|
158
156
|
now: number,
|
|
159
157
|
) {
|
|
160
158
|
const key = channelKey(node, channel);
|
|
161
|
-
const running =
|
|
162
|
-
const velocity = running
|
|
163
|
-
|
|
164
|
-
: 0;
|
|
165
|
-
numbers.delete(key);
|
|
159
|
+
const running = channels.get(key);
|
|
160
|
+
const velocity = running ? sample(running, now).velocity : 0;
|
|
161
|
+
channels.delete(key);
|
|
166
162
|
if (from === to && velocity === 0) return;
|
|
167
|
-
|
|
163
|
+
channels.set(key, { generator: timing.motion(from, to, velocity), start });
|
|
168
164
|
}
|
|
169
165
|
|
|
170
166
|
/** Mixes one string channel from what is painted to what `to` asks for. */
|
|
@@ -175,22 +171,20 @@ export function createMotionStore(
|
|
|
175
171
|
start: number,
|
|
176
172
|
) {
|
|
177
173
|
const key = channelKey(to.key, channel);
|
|
178
|
-
|
|
174
|
+
channels.delete(key);
|
|
179
175
|
const was = stringOf(from, channel);
|
|
180
176
|
const goal = stringOf(to, channel);
|
|
181
177
|
if (was === undefined || goal === undefined || was === goal) return;
|
|
182
|
-
|
|
183
|
-
generator:
|
|
178
|
+
channels.set(key, {
|
|
179
|
+
generator: timing.motion(0, 1, 0),
|
|
184
180
|
start,
|
|
185
181
|
mix: stringMixer(to, channel, was, goal),
|
|
186
182
|
});
|
|
187
183
|
}
|
|
188
184
|
|
|
189
185
|
function drop(node: string) {
|
|
190
|
-
for (const
|
|
191
|
-
|
|
192
|
-
if (key.startsWith(node + SEPARATOR)) motions.delete(key);
|
|
193
|
-
}
|
|
186
|
+
for (const key of channels.keys()) {
|
|
187
|
+
if (key.startsWith(node + SEPARATOR)) channels.delete(key);
|
|
194
188
|
}
|
|
195
189
|
shapes.delete(node);
|
|
196
190
|
leaving.delete(node);
|
|
@@ -215,7 +209,7 @@ export function createMotionStore(
|
|
|
215
209
|
if (to.kind !== "path" || from.kind !== "path") return;
|
|
216
210
|
const previous = shapes.get(key);
|
|
217
211
|
shapes.delete(key);
|
|
218
|
-
|
|
212
|
+
channels.delete(channelKey(key, "d"));
|
|
219
213
|
const motion = to.motion;
|
|
220
214
|
const plan =
|
|
221
215
|
motion && from.geometry && to.geometry
|
|
@@ -228,7 +222,7 @@ export function createMotionStore(
|
|
|
228
222
|
}
|
|
229
223
|
for (const channel of previous?.plan.targets.keys() ?? []) {
|
|
230
224
|
if (!plan.targets.has(channel)) {
|
|
231
|
-
|
|
225
|
+
channels.delete(channelKey(key, `g${channel}`));
|
|
232
226
|
}
|
|
233
227
|
}
|
|
234
228
|
for (const [channel, goal] of plan.targets) {
|
|
@@ -260,43 +254,33 @@ export function createMotionStore(
|
|
|
260
254
|
);
|
|
261
255
|
next = { ...next, geometry, d: shape.motion.draw(geometry) };
|
|
262
256
|
}
|
|
263
|
-
const mixed: Partial<Record<StringChannel, string>> = {};
|
|
264
257
|
for (const channel of STRING_CHANNELS) {
|
|
265
|
-
const motion =
|
|
266
|
-
if (motion) {
|
|
267
|
-
|
|
268
|
-
motion.generator.at(elapsed(motion, now)).value,
|
|
269
|
-
);
|
|
258
|
+
const motion = channels.get(channelKey(key, channel));
|
|
259
|
+
if (motion?.mix) {
|
|
260
|
+
next = withString(next, channel, motion.mix(sample(motion, now).value));
|
|
270
261
|
}
|
|
271
262
|
}
|
|
272
|
-
next = withStrings(next, mixed) as typeof next;
|
|
273
263
|
const opacity = value(key, OPACITY, 1, now);
|
|
274
264
|
const drawn = value(key, DRAWN, 1, now);
|
|
275
265
|
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;
|
|
279
266
|
return {
|
|
280
267
|
...next,
|
|
281
268
|
paint: {
|
|
282
269
|
...next.paint,
|
|
283
270
|
opacity: (next.paint.opacity ?? 1) * clamp(opacity),
|
|
284
|
-
...(drawn < 1 &&
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
}
|
|
291
|
-
|
|
271
|
+
...(drawn < 1 &&
|
|
272
|
+
next.kind === "path" &&
|
|
273
|
+
next.geometry && {
|
|
274
|
+
drawn: {
|
|
275
|
+
fraction: clamp(drawn),
|
|
276
|
+
length: next.motion?.length?.(next.geometry) ?? 0,
|
|
277
|
+
},
|
|
278
|
+
}),
|
|
292
279
|
},
|
|
293
280
|
} as SceneNode;
|
|
294
281
|
}
|
|
295
282
|
|
|
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
|
-
*/
|
|
283
|
+
/** The painted list, with the nodes still leaving after the sibling they followed. */
|
|
300
284
|
function paintList(
|
|
301
285
|
list: readonly SceneNode[],
|
|
302
286
|
parent: string | null,
|
|
@@ -310,38 +294,25 @@ export function createMotionStore(
|
|
|
310
294
|
place(key);
|
|
311
295
|
}
|
|
312
296
|
}
|
|
313
|
-
|
|
297
|
+
place(first(parent));
|
|
314
298
|
for (const node of list) {
|
|
315
299
|
out.push(paintNode(node, now));
|
|
316
300
|
place(node.key);
|
|
317
301
|
}
|
|
318
|
-
|
|
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);
|
|
302
|
+
return out;
|
|
327
303
|
}
|
|
328
304
|
|
|
329
305
|
function prune(now: number) {
|
|
330
|
-
for (const
|
|
331
|
-
|
|
332
|
-
if (done(motion, now)) motions.delete(key);
|
|
333
|
-
}
|
|
306
|
+
for (const [key, motion] of channels) {
|
|
307
|
+
if (done(motion, now)) channels.delete(key);
|
|
334
308
|
}
|
|
335
309
|
const moving = new Set(
|
|
336
|
-
[...
|
|
337
|
-
(key) => key.split(SEPARATOR)[0],
|
|
338
|
-
),
|
|
310
|
+
[...channels.keys()].map((key) => key.split(SEPARATOR)[0]),
|
|
339
311
|
);
|
|
340
|
-
for (const
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
if (!moving.has(key)) leaving.delete(key);
|
|
312
|
+
for (const byNode of [shapes, leaving]) {
|
|
313
|
+
for (const key of byNode.keys()) {
|
|
314
|
+
if (!moving.has(key)) byNode.delete(key);
|
|
315
|
+
}
|
|
345
316
|
}
|
|
346
317
|
}
|
|
347
318
|
|
|
@@ -350,39 +321,30 @@ export function createMotionStore(
|
|
|
350
321
|
const before = index(target.nodes);
|
|
351
322
|
const after = index(scene.nodes);
|
|
352
323
|
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;
|
|
324
|
+
const node = before.byKey.get(key) ?? leaving.get(key)?.node;
|
|
357
325
|
return node && paintNode(node, now);
|
|
358
326
|
}
|
|
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
327
|
for (const key of new Set([...before.byKey.keys(), ...leaving.keys()])) {
|
|
370
328
|
if (after.byKey.has(key)) continue;
|
|
371
329
|
const base = (before.byKey.get(key) ??
|
|
372
330
|
leaving.get(key)?.node) as SceneNode;
|
|
373
|
-
const
|
|
374
|
-
|
|
375
|
-
|
|
331
|
+
const exit = leaving.get(key);
|
|
332
|
+
const siblings = exit?.siblings ?? before.siblings.get(key) ?? [];
|
|
333
|
+
// A whole guide group leaving goes at once, its lines with it.
|
|
334
|
+
const guide = base.kind !== "group" && GUIDE_ROLES.has(base.role ?? "");
|
|
335
|
+
const goal = exitTo(base, siblings, after.byKey) ?? (guide && base);
|
|
376
336
|
const from = painted(key);
|
|
377
337
|
if (!goal || !from) {
|
|
378
338
|
drop(key);
|
|
379
339
|
continue;
|
|
380
340
|
}
|
|
381
341
|
move(from, goal, now, now);
|
|
382
|
-
|
|
342
|
+
if (guide) {
|
|
343
|
+
spring1(key, OPACITY, value(key, OPACITY, 1, now), 0, now, now);
|
|
344
|
+
}
|
|
383
345
|
leaving.set(key, {
|
|
384
346
|
node: goal,
|
|
385
|
-
after:
|
|
347
|
+
after: exit ? exit.after : before.afters.get(key),
|
|
386
348
|
siblings,
|
|
387
349
|
});
|
|
388
350
|
}
|
|
@@ -396,23 +358,19 @@ export function createMotionStore(
|
|
|
396
358
|
now +
|
|
397
359
|
staggerDelay(timing, order.get(node.key) ?? 0, marks.length) * 1000;
|
|
398
360
|
const old = painted(node.key);
|
|
399
|
-
leaving.delete(node.key);
|
|
361
|
+
const returning = leaving.delete(node.key);
|
|
400
362
|
const from =
|
|
401
363
|
old ??
|
|
402
364
|
enterFrom(node, after.siblings.get(node.key) ?? [], before.byKey);
|
|
403
365
|
if (from) move(from, node, start, now);
|
|
404
|
-
const fading =
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
: fading
|
|
410
|
-
? value(node.key, OPACITY, 1, now)
|
|
411
|
-
: 0;
|
|
366
|
+
const fading = channels.get(channelKey(node.key, OPACITY));
|
|
367
|
+
if ((!from && node.enter !== "none") || fading) {
|
|
368
|
+
const shown = fading ? Math.max(0, sample(fading, now).value) : 0;
|
|
369
|
+
// A guide coming back starts from rest: its fade-out speed points the wrong way.
|
|
370
|
+
if (returning) channels.delete(channelKey(node.key, OPACITY));
|
|
412
371
|
spring1(node.key, OPACITY, shown, 1, start, now);
|
|
413
372
|
}
|
|
414
373
|
}
|
|
415
|
-
for (const key of after.byKey.keys()) guides.delete(key);
|
|
416
374
|
target = scene;
|
|
417
375
|
},
|
|
418
376
|
introduce(now) {
|
|
@@ -428,22 +386,18 @@ export function createMotionStore(
|
|
|
428
386
|
return any;
|
|
429
387
|
},
|
|
430
388
|
snap(scene) {
|
|
431
|
-
for (const motions of [
|
|
389
|
+
for (const motions of [channels, shapes, leaving]) {
|
|
432
390
|
motions.clear();
|
|
433
391
|
}
|
|
434
392
|
target = scene;
|
|
435
393
|
},
|
|
436
394
|
frame(now) {
|
|
437
395
|
prune(now);
|
|
438
|
-
if (
|
|
396
|
+
if (channels.size === 0) return target;
|
|
439
397
|
return { ...target, nodes: paintList(target.nodes, null, now) };
|
|
440
398
|
},
|
|
441
399
|
settled(now) {
|
|
442
|
-
return [
|
|
443
|
-
...numbers.values(),
|
|
444
|
-
...strings.values(),
|
|
445
|
-
...guides.values(),
|
|
446
|
-
].every((motion) => done(motion, now));
|
|
400
|
+
return [...channels.values()].every((motion) => done(motion, now));
|
|
447
401
|
},
|
|
448
402
|
};
|
|
449
403
|
}
|
|
@@ -11,6 +11,11 @@ type Points = Extract<SceneGeometry, { kind: "points" }>;
|
|
|
11
11
|
|
|
12
12
|
const POINT_FIELDS = ["x", "y", "x0", "y0"] as const;
|
|
13
13
|
|
|
14
|
+
/** Where `point` sits on its area's lower edge. */
|
|
15
|
+
function lowerEdge(point: GeometryPoint): GeometryPoint {
|
|
16
|
+
return { ...point, x: point.x0 ?? point.x, y: point.y0 ?? point.y };
|
|
17
|
+
}
|
|
18
|
+
|
|
14
19
|
/** A line, an area or a radar outline through its points. */
|
|
15
20
|
export function pointsPath(geometry: SceneGeometry): string {
|
|
16
21
|
if (geometry.kind !== "points") return "";
|
|
@@ -19,14 +24,7 @@ export function pointsPath(geometry: SceneGeometry): string {
|
|
|
19
24
|
return runs
|
|
20
25
|
.map((run) =>
|
|
21
26
|
shape === "area"
|
|
22
|
-
? areaPath(
|
|
23
|
-
run,
|
|
24
|
-
run.map((point) => ({
|
|
25
|
-
x: point.x0 ?? point.x,
|
|
26
|
-
y: point.y0 ?? point.y,
|
|
27
|
-
})),
|
|
28
|
-
curve,
|
|
29
|
-
)
|
|
27
|
+
? areaPath(run, run.map(lowerEdge), curve)
|
|
30
28
|
: linePath(run, curve),
|
|
31
29
|
)
|
|
32
30
|
.join("");
|
|
@@ -36,18 +34,14 @@ function channel(key: string, field: string): string {
|
|
|
36
34
|
return `${key}\u0001${field}`;
|
|
37
35
|
}
|
|
38
36
|
|
|
39
|
-
|
|
37
|
+
/** `point`'s numbers under `key`'s channel names: `point` may be a neighbour that `key` enters from or folds into. */
|
|
38
|
+
function fields(key: string, point: GeometryPoint, into: Map<string, number>) {
|
|
40
39
|
for (const field of POINT_FIELDS) {
|
|
41
40
|
const value = point[field];
|
|
42
|
-
if (value !== undefined) into.set(channel(
|
|
41
|
+
if (value !== undefined) into.set(channel(key, field), value);
|
|
43
42
|
}
|
|
44
43
|
}
|
|
45
44
|
|
|
46
|
-
/** `point`'s numbers under `key`'s channel names: a point entering from, or folding into, a neighbour. */
|
|
47
|
-
function as(key: string, point: GeometryPoint): GeometryPoint {
|
|
48
|
-
return { ...point, key };
|
|
49
|
-
}
|
|
50
|
-
|
|
51
45
|
/** The nearest of `order` around `index` that `has` knows: looking back first, then ahead. */
|
|
52
46
|
function neighbour(
|
|
53
47
|
order: readonly string[],
|
|
@@ -113,8 +107,8 @@ function pointsPlan(from: Points, to: Points): GeometryPlan | undefined {
|
|
|
113
107
|
const target = next.get(key) ?? neighbour(order, at, next);
|
|
114
108
|
const start = old.get(key) ?? neighbour(order, at, old) ?? target;
|
|
115
109
|
if (target === undefined || start === undefined) continue;
|
|
116
|
-
fields(
|
|
117
|
-
fields(
|
|
110
|
+
fields(key, start, starts);
|
|
111
|
+
fields(key, target, targets);
|
|
118
112
|
}
|
|
119
113
|
return order;
|
|
120
114
|
});
|
|
@@ -167,13 +161,7 @@ function collapsed(node: ScenePath): ScenePath | undefined {
|
|
|
167
161
|
...node,
|
|
168
162
|
geometry: {
|
|
169
163
|
...geometry,
|
|
170
|
-
runs: geometry.runs.map((run) =>
|
|
171
|
-
run.map((point) => ({
|
|
172
|
-
...point,
|
|
173
|
-
x: point.x0 ?? point.x,
|
|
174
|
-
y: point.y0 ?? point.y,
|
|
175
|
-
})),
|
|
176
|
-
),
|
|
164
|
+
runs: geometry.runs.map((run) => run.map(lowerEdge)),
|
|
177
165
|
},
|
|
178
166
|
};
|
|
179
167
|
}
|
|
@@ -53,16 +53,13 @@ export function stringMixer(
|
|
|
53
53
|
return morph ? morph(from, to) : (progress) => tweenPath(from, to, progress);
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
/** `node` with
|
|
57
|
-
export function
|
|
58
|
-
node:
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
return Object.keys(colours).length === 0
|
|
66
|
-
? shaped
|
|
67
|
-
: ({ ...shaped, paint: { ...shaped.paint, ...colours } } as SceneNode);
|
|
56
|
+
/** `node` with one string channel set to its moving value. */
|
|
57
|
+
export function withString<Node extends Exclude<SceneNode, { kind: "group" }>>(
|
|
58
|
+
node: Node,
|
|
59
|
+
channel: StringChannel,
|
|
60
|
+
value: string,
|
|
61
|
+
): Node {
|
|
62
|
+
if (channel !== "d")
|
|
63
|
+
return { ...node, paint: { ...node.paint, [channel]: value } };
|
|
64
|
+
return node.kind === "path" ? { ...node, d: value } : node;
|
|
68
65
|
}
|
|
@@ -1,34 +1,30 @@
|
|
|
1
|
-
import type
|
|
2
|
-
|
|
3
|
-
/** How a data update moves: what `<Chart animate>` accepts beyond `true`, `false` and a duration. */
|
|
4
|
-
export type ChartAnimation =
|
|
5
|
-
| {
|
|
6
|
-
readonly type?: "spring";
|
|
7
|
-
/** Perceived duration, in milliseconds. */
|
|
8
|
-
readonly duration?: number;
|
|
9
|
-
/** 0 never overshoots (the default: a bar past its value shows a number the data doesn't have); up to 0.5. */
|
|
10
|
-
readonly bounce?: number;
|
|
11
|
-
/** Milliseconds between marks in focus order, capped at 300 ms in all. */
|
|
12
|
-
readonly stagger?: number;
|
|
13
|
-
}
|
|
14
|
-
| {
|
|
15
|
-
readonly type: "tween";
|
|
16
|
-
readonly duration?: number;
|
|
17
|
-
readonly easing?: EasingName;
|
|
18
|
-
readonly stagger?: number;
|
|
19
|
-
};
|
|
1
|
+
import { type Generator, spring } from "@voila.dev/motion";
|
|
20
2
|
|
|
3
|
+
/**
|
|
4
|
+
* How every channel of a chart moves on an update. The store only asks it
|
|
5
|
+
* for one channel's motion, so a chart that never tweens never loads the
|
|
6
|
+
* tween: `tween()` builds one, the default is a spring.
|
|
7
|
+
*/
|
|
21
8
|
export interface ChartTiming {
|
|
22
|
-
readonly type: "spring" | "tween";
|
|
23
|
-
/** Seconds, as the motion package counts them. */
|
|
24
|
-
readonly duration: number;
|
|
25
|
-
readonly bounce: number;
|
|
26
|
-
readonly easing: EasingName;
|
|
27
9
|
/** Seconds between neighbours. */
|
|
28
10
|
readonly stagger: number;
|
|
11
|
+
/** One channel from `from` to `to`, leaving at `velocity` (units per second). */
|
|
12
|
+
motion(from: number, to: number, velocity: number): Generator;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** The spring `<Chart animate>` takes beyond `true`, `false` and a duration. */
|
|
16
|
+
export interface ChartSpring {
|
|
17
|
+
/** Perceived duration, in milliseconds. */
|
|
18
|
+
readonly duration?: number;
|
|
19
|
+
/** 0 never overshoots (the default: a bar past its value shows a number the data doesn't have); up to 0.5. */
|
|
20
|
+
readonly bounce?: number;
|
|
21
|
+
/** Milliseconds between marks in focus order, capped at 300 ms in all. */
|
|
22
|
+
readonly stagger?: number;
|
|
29
23
|
}
|
|
30
24
|
|
|
31
|
-
|
|
25
|
+
export type ChartAnimation = ChartSpring | ChartTiming;
|
|
26
|
+
|
|
27
|
+
export const DEFAULT_DURATION = 300;
|
|
32
28
|
const MAX_STAGGER_TOTAL = 0.3;
|
|
33
29
|
|
|
34
30
|
/** The timing an `animate` prop means, or `null` to snap. */
|
|
@@ -36,19 +32,18 @@ export function chartTiming(
|
|
|
36
32
|
animate: boolean | number | ChartAnimation,
|
|
37
33
|
): ChartTiming | null {
|
|
38
34
|
if (animate === false || animate === 0) return null;
|
|
39
|
-
|
|
35
|
+
if (typeof animate === "object" && "motion" in animate) return animate;
|
|
36
|
+
const options: ChartSpring =
|
|
40
37
|
typeof animate === "object"
|
|
41
38
|
? animate
|
|
42
39
|
: { duration: animate === true ? DEFAULT_DURATION : animate };
|
|
40
|
+
const duration = (options.duration ?? DEFAULT_DURATION) / 1000;
|
|
41
|
+
const bounce = Math.min(Math.max(options.bounce ?? 0, 0), 0.5);
|
|
43
42
|
return {
|
|
44
|
-
type: options.type ?? "spring",
|
|
45
|
-
duration: (options.duration ?? DEFAULT_DURATION) / 1000,
|
|
46
|
-
bounce: Math.min(
|
|
47
|
-
Math.max("bounce" in options ? (options.bounce ?? 0) : 0, 0),
|
|
48
|
-
0.5,
|
|
49
|
-
),
|
|
50
|
-
easing: ("easing" in options ? options.easing : undefined) ?? "easeOut",
|
|
51
43
|
stagger: (options.stagger ?? 0) / 1000,
|
|
44
|
+
motion(from, to, velocity) {
|
|
45
|
+
return spring({ from, to, velocity, duration, bounce });
|
|
46
|
+
},
|
|
52
47
|
};
|
|
53
48
|
}
|
|
54
49
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type EasingName, tween as tweenGenerator } from "@voila.dev/motion";
|
|
2
|
+
import { type ChartTiming, DEFAULT_DURATION } from "#/core/motion/timing.ts";
|
|
3
|
+
|
|
4
|
+
export interface ChartTweenOptions {
|
|
5
|
+
/** Milliseconds. */
|
|
6
|
+
readonly duration?: number;
|
|
7
|
+
readonly easing?: EasingName;
|
|
8
|
+
/** Milliseconds between marks in focus order, capped at 300 ms in all. */
|
|
9
|
+
readonly stagger?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A fixed curve instead of the spring, for `<Chart animate={tween()} />`.
|
|
14
|
+
* Its own import, so the easings only load in a chart that asks for them.
|
|
15
|
+
* The options stay on the timing as plain fields: `<Chart>` reads an inline
|
|
16
|
+
* `animate` by value, and a function alone would not tell two tweens apart.
|
|
17
|
+
*/
|
|
18
|
+
export function tween(
|
|
19
|
+
options: ChartTweenOptions = {},
|
|
20
|
+
): ChartTiming & ChartTweenOptions {
|
|
21
|
+
const duration = options.duration ?? DEFAULT_DURATION;
|
|
22
|
+
const easing = options.easing ?? "easeOut";
|
|
23
|
+
return {
|
|
24
|
+
duration,
|
|
25
|
+
easing,
|
|
26
|
+
stagger: (options.stagger ?? 0) / 1000,
|
|
27
|
+
motion(from, to) {
|
|
28
|
+
return tweenGenerator({
|
|
29
|
+
keyframes: [from, to],
|
|
30
|
+
duration: duration / 1000,
|
|
31
|
+
ease: easing,
|
|
32
|
+
});
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -66,10 +66,12 @@ export {
|
|
|
66
66
|
} from "#/core/motion/motion-store.ts";
|
|
67
67
|
export {
|
|
68
68
|
type ChartAnimation,
|
|
69
|
+
type ChartSpring,
|
|
69
70
|
type ChartTiming,
|
|
70
71
|
chartTiming,
|
|
71
72
|
} from "#/core/motion/timing.ts";
|
|
72
73
|
export { easeOutCubic, tweenScene } from "#/core/motion/tween.ts";
|
|
74
|
+
export { type ChartTweenOptions, tween } from "#/core/motion/tween-timing.ts";
|
|
73
75
|
export {
|
|
74
76
|
type ChartFocusStop,
|
|
75
77
|
findNearest,
|
package/src/react/chart.tsx
CHANGED
|
@@ -71,8 +71,9 @@ interface Props
|
|
|
71
71
|
readonly dataTable?: boolean;
|
|
72
72
|
/**
|
|
73
73
|
* How a data update moves: `true` (a spring with no bounce), a perceived
|
|
74
|
-
* duration in milliseconds,
|
|
75
|
-
* `{
|
|
74
|
+
* duration in milliseconds, `{ duration, bounce, stagger }`, or
|
|
75
|
+
* `tween({ duration, easing, stagger })`. `false` snaps; reduced motion
|
|
76
|
+
* always does.
|
|
76
77
|
*/
|
|
77
78
|
readonly animate?: boolean | number | ChartAnimation;
|
|
78
79
|
/**
|
|
@@ -179,10 +180,14 @@ export function Chart({
|
|
|
179
180
|
() => [definition, hidden, keyboardZooms],
|
|
180
181
|
[definition, hidden, keyboardZooms],
|
|
181
182
|
);
|
|
182
|
-
// An inline `animate={{ … }}`
|
|
183
|
+
// An inline `animate={{ … }}` or `tween({ … })` is read by value, not
|
|
184
|
+
// identity: a new object each render must not restart the motion.
|
|
185
|
+
const latestAnimate = React.useRef(animate);
|
|
186
|
+
latestAnimate.current = animate;
|
|
183
187
|
const animateKey = JSON.stringify(animate);
|
|
188
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: keyed on the value of `animate`.
|
|
184
189
|
const timing = React.useMemo(
|
|
185
|
-
() => chartTiming(
|
|
190
|
+
() => chartTiming(latestAnimate.current),
|
|
186
191
|
[animateKey],
|
|
187
192
|
);
|
|
188
193
|
const shown = useAnimatedScene(scene, animationTrigger, timing);
|