flowfig 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +152 -23
- package/dist/atlas.js +6 -2
- package/dist/auto.js +104 -9
- package/dist/browser.d.ts +1 -0
- package/dist/browser.js +9 -4
- package/dist/check.d.ts +4 -3
- package/dist/check.js +143 -8
- package/dist/cli.js +180 -70
- package/dist/diff.d.ts +2 -0
- package/dist/diff.js +79 -24
- package/dist/element-looks.js +4 -0
- package/dist/element-world.js +6 -0
- package/dist/element.d.ts +27 -0
- package/dist/element.js +67 -0
- package/dist/explain.d.ts +45 -0
- package/dist/explain.js +161 -0
- package/dist/frames.d.ts +34 -0
- package/dist/frames.js +117 -0
- package/dist/index.js +328 -152
- package/dist/load.js +9 -4
- package/dist/looks.d.ts +15 -0
- package/dist/looks.js +32 -0
- package/dist/mcp.d.ts +4 -0
- package/dist/mcp.js +14 -6
- package/dist/model.d.ts +105 -3
- package/dist/model.js +221 -21
- package/dist/scene.d.ts +2 -0
- package/dist/source.d.ts +4 -2
- package/dist/source.js +15 -9
- package/dist/svg.d.ts +12 -0
- package/dist/svg.js +245 -81
- package/dist/verify.d.ts +6 -0
- package/dist/verify.js +104 -56
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/world-art.d.ts +59 -0
- package/dist/world-art.js +854 -0
- package/dist/world.d.ts +233 -0
- package/dist/world.js +1577 -0
- package/package.json +9 -3
package/dist/source.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// No Node import: the React and SVG entries must stay browser-safe.
|
|
2
|
-
import { edgeId, nodes, toBeat } from './model.js';
|
|
2
|
+
import { edgeId, nodes, scenes, toBeat } from './model.js';
|
|
3
3
|
/** `path` or `path#symbol`. No spaces, one `#` at most, both parts non-empty. `null` for a bad form. */
|
|
4
4
|
export function parseSource(s) {
|
|
5
5
|
const m = /^([^\s#]+)(?:#([^\s#]+))?$/.exec(s);
|
|
@@ -12,18 +12,24 @@ export const headingSlug = (text) => text
|
|
|
12
12
|
.toLowerCase()
|
|
13
13
|
.replace(/[^\p{L}\p{N} -]/gu, '')
|
|
14
14
|
.replace(/ /g, '-');
|
|
15
|
-
/** Every box, edge and hop with its raw `source`, well-formed or not. The one owner list that `links` and `checkSpec` share. */
|
|
16
15
|
export function owners(fig) {
|
|
17
|
-
return
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
return scenes(fig).flatMap((s) => {
|
|
17
|
+
const ports = new Set(s.ports.map((p) => p.id));
|
|
18
|
+
const at = s.line ? ` [${s.line}]` : '';
|
|
19
|
+
const kind = s.line ? 'part' : 'box';
|
|
20
|
+
return [
|
|
21
|
+
...nodes(s.props.layout)
|
|
22
|
+
.filter((n) => !ports.has(n.id))
|
|
23
|
+
.map((n) => [`${kind} "${n.id}"${at}`, n.source, undefined, s.path.join('>')]),
|
|
24
|
+
...s.props.edges.map((e) => [`edge "${edgeId(e)}"${at}`, e.source, e.via, s.path.join('>')]),
|
|
25
|
+
...(s.props.steps ?? []).flatMap((st) => st.flow.flatMap((b) => toBeat(b).hops.map((h) => [`hop on "${h.edge}"${at}`, h.source, h.via, s.path.join('>')]))),
|
|
26
|
+
];
|
|
27
|
+
});
|
|
22
28
|
}
|
|
23
29
|
/** Every well-formed link in the figure, with the box, edge or hop that carries it. `bad-source` covers the rest. */
|
|
24
30
|
export function links(fig) {
|
|
25
|
-
return owners(fig).flatMap(([owner, source]) => {
|
|
31
|
+
return owners(fig).flatMap(([owner, source, , level]) => {
|
|
26
32
|
const p = source == null ? null : parseSource(source);
|
|
27
|
-
return p ? [{ owner, source: source, ...p }] : [];
|
|
33
|
+
return p ? [{ owner, source: source, ...p, ...(level && { level }) }] : [];
|
|
28
34
|
});
|
|
29
35
|
}
|
package/dist/svg.d.ts
CHANGED
|
@@ -23,11 +23,23 @@ export type SvgOptions = {
|
|
|
23
23
|
marks?: DiffMarks;
|
|
24
24
|
/** Links a box to an href, by box id. `flowfig atlas` sets it. */
|
|
25
25
|
links?: Record<string, string>;
|
|
26
|
+
/** Draws the level of this box as a plain figure: no tour, no script. */
|
|
27
|
+
scene?: string;
|
|
28
|
+
/** Adds the script that opens a box on a click, for a figure with parts. */
|
|
29
|
+
script?: boolean;
|
|
30
|
+
/** A prefix for animation names and path ids. Each page SVG needs its own. */
|
|
31
|
+
scope?: string;
|
|
32
|
+
};
|
|
33
|
+
export type Level = {
|
|
34
|
+
line: string;
|
|
35
|
+
props: FlowProps;
|
|
36
|
+
scene: Scene;
|
|
26
37
|
};
|
|
27
38
|
/** The SVG string and the scene it drew. The scene shape may change; use `toSvg` for the SVG only. */
|
|
28
39
|
export declare function render(spec: FlowProps, opts?: SvgOptions): {
|
|
29
40
|
svg: string;
|
|
30
41
|
scene: Scene;
|
|
42
|
+
levels?: Level[];
|
|
31
43
|
};
|
|
32
44
|
/** One self-contained animated SVG string for the figure. Needs no React and no browser. */
|
|
33
45
|
export declare function toSvg(fig: FlowProps, opts?: SvgOptions): string;
|
package/dist/svg.js
CHANGED
|
@@ -6,17 +6,20 @@ import { checkRendered, planFor } from './check.js';
|
|
|
6
6
|
import { autoLayout, rowBreaks } from './auto.js';
|
|
7
7
|
export { parseSource, links } from './source.js';
|
|
8
8
|
export { diff, formatDiff, mergeFigures } from './diff.js';
|
|
9
|
-
import { ASYNC_TAG_W, BASE_RATE, CARD_LINE, CARD_PAD, CARD_WIDTH, DARK, EDGE_OFF, EDGE_ON, LIGHT, TONES, toneFill, toneTint, labelPillW, ON_ACCENT, str, edgeId, groupGap, isGroup, isRows, LANE_PAD, LANE_ROW_GAP, LANE_BLOCK_GAP, FRAME_SIDE, laneGutter as gutterOf, STUB_ROOM, laneBlock, laneEnd, tightCopies, LABEL_LINE, itemWidth, labelLines, fitCap, diamondLines, diamondRoom, isLanesLayout, timelineBeats, timelineLayout, TL_AXIS_H, TL_AXIS_W, TL_BAR_H, TL_ROW_GAP, toBeat, altText,
|
|
9
|
+
import { ASYNC_TAG_W, BASE_RATE, CARD_LINE, CARD_PAD, CARD_WIDTH, DARK, EDGE_OFF, EDGE_ON, LIGHT, TONES, toneFill, toneTint, labelPillW, ON_ACCENT, str, edgeId, groupGap, isGroup, isRows, LANE_PAD, LANE_ROW_GAP, LANE_BLOCK_GAP, FRAME_SIDE, laneGutter as gutterOf, STUB_ROOM, laneBlock, laneEnd, tightCopies, LABEL_LINE, itemWidth, labelLines, fitCap, diamondLines, diamondRoom, isLanesLayout, timelineBeats, timelineLayout, TL_AXIS_H, TL_AXIS_W, TL_BAR_H, TL_ROW_GAP, toBeat, altText, beatTimes, n2, STEP_HOLD_MS, playheadItem, loopStartItem, dateLabelRaised, labelSpan, outsideLabelRect, edgeTip, scenes, tourPlan, flowOrder, OPEN_MS, } from './model.js';
|
|
10
10
|
export { LIGHT, DARK } from './model.js';
|
|
11
11
|
const LINE = CARD_LINE, CARD_SIDE = 8, ROW_GAP = 4;
|
|
12
12
|
const SUB_LINE = 15;
|
|
13
13
|
const MIN_W = 320;
|
|
14
14
|
const FRAME_TOP = 37, FRAME_BOTTOM = 18;
|
|
15
|
-
|
|
16
|
-
const
|
|
15
|
+
const color = (t, k) => {
|
|
16
|
+
const v = String(t[k]);
|
|
17
|
+
const depth = [...v].reduce((d, c) => (d < 0 ? d : d + (c === '(' ? 1 : c === ')' ? -1 : 0)), 0);
|
|
18
|
+
return /^[#a-zA-Z0-9 ,.%()/+-]+$/.test(v) && !/url\(/i.test(v) && depth === 0 ? v : LIGHT[k];
|
|
19
|
+
};
|
|
20
|
+
const vars = (t, cardOn) => `--accent:${color(t, 'accent')}; --fg:${color(t, 'fg')}; --muted:${color(t, 'muted')}; --bg:${color(t, 'bg')}; --surface:${color(t, 'surface')}; --border:${color(t, 'border')}; --card-on:${cardOn}; --tint:color-mix(in srgb, var(--accent) 10%, var(--surface));`;
|
|
17
21
|
const content = (c) => (isRows(c) ? c : str(c));
|
|
18
22
|
const esc = (s) => s.replace(/[<>&"]/g, (c) => ({ '<': '<', '>': '>', '&': '&', '"': '"' })[c]);
|
|
19
|
-
const n2 = (v) => Math.round(v * 10) / 10;
|
|
20
23
|
// Four decimals: one decimal rounds a 0.7 s hop away and the packet jumps.
|
|
21
24
|
const n4 = (v) => Math.round(v * 10000) / 10000;
|
|
22
25
|
const pct = (v) => Math.round(v * 10000) / 100 + '%';
|
|
@@ -162,6 +165,7 @@ function place(item, x, y, s, out) {
|
|
|
162
165
|
}
|
|
163
166
|
}
|
|
164
167
|
const MARK_COLOR = { added: TONES.green, removed: TONES.red, changed: TONES.orange };
|
|
168
|
+
const tagged = (op, id, inner) => op ? `<g data-diff="${op}" data-id="${esc(id)}">${inner}</g>` : inner;
|
|
165
169
|
const marked = (mark, inner) => mark === 'removed' ? `<g opacity="0.5" stroke-dasharray="5 4">${inner}</g>` : mark ? `<g stroke-width="2">${inner}</g>` : inner;
|
|
166
170
|
function legendOf(m) {
|
|
167
171
|
const count = (kind) => Object.values(m.boxes).filter((k) => k === kind).length + Object.values(m.edges).filter((k) => k === kind).length;
|
|
@@ -179,7 +183,9 @@ function legendSvg(items, W, H) {
|
|
|
179
183
|
let x = (W - legendWidth(items)) / 2;
|
|
180
184
|
return items
|
|
181
185
|
.map((i, k) => {
|
|
182
|
-
const sq = i.color === 'none'
|
|
186
|
+
const sq = i.color === 'none'
|
|
187
|
+
? ''
|
|
188
|
+
: `<rect x="${n2(x)}" y="${n2(H - 24)}" width="10" height="10" rx="2" fill="${i.color}" data-diff="${i.text.split(' ')[0]}"/>`;
|
|
183
189
|
const out = `${sq}<text x="${n2(x + (i.color === 'none' ? 0 : 16))}" y="${n2(H - 15)}" font-size="12" fill="var(--muted)">${esc(i.text)}</text>\n`;
|
|
184
190
|
x += widths[k] + 20;
|
|
185
191
|
return out;
|
|
@@ -187,10 +193,10 @@ function legendSvg(items, W, H) {
|
|
|
187
193
|
.join('');
|
|
188
194
|
}
|
|
189
195
|
const SYSTEM_FONT = "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif";
|
|
190
|
-
|
|
191
|
-
export function render(spec, opts = {}) {
|
|
196
|
+
function draw(spec, opts, at) {
|
|
192
197
|
const fig = autoLayout(spec);
|
|
193
|
-
const
|
|
198
|
+
const sc = opts.scope ?? '';
|
|
199
|
+
const pre = at?.prefix ?? sc;
|
|
194
200
|
const pad = opts.padding ?? 24;
|
|
195
201
|
const tl = fig.timeline && isLanesLayout(fig.layout) ? timelineLayout(fig, TL_AXIS_W) : null;
|
|
196
202
|
const synthetic = tl != null && !fig.steps?.length && !opts.marks;
|
|
@@ -293,10 +299,10 @@ export function render(spec, opts = {}) {
|
|
|
293
299
|
const dl = dia ? diamondLines(p.item, p.w) : null;
|
|
294
300
|
const room = (far) => diamondRoom(p.w, p.h, far);
|
|
295
301
|
const flat = (p.item.shape === 'decision' ? p.w - 70 : p.w) - 16;
|
|
296
|
-
const texts = labelLines(p.item, p.w).map((text) => ({
|
|
302
|
+
const texts = labelLines(p.item, p.w).map((text, i) => ({
|
|
297
303
|
text,
|
|
298
304
|
fontSize: 14,
|
|
299
|
-
room: dl ? room(dl.label.far) : flat,
|
|
305
|
+
room: dl ? room(dl.label[i].far) : flat,
|
|
300
306
|
}));
|
|
301
307
|
if (dl)
|
|
302
308
|
for (const l of dl.subs)
|
|
@@ -319,12 +325,10 @@ export function render(spec, opts = {}) {
|
|
|
319
325
|
const segs = [];
|
|
320
326
|
const hops = [];
|
|
321
327
|
const edgeOf = Object.fromEntries(fig.edges.map((e, i) => [ids[i], e]));
|
|
322
|
-
|
|
328
|
+
const times = at?.times ?? beatTimes(beats, opts.speed ?? fig.speed ?? 900);
|
|
323
329
|
beats.forEach((stepBeats, si) => {
|
|
324
330
|
stepBeats.forEach((b, bi) => {
|
|
325
|
-
const
|
|
326
|
-
const dur = beatMs(b, opts.speed ?? fig.speed ?? 900) / 1000 / BASE_RATE;
|
|
327
|
-
const hold = last ? STEP_HOLD_MS / 1000 / BASE_RATE : 0;
|
|
331
|
+
const { t0, t1, tEnd } = times.beats[si][bi];
|
|
328
332
|
b.hops.forEach((h, lane) => {
|
|
329
333
|
if (byId[h.edge])
|
|
330
334
|
hops.push({
|
|
@@ -336,16 +340,15 @@ export function render(spec, opts = {}) {
|
|
|
336
340
|
back: h.back,
|
|
337
341
|
data: h.data,
|
|
338
342
|
tone: h.tone && TONES[h.tone],
|
|
339
|
-
t0
|
|
340
|
-
t1
|
|
341
|
-
tEnd
|
|
343
|
+
t0,
|
|
344
|
+
t1,
|
|
345
|
+
tEnd,
|
|
342
346
|
});
|
|
343
347
|
});
|
|
344
|
-
segs.push({ t0
|
|
345
|
-
t += dur + hold;
|
|
348
|
+
segs.push({ t0, t1: bi === stepBeats.length - 1 ? times.ends[si] : tEnd, si, bi });
|
|
346
349
|
});
|
|
347
350
|
});
|
|
348
|
-
const total =
|
|
351
|
+
const total = at?.total ?? (times.ends.at(-1) || 1);
|
|
349
352
|
const shownAt = segs.map(({ si, bi }) => Object.assign({}, ...beats[si].slice(0, bi + 1).map((b) => b.show)));
|
|
350
353
|
const litEdges = segs.map(({ si, bi }) => new Set(beats[si].slice(0, bi + 1).flatMap((b) => b.hops.map((h) => h.edge))));
|
|
351
354
|
const litNodes = segs.map((seg, i) => {
|
|
@@ -383,7 +386,7 @@ export function render(spec, opts = {}) {
|
|
|
383
386
|
return '';
|
|
384
387
|
const key = prefix + on.map((x) => (x ? 1 : 0)).join('');
|
|
385
388
|
if (!seen.has(key)) {
|
|
386
|
-
const name =
|
|
389
|
+
const name = `${pre}a${seen.size}`;
|
|
387
390
|
seen.set(key, name);
|
|
388
391
|
const frames = runs(on.map((x) => (x ? onCss : offCss)));
|
|
389
392
|
css.push(`@keyframes ${name} { ${frames} }\n.${name} { animation: ${name} ${n2(total)}s infinite step-end; }`);
|
|
@@ -400,7 +403,7 @@ export function render(spec, opts = {}) {
|
|
|
400
403
|
return '';
|
|
401
404
|
const key = prefix + values.join('|');
|
|
402
405
|
if (!seen.has(key)) {
|
|
403
|
-
const name =
|
|
406
|
+
const name = `${pre}a${seen.size}`;
|
|
404
407
|
seen.set(key, name);
|
|
405
408
|
const kf = runs(values);
|
|
406
409
|
css.push(`@keyframes ${name} { ${kf} }\n.${name} { animation: ${name} ${n2(total)}s infinite step-end; }`);
|
|
@@ -464,7 +467,7 @@ export function render(spec, opts = {}) {
|
|
|
464
467
|
const kf = merged.map((f) => `${pct(f.a / total)},${pct(f.b / total - 0.0001)} { ${f.css} }`);
|
|
465
468
|
const key = (shape ? 'B' : 'R') + kf.join(' ');
|
|
466
469
|
if (!seen.has(key)) {
|
|
467
|
-
const name =
|
|
470
|
+
const name = `${pre}a${seen.size}`;
|
|
468
471
|
seen.set(key, name);
|
|
469
472
|
css.push(`@keyframes ${name} { ${kf.join(' ')} }\n.${name} { animation: ${name} ${n2(total)}s infinite linear; }`);
|
|
470
473
|
}
|
|
@@ -482,9 +485,11 @@ export function render(spec, opts = {}) {
|
|
|
482
485
|
}
|
|
483
486
|
const diffMark = opts.marks?.boxes[item.id];
|
|
484
487
|
const href = opts.links?.[item.id];
|
|
488
|
+
const fade = at?.fades?.get(item.id);
|
|
485
489
|
const tip = (inner) => {
|
|
486
|
-
const body = marked(diffMark, item.source ? `<g><title>${esc(item.source)}</title>${inner}</g>` : inner);
|
|
487
|
-
|
|
490
|
+
const body = tagged(diffMark, item.id, marked(diffMark, item.source ? `<g><title>${esc(item.source)}</title>${inner}</g>` : inner));
|
|
491
|
+
const linked = href == null ? body : `<a href="${esc(href)}" tabindex="-1">${body}</a>`;
|
|
492
|
+
return fade ? `<g class="${fade}">${linked}</g>` : linked;
|
|
488
493
|
};
|
|
489
494
|
const bt = diffMark ? MARK_COLOR[diffMark] : item.tone && TONES[item.tone];
|
|
490
495
|
const stroke = cls(boxAnim(item.id, true, bt));
|
|
@@ -501,23 +506,29 @@ export function render(spec, opts = {}) {
|
|
|
501
506
|
return tip(shape + `<text x="${n2(tx)}" y="${n2(cy + 4.5)}" class="bar">${esc(label)}</text>`);
|
|
502
507
|
}
|
|
503
508
|
const rim = item.shape === 'store' ? cls(boxAnim(item.id, false, bt)) : '';
|
|
509
|
+
const port = at?.ports.has(item.id);
|
|
504
510
|
const fill0 = bt ? toneTint(bt, 'var(--bg)') : 'var(--bg)';
|
|
505
511
|
const stroke0 = bt ?? 'var(--border)';
|
|
506
512
|
const cx = p.x + p.w / 2;
|
|
507
513
|
const contents = cards.get(item.id);
|
|
508
514
|
const cardTop = p.y + p.h - 10 - (contents ? cardH.get(item.id) : 0);
|
|
509
515
|
const dl = item.shape === 'decision' && !contents ? diamondLines(item, p.w) : null;
|
|
510
|
-
const labelY = item.shape === 'store' ? p.y + 24 + 13 : contents ? p.y + 10 + 13 : p.y + p.h / 2 + (dl ? dl.label.dy : item.sub ? -2 : 5);
|
|
516
|
+
const labelY = item.shape === 'store' ? p.y + 24 + 13 : contents ? p.y + 10 + 13 : p.y + p.h / 2 + (dl ? dl.label[0].dy : item.sub ? -2 : 5);
|
|
511
517
|
const shape = item.shape === 'decision'
|
|
512
518
|
? `<polygon points="${n2(cx)},${n2(p.y)} ${n2(p.x + p.w)},${n2(p.y + p.h / 2)} ${n2(cx)},${n2(p.y + p.h)} ${n2(p.x)},${n2(p.y + p.h / 2)}" fill="${fill0}" stroke="${stroke0}"${stroke}/>`
|
|
513
519
|
: item.shape === 'store'
|
|
514
520
|
? `<path d="M${n2(p.x)} ${n2(p.y + 12)} a ${n2(p.w / 2)} 12 0 0 1 ${n2(p.w)} 0 v ${n2(p.h - 24)} a ${n2(p.w / 2)} 12 0 0 1 ${n2(-p.w)} 0 z" fill="${fill0}" stroke="${stroke0}"${stroke}/>` +
|
|
515
521
|
`<path d="M${n2(p.x)} ${n2(p.y + 12)} a ${n2(p.w / 2)} 12 0 0 0 ${n2(p.w)} 0" fill="none" stroke="${stroke0}"${rim}/>`
|
|
516
|
-
: `<rect x="${n2(p.x)}" y="${n2(p.y)}" width="${n2(p.w)}" height="${n2(p.h)}" rx="10" fill="${fill0}" stroke="${stroke0}"${stroke}/>`;
|
|
522
|
+
: `<rect x="${n2(p.x)}" y="${n2(p.y)}" width="${n2(p.w)}" height="${n2(p.h)}" rx="10" fill="${fill0}" stroke="${stroke0}"${port ? ' stroke-dasharray="4 3"' : ''}${stroke}/>`;
|
|
523
|
+
const opens = at?.owners.has(item.id) && item.shape !== 'decision'
|
|
524
|
+
? `<rect x="${n2(p.x + 3)}" y="${n2(p.y + 3)}" width="${n2(p.w)}" height="${n2(p.h)}" rx="10" fill="var(--bg)" stroke="var(--border)"/>`
|
|
525
|
+
: '';
|
|
517
526
|
const lines = labelLines(item, p.w);
|
|
518
527
|
const extra = (lines.length - 1) * LABEL_LINE;
|
|
519
|
-
const firstY = item.shape === 'store' || contents ? labelY : labelY - extra / 2;
|
|
520
|
-
const label = lines
|
|
528
|
+
const firstY = item.shape === 'store' || contents || dl ? labelY : labelY - extra / 2;
|
|
529
|
+
const label = lines
|
|
530
|
+
.map((l, i) => `<text x="${n2(cx)}" y="${n2(firstY + i * LABEL_LINE)}"${cls('label', port && 'muted')}>${esc(l)}</text>`)
|
|
531
|
+
.join('');
|
|
521
532
|
const sub = dl
|
|
522
533
|
? dl.subs.map((l) => `<text x="${n2(cx)}" y="${n2(p.y + p.h / 2 + l.dy)}" class="sub">${esc(l.text)}</text>`).join('')
|
|
523
534
|
: item.sub
|
|
@@ -532,7 +543,7 @@ export function render(spec, opts = {}) {
|
|
|
532
543
|
: item.mark === 'end'
|
|
533
544
|
? `<circle cx="${mx}" cy="${my}" r="6.25" fill="none" stroke="${dot}" stroke-width="1.5"/><circle cx="${mx}" cy="${my}" r="4" fill="${dot}"/>`
|
|
534
545
|
: '';
|
|
535
|
-
return tip(shape + label + sub + mark + (contents ? card(p, cardTop, contents) : ''));
|
|
546
|
+
return tip(opens + shape + label + sub + mark + (contents ? card(p, cardTop, contents) : ''));
|
|
536
547
|
});
|
|
537
548
|
function card(p, top, contents) {
|
|
538
549
|
const id = p.item.id;
|
|
@@ -603,9 +614,9 @@ export function render(spec, opts = {}) {
|
|
|
603
614
|
const tip = edgeTip(r.id, e.source, beats.flat());
|
|
604
615
|
const path0 = r.stub
|
|
605
616
|
? r.stub.parts
|
|
606
|
-
.map((d) => `<path d="${d}" fill="none" stroke="${line}" stroke-width="${EDGE_OFF}" marker-end="url(
|
|
607
|
-
.join('') + `<path id="p-${esc(r.id)}" d="${r.d}" fill="none" stroke="none"/>`
|
|
608
|
-
: `<path id="p-${esc(r.id)}" d="${r.d}" fill="none" stroke="${line}" stroke-width="${EDGE_OFF}" marker-end="url(
|
|
617
|
+
.map((d) => `<path d="${d}" fill="none" stroke="${line}" stroke-width="${EDGE_OFF}" marker-end="url(#${sc}arrow)"${lit}/>`)
|
|
618
|
+
.join('') + `<path id="${pre}p-${esc(r.id)}" d="${r.d}" fill="none" stroke="none"/>`
|
|
619
|
+
: `<path id="${pre}p-${esc(r.id)}" d="${r.d}" fill="none" stroke="${line}" stroke-width="${EDGE_OFF}" marker-end="url(#${sc}arrow)"${lit}/>`;
|
|
609
620
|
const path = tip ? `<g><title>${esc(tip)}</title>${path0}</g>` : path0;
|
|
610
621
|
const pillColor = em ? MARK_COLOR[em] : undefined;
|
|
611
622
|
const pill = (x, y, lw, text) => `<rect x="${n2(x - lw / 2)}" y="${n2(y - 9)}" width="${n2(lw)}" height="18" rx="9" fill="${pillColor ? toneTint(pillColor, 'var(--bg)', 14) : 'var(--bg)'}" stroke="${pillColor ?? 'var(--border)'}"` +
|
|
@@ -625,11 +636,11 @@ export function render(spec, opts = {}) {
|
|
|
625
636
|
fonts.push(11);
|
|
626
637
|
label = pill(r.mid.x, r.mid.y, lw, str(e.label));
|
|
627
638
|
}
|
|
628
|
-
return marked(em === 'removed' ? em : undefined, hidden ? `<g opacity="0"${shown}>${path}${label}</g>` : path + label);
|
|
639
|
+
return tagged(em, r.id, marked(em === 'removed' ? em : undefined, hidden ? `<g opacity="0"${shown}>${path}${label}</g>` : path + label));
|
|
629
640
|
});
|
|
630
641
|
const packets = hops.map((h, i) => {
|
|
631
|
-
const t0 = h.t0 / total, t1 = h.t1 / total, tEnd = h.tEnd / total;
|
|
632
|
-
const name =
|
|
642
|
+
const t0 = h.t0 / total, t1 = Math.min(h.t1 / total, 1), tEnd = h.tEnd / total, reach = n4((t1 - t0) / (h.t1 / total - t0));
|
|
643
|
+
const name = `${pre}p${i}`;
|
|
633
644
|
css.push(`@keyframes ${name} { 0%,${pct(t0)} { opacity: 0 } ${pct(t0 + 0.0001)},${pct(tEnd - 0.0001)} { opacity: 1 } ${pct(tEnd)},100% { opacity: 0 } }\n` +
|
|
634
645
|
`.${name} { animation: ${name} ${n2(total)}s infinite step-end; }`);
|
|
635
646
|
const chip = (() => {
|
|
@@ -644,8 +655,8 @@ export function render(spec, opts = {}) {
|
|
|
644
655
|
lines.map((l, li) => `<text x="0" y="${n2(-boxH - 12 + 15 * li + 15)}" class="chip">${esc(l)}</text>`).join(''));
|
|
645
656
|
})();
|
|
646
657
|
return (`<g${cls(name)} opacity="0"><circle r="10" fill="${h.tone ?? 'var(--accent)'}" opacity="0.2"/><circle r="4.5" fill="${h.tone ?? 'var(--accent)'}"/>${chip}` +
|
|
647
|
-
`<animateMotion dur="${n2(total)}s" repeatCount="indefinite" keyTimes="0;${n4(t0)};${n4(t1)};1" keyPoints="${h.back ?
|
|
648
|
-
`<mpath href="
|
|
658
|
+
`<animateMotion dur="${n2(total)}s" repeatCount="indefinite" keyTimes="0;${n4(t0)};${n4(t1)};1" keyPoints="${h.back ? `1;1;${n4(1 - reach)};${n4(1 - reach)}` : `0;0;${reach};${reach}`}" calcMode="linear">` +
|
|
659
|
+
`<mpath href="#${pre}p-${esc(h.edge)}" xlink:href="#${pre}p-${esc(h.edge)}"/></animateMotion></g>`);
|
|
649
660
|
});
|
|
650
661
|
let axisSvg = '', lineSvg = '', todaySvg = '';
|
|
651
662
|
if (tl) {
|
|
@@ -697,7 +708,7 @@ export function render(spec, opts = {}) {
|
|
|
697
708
|
at = home;
|
|
698
709
|
}
|
|
699
710
|
});
|
|
700
|
-
const name =
|
|
711
|
+
const name = `${pre}a${seen.size}`;
|
|
701
712
|
seen.set('playhead', name);
|
|
702
713
|
const kf = pts.map(([t, x]) => `${pct(t / total)} { transform: translateX(${n2(x - home)}px) }`).join(' ');
|
|
703
714
|
css.push(`@keyframes ${name} { ${kf} }\n.${name} { animation: ${name} ${n2(total)}s infinite linear; }`);
|
|
@@ -706,7 +717,7 @@ export function render(spec, opts = {}) {
|
|
|
706
717
|
const on = dates.map((x) => x.d === d);
|
|
707
718
|
const key = `d${d}${on.map((x) => (x ? 1 : 0)).join('')}`;
|
|
708
719
|
if (!seen.has(key)) {
|
|
709
|
-
const nm =
|
|
720
|
+
const nm = `${pre}a${seen.size}`;
|
|
710
721
|
seen.set(key, nm);
|
|
711
722
|
const fr = dates
|
|
712
723
|
.map((x) => `${pct(x.a / total)},${pct(x.b / total - 0.0001)} { opacity: ${x.d === d ? 1 : 0} } ${pct(x.b / total)} { opacity: 0 }`)
|
|
@@ -739,17 +750,20 @@ export function render(spec, opts = {}) {
|
|
|
739
750
|
const H = (only ? pad + rail.height + capH : mapH + (rail ? RAIL.gap + rail.height : 0) + capH) + legendH;
|
|
740
751
|
const shift = (W - (bounds.w + pad * 2 + arcL + arcR)) / 2 + arcL;
|
|
741
752
|
const railX = rail ? (W - rail.width) / 2 : 0;
|
|
753
|
+
const still = (on) => !!at?.fades && on.every(Boolean);
|
|
742
754
|
const labels = rail || synthetic
|
|
743
755
|
? []
|
|
744
756
|
: steps.map((_, si) => {
|
|
745
757
|
const on = segs.map((s) => s.si === si);
|
|
746
|
-
return `<text x="${n2(W / 2)}" y="${n2(H - capH + 16)}" opacity="0"${cls('steplabel', anim(on, 'opacity: 1', 'opacity: 0', 's'))}>${esc(str(steps[si].label))}</text>`;
|
|
758
|
+
return `<text x="${n2(W / 2)}" y="${n2(H - legendH - capH + 16)}"${still(on) ? '' : ' opacity="0"'}${cls('steplabel', !still(on) && anim(on, 'opacity: 1', 'opacity: 0', 's'))}>${esc(str(steps[si].label))}</text>`;
|
|
747
759
|
});
|
|
748
760
|
const said = [...new Set(captions)].map((text) => {
|
|
749
761
|
const on = captions.map((c) => c === text);
|
|
750
762
|
const lines = wrap(text, wrapW, 13.5);
|
|
751
|
-
return (`<g opacity="0"${cls(anim(on, 'opacity: 1', 'opacity: 0', 'y'))}>` +
|
|
752
|
-
lines
|
|
763
|
+
return (`<g${still(on) ? '' : ` opacity="0"${cls(anim(on, 'opacity: 1', 'opacity: 0', 'y'))}`}>` +
|
|
764
|
+
lines
|
|
765
|
+
.map((l, li) => `<text x="${n2(W / 2)}" y="${n2(H - legendH - capH + capTop + 16 + li * 20)}" class="caption">${esc(l)}</text>`)
|
|
766
|
+
.join('') +
|
|
753
767
|
`</g>`);
|
|
754
768
|
});
|
|
755
769
|
const railSvg = !rail
|
|
@@ -818,47 +832,15 @@ export function render(spec, opts = {}) {
|
|
|
818
832
|
: '';
|
|
819
833
|
const tip = edgeTip(row.edge, fig.edges[ids.indexOf(row.edge)].source, beats.flat());
|
|
820
834
|
parts.push(`<g${cls('railrow', moveRow(i))}>${tip ? `<title>${esc(tip)}</title>` : ''}<g${cls(anim(next, 'opacity: .45', 'opacity: 1', 'r'))}>` +
|
|
821
|
-
`<path d="M ${n2(x1)} ${n2(ly)} H ${n2(x2)}" fill="none" stroke="var(--muted)" stroke-width="${EDGE_OFF}"${row.async ? ' stroke-dasharray="4 3"' : ''} marker-end="url(
|
|
835
|
+
`<path d="M ${n2(x1)} ${n2(ly)} H ${n2(x2)}" fill="none" stroke="var(--muted)" stroke-width="${EDGE_OFF}"${row.async ? ' stroke-dasharray="4 3"' : ''} marker-end="url(#${sc}arrow)"${cls(lit)}/>` +
|
|
822
836
|
tag +
|
|
823
837
|
pill +
|
|
824
838
|
`</g></g>`);
|
|
825
839
|
});
|
|
826
840
|
return `<g transform="translate(${n2(railX)} 0)">${parts.join('')}</g>`;
|
|
827
841
|
})();
|
|
828
|
-
const { font, ...t0 } = { ...LIGHT, ...fig.theme, ...opts.theme };
|
|
829
|
-
const alt = altText(fig);
|
|
830
842
|
const holdAt = beats[0]?.length ? n2(segs[beats[0].length - 1].t1 - 0.1) : 0;
|
|
831
|
-
const
|
|
832
|
-
<title>${esc(alt.title)}</title>
|
|
833
|
-
<desc>${esc(alt.desc)}</desc>
|
|
834
|
-
<style>
|
|
835
|
-
svg { ${vars(t0, '#eef5fd')} }
|
|
836
|
-
@media (prefers-color-scheme: dark) { svg { ${vars(DARK, '#1d2733')} } }
|
|
837
|
-
.label { fill: var(--fg); font-size: 14px; font-weight: 500; text-anchor: middle; }
|
|
838
|
-
.sub { fill: var(--muted); font-size: 12px; text-anchor: middle; }
|
|
839
|
-
.frame { fill: var(--muted); font-size: 11px; font-weight: 600; letter-spacing: .04em; }
|
|
840
|
-
.edgelabel { fill: var(--muted); font-size: 11px; text-anchor: middle; font-family: ui-monospace, Menlo, monospace; }
|
|
841
|
-
.row { fill: var(--fg); font-size: 11px; }
|
|
842
|
-
.row.mono { font-size: 10.5px; font-family: ui-monospace, Menlo, monospace; }
|
|
843
|
-
.muted { fill: var(--muted); }
|
|
844
|
-
.bar { fill: var(--fg); font-size: 13px; font-weight: 500; }
|
|
845
|
-
.tick { fill: var(--muted); font-size: 11px; }
|
|
846
|
-
.today { fill: var(--accent); font-size: 11px; font-weight: 600; }
|
|
847
|
-
.tag { font-size: 9px; font-weight: 600; letter-spacing: .03em; text-anchor: middle; }
|
|
848
|
-
.mark { fill: var(--accent); font-size: 11px; font-weight: 600; text-anchor: end; }
|
|
849
|
-
.chip { fill: ${ON_ACCENT}; font-size: 11.5px; text-anchor: middle; }
|
|
850
|
-
.steplabel { fill: var(--fg); font-size: 13px; font-weight: 600; text-anchor: middle; font-family: ui-monospace, Menlo, monospace; }
|
|
851
|
-
.caption { fill: var(--muted); font-size: 13.5px; text-anchor: middle; }
|
|
852
|
-
.railcol { fill: var(--fg); font-size: 12px; font-weight: 500; text-anchor: middle; }
|
|
853
|
-
.railphase { fill: var(--fg); font-size: 13px; font-weight: 600; font-family: ui-monospace, Menlo, monospace; }
|
|
854
|
-
.railphase.muted { fill: var(--muted); }
|
|
855
|
-
.end { text-anchor: end; }
|
|
856
|
-
${css.join('\n')}
|
|
857
|
-
@media (prefers-reduced-motion: reduce) { * { animation-play-state: paused !important; animation-delay: -${holdAt}s !important; } }
|
|
858
|
-
</style>
|
|
859
|
-
<defs><marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M 0 1 L 9 5 L 0 9 z" fill="context-stroke"/></marker></defs>
|
|
860
|
-
<rect width="100%" height="100%" fill="var(--bg)"/>
|
|
861
|
-
${only
|
|
843
|
+
const body = `${only
|
|
862
844
|
? ''
|
|
863
845
|
: `<g transform="translate(${n2(shift)} ${arcT})">
|
|
864
846
|
${axisSvg}
|
|
@@ -872,8 +854,7 @@ ${todaySvg}
|
|
|
872
854
|
`}${railSvg}
|
|
873
855
|
${labels.join('\n')}
|
|
874
856
|
${said.join('\n')}
|
|
875
|
-
${legendSvg(legend, W, H)}
|
|
876
|
-
`;
|
|
857
|
+
${legendSvg(legend, W, H)}`;
|
|
877
858
|
if (rail)
|
|
878
859
|
fonts.push(12, 13);
|
|
879
860
|
if (legend.length)
|
|
@@ -923,7 +904,189 @@ ${legendSvg(legend, W, H)}</svg>
|
|
|
923
904
|
lanes: placed.filter((p) => p.lane).map((p) => ({ id: str(p.item.label), rect: { x: p.x, y: p.y, w: p.w, h: p.h } })),
|
|
924
905
|
}),
|
|
925
906
|
};
|
|
926
|
-
|
|
907
|
+
const frame = { x: shift - arcL, w: bounds.w + pad * 2 + arcL + arcR, h: mapH };
|
|
908
|
+
return { W, H, css: css.join('\n'), body, holdAt, scene, fig, frame };
|
|
909
|
+
}
|
|
910
|
+
const page = (fig, opts, W, H, css, holdAt, body) => {
|
|
911
|
+
const { font, ...t0 } = { ...LIGHT, ...fig.theme, ...opts.theme };
|
|
912
|
+
const alt = altText(fig);
|
|
913
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="${n2(W)}" height="${n2(H)}" viewBox="0 0 ${n2(W)} ${n2(H)}" font-family="${esc(font ?? SYSTEM_FONT)}" role="img">
|
|
914
|
+
<title>${esc(alt.title)}</title>
|
|
915
|
+
<desc>${esc(alt.desc)}</desc>
|
|
916
|
+
<style>
|
|
917
|
+
svg { ${vars(t0, '#eef5fd')} }
|
|
918
|
+
@media (prefers-color-scheme: dark) { svg { ${vars(DARK, '#1d2733')} } }
|
|
919
|
+
.label { fill: var(--fg); font-size: 14px; font-weight: 500; text-anchor: middle; }
|
|
920
|
+
.sub { fill: var(--muted); font-size: 12px; text-anchor: middle; }
|
|
921
|
+
.frame { fill: var(--muted); font-size: 11px; font-weight: 600; letter-spacing: .04em; }
|
|
922
|
+
.edgelabel { fill: var(--muted); font-size: 11px; text-anchor: middle; font-family: ui-monospace, Menlo, monospace; }
|
|
923
|
+
.row { fill: var(--fg); font-size: 11px; }
|
|
924
|
+
.row.mono { font-size: 10.5px; font-family: ui-monospace, Menlo, monospace; }
|
|
925
|
+
.muted { fill: var(--muted); }
|
|
926
|
+
.bar { fill: var(--fg); font-size: 13px; font-weight: 500; }
|
|
927
|
+
.tick { fill: var(--muted); font-size: 11px; }
|
|
928
|
+
.today { fill: var(--accent); font-size: 11px; font-weight: 600; }
|
|
929
|
+
.tag { font-size: 9px; font-weight: 600; letter-spacing: .03em; text-anchor: middle; }
|
|
930
|
+
.mark { fill: var(--accent); font-size: 11px; font-weight: 600; text-anchor: end; }
|
|
931
|
+
.chip { fill: ${ON_ACCENT}; font-size: 11.5px; text-anchor: middle; }
|
|
932
|
+
.steplabel { fill: var(--fg); font-size: 13px; font-weight: 600; text-anchor: middle; font-family: ui-monospace, Menlo, monospace; }
|
|
933
|
+
.caption { fill: var(--muted); font-size: 13.5px; text-anchor: middle; }
|
|
934
|
+
.railcol { fill: var(--fg); font-size: 12px; font-weight: 500; text-anchor: middle; }
|
|
935
|
+
.railphase { fill: var(--fg); font-size: 13px; font-weight: 600; font-family: ui-monospace, Menlo, monospace; }
|
|
936
|
+
.railphase.muted { fill: var(--muted); }
|
|
937
|
+
.end { text-anchor: end; }
|
|
938
|
+
${css}
|
|
939
|
+
@media (prefers-reduced-motion: reduce) { * { animation-play-state: paused !important; animation-delay: -${holdAt}s !important; } }
|
|
940
|
+
</style>
|
|
941
|
+
<defs><marker id="${opts.scope ?? ''}arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M 0 1 L 9 5 L 0 9 z" fill="context-stroke"/></marker></defs>
|
|
942
|
+
<rect width="100%" height="100%" fill="var(--bg)"/>
|
|
943
|
+
${body}</svg>
|
|
944
|
+
`;
|
|
945
|
+
};
|
|
946
|
+
const SCRIPT = `(()=>{const s=document.currentScript.closest('svg'),G=[...s.querySelectorAll('.scene')],S=[G[0]],R=matchMedia('(prefers-reduced-motion: reduce)').matches,
|
|
947
|
+
W=(g,k)=>g.dataset[k].split(' ').map(p=>p.split(',').map(Number));let q=[],L=W(G[0],'loop'),w=[0,-1],o,t=0,p=performance.now();
|
|
948
|
+
const seek=t=>{s.pauseAnimations();s.setCurrentTime(t);for(const a of s.getAnimations({subtree:true}))a.pause(),a.currentTime=t*1e3+a.effect.getTiming().delay},
|
|
949
|
+
tick=n=>{if(t>=w[1]){o=q.length>0;if(!o)q=L.slice();w=q.shift();t=w[0]}seek(R?w[1]:Math.min(t,w[1]));t+=R?(o?1e9:0):(n-p)/1e3;p=n;requestAnimationFrame(tick)},
|
|
950
|
+
show=()=>{const c=S.at(-1);for(const g of G){g.style.pointerEvents=g==c?'':'none';for(const x of g.querySelectorAll('.hit,.back'))x.setAttribute('role','button'),x.setAttribute('tabindex',g==c?0:-1)}},
|
|
951
|
+
go=(a,b)=>{q=a;L=b;w=[0,-1];t=0;show()},
|
|
952
|
+
back=()=>{if(S.length>1){const g=S.pop();go(W(g,'back'),W(S.at(-1),'loop'))}},
|
|
953
|
+
act=e=>{const x=e.target.closest('.hit,.back');if(!x||x.closest('.scene')!=S.at(-1))return;if(x.classList.contains('back'))back();else{const g=G.find(g=>g.id=='s-'+x.dataset.open);S.push(g);go(W(g,'open'),W(g,'loop'))}return 1};
|
|
954
|
+
s.setAttribute('role','group');s.addEventListener('click',act);
|
|
955
|
+
s.addEventListener('keydown',e=>{const k=e.key;if(k=='Escape'||k=='Backspace')back();else if(!((k=='Enter'||k==' ')&&act(e)))return;e.preventDefault()});
|
|
956
|
+
show();requestAnimationFrame(tick)})()`;
|
|
957
|
+
const BAND = 28;
|
|
958
|
+
/** The SVG string and the scene it drew. The scene shape may change; use `toSvg` for the SVG only. */
|
|
959
|
+
export function render(spec, opts = {}) {
|
|
960
|
+
const { scene: one, script, ...rest } = opts;
|
|
961
|
+
const all = scenes(spec);
|
|
962
|
+
const key = (path) => path.join('>');
|
|
963
|
+
const under = (list, path) => list.filter((x) => x.path.length === path.length + 1 && key(x.path.slice(0, -1)) === key(path));
|
|
964
|
+
const at = (s) => ({
|
|
965
|
+
ports: new Set(s.ports.map((p) => p.id)),
|
|
966
|
+
owners: new Map(under(all, s.path).map((x) => [x.path.at(-1), x.parts])),
|
|
967
|
+
});
|
|
968
|
+
if (one != null) {
|
|
969
|
+
const s = all.find((x) => x.path.at(-1) === one);
|
|
970
|
+
if (!s)
|
|
971
|
+
throw new Error(`box "${one}" has no level to open`);
|
|
972
|
+
const plain = (g) => ({
|
|
973
|
+
...g,
|
|
974
|
+
children: g.children.map((c) => (isGroup(c) ? plain(c) : { ...c, inside: undefined })),
|
|
975
|
+
});
|
|
976
|
+
const d = draw({ ...s.props, layout: plain(s.props.layout) }, rest, at(s));
|
|
977
|
+
return { svg: page(d.fig, rest, d.W, d.H, d.css, d.holdAt, d.body), scene: d.scene };
|
|
978
|
+
}
|
|
979
|
+
if (all.length < 2) {
|
|
980
|
+
const d = draw(spec, rest);
|
|
981
|
+
return { svg: page(d.fig, rest, d.W, d.H, d.css, d.holdAt, d.body), scene: d.scene };
|
|
982
|
+
}
|
|
983
|
+
const plan = tourPlan(spec, opts.speed ?? spec.speed);
|
|
984
|
+
const total = plan.ms / 1000, eps = plan.ms / 10000;
|
|
985
|
+
const { fade, grow, stagger, hide, shrink } = OPEN_MS;
|
|
986
|
+
const tour = [];
|
|
987
|
+
const uid = `${opts.scope ?? ''}f${([...JSON.stringify(spec)].reduce((h, c) => (h * 31 + c.charCodeAt(0)) | 0, 7) >>> 0).toString(36)}`;
|
|
988
|
+
const track = (pts) => {
|
|
989
|
+
const name = `${uid}t${tour.length}`;
|
|
990
|
+
const kf = pts.map(([t, v, ease]) => `${pct(t / plan.ms)} { ${v}${ease ? `; animation-timing-function: ${ease}` : ''} }`).join(' ');
|
|
991
|
+
tour.push(`@keyframes ${name} { ${kf} }\n.${name} { animation: ${name} ${n2(total)}s infinite linear; }`);
|
|
992
|
+
return name;
|
|
993
|
+
};
|
|
994
|
+
const fadeAt = (o) => o.grown - grow - fade;
|
|
995
|
+
const dips = (path) => under(plan.opens, path).flatMap((n) => [
|
|
996
|
+
[fadeAt(n), 1],
|
|
997
|
+
[fadeAt(n) + fade, 0],
|
|
998
|
+
[n.back[1] - fade, 0],
|
|
999
|
+
[n.back[1], 1],
|
|
1000
|
+
]);
|
|
1001
|
+
const drawn = all.map((s, i) => {
|
|
1002
|
+
const o = plan.opens.find((x) => key(x.path) === key(s.path));
|
|
1003
|
+
const props = o ? { ...s.props, steps: o.step === -1 ? [] : [s.props.steps[o.step]] } : spec;
|
|
1004
|
+
const fades = o &&
|
|
1005
|
+
new Map(flowOrder(s).map((id, k) => {
|
|
1006
|
+
const t = o.grown + stagger * k;
|
|
1007
|
+
return [
|
|
1008
|
+
id,
|
|
1009
|
+
track([
|
|
1010
|
+
[0, 'opacity: 0'],
|
|
1011
|
+
[t, 'opacity: 0'],
|
|
1012
|
+
[t + fade, 'opacity: 1'],
|
|
1013
|
+
[plan.ms, 'opacity: 1'],
|
|
1014
|
+
]),
|
|
1015
|
+
];
|
|
1016
|
+
}));
|
|
1017
|
+
const times = o ? o.times : plan.times;
|
|
1018
|
+
return { s, o, props, d: draw(props, rest, { prefix: `${uid}s${i}`, ...at(s), fades, times, total }) };
|
|
1019
|
+
});
|
|
1020
|
+
const W = Math.max(...drawn.map((x) => x.d.W)), H = Math.max(...drawn.map((x) => x.d.H)) + BAND;
|
|
1021
|
+
const off = (d) => [(W - d.W) / 2, (H + BAND - d.H) / 2];
|
|
1022
|
+
const windows = (w, at) => (w.length ? w : [[at, at]]).map(([a, b]) => `${n2(a / 1000)},${n2(b / 1000)}`).join(' ');
|
|
1023
|
+
const groups = drawn.map(({ s, o, d }) => {
|
|
1024
|
+
const shown = o
|
|
1025
|
+
? [
|
|
1026
|
+
[0, 0],
|
|
1027
|
+
[o.grown, 0],
|
|
1028
|
+
[o.grown + eps, 1],
|
|
1029
|
+
...dips(s.path),
|
|
1030
|
+
[o.back[1] - fade - shrink - hide, 1],
|
|
1031
|
+
[o.back[1] - fade - shrink, 0],
|
|
1032
|
+
[plan.ms, 0],
|
|
1033
|
+
]
|
|
1034
|
+
: [[0, 1], ...dips(s.path), [plan.ms, 1]];
|
|
1035
|
+
const band = o
|
|
1036
|
+
? `<text class="back ${uid}back" x="16" y="19">Back</text><text class="${uid}path" x="${n2(16 + textWidth('Back', 14) + 12)}" y="19">${esc(s.line)}</text>`
|
|
1037
|
+
: '';
|
|
1038
|
+
const area = d.scene.area;
|
|
1039
|
+
const hits = !script
|
|
1040
|
+
? ''
|
|
1041
|
+
: under(all, s.path)
|
|
1042
|
+
.map((x) => {
|
|
1043
|
+
const r = d.scene.boxes.find((b) => b.id === x.path.at(-1)).rect;
|
|
1044
|
+
return `<rect class="hit" x="${n2(r.x - area.x)}" y="${n2(r.y - area.y)}" width="${n2(r.w)}" height="${n2(r.h)}" fill="transparent" tabindex="0" role="button" aria-label="Open ${esc(x.name)}, ${x.parts} parts" data-open="${esc(x.path.at(-1))}"/>`;
|
|
1045
|
+
})
|
|
1046
|
+
.join('');
|
|
1047
|
+
const data = !script
|
|
1048
|
+
? ''
|
|
1049
|
+
: o
|
|
1050
|
+
? ` data-open="${windows([o.open], 0)}" data-loop="${windows(o.loop, o.open[1])}" data-back="${windows([o.back], 0)}"`
|
|
1051
|
+
: ` data-loop="${windows(plan.loop, 0)}"`;
|
|
1052
|
+
return (`<g class="${track(shown.map(([t, v]) => [t, `opacity: ${v}`]))}"><g class="scene"${o ? ` id="s-${esc(s.path.at(-1))}"` : ''}${data}>${band}` +
|
|
1053
|
+
`<g transform="translate(${n2(off(d)[0])} ${n2(off(d)[1])})">\n${d.body}${hits}</g></g></g>`);
|
|
1054
|
+
});
|
|
1055
|
+
const frames = drawn.slice(1).map(({ s, o, d }) => {
|
|
1056
|
+
const parent = drawn.find((x) => key(x.s.path) === key(s.path.slice(0, -1))).d;
|
|
1057
|
+
const r = parent.scene.boxes.find((b) => b.id === s.path.at(-1)).rect;
|
|
1058
|
+
const [px, py] = off(parent), fx = off(d)[0] + d.frame.x, fy = off(d)[1];
|
|
1059
|
+
const dx = px + r.x - parent.scene.area.x - fx, dy = py + r.y - parent.scene.area.y - fy;
|
|
1060
|
+
const small = `transform: translate(${n2(dx)}px, ${n2(dy)}px) scale(${n4(r.w / d.frame.w)}, ${n4(r.h / d.frame.h)})`;
|
|
1061
|
+
const full = 'transform: translate(0px, 0px) scale(1, 1)';
|
|
1062
|
+
const from = fadeAt(o), shut = o.back[1] - fade;
|
|
1063
|
+
const name = track([
|
|
1064
|
+
[0, `opacity: 0; ${small}`],
|
|
1065
|
+
[from, `opacity: 0; ${small}`],
|
|
1066
|
+
[from + eps, `opacity: 1; ${small}`],
|
|
1067
|
+
[from + fade, `opacity: 1; ${small}`, 'cubic-bezier(0.2, 0, 0, 1)'],
|
|
1068
|
+
[o.grown, `opacity: 1; ${full}`],
|
|
1069
|
+
...dips(s.path).map(([t, v]) => [t, `opacity: ${v}; ${full}`]),
|
|
1070
|
+
[shut - shrink, `opacity: 1; ${full}`, 'cubic-bezier(0.4, 0, 1, 1)'],
|
|
1071
|
+
[shut, `opacity: 1; ${small}`],
|
|
1072
|
+
[shut + eps, `opacity: 0; ${small}`],
|
|
1073
|
+
[plan.ms, `opacity: 0; ${small}`],
|
|
1074
|
+
]);
|
|
1075
|
+
return (`<rect class="open-frame ${name}" data-for="${esc(s.path.at(-1))}" x="${n2(fx)}" y="${n2(fy)}" width="${n2(d.frame.w)}" height="${n2(d.frame.h)}" rx="10"` +
|
|
1076
|
+
` fill="none" stroke="var(--border)" vector-effect="non-scaling-stroke" style="transform-box: view-box; transform-origin: ${n2(fx)}px ${n2(fy)}px"/>`);
|
|
1077
|
+
});
|
|
1078
|
+
const root = drawn[0].d;
|
|
1079
|
+
const css = [
|
|
1080
|
+
...drawn.map((x) => x.d.css),
|
|
1081
|
+
`.${uid}back { fill: var(--accent); font-size: 14px; }\n.${uid}path { fill: var(--fg); font-size: 14px; }`,
|
|
1082
|
+
...tour,
|
|
1083
|
+
];
|
|
1084
|
+
const body = `${groups.join('\n')}\n<g pointer-events="none">${frames.join('')}</g>\n${script ? `<script><![CDATA[${SCRIPT}]]></script>\n` : ''}`;
|
|
1085
|
+
return {
|
|
1086
|
+
svg: page(root.fig, rest, W, H, css.join('\n'), root.holdAt, body),
|
|
1087
|
+
scene: root.scene,
|
|
1088
|
+
levels: drawn.map(({ s, props, d }) => ({ line: s.line, props, scene: d.scene })),
|
|
1089
|
+
};
|
|
927
1090
|
}
|
|
928
1091
|
/** One self-contained animated SVG string for the figure. Needs no React and no browser. */
|
|
929
1092
|
export function toSvg(fig, opts = {}) {
|
|
@@ -931,5 +1094,6 @@ export function toSvg(fig, opts = {}) {
|
|
|
931
1094
|
}
|
|
932
1095
|
/** Every fault `flowfig check` knows about, for this figure as the SVG lays it out. */
|
|
933
1096
|
export function check(fig, opts = {}) {
|
|
934
|
-
|
|
1097
|
+
const r = render(fig, opts);
|
|
1098
|
+
return checkRendered(fig, opts, r.scene, r.levels);
|
|
935
1099
|
}
|
package/dist/verify.d.ts
CHANGED
|
@@ -13,8 +13,14 @@ export type Coverage = {
|
|
|
13
13
|
id: string;
|
|
14
14
|
reason: string;
|
|
15
15
|
}[];
|
|
16
|
+
/** The coverage of each level inside a box, by path line. */
|
|
17
|
+
levels?: {
|
|
18
|
+
path: string;
|
|
19
|
+
coverage: Coverage;
|
|
20
|
+
}[];
|
|
16
21
|
};
|
|
17
22
|
export declare const coverageLine: (figure: string, c: Coverage) => string;
|
|
23
|
+
export declare const levelLines: (c: Coverage) => string[];
|
|
18
24
|
export declare const unsureLines: (c: Coverage) => string[];
|
|
19
25
|
export declare const SPEC_MARK = "<metadata id=\"figure-spec\">";
|
|
20
26
|
export declare function detailFindings(fig: FlowProps, root: string, read?: Read): Finding[];
|