@markdy/renderer-dom 0.8.14 → 0.8.16

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
@@ -11,7 +11,8 @@ Web Animations API renderer for [MarkdyScript](../../docs/SYNTAX.md) scenes. Tra
11
11
  - **Focused diagram visuals** — flowchart shapes, tree buses, state self-loops, sequence lifelines/messages, constellation orbits, group zones, and annotations
12
12
  - **Semantic node cards** — compact SVG glyphs for browsers, services, gateways, queues, workers, databases, storage, CDN, security, platform, and more
13
13
  - **Seek-safe** — manual `currentTime` control enables reliable `seek()` in any direction
14
- - **Playback-rate controls** — set timeline speed to slow down or speed up diagrams without rebuilding animations
14
+ - **Playback-rate controls** — set normalized timeline speed to slow down or speed up diagrams without rebuilding animations
15
+ - **Interactive viewport** — opt into wheel zoom and drag pan while click-to-pause stays available
15
16
  - **Semantic themes** — `paper`, `editorial`, `nebula`, `midnight`, `blueprint`, and `graphite`, with per-role node and edge colors
16
17
  - **Single dependency** — only `@markdy/core`
17
18
 
@@ -73,7 +74,8 @@ diagram.destroy(); // clean up DOM + cancel animations
73
74
  | `copyright` | `boolean` | `true` | Show a small "Powered by Markdy" badge below the animation |
74
75
  | `progressBar` | `boolean` | `true` | Deprecated compatibility flag for the rainbow scene-boundary progress bar |
75
76
  | `sceneBoundaryProgress` | `boolean` | `progressBar ?? true` | Preferred flag for the rainbow scene-boundary progress bar |
76
- | `playbackRate` | `number` | `1` | Timeline speed multiplier |
77
+ | `playbackRate` | `number` | `1` | Normalized timeline speed multiplier; `1` is Markdy's normal pace |
78
+ | `interactiveViewport` | `boolean` | `false` | Enable wheel zoom and drag pan on the rendered viewport |
77
79
  | `onWarning` | `(warning: Diagnostic) => void` | `console.warn` | Called for each soft parse warning |
78
80
  | `onTimeUpdate` | `(seconds: number, durationSeconds: number) => void` | — | Called whenever playback or seek changes the current time |
79
81
  | `onPlayStateChange` | `(playing: boolean) => void` | — | Called when playback starts or pauses |
package/dist/index.d.ts CHANGED
@@ -20,8 +20,10 @@ interface DiagramOptions {
20
20
  progressBar?: boolean;
21
21
  /** Show rainbow progress around scene boundary. Defaults to true. */
22
22
  sceneBoundaryProgress?: boolean;
23
- /** Playback speed multiplier. Defaults to 1. */
23
+ /** Playback speed multiplier. Defaults to 1, where 1 is Markdy's normal pace. */
24
24
  playbackRate?: number;
25
+ /** Enable wheel zoom and drag pan on the rendered viewport. Defaults to false. */
26
+ interactiveViewport?: boolean;
25
27
  onWarning?: (warning: Diagnostic) => void;
26
28
  onTimeUpdate?: (seconds: number, durationSeconds: number) => void;
27
29
  onPlayStateChange?: (playing: boolean) => void;
package/dist/index.js CHANGED
@@ -1810,6 +1810,12 @@ function applyThemeToScene(scene, theme) {
1810
1810
  }
1811
1811
 
1812
1812
  // src/diagram.ts
1813
+ var NORMAL_PLAYBACK_RATE = 4 / 5;
1814
+ var DEFAULT_PLAYBACK_RATE = 1;
1815
+ var MIN_VIEWPORT_ZOOM = 0.5;
1816
+ var MAX_VIEWPORT_ZOOM = 3;
1817
+ var VIEWPORT_ZOOM_STEP = 15e-4;
1818
+ var DRAG_CLICK_THRESHOLD_PX = 4;
1813
1819
  function createBeatCaptionLayer(doc, beats) {
1814
1820
  const layer = doc.createElement("div");
1815
1821
  layer.className = "markdy-beat-caption-layer";
@@ -1856,7 +1862,8 @@ function createDiagram(opts) {
1856
1862
  assets,
1857
1863
  progressBar,
1858
1864
  sceneBoundaryProgress,
1859
- playbackRate: initialPlaybackRate = 1,
1865
+ playbackRate: initialPlaybackRate = DEFAULT_PLAYBACK_RATE,
1866
+ interactiveViewport = false,
1860
1867
  onWarning = (w) => console.warn(`[markdy] line ${w.line}: ${w.message}`),
1861
1868
  onTimeUpdate,
1862
1869
  onPlayStateChange
@@ -1937,10 +1944,19 @@ function createDiagram(opts) {
1937
1944
  });
1938
1945
  applyThemeToScene(scene, plan.theme);
1939
1946
  viewport.appendChild(scene);
1947
+ const viewportTransform = document.createElement("div");
1948
+ viewportTransform.className = "markdy-viewport-transform";
1949
+ Object.assign(viewportTransform.style, {
1950
+ position: "absolute",
1951
+ inset: "0",
1952
+ transformOrigin: "0 0",
1953
+ willChange: interactiveViewport ? "transform" : "auto"
1954
+ });
1955
+ scene.appendChild(viewportTransform);
1940
1956
  const sceneContent = document.createElement("div");
1941
1957
  sceneContent.className = "markdy-scene-content";
1942
1958
  Object.assign(sceneContent.style, { position: "absolute", inset: "0" });
1943
- scene.appendChild(sceneContent);
1959
+ viewportTransform.appendChild(sceneContent);
1944
1960
  const titleEl = createTitleEl(plan.title);
1945
1961
  sceneContent.appendChild(titleEl);
1946
1962
  const cameraLayer = document.createElement("div");
@@ -1998,9 +2014,11 @@ function createDiagram(opts) {
1998
2014
  nodeLayer.appendChild(el);
1999
2015
  nodeEls.set(node.id, el);
2000
2016
  }
2017
+ let fitScale = 1;
2001
2018
  function scaleScene() {
2002
- const s = viewport.clientWidth / plan.meta.width;
2003
- scene.style.transform = `scale(${s})`;
2019
+ const width = viewport.clientWidth || plan.meta.width;
2020
+ fitScale = width / plan.meta.width;
2021
+ scene.style.transform = `scale(${fitScale})`;
2004
2022
  }
2005
2023
  scaleScene();
2006
2024
  const resizeObserver = new ResizeObserver(scaleScene);
@@ -2042,16 +2060,75 @@ function createDiagram(opts) {
2042
2060
  anim.currentTime = 0;
2043
2061
  }
2044
2062
  let sceneMs = 0;
2045
- let playbackRate = Number.isFinite(initialPlaybackRate) && initialPlaybackRate > 0 ? initialPlaybackRate : 1;
2063
+ let playbackRate = Number.isFinite(initialPlaybackRate) && initialPlaybackRate > 0 ? initialPlaybackRate : DEFAULT_PLAYBACK_RATE;
2046
2064
  let lastRafTs = null;
2047
2065
  let isPlaying = false;
2048
2066
  let rafId = null;
2067
+ let viewportScale = 1;
2068
+ let viewportPanX = 0;
2069
+ let viewportPanY = 0;
2070
+ let activePointerId = null;
2071
+ let dragStartX = 0;
2072
+ let dragStartY = 0;
2073
+ let dragLastX = 0;
2074
+ let dragLastY = 0;
2075
+ let dragMoved = false;
2076
+ let suppressNextClick = false;
2077
+ function applyViewportTransform() {
2078
+ viewportTransform.style.transform = `translate(${viewportPanX}px, ${viewportPanY}px) scale(${viewportScale})`;
2079
+ }
2080
+ function handleViewportWheel(event) {
2081
+ event.preventDefault();
2082
+ const rect = viewport.getBoundingClientRect();
2083
+ const pointerX = (event.clientX - rect.left) / fitScale;
2084
+ const pointerY = (event.clientY - rect.top) / fitScale;
2085
+ const nextScale = Math.min(MAX_VIEWPORT_ZOOM, Math.max(MIN_VIEWPORT_ZOOM, viewportScale * Math.exp(-event.deltaY * VIEWPORT_ZOOM_STEP)));
2086
+ if (nextScale === viewportScale) return;
2087
+ const sceneX = (pointerX - viewportPanX) / viewportScale;
2088
+ const sceneY = (pointerY - viewportPanY) / viewportScale;
2089
+ viewportScale = nextScale;
2090
+ viewportPanX = pointerX - sceneX * viewportScale;
2091
+ viewportPanY = pointerY - sceneY * viewportScale;
2092
+ applyViewportTransform();
2093
+ }
2094
+ function handleViewportPointerDown(event) {
2095
+ if (event.button !== 0 || activePointerId !== null) return;
2096
+ activePointerId = event.pointerId;
2097
+ dragStartX = event.clientX;
2098
+ dragStartY = event.clientY;
2099
+ dragLastX = event.clientX;
2100
+ dragLastY = event.clientY;
2101
+ dragMoved = false;
2102
+ viewport.setPointerCapture(event.pointerId);
2103
+ viewport.style.cursor = "grabbing";
2104
+ }
2105
+ function handleViewportPointerMove(event) {
2106
+ if (event.pointerId !== activePointerId) return;
2107
+ const deltaX = (event.clientX - dragLastX) / fitScale;
2108
+ const deltaY = (event.clientY - dragLastY) / fitScale;
2109
+ const totalX = event.clientX - dragStartX;
2110
+ const totalY = event.clientY - dragStartY;
2111
+ if (!dragMoved && Math.hypot(totalX, totalY) >= DRAG_CLICK_THRESHOLD_PX) dragMoved = true;
2112
+ dragLastX = event.clientX;
2113
+ dragLastY = event.clientY;
2114
+ viewportPanX += deltaX;
2115
+ viewportPanY += deltaY;
2116
+ applyViewportTransform();
2117
+ }
2118
+ function handleViewportPointerEnd(event) {
2119
+ if (event.pointerId !== activePointerId) return;
2120
+ if (dragMoved) suppressNextClick = true;
2121
+ if (viewport.hasPointerCapture(event.pointerId)) viewport.releasePointerCapture(event.pointerId);
2122
+ activePointerId = null;
2123
+ dragMoved = false;
2124
+ viewport.style.cursor = interactiveViewport ? "grab" : "pointer";
2125
+ }
2049
2126
  function applyCurrentTime() {
2050
2127
  for (const anim of allAnims) anim.currentTime = sceneMs;
2051
2128
  onTimeUpdate?.(sceneMs / 1e3, durationSeconds);
2052
2129
  }
2053
2130
  function rafTick(timestamp) {
2054
- if (lastRafTs !== null) sceneMs += (timestamp - lastRafTs) * playbackRate;
2131
+ if (lastRafTs !== null) sceneMs += (timestamp - lastRafTs) * playbackRate * NORMAL_PLAYBACK_RATE;
2055
2132
  lastRafTs = timestamp;
2056
2133
  if (totalDurationMs > 0 && sceneMs >= totalDurationMs) {
2057
2134
  if (loop) sceneMs = sceneMs % totalDurationMs;
@@ -2122,8 +2199,20 @@ function createDiagram(opts) {
2122
2199
  if (viewport.parentNode === container) container.removeChild(viewport);
2123
2200
  }
2124
2201
  };
2125
- viewport.style.cursor = "pointer";
2202
+ viewport.style.cursor = interactiveViewport ? "grab" : "pointer";
2203
+ if (interactiveViewport) {
2204
+ viewport.style.touchAction = "none";
2205
+ viewport.addEventListener("wheel", handleViewportWheel, { passive: false });
2206
+ viewport.addEventListener("pointerdown", handleViewportPointerDown);
2207
+ viewport.addEventListener("pointermove", handleViewportPointerMove);
2208
+ viewport.addEventListener("pointerup", handleViewportPointerEnd);
2209
+ viewport.addEventListener("pointercancel", handleViewportPointerEnd);
2210
+ }
2126
2211
  viewport.addEventListener("click", () => {
2212
+ if (suppressNextClick) {
2213
+ suppressNextClick = false;
2214
+ return;
2215
+ }
2127
2216
  if (isPlaying) diagram.pause();
2128
2217
  else {
2129
2218
  if (!loop && sceneMs >= totalDurationMs) sceneMs = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markdy/renderer-dom",
3
- "version": "0.8.14",
3
+ "version": "0.8.16",
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.14"
47
+ "@markdy/core": "0.8.16"
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.14"
54
+ "@markdy/stdlib-systems": "0.8.16"
55
55
  },
56
56
  "scripts": {
57
57
  "build": "tsup",