pele-animate 0.11.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 +5 -2
- package/package.json +1 -1
- package/pele-animate.css +5 -4
- package/pele-animate.js +25 -10
package/README.md
CHANGED
|
@@ -82,7 +82,9 @@ flowchart LR
|
|
|
82
82
|
```
|
|
83
83
|
|
|
84
84
|
`form: "square", h: 88` makes a large frameless glyph; leave both out for a small icon in a frame.
|
|
85
|
-
`%% pa: 1=P 2=O 3=D` pins nodes to steps (optional).
|
|
85
|
+
`%% pa: 1=P 2=O 3=D` pins nodes to steps (optional). `X>Y` pins the edge from X to Y, so an edge between nodes that are
|
|
86
|
+
already visible can have its own step: `%% pa: 1=A,B 2=C,B>C 3=C>B`. Pin the nodes first; an edge pinned before its target appears lands early.
|
|
87
|
+
Sequence diagrams animate actors, then messages in order.
|
|
86
88
|
|
|
87
89
|
## API
|
|
88
90
|
|
|
@@ -91,7 +93,8 @@ flowchart LR
|
|
|
91
93
|
| `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 |
|
|
92
94
|
| `animate(svg, { hover, source })` | Animate one SVG; you handle re-renders |
|
|
93
95
|
| `replay(svg)` | Play the intro again |
|
|
94
|
-
| `parseOrder(source)` | Read the `%% pa:` pins |
|
|
96
|
+
| `parseOrder(source)` | Read the `%% pa:` pins: `Map(nodeId or 'X>Y' -> level)` |
|
|
97
|
+
| `svg.paSteps` | Set by `animate()`: one array per step, each item `[element, parts that animate in]`. Step mode uses it; read it to check the order |
|
|
95
98
|
| `registerGlyphs(pack, strokeWidth = 1.1)` (`/glyphs`) | Add glyphs: `{ name: '<inner markup of a 24x24 svg>' }`; colors via `--pa-solid` / `--pa-light` |
|
|
96
99
|
| `icons` (`/glyphs`) | Pele `icons` resolver |
|
|
97
100
|
|
package/package.json
CHANGED
package/pele-animate.css
CHANGED
|
@@ -58,8 +58,9 @@
|
|
|
58
58
|
.pa-scene.pa-stepping { flex-direction: column; align-items: center; gap: .75rem; }
|
|
59
59
|
.pa-controls { display: flex; align-items: center; gap: .125rem; padding: .25rem; border-radius: 999px; cursor: default;
|
|
60
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
|
|
62
|
-
.pa-controls button:
|
|
63
|
-
.pa-controls button:disabled {
|
|
61
|
+
.pa-scene .pa-controls > * { margin: 0; } /* host sites often add sibling margins (e.g. prose `* + *` rules) */
|
|
62
|
+
.pa-controls button { display: flex; align-items: center; justify-content: center; width: 2rem; height: 2rem; padding: 0; border: 0; border-radius: 50%; background: none; color: inherit; font: inherit; font-size: 1rem; cursor: pointer; }
|
|
63
|
+
.pa-controls button:hover:not([aria-disabled=true]) { background: var(--pele-surface-alt); }
|
|
64
|
+
.pa-controls button[aria-disabled=true] { opacity: .35; cursor: default; }
|
|
64
65
|
.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; }
|
|
66
|
+
.pa-count { min-width: 4ch; line-height: 2rem; padding: 0 .5rem; text-align: center; font-variant-numeric: tabular-nums; }
|
package/pele-animate.js
CHANGED
|
@@ -6,7 +6,8 @@ const MAX_INTRO = 3; // a long diagram squeezes its steps so the intro never run
|
|
|
6
6
|
const TOKEN = 0.5; // seconds a token takes to cross an edge
|
|
7
7
|
const reduced = () => matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
8
8
|
|
|
9
|
-
// `%% pa: 1=A,B 2=C` pins nodes to steps (1-based);
|
|
9
|
+
// `%% pa: 1=A,B 2=C 3=A>B` pins nodes to steps (1-based); `X>Y` pins the edge from X to Y. Returns Map(nodeId or 'X>Y' -> level).
|
|
10
|
+
// Everything else keeps its BFS level.
|
|
10
11
|
export function parseOrder(source = '') {
|
|
11
12
|
const m = source.match(/^\s*%%\s*pa:\s*(.+)$/m);
|
|
12
13
|
const levels = new Map();
|
|
@@ -103,8 +104,11 @@ export function animate(svg, { hover = svg, source = '' } = {}) {
|
|
|
103
104
|
// Start after the source is up, but never so early that the arrow lands before its target appears (pinned steps).
|
|
104
105
|
edges.forEach((e, i) => {
|
|
105
106
|
const [s, d] = pairs[i] ?? [];
|
|
106
|
-
|
|
107
|
-
|
|
107
|
+
const pin = level.get(`${s}>${d}`); // edge pinned to a step: starts with that step, once its source is up
|
|
108
|
+
step.set(e, pin ?? Math.max(level.get(s) ?? 0, level.get(d) ?? 0));
|
|
109
|
+
times.set(e, pin !== undefined
|
|
110
|
+
? Math.max(pin * u, (level.get(s) ?? 0) * u + 0.3 * k)
|
|
111
|
+
: Math.max((level.get(s) ?? 0) * u + 0.3 * k, (level.get(d) ?? 0) * u - 0.5 * k));
|
|
108
112
|
});
|
|
109
113
|
}
|
|
110
114
|
|
|
@@ -169,6 +173,15 @@ export function animate(svg, { hover = svg, source = '' } = {}) {
|
|
|
169
173
|
}
|
|
170
174
|
|
|
171
175
|
const STEP_MS = 1200; // autoplay pace in step mode
|
|
176
|
+
// Control icons are SVG, not text glyphs: glyphs like ‹ and ▶ sit at different heights in every font.
|
|
177
|
+
const icon = (d, fill) => `<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true" ${fill ? 'fill="currentColor"' : 'fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"'}>${d}</svg>`;
|
|
178
|
+
const ICON = {
|
|
179
|
+
prev: icon('<path d="M15 5l-7 7 7 7"/>'),
|
|
180
|
+
next: icon('<path d="M9 5l7 7-7 7"/>'),
|
|
181
|
+
play: icon('<path d="M8 5v14l11-7z"/>', 1),
|
|
182
|
+
pause: icon('<path d="M7 5h3.5v14H7zM13.5 5H17v14h-3.5z"/>', 1),
|
|
183
|
+
exit: icon('<path d="M6 6l12 12M18 6L6 18"/>'),
|
|
184
|
+
};
|
|
172
185
|
|
|
173
186
|
// Show steps 0..k and hide the rest; `pop` animates the elements of step k.
|
|
174
187
|
function showStep(svg, k, pop) {
|
|
@@ -197,17 +210,18 @@ export function watch(container, source = '', { controls = true } = {}) {
|
|
|
197
210
|
bar.setAttribute('role', 'toolbar');
|
|
198
211
|
bar.setAttribute('aria-label', 'Step through the diagram');
|
|
199
212
|
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)"
|
|
213
|
+
bar.innerHTML = '<button type="button" data-a="prev" aria-label="Previous step" title="Previous step (\u2190)">' + ICON.prev + '</button>'
|
|
201
214
|
+ '<button type="button" data-a="play"></button>'
|
|
202
|
-
+ '<button type="button" data-a="next" aria-label="Next step" title="Next step (\u2192)"
|
|
215
|
+
+ '<button type="button" data-a="next" aria-label="Next step" title="Next step (\u2192)">' + ICON.next + '</button>'
|
|
203
216
|
+ '<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)"
|
|
217
|
+
+ '<button type="button" data-a="exit" aria-label="Exit step mode" title="Exit step mode (Esc)">' + ICON.exit + '</button>';
|
|
205
218
|
const btn = (a) => bar.querySelector(`[data-a=${a}]`);
|
|
206
219
|
const last = () => (cur()?.paSteps?.length ?? 1) - 1;
|
|
207
220
|
const ui = () => {
|
|
208
|
-
|
|
209
|
-
btn('
|
|
210
|
-
btn('
|
|
221
|
+
// aria-disabled, not `disabled`: a disabled button passes hover to the container, whose tooltip then shows over it
|
|
222
|
+
btn('prev').setAttribute('aria-disabled', String(k <= 0));
|
|
223
|
+
btn('next').setAttribute('aria-disabled', String(k >= last()));
|
|
224
|
+
btn('play').innerHTML = timer ? ICON.pause : ICON.play;
|
|
211
225
|
const label = timer ? 'Pause (Space)' : 'Play (Space)';
|
|
212
226
|
btn('play').setAttribute('aria-label', label);
|
|
213
227
|
btn('play').title = label;
|
|
@@ -251,7 +265,8 @@ export function watch(container, source = '', { controls = true } = {}) {
|
|
|
251
265
|
const actions = { prev: () => manual(k - 1), next: () => manual(k + 1), play: toggle, exit };
|
|
252
266
|
bar.addEventListener('click', (e) => {
|
|
253
267
|
e.stopPropagation(); // else the container's click would re-enter step mode right after Exit
|
|
254
|
-
|
|
268
|
+
const b = e.target.closest('button');
|
|
269
|
+
if (b?.getAttribute('aria-disabled') !== 'true') actions[b?.dataset.a]?.();
|
|
255
270
|
});
|
|
256
271
|
|
|
257
272
|
const run = () => {
|