@markdy/renderer-dom 0.8.20 → 0.8.22

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/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { BeatRange, Diagnostic } from '@markdy/core';
1
+ import { BeatRange, Diagnostic, RenderPlan } from '@markdy/core';
2
2
 
3
3
  /**
4
4
  * Markdy diagram runtime — renders a RenderPlan via WAAPI.
@@ -54,4 +54,54 @@ type IconSpec = SvgSpec;
54
54
  /** Read-only monochrome glyph registry; callers can inspect or choose keys without injecting markup. */
55
55
  declare const ICON_REGISTRY: Readonly<Record<string, IconSpec>>;
56
56
 
57
- export { type Diagram, type DiagramOptions, ICON_REGISTRY, type IconSpec, createDiagram };
57
+ /**
58
+ * packages/renderer-dom/src/export/gif-encoder.ts
59
+ * Pure TypeScript GIF89a Encoder with LZW Compression & Floyd-Steinberg Dithering.
60
+ * Zero external dependencies.
61
+ */
62
+ interface AnimationRecordFrame {
63
+ imageData: ImageData;
64
+ delayMs: number;
65
+ }
66
+ interface GifExportOptions {
67
+ dither?: boolean;
68
+ loop?: boolean;
69
+ }
70
+ declare function encodeGifSequence(frames: AnimationRecordFrame[], options?: GifExportOptions): Uint8Array;
71
+
72
+ /**
73
+ * packages/renderer-dom/src/export/svg-exporter.ts
74
+ * Standalone Vector SVG & Figma-compatible design token asset export.
75
+ * Zero external dependencies.
76
+ */
77
+ interface SvgExportOptions {
78
+ includeThemeStyles?: boolean;
79
+ transparentBackground?: boolean;
80
+ scale?: number;
81
+ }
82
+ declare function exportDiagramAsVectorSvg(containerEl: HTMLElement, options?: SvgExportOptions): string;
83
+
84
+ /**
85
+ * packages/renderer-dom/src/presentation-controller.ts
86
+ * Interactive Beat Navigation & Keyboard-driven presentation controller.
87
+ * Zero external dependencies.
88
+ */
89
+
90
+ interface ControllerOptions {
91
+ enableKeyboard?: boolean;
92
+ onBeatChange?: (beatName: string, index: number) => void;
93
+ }
94
+ declare class DiagramPresentationController {
95
+ private diagram;
96
+ private plan;
97
+ private currentBeatIndex;
98
+ constructor(diagram: Diagram, plan: RenderPlan, options?: ControllerOptions);
99
+ nextBeat(): void;
100
+ prevBeat(): void;
101
+ getCurrentBeatIndex(): number;
102
+ togglePlay(): void;
103
+ setSpeed(rate: number): void;
104
+ private attachKeyboardListener;
105
+ }
106
+
107
+ export { type AnimationRecordFrame, type ControllerOptions, type Diagram, type DiagramOptions, DiagramPresentationController, type GifExportOptions, ICON_REGISTRY, type IconSpec, type SvgExportOptions, createDiagram, encodeGifSequence, exportDiagramAsVectorSvg };
package/dist/index.js CHANGED
@@ -327,6 +327,25 @@ function ensureDefs(svg, theme, id) {
327
327
  arrow.appendChild(path);
328
328
  defs.appendChild(arrow);
329
329
  }
330
+ const filter = document.createElementNS("http://www.w3.org/2000/svg", "filter");
331
+ filter.setAttribute("id", `${id}-sketchy`);
332
+ filter.setAttribute("x", "-4%");
333
+ filter.setAttribute("y", "-4%");
334
+ filter.setAttribute("width", "108%");
335
+ filter.setAttribute("height", "108%");
336
+ const turb = document.createElementNS("http://www.w3.org/2000/svg", "feTurbulence");
337
+ turb.setAttribute("type", "fractalNoise");
338
+ turb.setAttribute("baseFrequency", "0.02");
339
+ turb.setAttribute("numOctaves", "2");
340
+ turb.setAttribute("seed", "4");
341
+ turb.setAttribute("result", "noise");
342
+ const disp = document.createElementNS("http://www.w3.org/2000/svg", "feDisplacementMap");
343
+ disp.setAttribute("in", "SourceGraphic");
344
+ disp.setAttribute("in2", "noise");
345
+ disp.setAttribute("scale", "1.8");
346
+ filter.appendChild(turb);
347
+ filter.appendChild(disp);
348
+ defs.appendChild(filter);
330
349
  svg.prepend(defs);
331
350
  }
332
351
  function createEdgeRuntime(svg, from, to, kind, label, theme, sceneId, routeObstacles, labelObstacles, bounds, lane) {
@@ -742,11 +761,25 @@ function ensureAnnotationStyles(doc) {
742
761
  .markdy-annotation {
743
762
  position: absolute;
744
763
  max-width: 220px;
745
- font-family: var(--md-font-title, Georgia, serif);
764
+ font-family: var(--md-font-title, Georgia, "Times New Roman", serif);
746
765
  font-size: 14px;
747
766
  font-style: italic;
748
767
  color: var(--md-text);
749
- line-height: 1.35;
768
+ line-height: 1.4;
769
+ letter-spacing: 0.01em;
770
+ opacity: 0;
771
+ transform: translateY(6px);
772
+ transition: opacity 0.5s cubic-bezier(0.2, 0.8, 0.2, 1), transform 0.5s cubic-bezier(0.2, 0.8, 0.2, 1);
773
+ }
774
+ .markdy-annotation[data-visible="1"] {
775
+ opacity: 1;
776
+ transform: none;
777
+ }
778
+ .markdy-annotation[data-intent="accent"] {
779
+ color: var(--md-accent);
780
+ }
781
+ .markdy-annotation[data-intent="muted"] {
782
+ color: var(--md-text-muted);
750
783
  }
751
784
  .markdy-annotation__leader {
752
785
  position: absolute;
@@ -790,10 +823,13 @@ function mountAnnotations(layer, annotations, nodes, theme, bounds) {
790
823
  const path = doc.createElementNS("http://www.w3.org/2000/svg", "path");
791
824
  path.setAttribute("d", `M ${ax} ${ay} Q ${(ax + tx) / 2} ${(ay + ty) / 2 - 20} ${tx} ${ty}`);
792
825
  path.setAttribute("fill", "none");
793
- path.setAttribute("stroke", theme.textMuted);
826
+ const intent = typeof ann.intent === "string" ? ann.intent : "neutral";
827
+ const leaderColor = intent === "accent" ? theme.accent : intent === "muted" ? theme.soft ?? theme.textMuted : theme.textMuted;
828
+ const leaderOpacity = intent === "accent" ? "0.60" : intent === "muted" ? "0.40" : "0.50";
829
+ path.setAttribute("stroke", leaderColor);
794
830
  path.setAttribute("stroke-width", "1");
795
831
  path.setAttribute("stroke-dasharray", "4 3");
796
- path.setAttribute("opacity", "0.55");
832
+ path.setAttribute("opacity", leaderOpacity);
797
833
  svg.appendChild(path);
798
834
  const dot = doc.createElementNS("http://www.w3.org/2000/svg", "circle");
799
835
  dot.setAttribute("cx", String(tx));
@@ -1161,6 +1197,45 @@ function ensureNodeStyles(doc) {
1161
1197
  .markdy-scene-root[data-flat="1"] .markdy-node[data-visible="1"] {
1162
1198
  box-shadow: inset 0 0 0 1px var(--md-hairline, color-mix(in srgb, var(--md-border) 50%, transparent));
1163
1199
  }
1200
+ .markdy-scene-root[data-flat="1"] .markdy-node[data-focal="1"] {
1201
+ box-shadow: inset 0 0 0 1.5px color-mix(in srgb, var(--md-accent) 60%, transparent);
1202
+ background:
1203
+ linear-gradient(180deg,
1204
+ color-mix(in srgb, var(--md-accent-tint, var(--md-accent)) 8%, transparent),
1205
+ color-mix(in srgb, var(--md-accent-tint, var(--md-accent)) 4%, transparent));
1206
+ }
1207
+ .markdy-node[data-kind="external"] {
1208
+ border: 1.2px dashed color-mix(in srgb, var(--md-ink, var(--md-text)) 30%, transparent);
1209
+ background: color-mix(in srgb, var(--md-ink, var(--md-text)) 3%, transparent);
1210
+ box-shadow: none;
1211
+ }
1212
+ .markdy-node[data-kind="optional"] {
1213
+ border: 1px dashed color-mix(in srgb, var(--md-ink, var(--md-text)) 20%, transparent);
1214
+ background: color-mix(in srgb, var(--md-ink, var(--md-text)) 2%, transparent);
1215
+ box-shadow: none;
1216
+ opacity: 0.7;
1217
+ }
1218
+ .markdy-scene-root[data-markdy-theme="terminal"] .markdy-node {
1219
+ background: var(--md-node-surface, #141414);
1220
+ border: 1px solid #2b2b2b;
1221
+ box-shadow: none;
1222
+ font-family: var(--md-font-mono, ui-monospace, monospace);
1223
+ }
1224
+ .markdy-scene-root[data-markdy-theme="terminal"] .markdy-node[data-focal="1"] {
1225
+ border-color: #ff5a36;
1226
+ box-shadow: 0 0 18px -8px rgba(255, 90, 54, 0.45);
1227
+ }
1228
+ .markdy-scene-root[data-markdy-theme="sketchy"] .markdy-node {
1229
+ background: #ffffff;
1230
+ border: 1.5px solid #2d3142;
1231
+ box-shadow: 3px 3px 0 rgba(45, 49, 66, 0.10);
1232
+ border-radius: var(--md-radius-md, 4px);
1233
+ }
1234
+ .markdy-scene-root[data-markdy-theme="sketchy"] .markdy-node[data-focal="1"] {
1235
+ border-color: #eb6c36;
1236
+ background: rgba(235, 108, 54, 0.06);
1237
+ box-shadow: 3px 3px 0 rgba(235, 108, 54, 0.12);
1238
+ }
1164
1239
  `;
1165
1240
  doc.head.appendChild(style);
1166
1241
  }
@@ -1314,6 +1389,60 @@ var ICONS = {
1314
1389
  ["path", { d: "M12 3 21 12 12 21 3 12 12 3Z" }],
1315
1390
  ["path", { d: "M12 8v4" }],
1316
1391
  ["path", { d: "M12 16h.01" }]
1392
+ ],
1393
+ hub: [
1394
+ ["circle", { cx: "12", cy: "12", r: "7.5" }],
1395
+ ["circle", { cx: "12", cy: "12", r: "2.8" }]
1396
+ ],
1397
+ station: [
1398
+ ["rect", { x: "4", y: "4", width: "16", height: "16", rx: "4" }],
1399
+ ["circle", { cx: "12", cy: "12", r: "3" }]
1400
+ ],
1401
+ bronze: [
1402
+ ["rect", { x: "4", y: "5", width: "16", height: "5", rx: "1.5" }],
1403
+ ["rect", { x: "4", y: "14", width: "16", height: "5", rx: "1.5" }]
1404
+ ],
1405
+ silver: [
1406
+ ["path", { d: "M12 2 20 7v10l-8 5-8-5V7z" }]
1407
+ ],
1408
+ gold: [
1409
+ ["path", { d: "M12 2.5 15 8.5l6.5 1-4.7 4.6 1.1 6.4L12 17.5 6.1 20.5l1.1-6.4L2.5 9.5l6.5-1z" }]
1410
+ ],
1411
+ terminal: [
1412
+ ["rect", { x: "3", y: "4", width: "18", height: "16", rx: "2.5" }],
1413
+ ["path", { d: "m7 9 3 3-3 3M13 15h4" }]
1414
+ ],
1415
+ cloud: [
1416
+ ["path", { d: "M7 16a4 4 0 0 1-.88-7.9 5 5 0 0 1 9.76-1.1A4 4 0 0 1 17 16H7z" }]
1417
+ ],
1418
+ firewall: [
1419
+ ["path", { d: "M12 3v18M3 8h18M3 16h18M7.5 3v5M16.5 3v5M7.5 16v5M16.5 16v5M12 8v8" }]
1420
+ ],
1421
+ alert: [
1422
+ ["path", { d: "M12 3 2 20h20L12 3z" }],
1423
+ ["path", { d: "M12 9v4M12 17h.01" }]
1424
+ ],
1425
+ sync: [
1426
+ ["path", { d: "M20 11A8 8 0 0 0 5.6 6.4L3 9" }],
1427
+ ["path", { d: "M3 4v5h5M4 13a8 8 0 0 0 14.4 4.6L21 15" }],
1428
+ ["path", { d: "M21 20v-5h-5" }]
1429
+ ],
1430
+ search: [
1431
+ ["circle", { cx: "11", cy: "11", r: "6.5" }],
1432
+ ["path", { d: "m19 19-3.5-3.5" }]
1433
+ ],
1434
+ log: [
1435
+ ["rect", { x: "4", y: "3", width: "16", height: "18", rx: "2" }],
1436
+ ["path", { d: "M8 7h8M8 11h8M8 15h4" }]
1437
+ ],
1438
+ layers: [
1439
+ ["path", { d: "M12 2 2 7l10 5 10-5-10-5z" }],
1440
+ ["path", { d: "M2 17l10 5 10-5" }],
1441
+ ["path", { d: "M2 12l10 5 10-5" }]
1442
+ ],
1443
+ nested: [
1444
+ ["rect", { x: "3", y: "3", width: "18", height: "18", rx: "3" }],
1445
+ ["rect", { x: "7", y: "7", width: "10", height: "10", rx: "2" }]
1317
1446
  ]
1318
1447
  };
1319
1448
  var ICON_REGISTRY = Object.freeze(ICONS);
@@ -1340,6 +1469,14 @@ function iconKeyForNode(node) {
1340
1469
  if (node.kind === "api" || node.kind === "service" || node.kind === "microservice" || node.kind === "backend" || node.kind === "server" || node.kind === "handler" || node.kind === "controller") return "server";
1341
1470
  if (node.kind === "browser" || node.kind === "web" || node.kind === "frontend" || node.kind === "app") return "browser";
1342
1471
  if (node.kind === "user" || node.kind === "client") return "user";
1472
+ if (node.kind === "cloud") return "cloud";
1473
+ if (node.kind === "firewall") return "firewall";
1474
+ if (node.kind === "alert" || node.kind === "alarm") return "alert";
1475
+ if (node.kind === "sync") return "sync";
1476
+ if (node.kind === "search") return "search";
1477
+ if (node.kind === "log" || node.kind === "audit") return "log";
1478
+ if (node.kind === "layers" || node.kind === "stack") return "layers";
1479
+ if (node.kind === "nested") return "nested";
1343
1480
  if (node.kind === "decision" || node.kind === "condition") return "decision";
1344
1481
  if (node.kind === "cache") return "cache";
1345
1482
  return ICONS[node.kind] ? node.kind : node.role;
@@ -1720,6 +1857,26 @@ function ensureSceneStyles(doc) {
1720
1857
  linear-gradient(180deg, transparent 0%, var(--md-vignette) 100%);
1721
1858
  opacity: 0.8;
1722
1859
  }
1860
+ .markdy-scene-root[data-markdy-theme="terminal"]::before {
1861
+ background:
1862
+ radial-gradient(circle at 50% 50%, rgba(255, 255, 255, 0.06) 1px, transparent 1px) 0 0 / 22px 22px;
1863
+ mask-image: none;
1864
+ opacity: 0.4;
1865
+ }
1866
+ .markdy-scene-root[data-markdy-theme="terminal"]::after {
1867
+ background:
1868
+ radial-gradient(ellipse at 50% 0%, rgba(255, 90, 54, 0.08), transparent 50%),
1869
+ linear-gradient(180deg, transparent, rgba(0, 0, 0, 0.5) 100%);
1870
+ opacity: 0.7;
1871
+ }
1872
+ .markdy-scene-root[data-markdy-theme="sketchy"]::before {
1873
+ background: none;
1874
+ opacity: 0;
1875
+ }
1876
+ .markdy-scene-root[data-markdy-theme="sketchy"]::after {
1877
+ background: none;
1878
+ opacity: 0;
1879
+ }
1723
1880
  @keyframes markdy-star-twinkle {
1724
1881
  from { opacity: 0.24; transform: scale(0.85); }
1725
1882
  to { opacity: 0.9; transform: scale(1.15); }
@@ -1797,6 +1954,21 @@ function ensureSceneStyles(doc) {
1797
1954
  opacity: 1 !important;
1798
1955
  color: #64748b !important;
1799
1956
  }
1957
+ @media (prefers-reduced-motion: reduce) {
1958
+ .markdy-node,
1959
+ .markdy-beat-caption,
1960
+ .markdy-constellation-star {
1961
+ animation-duration: 0.001ms !important;
1962
+ animation-iteration-count: 1 !important;
1963
+ transition-duration: 0.001ms !important;
1964
+ }
1965
+ .markdy-node { opacity: 1 !important; transform: none !important; }
1966
+ .markdy-constellation-star { animation: none !important; opacity: 0.7 !important; }
1967
+ }
1968
+ @media print {
1969
+ .markdy-node { opacity: 1 !important; transform: none !important; }
1970
+ .markdy-scene-root::before, .markdy-scene-root::after { display: none !important; }
1971
+ }
1800
1972
  `;
1801
1973
  doc.head.appendChild(style);
1802
1974
  }
@@ -2082,8 +2254,8 @@ function createDiagram(opts) {
2082
2254
  scene.style.transform = `scale(${fitScale})`;
2083
2255
  }
2084
2256
  scaleScene();
2085
- const resizeObserver = new ResizeObserver(scaleScene);
2086
- resizeObserver.observe(viewport);
2257
+ const resizeObserver = typeof ResizeObserver === "function" ? new ResizeObserver(scaleScene) : null;
2258
+ resizeObserver?.observe(viewport);
2087
2259
  const edgeRuntimes = /* @__PURE__ */ new Map();
2088
2260
  const edgeSceneId = createEdgeSceneId();
2089
2261
  const sequenceAnims = plan.diagramType === "sequence" ? mountSequenceLayer(
@@ -2288,7 +2460,7 @@ function createDiagram(opts) {
2288
2460
  destroy() {
2289
2461
  diagram.pause();
2290
2462
  for (const anim of allAnims) anim.cancel();
2291
- resizeObserver.disconnect();
2463
+ resizeObserver?.disconnect();
2292
2464
  if (progressEl?.parentNode === viewport) viewport.removeChild(progressEl);
2293
2465
  if (badge?.parentNode) badge.parentNode.removeChild(badge);
2294
2466
  if (footer?.parentNode) footer.parentNode.removeChild(footer);
@@ -2398,7 +2570,292 @@ function createDiagram(opts) {
2398
2570
  if (autoplay) diagram.play();
2399
2571
  return diagram;
2400
2572
  }
2573
+
2574
+ // src/export/gif-encoder.ts
2575
+ var GIF_HEADER = "GIF89a";
2576
+ var PALETTE_RGB332 = (() => {
2577
+ const palette = new Uint8Array(256 * 3);
2578
+ for (let i = 0; i < 256; i++) {
2579
+ const r = Math.round((i >> 5 & 7) * 255 / 7);
2580
+ const g = Math.round((i >> 2 & 7) * 255 / 7);
2581
+ const b = Math.round((i & 3) * 255 / 3);
2582
+ palette[i * 3] = r;
2583
+ palette[i * 3 + 1] = g;
2584
+ palette[i * 3 + 2] = b;
2585
+ }
2586
+ return palette;
2587
+ })();
2588
+ function clamp2(value, min = 0, max = 255) {
2589
+ return value < min ? min : value > max ? max : value;
2590
+ }
2591
+ function nearestColorIndex(r, g, b) {
2592
+ return (clamp2(r) & 224 | (clamp2(g) & 224) >> 3 | (clamp2(b) & 192) >> 6) & 255;
2593
+ }
2594
+ function quantizeFrame(imageData, dither) {
2595
+ const { width, height, data } = imageData;
2596
+ const pixelCount = width * height;
2597
+ const out = new Uint8Array(pixelCount);
2598
+ if (!dither) {
2599
+ for (let i = 0; i < pixelCount; i++) {
2600
+ const idx = i * 4;
2601
+ out[i] = (data[idx] & 224 | (data[idx + 1] & 224) >> 3 | (data[idx + 2] & 192) >> 6) & 255;
2602
+ }
2603
+ return out;
2604
+ }
2605
+ const errR = new Float32Array(pixelCount);
2606
+ const errG = new Float32Array(pixelCount);
2607
+ const errB = new Float32Array(pixelCount);
2608
+ for (let i = 0; i < pixelCount; i++) {
2609
+ const idx = i * 4;
2610
+ errR[i] = data[idx];
2611
+ errG[i] = data[idx + 1];
2612
+ errB[i] = data[idx + 2];
2613
+ }
2614
+ for (let y = 0; y < height; y++) {
2615
+ for (let x = 0; x < width; x++) {
2616
+ const idx = y * width + x;
2617
+ const r = clamp2(Math.round(errR[idx]));
2618
+ const g = clamp2(Math.round(errG[idx]));
2619
+ const b = clamp2(Math.round(errB[idx]));
2620
+ const colorIdx = nearestColorIndex(r, g, b);
2621
+ out[idx] = colorIdx;
2622
+ const pr = PALETTE_RGB332[colorIdx * 3];
2623
+ const pg = PALETTE_RGB332[colorIdx * 3 + 1];
2624
+ const pb = PALETTE_RGB332[colorIdx * 3 + 2];
2625
+ const dr = errR[idx] - pr;
2626
+ const dg = errG[idx] - pg;
2627
+ const db = errB[idx] - pb;
2628
+ if (x + 1 < width) {
2629
+ errR[idx + 1] += dr * 7 / 16;
2630
+ errG[idx + 1] += dg * 7 / 16;
2631
+ errB[idx + 1] += db * 7 / 16;
2632
+ }
2633
+ if (y + 1 < height) {
2634
+ if (x > 0) {
2635
+ errR[idx + width - 1] += dr * 3 / 16;
2636
+ errG[idx + width - 1] += dg * 3 / 16;
2637
+ errB[idx + width - 1] += db * 3 / 16;
2638
+ }
2639
+ errR[idx + width] += dr * 5 / 16;
2640
+ errG[idx + width] += dg * 5 / 16;
2641
+ errB[idx + width] += db * 5 / 16;
2642
+ if (x + 1 < width) {
2643
+ errR[idx + width + 1] += dr * 1 / 16;
2644
+ errG[idx + width + 1] += dg * 1 / 16;
2645
+ errB[idx + width + 1] += db * 1 / 16;
2646
+ }
2647
+ }
2648
+ }
2649
+ }
2650
+ return out;
2651
+ }
2652
+ function lzwCompress(pixels, minCodeSize = 8) {
2653
+ const clearCode = 1 << minCodeSize;
2654
+ const endCode = clearCode + 1;
2655
+ let codeSize = minCodeSize + 1;
2656
+ let nextCode = endCode + 1;
2657
+ let dict = /* @__PURE__ */ new Map();
2658
+ for (let i = 0; i < clearCode; i++) {
2659
+ dict.set(String(i), i);
2660
+ }
2661
+ const codes = [clearCode];
2662
+ let prefix = String(pixels[0] ?? 0);
2663
+ for (let i = 1; i < pixels.length; i++) {
2664
+ const suffix = String(pixels[i]);
2665
+ const combo = `${prefix},${suffix}`;
2666
+ if (dict.has(combo)) {
2667
+ prefix = combo;
2668
+ continue;
2669
+ }
2670
+ codes.push(dict.get(prefix));
2671
+ dict.set(combo, nextCode++);
2672
+ prefix = suffix;
2673
+ if (nextCode === 1 << codeSize && codeSize < 12) {
2674
+ codeSize++;
2675
+ }
2676
+ if (nextCode >= 4095) {
2677
+ codes.push(clearCode);
2678
+ dict = /* @__PURE__ */ new Map();
2679
+ for (let j = 0; j < clearCode; j++) dict.set(String(j), j);
2680
+ codeSize = minCodeSize + 1;
2681
+ nextCode = endCode + 1;
2682
+ }
2683
+ }
2684
+ codes.push(dict.get(prefix));
2685
+ codes.push(endCode);
2686
+ const bytes = [];
2687
+ let bitBuffer = 0;
2688
+ let bitCount = 0;
2689
+ codeSize = minCodeSize + 1;
2690
+ nextCode = endCode + 1;
2691
+ for (const code of codes) {
2692
+ bitBuffer |= code << bitCount;
2693
+ bitCount += codeSize;
2694
+ while (bitCount >= 8) {
2695
+ bytes.push(bitBuffer & 255);
2696
+ bitBuffer >>= 8;
2697
+ bitCount -= 8;
2698
+ }
2699
+ if (code === clearCode) {
2700
+ codeSize = minCodeSize + 1;
2701
+ nextCode = endCode + 1;
2702
+ } else if (code !== endCode) {
2703
+ nextCode++;
2704
+ if (nextCode === 1 << codeSize && codeSize < 12) {
2705
+ codeSize++;
2706
+ }
2707
+ }
2708
+ }
2709
+ if (bitCount > 0) {
2710
+ bytes.push(bitBuffer & 255);
2711
+ }
2712
+ return new Uint8Array(bytes);
2713
+ }
2714
+ function encodeGifSequence(frames, options = {}) {
2715
+ if (frames.length === 0) {
2716
+ throw new Error("Cannot encode GIF without frames");
2717
+ }
2718
+ const { width, height } = frames[0].imageData;
2719
+ const dither = options.dither ?? true;
2720
+ const buffer = [];
2721
+ const pushString = (str) => {
2722
+ for (let i = 0; i < str.length; i++) buffer.push(str.charCodeAt(i));
2723
+ };
2724
+ const pushU16 = (val) => {
2725
+ buffer.push(val & 255, val >> 8 & 255);
2726
+ };
2727
+ pushString(GIF_HEADER);
2728
+ pushU16(width);
2729
+ pushU16(height);
2730
+ buffer.push(247);
2731
+ buffer.push(0);
2732
+ buffer.push(0);
2733
+ for (let i = 0; i < PALETTE_RGB332.length; i++) {
2734
+ buffer.push(PALETTE_RGB332[i]);
2735
+ }
2736
+ if (options.loop !== false) {
2737
+ buffer.push(33, 255, 11);
2738
+ pushString("NETSCAPE2.0");
2739
+ buffer.push(3, 1, 0, 0, 0);
2740
+ }
2741
+ for (const frame of frames) {
2742
+ const quantized = quantizeFrame(frame.imageData, dither);
2743
+ const compressed = lzwCompress(quantized);
2744
+ const delayHundredths = Math.max(2, Math.round(frame.delayMs / 10));
2745
+ buffer.push(33, 249, 4, 4);
2746
+ pushU16(delayHundredths);
2747
+ buffer.push(0, 0);
2748
+ buffer.push(44);
2749
+ pushU16(0);
2750
+ pushU16(0);
2751
+ pushU16(width);
2752
+ pushU16(height);
2753
+ buffer.push(0);
2754
+ buffer.push(8);
2755
+ for (let offset = 0; offset < compressed.length; offset += 255) {
2756
+ const chunk = compressed.slice(offset, offset + 255);
2757
+ buffer.push(chunk.length, ...chunk);
2758
+ }
2759
+ buffer.push(0);
2760
+ }
2761
+ buffer.push(59);
2762
+ return new Uint8Array(buffer);
2763
+ }
2764
+
2765
+ // src/export/svg-exporter.ts
2766
+ function exportDiagramAsVectorSvg(containerEl, options = {}) {
2767
+ const svgEl = containerEl.querySelector("svg");
2768
+ if (!svgEl) throw new Error("No Markdy SVG element found in container");
2769
+ const clonedSvg = svgEl.cloneNode(true);
2770
+ const width = svgEl.getAttribute("width") || String(svgEl.clientWidth || 800);
2771
+ const height = svgEl.getAttribute("height") || String(svgEl.clientHeight || 400);
2772
+ clonedSvg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
2773
+ clonedSvg.setAttribute("xmlns:xlink", "http://www.w3.org/1999/xlink");
2774
+ clonedSvg.setAttribute("width", width);
2775
+ clonedSvg.setAttribute("height", height);
2776
+ clonedSvg.setAttribute("viewBox", `0 0 ${width} ${height}`);
2777
+ if (options.transparentBackground) {
2778
+ const bgRect = clonedSvg.querySelector("rect");
2779
+ if (bgRect) bgRect.remove();
2780
+ }
2781
+ if (options.includeThemeStyles !== false) {
2782
+ const styleEl = document.createElementNS("http://www.w3.org/2000/svg", "style");
2783
+ styleEl.textContent = `
2784
+ text { font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
2785
+ .markdy-node { transition: opacity 0.3s ease; }
2786
+ `;
2787
+ clonedSvg.insertBefore(styleEl, clonedSvg.firstChild);
2788
+ }
2789
+ const serializer = new XMLSerializer();
2790
+ return `<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2791
+ ` + serializer.serializeToString(clonedSvg);
2792
+ }
2793
+
2794
+ // src/presentation-controller.ts
2795
+ var DiagramPresentationController = class {
2796
+ diagram;
2797
+ plan;
2798
+ currentBeatIndex = 0;
2799
+ constructor(diagram, plan, options = {}) {
2800
+ this.diagram = diagram;
2801
+ this.plan = plan;
2802
+ if (options.enableKeyboard !== false && typeof window !== "undefined") {
2803
+ this.attachKeyboardListener();
2804
+ }
2805
+ }
2806
+ nextBeat() {
2807
+ if (this.currentBeatIndex < this.plan.beats.length - 1) {
2808
+ this.currentBeatIndex++;
2809
+ const beat = this.plan.beats[this.currentBeatIndex];
2810
+ this.diagram.seek(beat.start);
2811
+ }
2812
+ }
2813
+ prevBeat() {
2814
+ if (this.currentBeatIndex > 0) {
2815
+ this.currentBeatIndex--;
2816
+ const beat = this.plan.beats[this.currentBeatIndex];
2817
+ this.diagram.seek(beat.start);
2818
+ }
2819
+ }
2820
+ getCurrentBeatIndex() {
2821
+ return this.currentBeatIndex;
2822
+ }
2823
+ togglePlay() {
2824
+ this.diagram.play();
2825
+ }
2826
+ setSpeed(rate) {
2827
+ this.diagram.setPlaybackRate(rate);
2828
+ }
2829
+ attachKeyboardListener() {
2830
+ window.addEventListener("keydown", (e) => {
2831
+ if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return;
2832
+ switch (e.key) {
2833
+ case "ArrowRight":
2834
+ case "PageDown":
2835
+ this.nextBeat();
2836
+ break;
2837
+ case "ArrowLeft":
2838
+ case "PageUp":
2839
+ this.prevBeat();
2840
+ break;
2841
+ case " ":
2842
+ e.preventDefault();
2843
+ this.togglePlay();
2844
+ break;
2845
+ case "1":
2846
+ this.setSpeed(1);
2847
+ break;
2848
+ case "2":
2849
+ this.setSpeed(2);
2850
+ break;
2851
+ }
2852
+ });
2853
+ }
2854
+ };
2401
2855
  export {
2856
+ DiagramPresentationController,
2402
2857
  ICON_REGISTRY,
2403
- createDiagram
2858
+ createDiagram,
2859
+ encodeGifSequence,
2860
+ exportDiagramAsVectorSvg
2404
2861
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markdy/renderer-dom",
3
- "version": "0.8.20",
3
+ "version": "0.8.22",
4
4
  "description": "Browser renderer for diagram-native animated MarkdyScript architecture diagrams, built on the Web Animations API.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -44,14 +44,14 @@
44
44
  "access": "public"
45
45
  },
46
46
  "dependencies": {
47
- "@markdy/core": "0.8.20"
47
+ "@markdy/core": "0.8.22"
48
48
  },
49
49
  "devDependencies": {
50
50
  "jsdom": "^29.1.1",
51
51
  "tsup": "^8.5.1",
52
52
  "typescript": "^5.9.3",
53
53
  "vitest": "^4.1.7",
54
- "@markdy/stdlib-systems": "0.8.20"
54
+ "@markdy/stdlib-systems": "0.8.22"
55
55
  },
56
56
  "scripts": {
57
57
  "build": "tsup",