pele-animate 0.9.0 → 0.11.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 +45 -4
- package/glyphs.js +1 -0
- package/package.json +1 -1
- package/pele-animate.css +32 -12
- package/pele-animate.js +144 -7
- package/preset-doverunner.js +7 -0
package/README.md
CHANGED
|
@@ -6,10 +6,47 @@ Unofficial add-on; not affiliated with or endorsed by the Pele or Obsidian teams
|
|
|
6
6
|
The source stays valid Mermaid, so GitHub or Obsidian still show a normal diagram.
|
|
7
7
|
|
|
8
8
|
- Nodes appear in order (BFS from the sources, or pinned with a `%% pa:` comment), edges draw in, labels fade in.
|
|
9
|
-
- Hover or focus: tokens travel the edges in sequence. Click or Enter
|
|
9
|
+
- Hover or focus: tokens travel the edges in sequence. The intro plays once on scroll-in. Click (or Enter) enters step mode: previous / play-pause / next buttons, with Space (play/pause), ←/→ (step) and Esc (leave).
|
|
10
10
|
- `@{ icon: "name" }` nodes become illustrations from a glyph pack. Unknown names fall back to a plain Pele box.
|
|
11
|
+
- Sequence diagrams: participant boxes in a row get the tallest height (Pele sizes each to its own text). Group and box titles are bold.
|
|
11
12
|
- Idle state is the finished picture; `prefers-reduced-motion` turns the motion off. Light and dark themes.
|
|
12
13
|
|
|
14
|
+
## Compare
|
|
15
|
+
|
|
16
|
+
The same Mermaid source, three renderers. The pele-animate images loop: the intro draws in, then tokens travel the edges (as on hover).
|
|
17
|
+
Regenerate with `examples/capture.sh`.
|
|
18
|
+
|
|
19
|
+
```mermaid
|
|
20
|
+
flowchart LR
|
|
21
|
+
A[Player] -->|auth token| B[OTT service]
|
|
22
|
+
A -->|license request| C[DRM server]
|
|
23
|
+
C -->|license| A
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
| Renderer | Result |
|
|
27
|
+
|---|---|
|
|
28
|
+
| Mermaid | <img src="https://raw.githubusercontent.com/dkdev24/pele-animate/main/examples/m1.png" width="560" alt="Flowchart rendered by Mermaid"> |
|
|
29
|
+
| Pele | <img src="https://raw.githubusercontent.com/dkdev24/pele-animate/main/examples/p1.png" width="560" alt="Flowchart rendered by Pele"> |
|
|
30
|
+
| pele-animate | <img src="https://raw.githubusercontent.com/dkdev24/pele-animate/main/examples/a1.gif" width="560" alt="Animated flowchart: nodes appear, edges draw in, tokens travel"> |
|
|
31
|
+
|
|
32
|
+
```mermaid
|
|
33
|
+
sequenceDiagram
|
|
34
|
+
Player->>OTT: sign in
|
|
35
|
+
OTT-->>Player: token
|
|
36
|
+
Player->>DRM: token
|
|
37
|
+
DRM-->>Player: license
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
| Renderer | Result |
|
|
41
|
+
|---|---|
|
|
42
|
+
| Mermaid | <img src="https://raw.githubusercontent.com/dkdev24/pele-animate/main/examples/m2.png" width="560" alt="Sequence diagram rendered by Mermaid"> |
|
|
43
|
+
| Pele | <img src="https://raw.githubusercontent.com/dkdev24/pele-animate/main/examples/p2.png" width="560" alt="Sequence diagram rendered by Pele"> |
|
|
44
|
+
| pele-animate | <img src="https://raw.githubusercontent.com/dkdev24/pele-animate/main/examples/a2.gif" width="560" alt="Animated sequence diagram: messages appear in order, tokens travel"> |
|
|
45
|
+
|
|
46
|
+
With glyphs (`@{ icon }` nodes, `%% pa: 1=P 2=O 3=D` pins the order):
|
|
47
|
+
|
|
48
|
+
<img src="https://raw.githubusercontent.com/dkdev24/pele-animate/main/examples/a3.gif" width="560" alt="Animated scene with illustrated Player, OTT service and DRM server">
|
|
49
|
+
|
|
13
50
|
## Install
|
|
14
51
|
|
|
15
52
|
```sh
|
|
@@ -51,15 +88,15 @@ flowchart LR
|
|
|
51
88
|
|
|
52
89
|
| Export | |
|
|
53
90
|
|---|---|
|
|
54
|
-
| `watch(container, source?)` | Animate the SVG inside `container` (and every re-render of it); adds the scene panel
|
|
91
|
+
| `watch(container, source?)` | Animate the SVG inside `container` (and every re-render of it); adds the scene panel and step mode (click / Enter); pass `{ controls: false }` as a third argument for click-to-replay instead |
|
|
55
92
|
| `animate(svg, { hover, source })` | Animate one SVG; you handle re-renders |
|
|
56
93
|
| `replay(svg)` | Play the intro again |
|
|
57
94
|
| `parseOrder(source)` | Read the `%% pa:` pins |
|
|
58
95
|
| `registerGlyphs(pack, strokeWidth = 1.1)` (`/glyphs`) | Add glyphs: `{ name: '<inner markup of a 24x24 svg>' }`; colors via `--pa-solid` / `--pa-light` |
|
|
59
96
|
| `icons` (`/glyphs`) | Pele `icons` resolver |
|
|
60
97
|
|
|
61
|
-
Generic glyphs: `player`, `server`, `key`, `document`, `shield`, `phone`. `preset-doverunner`: `package`, `token`, `lock`,
|
|
62
|
-
`database`, `bits-a`, `bits-b`.
|
|
98
|
+
Generic glyphs: `player`, `server`, `key`, `document`, `shield`, `phone`, `cloud`. `preset-doverunner`: `package`, `token`, `lock`,
|
|
99
|
+
`database`, `video`, `media`, `encoder`, `storage`, `plugin`, `variants`, `watermark`, `bits-a`, `bits-b`.
|
|
63
100
|
|
|
64
101
|
## Theme
|
|
65
102
|
|
|
@@ -71,3 +108,7 @@ variable on it; defaults are `:where()` rules, so your stylesheet wins.
|
|
|
71
108
|
- Relies on Pele's SVG structure (`.pele-node`, `.pele-edge`, `data-id`); `pele` is a peer dependency pinned to `^1.7.0`.
|
|
72
109
|
- Flowcharts and sequence diagrams only. No types yet.
|
|
73
110
|
- Glyph art is square-boxed; wide art is drawn as a wide glyph inside the square.
|
|
111
|
+
|
|
112
|
+
## Changelog
|
|
113
|
+
|
|
114
|
+
See [CHANGELOG.md](CHANGELOG.md).
|
package/glyphs.js
CHANGED
|
@@ -19,4 +19,5 @@ registerGlyphs({
|
|
|
19
19
|
document: `<path d="M6 2.5h8l4.5 4.5v14a1 1 0 0 1-1 1h-11.5a1 1 0 0 1-1-1v-17.5a1 1 0 0 1 1-1z" fill="${L}" stroke="${S}"/><path d="M14 2.5V7h4.5" fill="none" stroke="${S}"/><path d="M8.5 12h7M8.5 15.5h7" stroke="${S}"/>`,
|
|
20
20
|
shield: `<path d="M12 2.5l8 3v6c0 5-3.4 8.5-8 10-4.6-1.5-8-5-8-10v-6z" fill="${S}" stroke="none"/><path d="M8.5 12l2.6 2.6 4.6-5" fill="none" stroke="${W}" stroke-width="2.2"/>`,
|
|
21
21
|
phone: `<rect x="6.5" y="2" width="11" height="20" rx="2.5" fill="${W}" stroke="${S}"/><path d="M10.5 3.8h3" stroke="${S}"/><rect x="8.5" y="6" width="7" height="4" rx="1" fill="${S}" stroke="none"/><path d="M8.5 12.5h7M8.5 15h7M8.5 17.5h4" stroke="${S}"/><path d="M10.5 20.2h3" stroke="${S}"/>`,
|
|
22
|
+
cloud: `<path d="M7 18a4 4 0 0 1-.6-7.96A5.5 5.5 0 0 1 17 8.5a4.75 4.75 0 0 1 .5 9.5z" fill="${L}" stroke="${S}"/>`,
|
|
22
23
|
});
|
package/package.json
CHANGED
package/pele-animate.css
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
/* Draw-in once when scrolled into view; a dot travels each edge while hovered/focused. Idle = finished picture. */
|
|
2
|
-
.pa-ready:not(.pa-in) .pele-node,
|
|
3
|
-
.pa-ready:not(.pa-in) .pele-edge-label,
|
|
4
|
-
.pa-ready:not(.pa-in) .pele-lifeline,
|
|
5
|
-
.pa-ready:not(.pa-in) .pele-marker { opacity: 0; }
|
|
6
|
-
.pa-ready:not(.pa-in) .pele-edge > path:not(.pele-marker) { stroke-dasharray: var(--len); stroke-dashoffset: var(--len); }
|
|
2
|
+
.pa-ready:not(.pa-in):not(.pa-step) .pele-node,
|
|
3
|
+
.pa-ready:not(.pa-in):not(.pa-step) .pele-edge-label,
|
|
4
|
+
.pa-ready:not(.pa-in):not(.pa-step) .pele-lifeline,
|
|
5
|
+
.pa-ready:not(.pa-in):not(.pa-step) .pele-marker { opacity: 0; }
|
|
6
|
+
.pa-ready:not(.pa-in):not(.pa-step) .pele-edge > path:not(.pele-marker) { stroke-dasharray: var(--len); stroke-dashoffset: var(--len); }
|
|
7
7
|
|
|
8
8
|
/* Keyframes carry the dash, so edges that are dashed in the diagram go back to their own dash pattern afterwards. */
|
|
9
|
-
.pa-ready.pa-in .pele-node,
|
|
10
|
-
.pa-ready.pa-in .pele-edge-label,
|
|
11
|
-
.pa-ready.pa-in .pele-lifeline,
|
|
12
|
-
.pa-ready.pa-in .pele-marker { animation: pa-fade .4s ease-out backwards; animation-delay: var(--d, 0s); }
|
|
13
|
-
.pa-ready.pa-in .pele-edge > path:not(.pele-marker) { animation: pa-draw .4s ease-out backwards; animation-delay: var(--d, 0s); }
|
|
14
|
-
.pa-ready.pa-done * { animation: none !important; }
|
|
9
|
+
.pa-ready.pa-in:not(.pa-step) .pele-node,
|
|
10
|
+
.pa-ready.pa-in:not(.pa-step) .pele-edge-label,
|
|
11
|
+
.pa-ready.pa-in:not(.pa-step) .pele-lifeline,
|
|
12
|
+
.pa-ready.pa-in:not(.pa-step) .pele-marker { animation: pa-fade .4s ease-out backwards; animation-delay: var(--d, 0s); }
|
|
13
|
+
.pa-ready.pa-in:not(.pa-step) .pele-edge > path:not(.pele-marker) { animation: pa-draw .4s ease-out backwards; animation-delay: var(--d, 0s); }
|
|
14
|
+
.pa-ready.pa-done:not(.pa-step) * { animation: none !important; }
|
|
15
15
|
@keyframes pa-fade { from { opacity: 0; } }
|
|
16
16
|
@keyframes pa-draw { from { stroke-dasharray: var(--len); stroke-dashoffset: var(--len); } to { stroke-dasharray: var(--len); stroke-dashoffset: 0; } }
|
|
17
17
|
|
|
@@ -19,10 +19,17 @@
|
|
|
19
19
|
|
|
20
20
|
@media (prefers-reduced-motion: reduce) {
|
|
21
21
|
.pa-ready * { animation: none !important; }
|
|
22
|
-
.pa-ready .pele-node, .pa-ready .pele-edge-label, .pa-ready .pele-lifeline, .pa-ready .pele-marker { opacity: 1 !important; }
|
|
22
|
+
.pa-ready:not(.pa-step) .pele-node, .pa-ready:not(.pa-step) .pele-edge-label, .pa-ready:not(.pa-step) .pele-lifeline, .pa-ready:not(.pa-step) .pele-marker { opacity: 1 !important; }
|
|
23
23
|
.pa-ready .pele-edge > path { stroke-dasharray: none; stroke-dashoffset: 0 !important; }
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/* Step mode: elements are shown by step; the ones just revealed fade / draw in. */
|
|
27
|
+
.pa-step .pa-hid { opacity: 0; }
|
|
28
|
+
.pa-step .pa-dot { display: none; }
|
|
29
|
+
.pa-step .pa-pop { animation: pa-fade .4s ease-out backwards; }
|
|
30
|
+
.pa-step .pele-edge > path:not(.pele-marker).pa-pop { animation-name: pa-draw; }
|
|
31
|
+
.pa-step .pele-edge > .pele-marker.pa-pop { animation-delay: .3s; }
|
|
32
|
+
|
|
26
33
|
/* Scene frame: tinted panel like the Lab product cards. Override any --pa-* / --pele-* on the container to retheme. */
|
|
27
34
|
:where(.pa-scene) {
|
|
28
35
|
--pa-bg: #efeafa; --pa-solid: #4F378B; --pa-light: #e4dcf6;
|
|
@@ -43,3 +50,16 @@
|
|
|
43
50
|
.pa-art > rect:first-child { fill: none; stroke: none; }
|
|
44
51
|
/* Edge labels sit on the line; give them the panel color so the line doesn't strike through the text. */
|
|
45
52
|
.pa-scene .pele-edge-label rect { fill: var(--pa-bg); }
|
|
53
|
+
|
|
54
|
+
/* Group and box titles read as headings. */
|
|
55
|
+
:where(.pa-scene) .pele-cluster-label { font-weight: 700; }
|
|
56
|
+
|
|
57
|
+
/* Step controls: previous | play/pause | next, shown under the diagram in step mode. */
|
|
58
|
+
.pa-scene.pa-stepping { flex-direction: column; align-items: center; gap: .75rem; }
|
|
59
|
+
.pa-controls { display: flex; align-items: center; gap: .125rem; padding: .25rem; border-radius: 999px; cursor: default;
|
|
60
|
+
background: var(--pele-surface); color: var(--pele-fg); border: 1px solid var(--pele-border); font: 500 .8rem/1 system-ui, sans-serif; }
|
|
61
|
+
.pa-controls button { width: 2rem; height: 2rem; padding: 0; border: 0; border-radius: 50%; background: none; color: inherit; font: inherit; font-size: 1rem; cursor: pointer; }
|
|
62
|
+
.pa-controls button:hover:not(:disabled) { background: var(--pele-surface-alt); }
|
|
63
|
+
.pa-controls button:disabled { opacity: .35; cursor: default; }
|
|
64
|
+
.pa-controls button:focus-visible { outline: 2px solid var(--pa-solid); }
|
|
65
|
+
.pa-count { min-width: 4ch; padding: 0 .5rem; text-align: center; font-variant-numeric: tabular-nums; }
|
package/pele-animate.js
CHANGED
|
@@ -48,8 +48,37 @@ function fitGlyphs(svg) {
|
|
|
48
48
|
});
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
// Pele sizes each sequence participant box to its own text, which leaves ragged rows. Give every box in a row the tallest height:
|
|
52
|
+
// header boxes grow upward (bottoms stay on the lifeline start), footer boxes grow downward (tops stay on the lifeline end).
|
|
53
|
+
function equalizeActors(svg) {
|
|
54
|
+
if (!svg.classList.contains('pele-sequence') || svg.dataset.paEqualized) return;
|
|
55
|
+
svg.dataset.paEqualized = '1';
|
|
56
|
+
for (const footer of [false, true]) {
|
|
57
|
+
const boxes = [...svg.querySelectorAll('g.pele-actor-participant')].filter((g) => g.classList.contains('pele-actor-footer') === footer);
|
|
58
|
+
const rects = boxes.map((g) => g.querySelector('rect')).filter(Boolean);
|
|
59
|
+
if (rects.length < 2) continue;
|
|
60
|
+
const num = (r, a) => +r.getAttribute(a);
|
|
61
|
+
const heights = rects.map((r) => num(r, 'height'));
|
|
62
|
+
const max = Math.max(...heights);
|
|
63
|
+
const top = Math.min(...rects.map((r) => num(r, 'y')));
|
|
64
|
+
const bottom = Math.max(...rects.map((r) => num(r, 'y') + num(r, 'height')));
|
|
65
|
+
boxes.forEach((g, i) => {
|
|
66
|
+
const grow = max - heights[i];
|
|
67
|
+
if (!grow) return;
|
|
68
|
+
rects[i].setAttribute('y', String(footer ? top : bottom - max));
|
|
69
|
+
rects[i].setAttribute('height', String(max));
|
|
70
|
+
const dy = footer ? grow / 2 : -grow / 2; // re-centre the label
|
|
71
|
+
g.querySelectorAll('text, tspan').forEach((t) => {
|
|
72
|
+
const y = t.getAttribute('y');
|
|
73
|
+
if (y !== null) t.setAttribute('y', String(+y + dy));
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
51
79
|
export function animate(svg, { hover = svg, source = '' } = {}) {
|
|
52
80
|
fitGlyphs(svg);
|
|
81
|
+
equalizeActors(svg);
|
|
53
82
|
svg.querySelectorAll('.pa-dot').forEach((d) => d.remove());
|
|
54
83
|
svg.classList.add('pa-ready');
|
|
55
84
|
const nodes = [...svg.querySelectorAll('.pele-node')];
|
|
@@ -57,26 +86,38 @@ export function animate(svg, { hover = svg, source = '' } = {}) {
|
|
|
57
86
|
const ids = [...new Set(nodes.map((n) => n.dataset.id))];
|
|
58
87
|
const set = (el, t) => el?.style.setProperty('--d', `${t}s`);
|
|
59
88
|
const times = new Map(); // edge element -> start time
|
|
89
|
+
const step = new Map(); // node / edge element -> step index (BFS level, or message number in a sequence)
|
|
60
90
|
|
|
61
91
|
if (svg.classList.contains('pele-sequence')) {
|
|
62
92
|
const u = Math.min(UNIT, MAX_INTRO / Math.max(1, edges.length));
|
|
63
93
|
nodes.forEach((n) => set(n, 0));
|
|
64
94
|
svg.querySelectorAll('.pele-lifeline').forEach((l) => set(l, 0.2));
|
|
65
|
-
edges.forEach((e, i) => times.set(e, 0.5 + i * u));
|
|
95
|
+
edges.forEach((e, i) => { times.set(e, 0.5 + i * u); step.set(e, i + 1); });
|
|
96
|
+
nodes.forEach((n) => step.set(n, 0));
|
|
66
97
|
} else {
|
|
67
98
|
const pairs = edges.map((e) => endpoints(e.dataset.id, ids));
|
|
68
99
|
const level = bfs(ids, pairs.filter(Boolean), parseOrder(source));
|
|
69
100
|
const u = Math.min(UNIT, MAX_INTRO / (Math.max(0, ...level.values()) + 1));
|
|
70
101
|
const k = u / UNIT;
|
|
71
|
-
nodes.forEach((n) => set(n, level.get(n.dataset.id) * u));
|
|
102
|
+
nodes.forEach((n) => { set(n, level.get(n.dataset.id) * u); step.set(n, level.get(n.dataset.id)); });
|
|
72
103
|
// Start after the source is up, but never so early that the arrow lands before its target appears (pinned steps).
|
|
73
104
|
edges.forEach((e, i) => {
|
|
74
105
|
const [s, d] = pairs[i] ?? [];
|
|
106
|
+
step.set(e, Math.max(level.get(s) ?? 0, level.get(d) ?? 0));
|
|
75
107
|
times.set(e, Math.max((level.get(s) ?? 0) * u + 0.3 * k, (level.get(d) ?? 0) * u - 0.5 * k));
|
|
76
108
|
});
|
|
77
109
|
}
|
|
78
110
|
|
|
79
111
|
const labels = new Map([...svg.querySelectorAll('.pele-edge-label')].map((l) => [l.dataset.id, l]));
|
|
112
|
+
|
|
113
|
+
// Step mode: each step's elements are revealed together; [element, parts that animate in].
|
|
114
|
+
const groups = new Map();
|
|
115
|
+
const add = (n, el, pop = [el]) => el && (groups.get(n) ?? groups.set(n, []).get(n)).push([el, pop]);
|
|
116
|
+
nodes.forEach((n) => add(step.get(n), n));
|
|
117
|
+
svg.querySelectorAll('.pele-lifeline').forEach((l) => add(0, l));
|
|
118
|
+
for (const e of edges) { add(step.get(e), e, [...e.querySelectorAll('path')]); add(step.get(e), labels.get(e.dataset.id)); }
|
|
119
|
+
svg.paSteps = [...groups].sort((a, b) => a[0] - b[0]).map(([, items]) => items);
|
|
120
|
+
|
|
80
121
|
const dots = [];
|
|
81
122
|
for (const [e, t] of times) {
|
|
82
123
|
const path = e.querySelector('path:not(.pele-marker)');
|
|
@@ -97,7 +138,7 @@ export function animate(svg, { hover = svg, source = '' } = {}) {
|
|
|
97
138
|
const cycle = Math.max(0, ...times.values()) + TOKEN + 0.8;
|
|
98
139
|
let running = [];
|
|
99
140
|
const play = () => {
|
|
100
|
-
if (reduced() || running.length) return;
|
|
141
|
+
if (reduced() || running.length || svg.classList.contains('pa-step')) return;
|
|
101
142
|
running = dots.map(([dot, t]) => {
|
|
102
143
|
const a = t / cycle, b = (t + TOKEN) / cycle, e = 0.001;
|
|
103
144
|
return dot.animate(
|
|
@@ -127,22 +168,118 @@ export function animate(svg, { hover = svg, source = '' } = {}) {
|
|
|
127
168
|
io.observe(svg);
|
|
128
169
|
}
|
|
129
170
|
|
|
171
|
+
const STEP_MS = 1200; // autoplay pace in step mode
|
|
172
|
+
|
|
173
|
+
// Show steps 0..k and hide the rest; `pop` animates the elements of step k.
|
|
174
|
+
function showStep(svg, k, pop) {
|
|
175
|
+
svg.classList.add('pa-step');
|
|
176
|
+
svg.querySelectorAll('.pa-pop').forEach((el) => el.classList.remove('pa-pop'));
|
|
177
|
+
svg.paSteps.forEach((items, i) => items.forEach(([el]) => el.classList.toggle('pa-hid', i > k)));
|
|
178
|
+
void svg.getBoundingClientRect();
|
|
179
|
+
if (pop) svg.paSteps[k].forEach(([, parts]) => parts.forEach((p) => p.classList.add('pa-pop')));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function leaveStep(svg) {
|
|
183
|
+
svg.classList.remove('pa-step');
|
|
184
|
+
svg.querySelectorAll('.pa-hid, .pa-pop').forEach((el) => el.classList.remove('pa-hid', 'pa-pop'));
|
|
185
|
+
svg.classList.add('pa-in', 'pa-done');
|
|
186
|
+
}
|
|
187
|
+
|
|
130
188
|
// mount() re-renders on container resize; re-animate each new <svg>, skipping the intro once it has played.
|
|
131
|
-
|
|
189
|
+
// Click (or Enter/Space) enters step mode: previous | play/pause | next, Space, ←/→, Esc to leave. `controls: false` keeps click-to-replay.
|
|
190
|
+
export function watch(container, source = '', { controls = true } = {}) {
|
|
132
191
|
let played = false;
|
|
192
|
+
let k = -1; // current step; -1 = not in step mode
|
|
193
|
+
let timer = 0;
|
|
194
|
+
const cur = () => container.querySelector('svg.pele');
|
|
195
|
+
const bar = document.createElement('div');
|
|
196
|
+
bar.className = 'pa-controls';
|
|
197
|
+
bar.setAttribute('role', 'toolbar');
|
|
198
|
+
bar.setAttribute('aria-label', 'Step through the diagram');
|
|
199
|
+
bar.title = ''; // an empty title stops the container's tooltip showing through the gaps
|
|
200
|
+
bar.innerHTML = '<button type="button" data-a="prev" aria-label="Previous step" title="Previous step (\u2190)">\u2039</button>'
|
|
201
|
+
+ '<button type="button" data-a="play"></button>'
|
|
202
|
+
+ '<button type="button" data-a="next" aria-label="Next step" title="Next step (\u2192)">\u203A</button>'
|
|
203
|
+
+ '<span class="pa-count" aria-live="polite" title="Current step"></span>'
|
|
204
|
+
+ '<button type="button" data-a="exit" aria-label="Exit step mode" title="Exit step mode (Esc)">\u2715\uFE0E</button>';
|
|
205
|
+
const btn = (a) => bar.querySelector(`[data-a=${a}]`);
|
|
206
|
+
const last = () => (cur()?.paSteps?.length ?? 1) - 1;
|
|
207
|
+
const ui = () => {
|
|
208
|
+
btn('prev').disabled = k <= 0;
|
|
209
|
+
btn('next').disabled = k >= last();
|
|
210
|
+
btn('play').textContent = timer ? '\u275A\u275A' : '\u25B6\uFE0E';
|
|
211
|
+
const label = timer ? 'Pause (Space)' : 'Play (Space)';
|
|
212
|
+
btn('play').setAttribute('aria-label', label);
|
|
213
|
+
btn('play').title = label;
|
|
214
|
+
bar.querySelector('.pa-count').textContent = `${k + 1} / ${last() + 1}`;
|
|
215
|
+
};
|
|
216
|
+
const pause = () => { clearTimeout(timer); timer = 0; };
|
|
217
|
+
const go = (n) => {
|
|
218
|
+
const svg = cur();
|
|
219
|
+
if (!svg?.paSteps) return;
|
|
220
|
+
const to = Math.max(0, Math.min(last(), n));
|
|
221
|
+
showStep(svg, to, to > k);
|
|
222
|
+
k = to;
|
|
223
|
+
ui();
|
|
224
|
+
};
|
|
225
|
+
const tick = () => {
|
|
226
|
+
if (k >= last()) { pause(); return ui(); }
|
|
227
|
+
go(k + 1);
|
|
228
|
+
timer = setTimeout(tick, STEP_MS);
|
|
229
|
+
};
|
|
230
|
+
const toggle = () => {
|
|
231
|
+
if (timer) { pause(); return ui(); }
|
|
232
|
+
if (k >= last()) go(0);
|
|
233
|
+
timer = setTimeout(tick, STEP_MS);
|
|
234
|
+
ui();
|
|
235
|
+
};
|
|
236
|
+
const manual = (n) => { pause(); go(n); };
|
|
237
|
+
const enter = () => {
|
|
238
|
+
played = true;
|
|
239
|
+
container.classList.add('pa-stepping');
|
|
240
|
+
container.append(bar);
|
|
241
|
+
go(0);
|
|
242
|
+
};
|
|
243
|
+
const exit = () => {
|
|
244
|
+
pause();
|
|
245
|
+
k = -1;
|
|
246
|
+
bar.remove();
|
|
247
|
+
container.classList.remove('pa-stepping');
|
|
248
|
+
if (cur()) leaveStep(cur());
|
|
249
|
+
container.focus();
|
|
250
|
+
};
|
|
251
|
+
const actions = { prev: () => manual(k - 1), next: () => manual(k + 1), play: toggle, exit };
|
|
252
|
+
bar.addEventListener('click', (e) => {
|
|
253
|
+
e.stopPropagation(); // else the container's click would re-enter step mode right after Exit
|
|
254
|
+
actions[e.target.closest('button')?.dataset.a]?.();
|
|
255
|
+
});
|
|
256
|
+
|
|
133
257
|
const run = () => {
|
|
258
|
+
if (k >= 0 && !bar.isConnected) container.append(bar); // a re-render may have wiped it
|
|
134
259
|
const svg = container.querySelector('svg.pele:not(.pa-ready)');
|
|
135
260
|
if (!svg) return;
|
|
136
261
|
animate(svg, { hover: container, source });
|
|
137
262
|
if (played) svg.classList.add('pa-in', 'pa-done');
|
|
138
263
|
else svg.addEventListener('animationend', () => (played = true), { once: true });
|
|
264
|
+
if (k >= 0) { k = Math.min(k, last()); showStep(svg, k, false); ui(); }
|
|
139
265
|
};
|
|
140
266
|
new MutationObserver(run).observe(container, { childList: true });
|
|
141
267
|
container.classList.add('pa-scene');
|
|
142
268
|
container.tabIndex = 0;
|
|
143
|
-
container.title ||= 'Click to replay';
|
|
144
|
-
|
|
145
|
-
|
|
269
|
+
container.title ||= controls ? 'Click to step through' : 'Click to replay';
|
|
270
|
+
if (!controls) {
|
|
271
|
+
container.addEventListener('click', () => replay(cur()));
|
|
272
|
+
container.addEventListener('keydown', (e) => (e.key === 'Enter' || e.key === ' ') && (e.preventDefault(), replay(cur())));
|
|
273
|
+
} else {
|
|
274
|
+
container.addEventListener('click', () => k < 0 && enter());
|
|
275
|
+
container.addEventListener('keydown', (e) => {
|
|
276
|
+
const key = e.key;
|
|
277
|
+
if (e.target.closest('button') && (key === 'Enter' || key === ' ')) return; // native button activation
|
|
278
|
+
if (k < 0) return (key === 'Enter' || key === ' ') && (e.preventDefault(), enter());
|
|
279
|
+
const act = { ' ': toggle, ArrowRight: () => manual(k + 1), ArrowLeft: () => manual(k - 1), Escape: exit }[key];
|
|
280
|
+
if (act) { e.preventDefault(); act(); }
|
|
281
|
+
});
|
|
282
|
+
}
|
|
146
283
|
run();
|
|
147
284
|
}
|
|
148
285
|
|
package/preset-doverunner.js
CHANGED
|
@@ -15,6 +15,13 @@ registerGlyphs({
|
|
|
15
15
|
package: `<path d="M12 2.5l8.5 4.5v10L12 21.5 3.5 17V7z" fill="${L}" stroke="${S}"/><path d="M3.5 7L12 11.5 20.5 7M12 11.5v10" fill="none" stroke="${S}"/><path d="M8 4.8l8.5 4.5" stroke="${S}"/>`,
|
|
16
16
|
token: `<path d="M3 7.5A1.5 1.5 0 0 1 4.5 6h15A1.5 1.5 0 0 1 21 7.5V10a2 2 0 0 0 0 4v2.5a1.5 1.5 0 0 1-1.5 1.5h-15A1.5 1.5 0 0 1 3 16.5V14a2 2 0 0 0 0-4z" fill="${L}" stroke="${S}"/><circle cx="9" cy="12" r="2.2" fill="${S}" stroke="none"/><path d="M13.5 10.5h4.5M13.5 13.5h3" stroke="${S}"/>`,
|
|
17
17
|
lock: `<path d="M8.5 10.5V8a3.5 3.5 0 0 1 7 0v2.5" fill="none" stroke="${S}"/><rect x="5" y="10.5" width="14" height="10" rx="2" fill="${S}" stroke="none"/><circle cx="12" cy="15" r="1.5" fill="${W}" stroke="none"/><path d="M12 15.5v2.2" stroke="${W}"/>`,
|
|
18
|
+
video: `<rect x="2" y="6" width="13" height="12" rx="2" fill="${L}" stroke="${S}"/><path d="M15 10.8l6-3.6v9.6l-6-3.6z" fill="${S}" stroke="${S}"/><circle cx="5.6" cy="9.4" r="1" fill="${S}" stroke="none"/>`,
|
|
19
|
+
media: `<rect x="2" y="4" width="15" height="11" rx="2" fill="${L}" stroke="${S}"/><path d="M7.6 7.2v5.6l4.6-2.8z" fill="${S}" stroke="none"/><ellipse cx="14.8" cy="19" rx="2.3" ry="1.8" fill="${S}" stroke="none"/><path d="M17 19V9.5q3 .2 3.6 3.4" fill="none" stroke="${S}"/>`,
|
|
20
|
+
storage: `<ellipse cx="12" cy="6" rx="8" ry="2.6" fill="${L}" stroke="${S}"/><path d="M4 6l2.4 13.4c.4 1.3 3 2.1 5.6 2.1s5.2-.8 5.6-2.1L20 6" fill="${L}" stroke="${S}"/><path d="M4.6 9.6c1.6 1.2 4.2 1.8 7.4 1.8s5.8-.6 7.4-1.8" fill="none" stroke="${S}"/>`,
|
|
21
|
+
plugin: `<path d="M4 8h4.5a2.2 2.2 0 1 1 4.4 0H17v4.1a2.2 2.2 0 1 1 0 4.4V20H4z" fill="${L}" stroke="${S}"/><path d="M17 8h3v12h-3" fill="none" stroke="${S}"/>`,
|
|
22
|
+
variants: `<rect x="2.5" y="3" width="14" height="9.5" rx="2" fill="${L}" stroke="${S}"/><rect x="7.5" y="11.5" width="14" height="9.5" rx="2" fill="${W}" stroke="${S}"/><text x="9.5" y="10.4" font-size="6.5" font-weight="700" text-anchor="middle" fill="${S}" stroke="none">A</text><text x="14.5" y="19" font-size="6.5" font-weight="700" text-anchor="middle" fill="${S}" stroke="none">B</text>`,
|
|
23
|
+
watermark: `<rect x="2.5" y="4" width="19" height="16" rx="2" fill="${L}" stroke="${S}"/><path d="M12 7.4c2.9 3.3 4.2 5 4.2 7a4.2 4.2 0 0 1-8.4 0c0-2 1.3-3.7 4.2-7z" fill="${S}" stroke="none"/><path d="M10.2 14.6a1.9 1.9 0 0 0 1.4 1.9" fill="none" stroke="${W}"/>`,
|
|
24
|
+
encoder: `<rect x="1.5" y="4" width="9" height="16" rx="1.5" fill="${L}" stroke="${S}"/><rect x="4.3" y="7" width="3.4" height="10" rx=".6" fill="${W}" stroke="${S}"/><path d="M2.9 6.5h.9M2.9 10.5h.9M2.9 14.5h.9M8.2 6.5h.9M8.2 10.5h.9M8.2 14.5h.9" stroke="${S}"/><path d="M12 12h3.5M14 10l2 2-2 2" fill="none" stroke="${S}"/><rect x="17.6" y="6.2" width="2.4" height="2.8" rx=".5" fill="${S}" stroke="${S}"/><rect x="20.4" y="6.2" width="2.4" height="2.8" rx=".5" fill="${W}" stroke="${S}"/><rect x="17.6" y="10.6" width="2.4" height="2.8" rx=".5" fill="${W}" stroke="${S}"/><rect x="20.4" y="10.6" width="2.4" height="2.8" rx=".5" fill="${S}" stroke="${S}"/><rect x="17.6" y="15" width="2.4" height="2.8" rx=".5" fill="${S}" stroke="${S}"/><rect x="20.4" y="15" width="2.4" height="2.8" rx=".5" fill="${S}" stroke="${S}"/>`,
|
|
18
25
|
database: `<path d="M4 6v12c0 1.7 3.6 3 8 3s8-1.3 8-3V6" fill="${L}" stroke="${S}"/><ellipse cx="12" cy="6" rx="8" ry="3" fill="${L}" stroke="${S}"/><path d="M4 12c0 1.7 3.6 3 8 3s8-1.3 8-3" fill="none" stroke="${S}"/>`,
|
|
19
26
|
});
|
|
20
27
|
// Thinner line: the chips are small even at full box size.
|