@markdy/renderer-dom 0.8.26 → 0.8.28

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
@@ -33,6 +33,7 @@ interface DiagramOptions {
33
33
  onWarning?: (warning: Diagnostic) => void;
34
34
  onTimeUpdate?: (seconds: number, durationSeconds: number) => void;
35
35
  onPlayStateChange?: (playing: boolean) => void;
36
+ onEnded?: () => void;
36
37
  }
37
38
  interface Diagram {
38
39
  play(): void;
@@ -81,6 +82,21 @@ interface SvgExportOptions {
81
82
  }
82
83
  declare function exportDiagramAsVectorSvg(containerEl: HTMLElement, options?: SvgExportOptions): string;
83
84
 
85
+ interface TimelineController {
86
+ seek(seconds: number): void;
87
+ currentTime(): number;
88
+ duration(): number;
89
+ isPlaying(): boolean;
90
+ play(): void;
91
+ pause(): void;
92
+ }
93
+ interface GifDiagramExportOptions extends SvgExportOptions {
94
+ fps?: number;
95
+ pixelRatio?: number;
96
+ loop?: boolean;
97
+ }
98
+ declare function exportDiagramAsGif(container: HTMLElement, timeline: TimelineController, options?: GifDiagramExportOptions): Promise<Blob>;
99
+
84
100
  /**
85
101
  * packages/renderer-dom/src/export/png-exporter.ts
86
102
  * High-DPI raster PNG export with 2x retina scaling.
@@ -119,4 +135,4 @@ declare class DiagramPresentationController {
119
135
  destroy(): void;
120
136
  }
121
137
 
122
- export { type AnimationRecordFrame, type ControllerOptions, type Diagram, type DiagramOptions, DiagramPresentationController, type GifExportOptions, ICON_REGISTRY, type IconSpec, type PngExportOptions, type SvgExportOptions, createDiagram, encodeGifSequence, exportDiagramAsPng, exportDiagramAsVectorSvg };
138
+ export { type AnimationRecordFrame, type ControllerOptions, type Diagram, type DiagramOptions, DiagramPresentationController, type GifDiagramExportOptions, type GifExportOptions, ICON_REGISTRY, type IconSpec, type PngExportOptions, type SvgExportOptions, type TimelineController, createDiagram, encodeGifSequence, exportDiagramAsGif, exportDiagramAsPng, exportDiagramAsVectorSvg };
package/dist/index.js CHANGED
@@ -808,13 +808,14 @@ function ensureAnnotationStyles(doc) {
808
808
  doc.head.appendChild(style);
809
809
  }
810
810
  function positionForAnnotation(position, bounds, index) {
811
- const pad = 24;
811
+ const pad = 28;
812
+ const topPad = 68;
812
813
  const p = (position ?? "").toLowerCase();
813
- if (p.includes("top") && p.includes("right")) return { x: bounds.width - pad - 200, y: pad + index * 48 };
814
- if (p.includes("top") && p.includes("left")) return { x: pad, y: pad + index * 48 };
814
+ if (p.includes("top") && p.includes("right")) return { x: bounds.width - pad - 200, y: topPad + index * 48 };
815
+ if (p.includes("top") && p.includes("left")) return { x: pad, y: topPad + index * 48 };
815
816
  if (p.includes("bottom") && p.includes("right")) return { x: bounds.width - pad - 200, y: bounds.height - pad - 40 };
816
817
  if (p.includes("bottom") && p.includes("left")) return { x: pad, y: bounds.height - pad - 40 };
817
- return { x: bounds.width - pad - 200, y: pad + index * 48 };
818
+ return { x: bounds.width - pad - 200, y: topPad + index * 48 };
818
819
  }
819
820
  function mountAnnotations(layer, annotations, nodes, theme, bounds) {
820
821
  if (annotations.length === 0) return;
@@ -2090,7 +2091,8 @@ function createDiagram(opts) {
2090
2091
  copyright: explicitCopyright,
2091
2092
  onWarning = (w) => console.warn(`[markdy] line ${w.line}: ${w.message}`),
2092
2093
  onTimeUpdate,
2093
- onPlayStateChange
2094
+ onPlayStateChange,
2095
+ onEnded
2094
2096
  } = opts;
2095
2097
  const { ast, plan } = parseAndCompile(code);
2096
2098
  for (const w of ast.diagnostics) {
@@ -2110,11 +2112,16 @@ function createDiagram(opts) {
2110
2112
  const totalDurationMs = plan.duration * 1e3;
2111
2113
  const durationSeconds = plan.duration;
2112
2114
  const viewport = document.createElement("div");
2115
+ viewport.className = "markdy-viewport";
2113
2116
  Object.assign(viewport.style, {
2114
2117
  position: "relative",
2115
2118
  width: "100%",
2119
+ height: "100%",
2120
+ maxWidth: "100%",
2121
+ maxHeight: "100%",
2116
2122
  aspectRatio: `${plan.meta.width} / ${plan.meta.height}`,
2117
- overflow: "hidden"
2123
+ overflow: "hidden",
2124
+ boxSizing: "border-box"
2118
2125
  });
2119
2126
  container.appendChild(viewport);
2120
2127
  let progressEl = null;
@@ -2269,14 +2276,85 @@ function createDiagram(opts) {
2269
2276
  nodeEls.set(node.id, el);
2270
2277
  }
2271
2278
  let fitScale = 1;
2279
+ let sceneOffsetX = 0;
2280
+ let sceneOffsetY = 0;
2281
+ function computeContentBounds() {
2282
+ if (!plan.nodes || plan.nodes.length === 0) {
2283
+ return { minX: 0, minY: 0, maxX: plan.meta.width, maxY: plan.meta.height, width: plan.meta.width, height: plan.meta.height };
2284
+ }
2285
+ let minX = Infinity;
2286
+ let minY = Infinity;
2287
+ let maxX = -Infinity;
2288
+ let maxY = -Infinity;
2289
+ for (const node of plan.nodes) {
2290
+ minX = Math.min(minX, node.x);
2291
+ minY = Math.min(minY, node.y);
2292
+ maxX = Math.max(maxX, node.x + node.width);
2293
+ maxY = Math.max(maxY, node.y + node.height);
2294
+ }
2295
+ for (const gb of plan.groupBoundaries ?? []) {
2296
+ minX = Math.min(minX, gb.x);
2297
+ minY = Math.min(minY, gb.y);
2298
+ maxX = Math.max(maxX, gb.x + gb.width);
2299
+ maxY = Math.max(maxY, gb.y + gb.height);
2300
+ }
2301
+ for (const bus of plan.treeBuses ?? []) {
2302
+ minX = Math.min(minX, bus.parentX, ...bus.childXs.length ? bus.childXs : [bus.parentX]);
2303
+ minY = Math.min(minY, bus.parentY, bus.branchY, bus.childY);
2304
+ maxX = Math.max(maxX, bus.parentX, ...bus.childXs.length ? bus.childXs : [bus.parentX]);
2305
+ maxY = Math.max(maxY, bus.parentY, bus.branchY, bus.childY);
2306
+ }
2307
+ if (plan.title) {
2308
+ minY = Math.min(minY, 20);
2309
+ }
2310
+ const pad = 36;
2311
+ minX = Math.max(0, minX - pad);
2312
+ minY = Math.max(0, minY - pad);
2313
+ maxX = Math.min(plan.meta.width, maxX + pad);
2314
+ maxY = Math.min(plan.meta.height, maxY + pad);
2315
+ const width = Math.max(maxX - minX, 200);
2316
+ const height = Math.max(maxY - minY, 140);
2317
+ return { minX, minY, maxX, maxY, width, height };
2318
+ }
2272
2319
  function scaleScene() {
2273
- const width = viewport.clientWidth || plan.meta.width;
2274
- fitScale = width / plan.meta.width;
2320
+ const isBrowserLayout = (viewport.clientWidth || container.clientWidth || 0) > 0;
2321
+ if (!isBrowserLayout) {
2322
+ fitScale = 1;
2323
+ sceneOffsetX = 0;
2324
+ sceneOffsetY = 0;
2325
+ scene.style.left = "0px";
2326
+ scene.style.top = "0px";
2327
+ scene.style.transformOrigin = "0 0";
2328
+ scene.style.transform = "scale(1)";
2329
+ return;
2330
+ }
2331
+ const vWidth = viewport.clientWidth || container.clientWidth || plan.meta.width;
2332
+ const vHeight = viewport.clientHeight || container.clientHeight || vWidth * plan.meta.height / plan.meta.width;
2333
+ const canvasScaleX = vWidth / plan.meta.width;
2334
+ const canvasScaleY = vHeight / plan.meta.height;
2335
+ const baseCanvasScale = Math.min(canvasScaleX, canvasScaleY);
2336
+ const bounds = computeContentBounds();
2337
+ const contentScaleX = vWidth * 0.94 / bounds.width;
2338
+ const contentScaleY = vHeight * 0.94 / bounds.height;
2339
+ const optimalContentScale = Math.min(contentScaleX, contentScaleY);
2340
+ const chosenScale = Math.max(
2341
+ baseCanvasScale,
2342
+ Math.min(optimalContentScale, baseCanvasScale * 1.45)
2343
+ );
2344
+ fitScale = Number.isFinite(chosenScale) && chosenScale > 0 ? chosenScale : 1;
2345
+ const scaledWidth = plan.meta.width * fitScale;
2346
+ const scaledHeight = plan.meta.height * fitScale;
2347
+ sceneOffsetX = (vWidth - scaledWidth) / 2;
2348
+ sceneOffsetY = (vHeight - scaledHeight) / 2;
2349
+ scene.style.left = `${sceneOffsetX}px`;
2350
+ scene.style.top = `${sceneOffsetY}px`;
2351
+ scene.style.transformOrigin = "0 0";
2275
2352
  scene.style.transform = `scale(${fitScale})`;
2276
2353
  }
2277
2354
  scaleScene();
2278
2355
  const resizeObserver = typeof ResizeObserver === "function" ? new ResizeObserver(scaleScene) : null;
2279
2356
  resizeObserver?.observe(viewport);
2357
+ if (container !== viewport) resizeObserver?.observe(container);
2280
2358
  const edgeRuntimes = /* @__PURE__ */ new Map();
2281
2359
  const edgeSceneId = createEdgeSceneId();
2282
2360
  const sequenceAnims = plan.diagramType === "sequence" ? mountSequenceLayer(
@@ -2341,8 +2419,8 @@ function createDiagram(opts) {
2341
2419
  function handleViewportWheel(event) {
2342
2420
  event.preventDefault();
2343
2421
  const rect = viewport.getBoundingClientRect();
2344
- const pointerX = (event.clientX - rect.left) / fitScale;
2345
- const pointerY = (event.clientY - rect.top) / fitScale;
2422
+ const pointerX = (event.clientX - rect.left - sceneOffsetX) / fitScale;
2423
+ const pointerY = (event.clientY - rect.top - sceneOffsetY) / fitScale;
2346
2424
  const nextScale = Math.min(MAX_VIEWPORT_ZOOM, Math.max(MIN_VIEWPORT_ZOOM, viewportScale * Math.exp(-event.deltaY * VIEWPORT_ZOOM_STEP)));
2347
2425
  if (nextScale === viewportScale) return;
2348
2426
  const sceneX = (pointerX - viewportPanX) / viewportScale;
@@ -2426,6 +2504,7 @@ function createDiagram(opts) {
2426
2504
  emitPlayStateChange(false);
2427
2505
  lastRafTs = null;
2428
2506
  rafId = null;
2507
+ onEnded?.();
2429
2508
  return;
2430
2509
  }
2431
2510
  }
@@ -2784,6 +2863,19 @@ function encodeGifSequence(frames, options = {}) {
2784
2863
  }
2785
2864
 
2786
2865
  // src/export/svg-exporter.ts
2866
+ function copyRenderedStyles(source, clone) {
2867
+ if (typeof window === "undefined" || typeof window.getComputedStyle !== "function") return;
2868
+ const sourceElements = [source, ...Array.from(source.querySelectorAll("*"))];
2869
+ const cloneElements = [clone, ...Array.from(clone.querySelectorAll("*"))];
2870
+ for (let index = 0; index < Math.min(sourceElements.length, cloneElements.length); index++) {
2871
+ const computed = window.getComputedStyle(sourceElements[index]);
2872
+ const target = cloneElements[index].style;
2873
+ for (let propertyIndex = 0; propertyIndex < computed.length; propertyIndex++) {
2874
+ const property = computed.item(propertyIndex);
2875
+ target.setProperty(property, computed.getPropertyValue(property), computed.getPropertyPriority(property));
2876
+ }
2877
+ }
2878
+ }
2787
2879
  function exportDiagramAsVectorSvg(containerEl, options = {}) {
2788
2880
  const sceneEl = containerEl.classList?.contains("markdy-scene-root") ? containerEl : containerEl.querySelector(".markdy-scene-root") || containerEl.querySelector("svg") || (containerEl.tagName?.toLowerCase() === "svg" ? containerEl : null);
2789
2881
  if (!sceneEl) throw new Error("No Markdy scene element found in container");
@@ -2797,6 +2889,7 @@ function exportDiagramAsVectorSvg(containerEl, options = {}) {
2797
2889
  ${serializer2.serializeToString(clonedSvg)}`;
2798
2890
  }
2799
2891
  const clonedScene = sceneEl.cloneNode(true);
2892
+ copyRenderedStyles(sceneEl, clonedScene);
2800
2893
  const widthStr = clonedScene.style.width || String(sceneEl.clientWidth || 800);
2801
2894
  const heightStr = clonedScene.style.height || String(sceneEl.clientHeight || 400);
2802
2895
  let width = parseFloat(widthStr);
@@ -2809,6 +2902,8 @@ ${serializer2.serializeToString(clonedSvg)}`;
2809
2902
  clonedScene.style.transform = `scale(${scale})`;
2810
2903
  clonedScene.style.transformOrigin = "0 0";
2811
2904
  clonedScene.style.position = "relative";
2905
+ clonedScene.style.left = "0px";
2906
+ clonedScene.style.top = "0px";
2812
2907
  clonedScene.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
2813
2908
  if (options.transparentBackground) {
2814
2909
  clonedScene.style.background = "transparent";
@@ -2844,6 +2939,60 @@ ${serializer2.serializeToString(clonedSvg)}`;
2844
2939
  ` + serializer.serializeToString(svg);
2845
2940
  }
2846
2941
 
2942
+ // src/export/gif-exporter.ts
2943
+ var nextFrame = () => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
2944
+ async function rasterizeFrame(container, pixelRatio, options) {
2945
+ const svg = exportDiagramAsVectorSvg(container, options);
2946
+ const url = URL.createObjectURL(new Blob([svg], { type: "image/svg+xml;charset=utf-8" }));
2947
+ try {
2948
+ const image = new Image();
2949
+ await new Promise((resolve, reject) => {
2950
+ image.onload = () => resolve();
2951
+ image.onerror = () => reject(new Error("Failed to rasterize GIF frame"));
2952
+ image.src = url;
2953
+ });
2954
+ const canvas = document.createElement("canvas");
2955
+ canvas.width = image.naturalWidth * pixelRatio;
2956
+ canvas.height = image.naturalHeight * pixelRatio;
2957
+ const context = canvas.getContext("2d");
2958
+ if (!context) throw new Error("Could not create GIF canvas context");
2959
+ context.drawImage(image, 0, 0, canvas.width, canvas.height);
2960
+ return context.getImageData(0, 0, canvas.width, canvas.height);
2961
+ } finally {
2962
+ URL.revokeObjectURL(url);
2963
+ }
2964
+ }
2965
+ async function exportDiagramAsGif(container, timeline, options = {}) {
2966
+ const fps = Math.min(24, Math.max(1, Math.round(options.fps ?? 12)));
2967
+ const duration = timeline.duration();
2968
+ const frameCount = Math.max(1, Math.ceil(duration * fps));
2969
+ const delayMs = Math.max(20, Math.round(1e3 / fps));
2970
+ const priorTime = timeline.currentTime();
2971
+ const wasPlaying = timeline.isPlaying();
2972
+ timeline.pause();
2973
+ try {
2974
+ const frames = [];
2975
+ for (let frame = 0; frame < frameCount; frame++) {
2976
+ timeline.seek(Math.min(duration, frame / fps));
2977
+ await nextFrame();
2978
+ frames.push({
2979
+ imageData: await rasterizeFrame(container, options.pixelRatio ?? 1, options),
2980
+ delayMs
2981
+ });
2982
+ }
2983
+ timeline.seek(duration);
2984
+ await nextFrame();
2985
+ frames.push({ imageData: await rasterizeFrame(container, options.pixelRatio ?? 1, options), delayMs: Math.max(delayMs, 800) });
2986
+ const encoded = encodeGifSequence(frames, { dither: true, loop: options.loop ?? true });
2987
+ const bytes = new Uint8Array(encoded.byteLength);
2988
+ bytes.set(encoded);
2989
+ return new Blob([bytes.buffer], { type: "image/gif" });
2990
+ } finally {
2991
+ timeline.seek(priorTime);
2992
+ if (wasPlaying) timeline.play();
2993
+ }
2994
+ }
2995
+
2847
2996
  // src/export/png-exporter.ts
2848
2997
  async function exportDiagramAsPng(containerEl, options = {}) {
2849
2998
  const svgXml = exportDiagramAsVectorSvg(containerEl, options);
@@ -2853,7 +3002,10 @@ async function exportDiagramAsPng(containerEl, options = {}) {
2853
3002
  const img = new Image();
2854
3003
  await new Promise((resolve, reject) => {
2855
3004
  img.onload = () => resolve();
2856
- img.onerror = () => reject(new Error("Failed to rasterize SVG into Image for PNG export"));
3005
+ img.onerror = () => {
3006
+ URL.revokeObjectURL(url);
3007
+ reject(new Error("Failed to rasterize SVG into Image for PNG export"));
3008
+ };
2857
3009
  img.src = url;
2858
3010
  });
2859
3011
  const width = img.naturalWidth || 800;
@@ -2925,7 +3077,9 @@ var DiagramPresentationController = class {
2925
3077
  this.diagram.setPlaybackRate(rate);
2926
3078
  }
2927
3079
  onKeyDown = (e) => {
2928
- if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return;
3080
+ if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement || e.target instanceof HTMLSelectElement || e.target?.isContentEditable) {
3081
+ return;
3082
+ }
2929
3083
  switch (e.key) {
2930
3084
  case "ArrowRight":
2931
3085
  case "PageDown":
@@ -2960,6 +3114,7 @@ export {
2960
3114
  ICON_REGISTRY,
2961
3115
  createDiagram,
2962
3116
  encodeGifSequence,
3117
+ exportDiagramAsGif,
2963
3118
  exportDiagramAsPng,
2964
3119
  exportDiagramAsVectorSvg
2965
3120
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markdy/renderer-dom",
3
- "version": "0.8.26",
3
+ "version": "0.8.28",
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.26"
47
+ "@markdy/core": "0.8.28"
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.26"
54
+ "@markdy/stdlib-systems": "0.8.28"
55
55
  },
56
56
  "scripts": {
57
57
  "build": "tsup",