@anvpx/motion-scenes 0.0.0-stage → 0.1.1
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/LICENSE +21 -0
- package/README.md +39 -2
- package/dist/catalog.d.ts +17 -0
- package/dist/catalog.js +51 -0
- package/dist/custom.d.ts +86 -0
- package/dist/custom.js +176 -0
- package/dist/define-scene.d.ts +31 -0
- package/dist/define-scene.js +128 -0
- package/dist/html.d.ts +5 -0
- package/dist/html.js +14 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +11 -0
- package/dist/position.d.ts +21 -0
- package/dist/position.js +16 -0
- package/dist/registry.d.ts +4 -0
- package/dist/registry.js +68 -0
- package/dist/runtime/compose.d.ts +21 -0
- package/dist/runtime/compose.js +43 -0
- package/dist/runtime/easing.d.ts +14 -0
- package/dist/runtime/easing.js +45 -0
- package/dist/runtime/mount.d.ts +50 -0
- package/dist/runtime/mount.js +153 -0
- package/dist/runtime/timeline.d.ts +147 -0
- package/dist/runtime/timeline.js +199 -0
- package/dist/runtime-files.d.ts +4 -0
- package/dist/runtime-files.js +18 -0
- package/dist/scene-contract.d.ts +7 -0
- package/dist/scene-contract.js +104 -0
- package/dist/scene3d/post.d.ts +12 -0
- package/dist/scene3d/post.js +16 -0
- package/dist/scene3d/reflector.d.ts +9 -0
- package/dist/scene3d/reflector.js +17 -0
- package/dist/scene3d/textures.d.ts +9 -0
- package/dist/scene3d/textures.js +11 -0
- package/dist/scene3d/volumetric.d.ts +9 -0
- package/dist/scene3d/volumetric.js +16 -0
- package/dist/scenes/bug/index.d.ts +1 -0
- package/dist/scenes/bug/index.js +45 -0
- package/dist/scenes/bug/scene.d.ts +2 -0
- package/dist/scenes/bug/scene.js +6 -0
- package/dist/scenes/bug/style.css +30 -0
- package/dist/scenes/bullets/index.d.ts +1 -0
- package/dist/scenes/bullets/index.js +128 -0
- package/dist/scenes/bullets/scene.d.ts +2 -0
- package/dist/scenes/bullets/scene.js +6 -0
- package/dist/scenes/bullets/style.css +91 -0
- package/dist/scenes/callout/index.d.ts +9 -0
- package/dist/scenes/callout/index.js +95 -0
- package/dist/scenes/callout/scene.d.ts +2 -0
- package/dist/scenes/callout/scene.js +43 -0
- package/dist/scenes/callout/style.css +97 -0
- package/dist/scenes/caption/index.d.ts +1 -0
- package/dist/scenes/caption/index.js +72 -0
- package/dist/scenes/caption/scene.d.ts +11 -0
- package/dist/scenes/caption/scene.js +33 -0
- package/dist/scenes/caption/style.css +33 -0
- package/dist/scenes/chapter/index.d.ts +1 -0
- package/dist/scenes/chapter/index.js +29 -0
- package/dist/scenes/chapter/scene.d.ts +2 -0
- package/dist/scenes/chapter/scene.js +6 -0
- package/dist/scenes/chapter/style.css +25 -0
- package/dist/scenes/chart/index.d.ts +1 -0
- package/dist/scenes/chart/index.js +24 -0
- package/dist/scenes/chart/render.d.ts +4 -0
- package/dist/scenes/chart/render.js +257 -0
- package/dist/scenes/chart/scale.d.ts +11 -0
- package/dist/scenes/chart/scale.js +53 -0
- package/dist/scenes/chart/scene.d.ts +2 -0
- package/dist/scenes/chart/scene.js +73 -0
- package/dist/scenes/chart/schema.d.ts +36 -0
- package/dist/scenes/chart/schema.js +109 -0
- package/dist/scenes/chart/style.css +425 -0
- package/dist/scenes/code/index.d.ts +1 -0
- package/dist/scenes/code/index.js +109 -0
- package/dist/scenes/code/scene.d.ts +2 -0
- package/dist/scenes/code/scene.js +108 -0
- package/dist/scenes/code/style.css +155 -0
- package/dist/scenes/code/tokenize.d.ts +8 -0
- package/dist/scenes/code/tokenize.js +145 -0
- package/dist/scenes/compare/index.d.ts +1 -0
- package/dist/scenes/compare/index.js +128 -0
- package/dist/scenes/compare/scene.d.ts +2 -0
- package/dist/scenes/compare/scene.js +58 -0
- package/dist/scenes/compare/style.css +187 -0
- package/dist/scenes/countdown/index.d.ts +1 -0
- package/dist/scenes/countdown/index.js +46 -0
- package/dist/scenes/countdown/scene.d.ts +2 -0
- package/dist/scenes/countdown/scene.js +21 -0
- package/dist/scenes/countdown/style.css +32 -0
- package/dist/scenes/flow/index.d.ts +1 -0
- package/dist/scenes/flow/index.js +66 -0
- package/dist/scenes/flow/inspect.d.ts +26 -0
- package/dist/scenes/flow/inspect.js +25 -0
- package/dist/scenes/flow/layout.d.ts +83 -0
- package/dist/scenes/flow/layout.js +222 -0
- package/dist/scenes/flow/measure.d.ts +15 -0
- package/dist/scenes/flow/measure.js +33 -0
- package/dist/scenes/flow/plan.d.ts +42 -0
- package/dist/scenes/flow/plan.js +117 -0
- package/dist/scenes/flow/scene.d.ts +2 -0
- package/dist/scenes/flow/scene.js +6 -0
- package/dist/scenes/flow/schema.d.ts +49 -0
- package/dist/scenes/flow/schema.js +60 -0
- package/dist/scenes/flow/style.css +54 -0
- package/dist/scenes/hero/index.d.ts +3 -0
- package/dist/scenes/hero/index.js +77 -0
- package/dist/scenes/hero/scene.d.ts +2 -0
- package/dist/scenes/hero/scene.js +6 -0
- package/dist/scenes/hero/style.css +143 -0
- package/dist/scenes/icon-grid/index.d.ts +1 -0
- package/dist/scenes/icon-grid/index.js +69 -0
- package/dist/scenes/icon-grid/scene.d.ts +2 -0
- package/dist/scenes/icon-grid/scene.js +6 -0
- package/dist/scenes/icon-grid/style.css +74 -0
- package/dist/scenes/kinetic/index.d.ts +1 -0
- package/dist/scenes/kinetic/index.js +62 -0
- package/dist/scenes/kinetic/scene.d.ts +2 -0
- package/dist/scenes/kinetic/scene.js +98 -0
- package/dist/scenes/kinetic/style.css +94 -0
- package/dist/scenes/kpi-grid/index.d.ts +1 -0
- package/dist/scenes/kpi-grid/index.js +161 -0
- package/dist/scenes/kpi-grid/scene.d.ts +2 -0
- package/dist/scenes/kpi-grid/scene.js +6 -0
- package/dist/scenes/kpi-grid/style.css +141 -0
- package/dist/scenes/lower-third/index.d.ts +1 -0
- package/dist/scenes/lower-third/index.js +33 -0
- package/dist/scenes/lower-third/scene.d.ts +2 -0
- package/dist/scenes/lower-third/scene.js +6 -0
- package/dist/scenes/lower-third/style.css +45 -0
- package/dist/scenes/media-frame/index.d.ts +1 -0
- package/dist/scenes/media-frame/index.js +99 -0
- package/dist/scenes/media-frame/scene.d.ts +2 -0
- package/dist/scenes/media-frame/scene.js +85 -0
- package/dist/scenes/media-frame/style.css +275 -0
- package/dist/scenes/outro/index.d.ts +1 -0
- package/dist/scenes/outro/index.js +54 -0
- package/dist/scenes/outro/scene.d.ts +2 -0
- package/dist/scenes/outro/scene.js +6 -0
- package/dist/scenes/outro/style.css +92 -0
- package/dist/scenes/poster/index.d.ts +3 -0
- package/dist/scenes/poster/index.js +62 -0
- package/dist/scenes/poster/scene.d.ts +2 -0
- package/dist/scenes/poster/scene.js +6 -0
- package/dist/scenes/poster/style.css +102 -0
- package/dist/scenes/progress/index.d.ts +1 -0
- package/dist/scenes/progress/index.js +40 -0
- package/dist/scenes/progress/scene.d.ts +2 -0
- package/dist/scenes/progress/scene.js +17 -0
- package/dist/scenes/progress/style.css +31 -0
- package/dist/scenes/quote/index.d.ts +3 -0
- package/dist/scenes/quote/index.js +69 -0
- package/dist/scenes/quote/scene.d.ts +2 -0
- package/dist/scenes/quote/scene.js +6 -0
- package/dist/scenes/quote/style.css +86 -0
- package/dist/scenes/scene3d/index.d.ts +1 -0
- package/dist/scenes/scene3d/index.js +59 -0
- package/dist/scenes/scene3d/scene.d.ts +2 -0
- package/dist/scenes/scene3d/scene.js +436 -0
- package/dist/scenes/scene3d/style.css +43 -0
- package/dist/scenes/stat/index.d.ts +4 -0
- package/dist/scenes/stat/index.js +72 -0
- package/dist/scenes/stat/scene.d.ts +2 -0
- package/dist/scenes/stat/scene.js +6 -0
- package/dist/scenes/stat/style.css +95 -0
- package/dist/scenes/steps/index.d.ts +1 -0
- package/dist/scenes/steps/index.js +147 -0
- package/dist/scenes/steps/scene.d.ts +2 -0
- package/dist/scenes/steps/scene.js +6 -0
- package/dist/scenes/steps/style.css +190 -0
- package/dist/scenes/sticker/index.d.ts +1 -0
- package/dist/scenes/sticker/index.js +48 -0
- package/dist/scenes/sticker/scene.d.ts +2 -0
- package/dist/scenes/sticker/scene.js +57 -0
- package/dist/scenes/sticker/style.css +88 -0
- package/dist/scenes/table/index.d.ts +1 -0
- package/dist/scenes/table/index.js +131 -0
- package/dist/scenes/table/scene.d.ts +2 -0
- package/dist/scenes/table/scene.js +6 -0
- package/dist/scenes/table/style.css +102 -0
- package/dist/scenes/timeline/index.d.ts +1 -0
- package/dist/scenes/timeline/index.js +68 -0
- package/dist/scenes/timeline/scene.d.ts +2 -0
- package/dist/scenes/timeline/scene.js +6 -0
- package/dist/scenes/timeline/style.css +161 -0
- package/dist/scenes/title/index.d.ts +1 -0
- package/dist/scenes/title/index.js +52 -0
- package/dist/scenes/title/scene.d.ts +2 -0
- package/dist/scenes/title/scene.js +6 -0
- package/dist/scenes/title/style.css +80 -0
- package/dist/scenes/waveform/index.d.ts +1 -0
- package/dist/scenes/waveform/index.js +39 -0
- package/dist/scenes/waveform/scene.d.ts +2 -0
- package/dist/scenes/waveform/scene.js +35 -0
- package/dist/scenes/waveform/style.css +44 -0
- package/dist/shared/fit-text.d.ts +24 -0
- package/dist/shared/fit-text.js +82 -0
- package/dist/shared/format.d.ts +24 -0
- package/dist/shared/format.js +59 -0
- package/dist/shared/frame.d.ts +14 -0
- package/dist/shared/frame.js +21 -0
- package/dist/shared/icon-search.d.ts +9 -0
- package/dist/shared/icon-search.js +26 -0
- package/dist/shared/icon.d.ts +15 -0
- package/dist/shared/icon.js +28 -0
- package/dist/shared/index.d.ts +11 -0
- package/dist/shared/index.js +7 -0
- package/dist/shared/markup.d.ts +12 -0
- package/dist/shared/markup.js +43 -0
- package/dist/shared/measure.d.ts +7 -0
- package/dist/shared/measure.js +34 -0
- package/dist/shared/motion.d.ts +94 -0
- package/dist/shared/motion.js +117 -0
- package/dist/source-hash.d.ts +4 -0
- package/dist/source-hash.js +26 -0
- package/dist/test-fixtures.d.ts +9 -0
- package/dist/test-fixtures.js +22 -0
- package/dist/text-file.d.ts +1 -0
- package/dist/text-file.js +3 -0
- package/dist/words.d.ts +11 -0
- package/dist/words.js +93 -0
- package/package.json +36 -3
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { DEFAULT_FONTS, EDGE_SAMPLES, edgeGeometry, edgeLabelRoom, layoutFlow, } from "./layout.js";
|
|
2
|
+
import { textWidth } from "./measure.js";
|
|
3
|
+
/** Room (px) around an edge label's text for its background halo. */
|
|
4
|
+
const HALO = 8;
|
|
5
|
+
/** Extra clearance used when testing a label against nodes and other edges. */
|
|
6
|
+
const CLEARANCE = 4;
|
|
7
|
+
const LINE_HEIGHT = 1.2;
|
|
8
|
+
/**
|
|
9
|
+
* Sample positions along an edge tried for its label, middle outwards (never within the first
|
|
10
|
+
* and last tenth, where the card and the arrow head are).
|
|
11
|
+
*/
|
|
12
|
+
const POSITIONS = Array.from({ length: EDGE_SAMPLES + 1 }, (_, i) => i)
|
|
13
|
+
.filter((i) => i >= EDGE_SAMPLES * 0.1 && i <= EDGE_SAMPLES * 0.85)
|
|
14
|
+
.sort((a, b) => Math.abs(a - EDGE_SAMPLES / 2) - Math.abs(b - EDGE_SAMPLES / 2) || a - b);
|
|
15
|
+
/** Sideways shifts (in label heights) tried after the label sat on its own edge found no clear spot. */
|
|
16
|
+
const SHIFTS = [0, -1, 1];
|
|
17
|
+
/** A clash with a card or another label costs this many clashes with a bare edge. */
|
|
18
|
+
const HARD_COST = 10;
|
|
19
|
+
const cornersOverlap = (a, b) => Math.abs(a.x - b.x) * 2 < a.w + b.w && Math.abs(a.y - b.y) * 2 < a.h + b.h;
|
|
20
|
+
const nodeBox = (node) => ({
|
|
21
|
+
x: node.x + node.w / 2,
|
|
22
|
+
y: node.y + node.h / 2,
|
|
23
|
+
w: node.w + 2 * CLEARANCE,
|
|
24
|
+
h: node.h + 2 * CLEARANCE,
|
|
25
|
+
});
|
|
26
|
+
const hitsPoint = (box, point) => Math.abs(point.x - box.x) * 2 <= box.w + CLEARANCE &&
|
|
27
|
+
Math.abs(point.y - box.y) * 2 <= box.h + CLEARANCE;
|
|
28
|
+
const labelBox = (text, font, at) => ({
|
|
29
|
+
x: at.x,
|
|
30
|
+
y: at.y,
|
|
31
|
+
w: textWidth(text, font) + 2 * HALO,
|
|
32
|
+
h: Math.ceil(font * LINE_HEIGHT) + HALO,
|
|
33
|
+
});
|
|
34
|
+
const isHard = (overlap) => !overlap.startsWith("edge ");
|
|
35
|
+
/** A label box at `sample`, moved `shift` label sizes sideways (across the edge's main direction). */
|
|
36
|
+
const shiftedBox = (text, font, sample, shift, direction) => {
|
|
37
|
+
const probe = labelBox(text, font, sample);
|
|
38
|
+
const side = Math.round(shift * ((direction === "lr" ? probe.h : probe.w) + CLEARANCE));
|
|
39
|
+
return labelBox(text, font, direction === "lr" ? { x: sample.x, y: sample.y + side } : { x: sample.x + side, y: sample.y });
|
|
40
|
+
};
|
|
41
|
+
/** Label boxes in order of preference: on the edge from the middle outwards, then to either side. */
|
|
42
|
+
function* candidates(text, font, around) {
|
|
43
|
+
const own = around.edges[around.index]?.geometry.samples ?? [];
|
|
44
|
+
for (const shift of SHIFTS) {
|
|
45
|
+
for (const position of POSITIONS) {
|
|
46
|
+
const sample = own[position] ?? { x: 0, y: 0 };
|
|
47
|
+
yield shiftedBox(text, font, sample, shift, around.direction);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** What a label box still touches: cards, earlier labels and the other edges. */
|
|
52
|
+
const overlapsOf = (box, around) => [
|
|
53
|
+
...around.nodes
|
|
54
|
+
.filter((node) => cornersOverlap(box, nodeBox(node)))
|
|
55
|
+
.map((node) => `node ${node.id}`),
|
|
56
|
+
...around.placed.flatMap((other, i) => other !== undefined && cornersOverlap(box, other) ? [`label ${i + 1}`] : []),
|
|
57
|
+
...around.edges.flatMap((other, i) => i !== around.index && other.geometry.samples.some((point) => hitsPoint(box, point))
|
|
58
|
+
? [`edge ${i + 1}`]
|
|
59
|
+
: []),
|
|
60
|
+
];
|
|
61
|
+
const costOf = (overlaps) => overlaps.reduce((sum, overlap) => sum + (isHard(overlap) ? HARD_COST : 1), 0);
|
|
62
|
+
/**
|
|
63
|
+
* Where one label sits: the first candidate clear of every card, earlier label and other edge
|
|
64
|
+
* wins; otherwise the cheapest one (cards and labels weigh more than a crossing edge).
|
|
65
|
+
*/
|
|
66
|
+
const placeLabel = (text, font, around) => {
|
|
67
|
+
let best;
|
|
68
|
+
let bestCost = Number.POSITIVE_INFINITY;
|
|
69
|
+
for (const box of candidates(text, font, around)) {
|
|
70
|
+
const overlaps = overlapsOf(box, around);
|
|
71
|
+
const cost = costOf(overlaps);
|
|
72
|
+
if (cost === 0)
|
|
73
|
+
return { ...box, text, overlaps };
|
|
74
|
+
if (cost < bestCost) {
|
|
75
|
+
best = { ...box, text, overlaps };
|
|
76
|
+
bestCost = cost;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return best ?? { ...labelBox(text, font, { x: 0, y: 0 }), text, overlaps: [] };
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* The whole diagram layout: cards sized from their labels, edge geometry, and edge labels
|
|
83
|
+
* placed along their edges so they avoid cards, other edges and each other. Pure and
|
|
84
|
+
* deterministic; the scene renders it and `mk validate` lints it.
|
|
85
|
+
*/
|
|
86
|
+
export const planFlow = (graph, size, fonts = DEFAULT_FONTS) => {
|
|
87
|
+
const room = edgeLabelRoom(graph.edges, graph.direction, fonts.sm);
|
|
88
|
+
const minRoom = edgeLabelRoom(graph.edges, graph.direction, fonts.sm, 1);
|
|
89
|
+
const nodes = layoutFlow(graph.nodes, graph.edges, graph.direction, size, room, fonts, minRoom);
|
|
90
|
+
const byId = new Map(nodes.map((node) => [node.id, node]));
|
|
91
|
+
const geometries = graph.edges.map((edge) => {
|
|
92
|
+
const from = byId.get(edge.from);
|
|
93
|
+
const to = byId.get(edge.to);
|
|
94
|
+
if (!from || !to)
|
|
95
|
+
return undefined;
|
|
96
|
+
return { edge, geometry: edgeGeometry(from, to, graph.direction, to.layer > from.layer) };
|
|
97
|
+
});
|
|
98
|
+
const known = geometries.flatMap((row) => (row === undefined ? [] : [row]));
|
|
99
|
+
const placed = [];
|
|
100
|
+
const edges = known.map((row, index) => {
|
|
101
|
+
const text = row.edge.label;
|
|
102
|
+
if (text === undefined || text === "") {
|
|
103
|
+
placed.push(undefined);
|
|
104
|
+
return row;
|
|
105
|
+
}
|
|
106
|
+
const label = placeLabel(text, fonts.sm, {
|
|
107
|
+
index,
|
|
108
|
+
direction: graph.direction,
|
|
109
|
+
edges: known,
|
|
110
|
+
nodes,
|
|
111
|
+
placed,
|
|
112
|
+
});
|
|
113
|
+
placed.push(label);
|
|
114
|
+
return { ...row, label };
|
|
115
|
+
});
|
|
116
|
+
return { nodes, edges, font: nodes[0]?.font ?? fonts.md, edgeFont: fonts.sm };
|
|
117
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { mountTimeline } from "../../runtime/mount.js";
|
|
3
|
+
/** @type {import("../../runtime/mount.js").CreateScene} */
|
|
4
|
+
export function createScene(root, config, ctx) {
|
|
5
|
+
return mountTimeline(root, config, ctx, { baseDelay: 0.2, staggerFrames: 6, rise: 24 });
|
|
6
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
declare const graphSchema: z.ZodObject<{
|
|
3
|
+
nodes: z.ZodArray<z.ZodObject<{
|
|
4
|
+
id: z.ZodString;
|
|
5
|
+
label: z.ZodString;
|
|
6
|
+
}, z.core.$strict>>;
|
|
7
|
+
edges: z.ZodArray<z.ZodObject<{
|
|
8
|
+
from: z.ZodString;
|
|
9
|
+
to: z.ZodString;
|
|
10
|
+
label: z.ZodOptional<z.ZodString>;
|
|
11
|
+
}, z.core.$strict>>;
|
|
12
|
+
direction: z.ZodDefault<z.ZodEnum<{
|
|
13
|
+
lr: "lr";
|
|
14
|
+
tb: "tb";
|
|
15
|
+
}>>;
|
|
16
|
+
}, z.core.$strict>;
|
|
17
|
+
export type Graph = z.output<typeof graphSchema>;
|
|
18
|
+
/** A valid flow graph (ids, edges, node count) without the word budget. */
|
|
19
|
+
export declare const flowGraphSchema: z.ZodObject<{
|
|
20
|
+
nodes: z.ZodArray<z.ZodObject<{
|
|
21
|
+
id: z.ZodString;
|
|
22
|
+
label: z.ZodString;
|
|
23
|
+
}, z.core.$strict>>;
|
|
24
|
+
edges: z.ZodArray<z.ZodObject<{
|
|
25
|
+
from: z.ZodString;
|
|
26
|
+
to: z.ZodString;
|
|
27
|
+
label: z.ZodOptional<z.ZodString>;
|
|
28
|
+
}, z.core.$strict>>;
|
|
29
|
+
direction: z.ZodDefault<z.ZodEnum<{
|
|
30
|
+
lr: "lr";
|
|
31
|
+
tb: "tb";
|
|
32
|
+
}>>;
|
|
33
|
+
}, z.core.$strict>;
|
|
34
|
+
export declare const configSchema: z.ZodObject<{
|
|
35
|
+
nodes: z.ZodArray<z.ZodObject<{
|
|
36
|
+
id: z.ZodString;
|
|
37
|
+
label: z.ZodString;
|
|
38
|
+
}, z.core.$strict>>;
|
|
39
|
+
edges: z.ZodArray<z.ZodObject<{
|
|
40
|
+
from: z.ZodString;
|
|
41
|
+
to: z.ZodString;
|
|
42
|
+
label: z.ZodOptional<z.ZodString>;
|
|
43
|
+
}, z.core.$strict>>;
|
|
44
|
+
direction: z.ZodDefault<z.ZodEnum<{
|
|
45
|
+
lr: "lr";
|
|
46
|
+
tb: "tb";
|
|
47
|
+
}>>;
|
|
48
|
+
}, z.core.$strict>;
|
|
49
|
+
export {};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { limitWords, text } from "../../words.js";
|
|
3
|
+
const NODE_COUNT_MESSAGE = "need 2-8 nodes";
|
|
4
|
+
const nodeSchema = z.strictObject({
|
|
5
|
+
id: z.string().regex(/^[a-z0-9_-]+$/i, "id must be a-z, 0-9, _ or -"),
|
|
6
|
+
label: text,
|
|
7
|
+
});
|
|
8
|
+
const edgeSchema = z.strictObject({ from: z.string(), to: z.string(), label: text.optional() });
|
|
9
|
+
const graphSchema = z.strictObject({
|
|
10
|
+
nodes: z.array(nodeSchema).min(2, NODE_COUNT_MESSAGE).max(8, NODE_COUNT_MESSAGE),
|
|
11
|
+
edges: z.array(edgeSchema),
|
|
12
|
+
direction: z.enum(["lr", "tb"]).default("lr"),
|
|
13
|
+
});
|
|
14
|
+
const checkGraph = (graph, ctx) => {
|
|
15
|
+
const known = new Set();
|
|
16
|
+
const edgeKeys = new Set();
|
|
17
|
+
graph.nodes.forEach((node, index) => {
|
|
18
|
+
if (known.has(node.id)) {
|
|
19
|
+
ctx.addIssue({
|
|
20
|
+
code: "custom",
|
|
21
|
+
path: ["nodes", index, "id"],
|
|
22
|
+
message: `duplicate node id "${node.id}"`,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
known.add(node.id);
|
|
26
|
+
});
|
|
27
|
+
graph.edges.forEach((edge, index) => {
|
|
28
|
+
for (const end of ["from", "to"]) {
|
|
29
|
+
if (!known.has(edge[end])) {
|
|
30
|
+
ctx.addIssue({
|
|
31
|
+
code: "custom",
|
|
32
|
+
path: ["edges", index, end],
|
|
33
|
+
message: `unknown node "${edge[end]}"`,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const key = JSON.stringify([edge.from, edge.to]);
|
|
38
|
+
if (edgeKeys.has(key)) {
|
|
39
|
+
ctx.addIssue({
|
|
40
|
+
code: "custom",
|
|
41
|
+
path: ["edges", index, "to"],
|
|
42
|
+
message: `duplicate edge "${edge.from}" -> "${edge.to}"`,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
edgeKeys.add(key);
|
|
46
|
+
if (edge.from === edge.to) {
|
|
47
|
+
ctx.addIssue({
|
|
48
|
+
code: "custom",
|
|
49
|
+
path: ["edges", index, "to"],
|
|
50
|
+
message: "edge needs two nodes",
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
/** A valid flow graph (ids, edges, node count) without the word budget. */
|
|
56
|
+
export const flowGraphSchema = graphSchema.superRefine(checkGraph);
|
|
57
|
+
export const configSchema = limitWords(flowGraphSchema, (props) => [
|
|
58
|
+
...props.nodes.map((node) => node.label),
|
|
59
|
+
...props.edges.flatMap((edge) => (edge.label ? [edge.label] : [])),
|
|
60
|
+
]);
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#scene-root {
|
|
2
|
+
padding: 0;
|
|
3
|
+
}
|
|
4
|
+
#scene-root .mk-edges {
|
|
5
|
+
position: absolute;
|
|
6
|
+
left: 0;
|
|
7
|
+
top: 0;
|
|
8
|
+
}
|
|
9
|
+
#scene-root .mk-edge-line {
|
|
10
|
+
fill: none;
|
|
11
|
+
stroke: var(--mk-color-accent);
|
|
12
|
+
stroke-width: 4px;
|
|
13
|
+
stroke-linecap: round;
|
|
14
|
+
}
|
|
15
|
+
#scene-root .mk-edge-head {
|
|
16
|
+
fill: var(--mk-color-accent);
|
|
17
|
+
}
|
|
18
|
+
#scene-root .mk-edge-label {
|
|
19
|
+
font-size: var(--mk-font-size-sm);
|
|
20
|
+
font-weight: 600;
|
|
21
|
+
letter-spacing: 0.02em;
|
|
22
|
+
fill: var(--mk-color-fg);
|
|
23
|
+
text-anchor: middle;
|
|
24
|
+
dominant-baseline: central;
|
|
25
|
+
paint-order: stroke;
|
|
26
|
+
stroke: var(--mk-color-bg);
|
|
27
|
+
stroke-width: 14px;
|
|
28
|
+
stroke-linejoin: round;
|
|
29
|
+
}
|
|
30
|
+
#scene-root .mk-node {
|
|
31
|
+
position: absolute;
|
|
32
|
+
display: flex;
|
|
33
|
+
align-items: center;
|
|
34
|
+
justify-content: center;
|
|
35
|
+
flex-direction: column;
|
|
36
|
+
padding: 0 24px;
|
|
37
|
+
text-align: center;
|
|
38
|
+
white-space: nowrap;
|
|
39
|
+
font-weight: 700;
|
|
40
|
+
line-height: 1.2;
|
|
41
|
+
color: var(--mk-color-fg);
|
|
42
|
+
background: var(--mk-color-card);
|
|
43
|
+
border: var(--mk-stroke-card, 2px) solid var(--mk-color-divider);
|
|
44
|
+
border-radius: var(--mk-radius-xl);
|
|
45
|
+
box-shadow: var(
|
|
46
|
+
--mk-shadow-card,
|
|
47
|
+
0 24px 56px -24px color-mix(in srgb, var(--mk-color-fg) 40%, transparent)
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
#scene-root .mk-node.mk-src {
|
|
51
|
+
color: var(--mk-color-onAccent);
|
|
52
|
+
background: var(--mk-color-accent);
|
|
53
|
+
border-color: var(--mk-color-accent);
|
|
54
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { defineScene } from "../../define-scene.js";
|
|
3
|
+
import { animAttrs, esc } from "../../html.js";
|
|
4
|
+
import { frameCss } from "../../shared/frame.js";
|
|
5
|
+
import { accentHtml, stripAccent } from "../../shared/markup.js";
|
|
6
|
+
import { readText } from "../../text-file.js";
|
|
7
|
+
import { limitWords, present, text } from "../../words.js";
|
|
8
|
+
const LAYOUTS = ["bento", "center"];
|
|
9
|
+
/** One glass tile: a number, a label and an optional detail line. */
|
|
10
|
+
const tile = z.strictObject({
|
|
11
|
+
value: text.max(10).optional(),
|
|
12
|
+
label: text.max(24),
|
|
13
|
+
detail: text.max(32).optional(),
|
|
14
|
+
});
|
|
15
|
+
const configSchema = limitWords(z.strictObject({
|
|
16
|
+
title: text.max(70),
|
|
17
|
+
subtitle: text.max(120).optional(),
|
|
18
|
+
kicker: text.max(28).optional(),
|
|
19
|
+
tiles: z.array(tile).max(4).optional(),
|
|
20
|
+
layout: z.enum(LAYOUTS).default("bento"),
|
|
21
|
+
}), (props) => [
|
|
22
|
+
...present(stripAccent(props.title), props.subtitle, props.kicker),
|
|
23
|
+
...(props.tiles ?? []).flatMap((item) => present(item.value, item.label, item.detail)),
|
|
24
|
+
]);
|
|
25
|
+
/** Shorter headlines get the next size up, so the type fills the frame instead of floating in it. */
|
|
26
|
+
export const sizeClass = (visible) => {
|
|
27
|
+
if (visible <= 26)
|
|
28
|
+
return "mk-size-1";
|
|
29
|
+
return visible <= 46 ? "mk-size-2" : "mk-size-3";
|
|
30
|
+
};
|
|
31
|
+
const tileHtml = (item, step, index) => {
|
|
32
|
+
const value = item.value ? `<span class="mk-tile-value">${esc(item.value)}</span>` : "";
|
|
33
|
+
const detail = item.detail ? `<span class="mk-tile-detail">${esc(item.detail)}</span>` : "";
|
|
34
|
+
return `<div class="mk-tile" ${animAttrs("rise", step, `tile-${index}`)}>
|
|
35
|
+
${value}<span class="mk-tile-label">${esc(item.label)}</span>${detail}
|
|
36
|
+
</div>`;
|
|
37
|
+
};
|
|
38
|
+
const render = (props) => {
|
|
39
|
+
const lines = [];
|
|
40
|
+
let step = 0;
|
|
41
|
+
if (props.kicker) {
|
|
42
|
+
lines.push(`<p class="mk-kicker" ${animAttrs("rise", step, "kicker")}><i class="mk-kicker-dot"></i>${esc(props.kicker)}</p>`);
|
|
43
|
+
step += 1;
|
|
44
|
+
}
|
|
45
|
+
lines.push(`<h1 class="mk-title" ${animAttrs("rise", step, "title")}>${accentHtml(props.title)}</h1>`);
|
|
46
|
+
step += 1;
|
|
47
|
+
if (props.subtitle) {
|
|
48
|
+
lines.push(`<p class="mk-sub" ${animAttrs("rise", step, "subtitle")}>${esc(props.subtitle)}</p>`);
|
|
49
|
+
step += 1;
|
|
50
|
+
}
|
|
51
|
+
const tiles = (props.tiles ?? []).map((item, index) => tileHtml(item, step + index, index));
|
|
52
|
+
const grid = tiles.length > 0 ? `<div class="mk-tiles">\n${tiles.join("\n")}\n</div>` : "";
|
|
53
|
+
return `<div class="mk-hero ${sizeClass(stripAccent(props.title).length)}" data-layout="${props.layout}">
|
|
54
|
+
<div class="mk-mesh" aria-hidden="true"></div>
|
|
55
|
+
<div class="mk-copy">
|
|
56
|
+
${lines.join("\n")}
|
|
57
|
+
</div>
|
|
58
|
+
${grid}
|
|
59
|
+
</div>`;
|
|
60
|
+
};
|
|
61
|
+
/** Tall frames stack the copy above the tiles and drop the two-column bento. */
|
|
62
|
+
const TALL_CSS = `#scene-root .mk-hero { grid-template-columns: minmax(0, 1fr); align-content: center; }
|
|
63
|
+
#scene-root .mk-tiles { grid-template-columns: repeat(2, minmax(0, 1fr)); }`;
|
|
64
|
+
export const heroScene = defineScene({
|
|
65
|
+
type: "hero",
|
|
66
|
+
configSchema,
|
|
67
|
+
meta: {
|
|
68
|
+
purpose: "Landing-page hero: kicker, big headline with [x] accents, subline and up to 4 glass tiles; bento or centered.",
|
|
69
|
+
example: { title: "Design at the [speed] of thought", layout: "bento" },
|
|
70
|
+
},
|
|
71
|
+
style: readText("./style.css", import.meta.url),
|
|
72
|
+
script: readText("./scene.js", import.meta.url),
|
|
73
|
+
render: (props, ctx, root) => ({
|
|
74
|
+
body: render(props),
|
|
75
|
+
css: frameCss(ctx, root, { tall: TALL_CSS }),
|
|
76
|
+
}),
|
|
77
|
+
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { mountTimeline } from "../../runtime/mount.js";
|
|
3
|
+
/** @type {import("../../runtime/mount.js").CreateScene} */
|
|
4
|
+
export function createScene(root, config, ctx) {
|
|
5
|
+
return mountTimeline(root, config, ctx, { baseDelay: 0.25, staggerFrames: 5, rise: 44 });
|
|
6
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#scene-root .mk-hero {
|
|
2
|
+
position: relative;
|
|
3
|
+
display: grid;
|
|
4
|
+
grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr);
|
|
5
|
+
align-items: center;
|
|
6
|
+
gap: var(--mk-space-xl);
|
|
7
|
+
width: 100%;
|
|
8
|
+
}
|
|
9
|
+
#scene-root .mk-hero[data-layout="center"] {
|
|
10
|
+
grid-template-columns: minmax(0, 1fr);
|
|
11
|
+
justify-items: center;
|
|
12
|
+
gap: var(--mk-space-lg);
|
|
13
|
+
text-align: center;
|
|
14
|
+
}
|
|
15
|
+
#scene-root .mk-mesh {
|
|
16
|
+
position: absolute;
|
|
17
|
+
inset: -22%;
|
|
18
|
+
z-index: -1;
|
|
19
|
+
pointer-events: none;
|
|
20
|
+
filter: blur(30px);
|
|
21
|
+
transform: translate3d(calc(var(--drift, 0) * 7px), calc(var(--drift, 0) * -5px), 0);
|
|
22
|
+
background:
|
|
23
|
+
radial-gradient(
|
|
24
|
+
40% 44% at 16% 10%,
|
|
25
|
+
color-mix(in srgb, var(--mk-color-accent) 40%, transparent) 0%,
|
|
26
|
+
transparent 70%
|
|
27
|
+
),
|
|
28
|
+
radial-gradient(
|
|
29
|
+
36% 40% at 88% 82%,
|
|
30
|
+
color-mix(in srgb, var(--mk-color-accent2) 32%, transparent) 0%,
|
|
31
|
+
transparent 72%
|
|
32
|
+
),
|
|
33
|
+
radial-gradient(
|
|
34
|
+
48% 52% at 64% 4%,
|
|
35
|
+
color-mix(in srgb, var(--mk-color-deep) 58%, transparent) 0%,
|
|
36
|
+
transparent 76%
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
#scene-root .mk-copy {
|
|
40
|
+
display: flex;
|
|
41
|
+
flex-direction: column;
|
|
42
|
+
gap: var(--mk-space-md);
|
|
43
|
+
max-width: 100%;
|
|
44
|
+
}
|
|
45
|
+
#scene-root .mk-hero[data-layout="center"] .mk-copy {
|
|
46
|
+
align-items: center;
|
|
47
|
+
}
|
|
48
|
+
#scene-root .mk-kicker {
|
|
49
|
+
display: inline-flex;
|
|
50
|
+
align-items: center;
|
|
51
|
+
gap: var(--mk-space-sm);
|
|
52
|
+
align-self: flex-start;
|
|
53
|
+
padding: 10px 20px;
|
|
54
|
+
border-radius: var(--mk-radius-full);
|
|
55
|
+
border: 1px solid color-mix(in srgb, var(--mk-color-fg) 16%, transparent);
|
|
56
|
+
background: color-mix(in srgb, var(--mk-color-surface) 55%, transparent);
|
|
57
|
+
color: var(--mk-color-muted);
|
|
58
|
+
font-size: var(--mk-font-size-sm);
|
|
59
|
+
font-weight: 600;
|
|
60
|
+
letter-spacing: 0.04em;
|
|
61
|
+
text-transform: uppercase;
|
|
62
|
+
}
|
|
63
|
+
#scene-root .mk-hero[data-layout="center"] .mk-kicker {
|
|
64
|
+
align-self: center;
|
|
65
|
+
}
|
|
66
|
+
#scene-root .mk-kicker-dot {
|
|
67
|
+
width: 10px;
|
|
68
|
+
height: 10px;
|
|
69
|
+
border-radius: var(--mk-radius-full);
|
|
70
|
+
background: var(--mk-color-accent2);
|
|
71
|
+
}
|
|
72
|
+
#scene-root .mk-title {
|
|
73
|
+
font-family: var(--mk-font-display, var(--mk-font-sans));
|
|
74
|
+
font-weight: 800;
|
|
75
|
+
line-height: 1.02;
|
|
76
|
+
letter-spacing: -0.03em;
|
|
77
|
+
color: var(--mk-color-fg);
|
|
78
|
+
text-wrap: balance;
|
|
79
|
+
}
|
|
80
|
+
#scene-root .mk-size-1 .mk-title {
|
|
81
|
+
font-size: var(--mk-font-size-display);
|
|
82
|
+
}
|
|
83
|
+
#scene-root .mk-size-2 .mk-title {
|
|
84
|
+
font-size: var(--mk-font-size-title);
|
|
85
|
+
}
|
|
86
|
+
#scene-root .mk-size-3 .mk-title {
|
|
87
|
+
font-size: var(--mk-font-size-xl);
|
|
88
|
+
}
|
|
89
|
+
#scene-root .mk-title .accent {
|
|
90
|
+
background: linear-gradient(96deg, var(--mk-color-accent), var(--mk-color-accent2));
|
|
91
|
+
-webkit-background-clip: text;
|
|
92
|
+
background-clip: text;
|
|
93
|
+
color: transparent;
|
|
94
|
+
}
|
|
95
|
+
#scene-root .mk-sub {
|
|
96
|
+
max-width: 34ch;
|
|
97
|
+
color: var(--mk-color-muted);
|
|
98
|
+
font-size: var(--mk-font-size-lg);
|
|
99
|
+
line-height: 1.32;
|
|
100
|
+
}
|
|
101
|
+
#scene-root .mk-hero[data-layout="center"] .mk-sub {
|
|
102
|
+
max-width: 48ch;
|
|
103
|
+
}
|
|
104
|
+
#scene-root .mk-tiles {
|
|
105
|
+
display: grid;
|
|
106
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
107
|
+
gap: var(--mk-space-md);
|
|
108
|
+
width: 100%;
|
|
109
|
+
}
|
|
110
|
+
#scene-root .mk-hero[data-layout="center"] .mk-tiles {
|
|
111
|
+
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
112
|
+
max-width: 100%;
|
|
113
|
+
}
|
|
114
|
+
#scene-root .mk-tile {
|
|
115
|
+
display: flex;
|
|
116
|
+
flex-direction: column;
|
|
117
|
+
gap: var(--mk-space-xs);
|
|
118
|
+
padding: var(--mk-space-lg);
|
|
119
|
+
border-radius: var(--mk-radius-lg);
|
|
120
|
+
border: 1px solid color-mix(in srgb, var(--mk-color-fg) 13%, transparent);
|
|
121
|
+
background: color-mix(in srgb, var(--mk-color-surface) 60%, transparent);
|
|
122
|
+
backdrop-filter: blur(18px) saturate(140%);
|
|
123
|
+
box-shadow:
|
|
124
|
+
inset 0 1px 0 color-mix(in srgb, var(--mk-color-fg) 14%, transparent),
|
|
125
|
+
0 24px 48px -32px color-mix(in srgb, var(--mk-color-bg) 85%, transparent);
|
|
126
|
+
}
|
|
127
|
+
#scene-root .mk-tile-value {
|
|
128
|
+
font-family: var(--mk-font-display, var(--mk-font-sans));
|
|
129
|
+
font-size: var(--mk-font-size-lg);
|
|
130
|
+
font-weight: 800;
|
|
131
|
+
letter-spacing: -0.02em;
|
|
132
|
+
color: var(--mk-color-accent2);
|
|
133
|
+
}
|
|
134
|
+
#scene-root .mk-tile-label {
|
|
135
|
+
font-size: var(--mk-font-size-md);
|
|
136
|
+
font-weight: 600;
|
|
137
|
+
color: var(--mk-color-fg);
|
|
138
|
+
}
|
|
139
|
+
#scene-root .mk-tile-detail {
|
|
140
|
+
font-size: var(--mk-font-size-sm);
|
|
141
|
+
line-height: 1.3;
|
|
142
|
+
color: var(--mk-color-muted);
|
|
143
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const iconGridScene: import("@anvpx/motion-schema").ScenePlugin;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { defineScene } from "../../define-scene.js";
|
|
3
|
+
import { animAttrs, esc } from "../../html.js";
|
|
4
|
+
import { iconProblem, sceneIcon } from "../../shared/icon.js";
|
|
5
|
+
import { readText } from "../../text-file.js";
|
|
6
|
+
import { limitWords, present, text } from "../../words.js";
|
|
7
|
+
const COUNT_MESSAGE = "need 2-8 items";
|
|
8
|
+
const ICON_SIZE = 64;
|
|
9
|
+
const itemSchema = z.strictObject({
|
|
10
|
+
icon: text.superRefine((name, ctx) => {
|
|
11
|
+
const problem = iconProblem(name);
|
|
12
|
+
if (problem !== undefined)
|
|
13
|
+
ctx.addIssue({ code: "custom", message: problem });
|
|
14
|
+
}),
|
|
15
|
+
label: text,
|
|
16
|
+
detail: text.optional(),
|
|
17
|
+
});
|
|
18
|
+
const configSchema = limitWords(z.strictObject({
|
|
19
|
+
title: text.optional(),
|
|
20
|
+
items: z.array(itemSchema).min(2, COUNT_MESSAGE).max(8, COUNT_MESSAGE),
|
|
21
|
+
cols: z.number().int().min(2).max(4).optional(),
|
|
22
|
+
}), (props) => props.items.flatMap((item) => present(item.label, item.detail)).concat(present(props.title)));
|
|
23
|
+
/** Balanced default: one row up to 4 items, then 3 or 4 columns. */
|
|
24
|
+
const defaultCols = (count) => {
|
|
25
|
+
if (count <= 4)
|
|
26
|
+
return Math.max(2, count);
|
|
27
|
+
return count <= 6 ? 3 : 4;
|
|
28
|
+
};
|
|
29
|
+
const card = (item, index, offset) => {
|
|
30
|
+
const icon = sceneIcon(item.icon, { at: `props.items[${index}].icon`, size: ICON_SIZE });
|
|
31
|
+
const detail = item.detail ? `\n<p class="mk-detail">${esc(item.detail)}</p>` : "";
|
|
32
|
+
const attrs = animAttrs("rise", index + offset, `item-${index + 1}`);
|
|
33
|
+
return `<li class="mk-card" ${attrs}>
|
|
34
|
+
<span class="mk-chip">${icon}</span>
|
|
35
|
+
<p class="mk-label">${esc(item.label)}</p>${detail}
|
|
36
|
+
</li>`;
|
|
37
|
+
};
|
|
38
|
+
const render = (props) => {
|
|
39
|
+
const offset = props.title ? 1 : 0;
|
|
40
|
+
const head = props.title
|
|
41
|
+
? `<div class="mk-head">
|
|
42
|
+
<h1 class="mk-heading" ${animAttrs("rise", 0, "title")}>${esc(props.title)}</h1>
|
|
43
|
+
<span class="mk-rule" ${animAttrs("grow", 0, "title")}></span>
|
|
44
|
+
</div>
|
|
45
|
+
`
|
|
46
|
+
: "";
|
|
47
|
+
const cols = props.cols ?? defaultCols(props.items.length);
|
|
48
|
+
const cards = props.items.map((item, index) => card(item, index, offset));
|
|
49
|
+
const dense = cols === 4 && props.items.length > 4 ? " mk-dense" : "";
|
|
50
|
+
return `${head}<ul class="mk-grid mk-cols-${cols}${dense}">\n${cards.join("\n")}\n</ul>`;
|
|
51
|
+
};
|
|
52
|
+
export const iconGridScene = defineScene({
|
|
53
|
+
type: "icon-grid",
|
|
54
|
+
configSchema,
|
|
55
|
+
meta: {
|
|
56
|
+
purpose: "Features, benefits or categories as 2-8 icon cards with a short label and optional detail.",
|
|
57
|
+
example: {
|
|
58
|
+
title: "Why teams switch",
|
|
59
|
+
items: [
|
|
60
|
+
{ icon: "zap", label: "Fast setup", detail: "Live in a day" },
|
|
61
|
+
{ icon: "shield", label: "Secure by default" },
|
|
62
|
+
{ icon: "globe", label: "Works worldwide" },
|
|
63
|
+
],
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
style: readText("./style.css", import.meta.url),
|
|
67
|
+
script: readText("./scene.js", import.meta.url),
|
|
68
|
+
render: (props) => ({ body: render(props) }),
|
|
69
|
+
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { mountTimeline } from "../../runtime/mount.js";
|
|
3
|
+
/** @type {import("../../runtime/mount.js").CreateScene} */
|
|
4
|
+
export function createScene(root, config, ctx) {
|
|
5
|
+
return mountTimeline(root, config, ctx, { baseDelay: 0.2, staggerFrames: 4, rise: 36 });
|
|
6
|
+
}
|