pele-animate 0.15.0 → 0.16.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 CHANGED
@@ -16,7 +16,7 @@ Unofficial add-on; not affiliated with or endorsed by the Pele or Obsidian teams
16
16
  - **You set the order.** Nodes appear in the order of a `%% pa:` comment, or by default along the edges; edges draw in and labels fade in.
17
17
  - **Readers step through it.** Click (or Enter) starts step mode: previous / play-pause / next buttons, Space, ←/→ and Esc. Hover or focus sends tokens along the edges.
18
18
  - **Icons.** `@{ icon: "name" }` nodes use any of 4,368 [Tabler](https://github.com/tabler/tabler-icons) icons (loaded per category) or your own. Unknown names get a neutral rounded square.
19
- - Flowcharts and sequence diagrams. The finished picture is the resting state; `prefers-reduced-motion` turns the motion off. Light and dark themes.
19
+ - Flowcharts, sequence diagrams, architecture diagrams, state diagrams, C4 context, block, class and ER diagrams, timelines, journeys, git graphs and Gantt charts. The finished picture is the resting state; `prefers-reduced-motion` turns the motion off. Light and dark themes.
20
20
  - The source stays valid Mermaid. One catch: GitHub's own Mermaid renderer does not know the icon names and draws "?" boxes, so the icon examples in this repository are shown as source text.
21
21
 
22
22
  ## Install
@@ -7,7 +7,7 @@ section: Start
7
7
 
8
8
  # Diagram types
9
9
 
10
- pele-animate supports **flowcharts** and **sequence diagrams**. Pele renders other diagram types too, but they are outside what
10
+ pele-animate supports **flowcharts**, **sequence diagrams**, **architecture diagrams** and **state diagrams** and **C4 context diagrams** and **block diagrams**, **class diagrams**, **ER diagrams**, **timelines**, **user journeys**, **git graphs** and **Gantt charts**. Pele renders other diagram types too, but they are outside what
11
11
  pele-animate is built for.
12
12
 
13
13
  ## Flowcharts
@@ -36,8 +36,173 @@ sequenceDiagram
36
36
 
37
37
  **Limit:** Pele ignores `@{ icon }` on sequence participants (the box shows its id), so icons apply to flowcharts only.
38
38
 
39
+ ## Architecture diagrams
40
+
41
+ `architecture-beta`: services in groups, joined by edges. Services appear in step order like flowchart nodes, groups stay visible,
42
+ and `%% pa:` pins work the same way. The icon in parentheses is looked up in your icon packs, so Tabler names and `doverunner-*`
43
+ names work (import the packs as for [icons](./icons.md)).
44
+
45
+ ```mermaid-icons live
46
+ architecture-beta
47
+ group edge(cloud)[Edge]
48
+ group core(cloud)[Core]
49
+ service user(device-desktop)[User]
50
+ service cdn(cloud-computing)[CDN] in edge
51
+ service api(server)[API] in core
52
+ service db(database)[Database] in core
53
+ user:R --> L:cdn
54
+ cdn:R --> L:api
55
+ api:R --> L:db
56
+ ```
57
+
58
+ Icons here are small and fixed in size; `form` and `h` apply to flowcharts only. Edges are read from their written direction
59
+ (`user:R --> L:cdn` is user to cdn). A node id containing `-` can be mistaken for two ids on an edge, so use plain ids.
60
+
61
+ ## State diagrams
62
+
63
+ `stateDiagram-v2`: states appear in step order, transitions draw from their source state, and transition labels fade in. The
64
+ start and end markers are nodes too, with the ids `root_start` and `root_end`, so pins can name them.
65
+
66
+ ```mermaid live
67
+ stateDiagram-v2
68
+ %% pa: 1=root_start 2=Idle 3=Running 4=Paused 5=root_end
69
+ [*] --> Idle
70
+ Idle --> Running: start
71
+ Running --> Paused: pause
72
+ Paused --> Running: resume
73
+ Running --> [*]: done
74
+ ```
75
+
76
+ Without pins the order is breadth-first from the start marker, which shows `Paused` and the end marker together. Pele gives
77
+ state edges no readable ids, so pele-animate finds each transition's ends from where its path starts and stops. Composite
78
+ states and forks are untested. State diagrams take no icons.
79
+
80
+ ## C4 context diagrams
81
+
82
+ `C4Context`: people and systems appear in step order and each `Rel` draws from its source, with its label. Pins use the ids you
83
+ give `Person(...)` and `System(...)`; an edge pin is `3=app>drm`.
84
+
85
+ ```mermaid live
86
+ C4Context
87
+ %% pa: 1=viewer 2=app 3=drm,cdn
88
+ Person(viewer, "Viewer", "Watches video")
89
+ System(app, "Player app", "Plays protected video")
90
+ System_Ext(drm, "DRM service", "Issues licenses")
91
+ System(cdn, "CDN", "Delivers segments")
92
+ Rel(viewer, app, "uses")
93
+ Rel(app, drm, "license")
94
+ Rel(app, cdn, "segments")
95
+ ```
96
+
97
+ Pele lays C4 diagrams out in rows, so a long label between two boxes in the same row can leave no room for the arrowhead:
98
+ keep `Rel` labels short. Only `C4Context` has been checked; the other C4 types and boundaries are untested. C4 shapes take no icons.
99
+
100
+ ## Block diagrams
101
+
102
+ `block-beta`: blocks appear in step order and each arrow draws from its source block. Pins use the block ids.
103
+
104
+ ```mermaid live
105
+ block-beta
106
+ columns 3
107
+ %% pa: 1=client 2=server 3=db
108
+ client["Client"] space server["Server"]
109
+ space space db[("Database")]
110
+ client --> server
111
+ server --> db
112
+ ```
113
+
114
+ Pele routes arrows between blocks in straight lines or short bends, so place linked blocks in the same row or column; a diagonal
115
+ neighbour can give a bent arrow that runs along another one. Block diagrams take no icons.
116
+
117
+ ## Class diagrams
118
+
119
+ `classDiagram`: classes appear in step order, relationships draw from the class written first. `Animal <|-- Dog` reveals `Animal`
120
+ and then `Dog`, whichever way the arrowhead points. Pins use the class names.
121
+
122
+ ```mermaid live
123
+ classDiagram
124
+ %% pa: 1=Animal 2=Dog,Cat 3=Owner
125
+ Animal <|-- Dog
126
+ Animal <|-- Cat
127
+ Dog --> Owner : has
128
+ Animal : +name
129
+ Dog : +bark()
130
+ ```
131
+
132
+ ## ER diagrams
133
+
134
+ `erDiagram`: entities appear in step order, relationships draw from the entity written first (`CUSTOMER ||--o{ ORDER` reveals
135
+ `CUSTOMER`, then `ORDER`).
136
+
137
+ ```mermaid live
138
+ erDiagram
139
+ %% pa: 1=CUSTOMER 2=ORDER 3=LINE,PRODUCT
140
+ CUSTOMER ||--o{ ORDER : places
141
+ ORDER ||--|{ LINE : contains
142
+ PRODUCT ||--o{ LINE : in
143
+ ```
144
+
145
+ Neither takes icons.
146
+
147
+ ## Timelines, journeys, git graphs and Gantt charts
148
+
149
+ These have no edges to follow, so each steps through its items in reading order and `%% pa:` pins do nothing (as for sequence
150
+ diagrams). Section headers, axes, grids and legends stay visible.
151
+
152
+ - **`timeline`:** one step per time period, with its events.
153
+
154
+ ```mermaid live
155
+ timeline
156
+ title History
157
+ section Early
158
+ 2002 : Launch : Beta
159
+ 2004 : Growth
160
+ section Late
161
+ 2010 : Scale
162
+ ```
163
+
164
+ - **`journey`:** one step per task: the task, its face, and the line segment that joins it to the task before.
165
+
166
+ ```mermaid live
167
+ journey
168
+ title My day
169
+ section Work
170
+ Make tea: 5: Me, Bob
171
+ Do work: 3: Me
172
+ section Home
173
+ Relax: 4: Me
174
+ ```
175
+
176
+ - **`gitGraph`:** one step per commit in the order written, with the edge that ends on it. A branch's label and lane appear with
177
+ its first commit.
178
+
179
+ ```mermaid live
180
+ gitGraph
181
+ commit id: "a"
182
+ branch dev
183
+ commit id: "b"
184
+ checkout main
185
+ commit id: "c"
186
+ merge dev id: "m"
187
+ ```
188
+
189
+ - **`gantt`:** one step per task, top to bottom.
190
+
191
+ ```mermaid live
192
+ gantt
193
+ dateFormat YYYY-MM-DD
194
+ section Build
195
+ Design :a1, 2024-01-01, 3d
196
+ Code :after a1, 4d
197
+ section Ship
198
+ Test :2024-01-08, 2d
199
+ ```
200
+
201
+ None of these take icons. Only these small examples have been checked in a browser; long diagrams, `todayMarker`, tags, cherry-picks and
202
+ parallel tasks are untested.
203
+
39
204
  ## Everything else
40
205
 
41
- Other Mermaid types (class, state, ER, gantt, pie and the rest) render through Pele as usual. pele-animate is not designed for
206
+ Other Mermaid types (pie, quadrant, radar and the rest) render through Pele as usual. pele-animate is not designed for
42
207
  them and its behavior on them is untested, so use plain Pele for those. Step mode has been checked in a browser on
43
- flowcharts only; sequence diagrams have not been checked the same way yet.
208
+ the types above only; sequence diagrams have not been checked the same way yet.
package/docs/faq.md CHANGED
@@ -22,7 +22,7 @@ you want to explain in order.
22
22
  **How big is it?** The core (`pele-animate.js` plus CSS) is about 20 KB unminified. Icon packs are separate modules, so you only
23
23
  ship the categories you import (the largest, `system`, is about 199 KB unminified).
24
24
 
25
- **Which diagrams does it support?** Flowcharts and sequence diagrams ([diagram types](./diagram-types.md)).
25
+ **Which diagrams does it support?** Flowcharts, sequence diagrams, architecture diagrams, state diagrams, C4 context, block, class and ER diagrams, timelines, journeys, git graphs and Gantt charts ([diagram types](./diagram-types.md)).
26
26
 
27
27
  **Can I use my own icons?** Yes ([custom icons](./custom-icons.md)).
28
28
 
@@ -14,6 +14,12 @@ them. By default, the order follows the edges. A `%% pa:` comment lets you set i
14
14
 
15
15
  - **Flowchart:** nodes appear by level, counting from the nodes nothing points to. Each edge draws from its source once the source
16
16
  is on screen, and its target appears as the edge lands. Branches of a decision appear together.
17
+ - **Architecture diagram:** like a flowchart: services appear by level, edges follow their written direction, pins work the same way.
18
+ - **Timeline, journey, git graph, Gantt chart:** one step per item in reading order (period, task, commit, task); pins do nothing.
19
+ - **Class and ER diagrams:** like a flowchart, but a relationship runs from the class or entity written first.
20
+ - **Block diagram:** like a flowchart, from the blocks nothing points to.
21
+ - **C4 context diagram:** like a flowchart, from the people and systems nothing points to.
22
+ - **State diagram:** like a flowchart, starting from the start marker (`root_start`); the end marker is `root_end`.
17
23
  - **Sequence diagram:** actors first, then the messages in order.
18
24
 
19
25
  ## Pins
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pele-animate",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "description": "Step-by-step animated Mermaid diagrams with icons: an unofficial add-on for the Pele renderer.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/pele-animate.css CHANGED
@@ -2,14 +2,16 @@
2
2
  .pa-ready:not(.pa-in):not(.pa-step) .pele-node,
3
3
  .pa-ready:not(.pa-in):not(.pa-step) .pele-edge-label,
4
4
  .pa-ready:not(.pa-in):not(.pa-step) .pele-lifeline,
5
- .pa-ready:not(.pa-in):not(.pa-step) .pele-marker { opacity: 0; }
5
+ .pa-ready:not(.pa-in):not(.pa-step) .pele-marker,
6
+ .pa-ready:not(.pa-in):not(.pa-step) .pa-item { opacity: 0; }
6
7
  .pa-ready:not(.pa-in):not(.pa-step) .pele-edge > path:not(.pele-marker) { stroke-dasharray: var(--len); stroke-dashoffset: var(--len); }
7
8
 
8
9
  /* Keyframes carry the dash, so edges that are dashed in the diagram go back to their own dash pattern afterwards. */
9
10
  .pa-ready.pa-in:not(.pa-step) .pele-node,
10
11
  .pa-ready.pa-in:not(.pa-step) .pele-edge-label,
11
12
  .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-marker,
14
+ .pa-ready.pa-in:not(.pa-step) .pa-item { animation: pa-fade .4s ease-out backwards; animation-delay: var(--d, 0s); }
13
15
  .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
16
  .pa-ready.pa-done:not(.pa-step) * { animation: none !important; }
15
17
  @keyframes pa-fade { from { opacity: 0; } }
@@ -19,7 +21,7 @@
19
21
 
20
22
  @media (prefers-reduced-motion: reduce) {
21
23
  .pa-ready * { animation: none !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; }
24
+ .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, .pa-ready:not(.pa-step) .pa-item { opacity: 1 !important; }
23
25
  .pa-ready .pele-edge > path { stroke-dasharray: none; stroke-dashoffset: 0 !important; }
24
26
  }
25
27
 
@@ -51,6 +53,9 @@
51
53
  /* Edge labels sit on the line; give them the panel color so the line doesn't strike through the text. */
52
54
  .pa-scene .pele-edge-label rect { fill: var(--pa-bg); }
53
55
 
56
+ /* Pele colours text on solid fills (timeline periods, gantt bars, git branch tags) with --_bg, which the scene makes transparent: use the panel surface instead. */
57
+ .pa-scene text[fill="var(--_bg)"] { fill: var(--pele-surface); }
58
+
54
59
  /* Group and box titles read as headings. */
55
60
  :where(.pa-scene) .pele-cluster-label { font-weight: 700; }
56
61
 
package/pele-animate.js CHANGED
@@ -16,11 +16,33 @@ export function parseOrder(source = '') {
16
16
  return levels;
17
17
  }
18
18
 
19
+ // Edge ids by diagram type: flowchart `L_A_B_0`, architecture and C4 `A-B` (repeat edges get `-N`), block `1-A-B`, class `id_A_B_1`,
20
+ // ER `id_entity-A-0_entity-B-1_0`. Ends follow the written order (`A <|-- B` is A, then B).
21
+ const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
19
22
  function endpoints(edgeId, ids) {
20
- for (const s of ids) for (const d of ids) if (new RegExp(`^L_${s}_${d}_\\d+$`).test(edgeId)) return [s, d];
23
+ for (const s of ids)
24
+ for (const d of ids)
25
+ if (new RegExp(`^(?:L_${esc(s)}_${esc(d)}_\\d+|(?:\\d+-)?${esc(s)}-${esc(d)}(?:-\\d+)?|id_${esc(s)}_${esc(d)}_\\d+|id_entity-${esc(s)}-\\d+_entity-${esc(d)}-\\d+_\\d+)$`).test(edgeId)) return [s, d];
21
26
  return null;
22
27
  }
23
28
 
29
+ // State diagrams name edges `edge0`, `edge1`, ... with no ends in the id, so read the ends off the geometry:
30
+ // the node nearest to where the path starts (source) and ends (target).
31
+ function geoEndpoints(edge, boxes) {
32
+ const path = edge.querySelector('path:not(.pele-marker)');
33
+ if (!path) return null;
34
+ const near = (pt) => {
35
+ let best = null, bd = 12; // an end more than 12 px from every node is not an end
36
+ for (const [id, b] of boxes) {
37
+ const d = Math.hypot(Math.max(b.x - pt.x, 0, pt.x - b.x - b.width), Math.max(b.y - pt.y, 0, pt.y - b.y - b.height));
38
+ if (d < bd) { bd = d; best = id; }
39
+ }
40
+ return best;
41
+ };
42
+ const s = near(path.getPointAtLength(0)), d = near(path.getPointAtLength(path.getTotalLength()));
43
+ return s && d ? [s, d] : null;
44
+ }
45
+
24
46
  function bfs(ids, pairs, pinned) {
25
47
  const level = new Map(pinned);
26
48
  const hasIn = new Set(pairs.map(([, d]) => d));
@@ -97,11 +119,77 @@ export function prepare(source = '') {
97
119
  }
98
120
 
99
121
 
122
+ // Diagrams with no edges to follow (timeline, journey, gitGraph, gantt) step through their items in reading order.
123
+ // Returns [[element, ...], ...], one array per step, or null for any other diagram.
124
+ const pts = (path) => path.getAttribute('d').match(/-?[\d.]+/g)?.map(Number) ?? [];
125
+ function sequentialSteps(svg) {
126
+ const all = (sel) => [...svg.querySelectorAll(sel)];
127
+ if (svg.classList.contains('pele-timeline')) return all('.pele-period').map((p) => [p]);
128
+ if (svg.classList.contains('pele-gantt')) return all('.pele-task').map((t) => [t]);
129
+ if (svg.classList.contains('pele-journey')) {
130
+ const tasks = all('.pele-task'), faces = all('.pele-face');
131
+ const line = svg.querySelector('path.pele-edge');
132
+ // The satisfaction line is one polyline `M x,yL x,y...`: split it so each segment arrives with its task.
133
+ const segs = [];
134
+ if (line && /^M[-\d.,]+(L[-\d.,]+)+$/.test(line.getAttribute('d'))) {
135
+ const p = line.getAttribute('d').slice(1).split('L');
136
+ for (let i = 1; i < p.length; i++) {
137
+ const seg = line.cloneNode();
138
+ seg.setAttribute('d', `M${p[i - 1]}L${p[i]}`);
139
+ line.before(seg);
140
+ segs.push(seg);
141
+ }
142
+ line.remove();
143
+ }
144
+ return tasks.map((t, i) => [t, faces[i], segs[i - 1]].filter(Boolean));
145
+ }
146
+ if (svg.classList.contains('pele-gitGraph')) {
147
+ const commits = all('.pele-commit');
148
+ const at = (c) => { const e = c.querySelector('circle'); return [+e.getAttribute('cx'), +e.getAttribute('cy')]; };
149
+ const steps = commits.map((c) => [c]);
150
+ // an edge arrives with the commit it ends on
151
+ for (const e of all('.pele-edge')) {
152
+ const path = e.matches('path') ? e : e.querySelector('path');
153
+ const end = path.getPointAtLength(path.getTotalLength());
154
+ let best = steps.length - 1, bd = Infinity;
155
+ commits.forEach((c, i) => { const [x, y] = at(c), d = Math.hypot(x - end.x, y - end.y); if (d < bd) { bd = d; best = i; } });
156
+ steps[best]?.push(e);
157
+ }
158
+ // a branch (label and lane) appears with its first commit: the commit whose y is on the lane
159
+ for (const b of all('.pele-branch')) {
160
+ const y = pts(b.querySelector('.pele-lane'))[1];
161
+ const first = commits.findIndex((c) => Math.abs(at(c)[1] - y) < 1);
162
+ steps[first < 0 ? 0 : first].push(b);
163
+ }
164
+ return steps;
165
+ }
166
+ return null;
167
+ }
168
+
169
+ // Intro for sequentialSteps: each step fades in after the one before; the same steps drive step mode.
170
+ function introSteps(svg, steps) {
171
+ const u = Math.min(UNIT, MAX_INTRO / Math.max(1, steps.length));
172
+ steps.forEach((els, i) => els.forEach((el) => { el.classList.add('pa-item'); el.style.setProperty('--d', `${i * u}s`); }));
173
+ svg.paSteps = steps.map((els) => els.map((el) => [el, [el]]));
174
+ reveal(svg);
175
+ }
176
+
177
+ function reveal(svg) {
178
+ if (!('IntersectionObserver' in window)) return svg.classList.add('pa-in');
179
+ svg.classList.remove('pa-in');
180
+ const io = new IntersectionObserver(([en]) => {
181
+ if (en.isIntersecting) { svg.classList.add('pa-in'); io.disconnect(); }
182
+ }, { threshold: 0.3 });
183
+ io.observe(svg);
184
+ }
185
+
100
186
  export function animate(svg, { hover = svg, source = '' } = {}) {
101
187
  fitGlyphs(svg);
102
188
  equalizeActors(svg);
103
189
  svg.querySelectorAll('.pa-dot').forEach((d) => d.remove());
104
190
  svg.classList.add('pa-ready');
191
+ const seq = sequentialSteps(svg);
192
+ if (seq) return introSteps(svg, seq);
105
193
  const nodes = [...svg.querySelectorAll('.pele-node')];
106
194
  const edges = [...svg.querySelectorAll('.pele-edge')];
107
195
  const ids = [...new Set(nodes.map((n) => n.dataset.id))];
@@ -116,7 +204,17 @@ export function animate(svg, { hover = svg, source = '' } = {}) {
116
204
  edges.forEach((e, i) => { times.set(e, 0.5 + i * u); step.set(e, i + 1); });
117
205
  nodes.forEach((n) => step.set(n, 0));
118
206
  } else {
119
- const pairs = edges.map((e) => endpoints(e.dataset.id, ids));
207
+ const boxes = new Map();
208
+ const pairs = edges.map((e) => {
209
+ const found = endpoints(e.dataset.id, ids);
210
+ if (found) return found;
211
+ if (!boxes.size)
212
+ for (const n of nodes) {
213
+ const b = n.getBBox(), t = n.transform.baseVal.consolidate()?.matrix;
214
+ boxes.set(n.dataset.id, { x: b.x + (t?.e ?? 0), y: b.y + (t?.f ?? 0), width: b.width, height: b.height });
215
+ }
216
+ return geoEndpoints(e, boxes);
217
+ });
120
218
  const level = bfs(ids, pairs.filter(Boolean), parseOrder(source));
121
219
  const u = Math.min(UNIT, MAX_INTRO / (Math.max(0, ...level.values()) + 1));
122
220
  const k = u / UNIT;
@@ -184,12 +282,7 @@ export function animate(svg, { hover = svg, source = '' } = {}) {
184
282
  hover.addEventListener('focusin', play);
185
283
  hover.addEventListener('focusout', stop);
186
284
 
187
- if (!('IntersectionObserver' in window)) return svg.classList.add('pa-in');
188
- svg.classList.remove('pa-in');
189
- const io = new IntersectionObserver(([en]) => {
190
- if (en.isIntersecting) { svg.classList.add('pa-in'); io.disconnect(); }
191
- }, { threshold: 0.3 });
192
- io.observe(svg);
285
+ reveal(svg);
193
286
  }
194
287
 
195
288
  const STEP_MS = 1200; // autoplay pace in step mode