sketchmark 1.1.2 → 1.1.4

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.

Potentially problematic release.


This version of sketchmark might be problematic. Click here for more details.

package/dist/index.js CHANGED
@@ -8805,6 +8805,7 @@ function clearDashOverridesAfter(el, delayMs) {
8805
8805
  }, delayMs);
8806
8806
  }
8807
8807
  const NODE_DRAW_GUIDE_ATTR = "data-node-draw-guide";
8808
+ const TEXT_REVEAL_CLIP_ATTR = "data-text-reveal-clip-id";
8808
8809
  const GUIDED_NODE_SHAPES = new Set([
8809
8810
  "box",
8810
8811
  "circle",
@@ -8933,6 +8934,7 @@ function clearNodeDrawStyles(el) {
8933
8934
  });
8934
8935
  const text = nodeText(el);
8935
8936
  if (text) {
8937
+ clearTextReveal(text);
8936
8938
  text.style.opacity = text.style.transition = "";
8937
8939
  }
8938
8940
  }
@@ -8978,55 +8980,74 @@ function revealNodeInstant(el) {
8978
8980
  clearNodeDrawStyles(el);
8979
8981
  }
8980
8982
  // ── Text writing reveal (clipPath) ───────────────────────
8983
+ function clearTextReveal(textEl, clipId) {
8984
+ const activeClipId = textEl.getAttribute(TEXT_REVEAL_CLIP_ATTR);
8985
+ const shouldClearCurrentClip = !clipId || activeClipId === clipId;
8986
+ if (shouldClearCurrentClip) {
8987
+ textEl.removeAttribute("clip-path");
8988
+ textEl.removeAttribute(TEXT_REVEAL_CLIP_ATTR);
8989
+ }
8990
+ const clipIdToRemove = clipId ?? activeClipId;
8991
+ if (clipIdToRemove) {
8992
+ textEl.ownerSVGElement?.querySelector(`#${clipIdToRemove}`)?.remove();
8993
+ }
8994
+ }
8981
8995
  function animateTextReveal(textEl, delayMs, durationMs = ANIMATION.textRevealMs) {
8982
8996
  const ownerSvg = textEl.ownerSVGElement;
8997
+ clearTextReveal(textEl);
8983
8998
  if (!ownerSvg) {
8984
8999
  // fallback: just fade
8985
9000
  textEl.style.transition = `opacity ${ANIMATION.textFade}ms ease ${delayMs}ms`;
8986
9001
  textEl.style.opacity = "1";
8987
9002
  return;
8988
9003
  }
8989
- // Make text visible but clipped to zero width
9004
+ const bbox = textEl.getBBox?.();
9005
+ if (!bbox || bbox.width === 0) {
9006
+ // fallback if can't measure
9007
+ textEl.style.transition = `opacity ${ANIMATION.textFade}ms ease ${delayMs}ms`;
9008
+ textEl.style.opacity = "1";
9009
+ return;
9010
+ }
9011
+ let defs = ownerSvg.querySelector("defs");
9012
+ if (!defs) {
9013
+ defs = document.createElementNS(SVG_NS$1, "defs");
9014
+ ownerSvg.insertBefore(defs, ownerSvg.firstChild);
9015
+ }
9016
+ const clipId = `skm-clip-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
9017
+ const clipPath = document.createElementNS(SVG_NS$1, "clipPath");
9018
+ clipPath.setAttribute("id", clipId);
9019
+ const rect = document.createElementNS(SVG_NS$1, "rect");
9020
+ rect.setAttribute("x", String(bbox.x - 2));
9021
+ rect.setAttribute("y", String(bbox.y - 2));
9022
+ rect.setAttribute("width", "0");
9023
+ rect.setAttribute("height", String(bbox.height + 4));
9024
+ clipPath.appendChild(rect);
9025
+ defs.appendChild(clipPath);
9026
+ textEl.setAttribute("clip-path", `url(#${clipId})`);
9027
+ textEl.setAttribute(TEXT_REVEAL_CLIP_ATTR, clipId);
8990
9028
  textEl.style.opacity = "1";
8991
- // We need to wait for text to be visible before we can measure it
9029
+ requestAnimationFrame(() => requestAnimationFrame(() => {
9030
+ rect.style.transition = `width ${durationMs}ms cubic-bezier(.4,0,.2,1) ${delayMs}ms`;
9031
+ rect.setAttribute("width", String(bbox.width + 4));
9032
+ }));
9033
+ // Cleanup after animation
8992
9034
  setTimeout(() => {
8993
- const bbox = textEl.getBBox?.();
8994
- if (!bbox || bbox.width === 0) {
8995
- // fallback if can't measure
8996
- return;
8997
- }
8998
- let defs = ownerSvg.querySelector("defs");
8999
- if (!defs) {
9000
- defs = document.createElementNS(SVG_NS$1, "defs");
9001
- ownerSvg.insertBefore(defs, ownerSvg.firstChild);
9002
- }
9003
- const clipId = `skm-clip-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
9004
- const clipPath = document.createElementNS(SVG_NS$1, "clipPath");
9005
- clipPath.setAttribute("id", clipId);
9006
- const rect = document.createElementNS(SVG_NS$1, "rect");
9007
- rect.setAttribute("x", String(bbox.x - 2));
9008
- rect.setAttribute("y", String(bbox.y - 2));
9009
- rect.setAttribute("width", "0");
9010
- rect.setAttribute("height", String(bbox.height + 4));
9011
- clipPath.appendChild(rect);
9012
- defs.appendChild(clipPath);
9013
- textEl.setAttribute("clip-path", `url(#${clipId})`);
9014
- requestAnimationFrame(() => requestAnimationFrame(() => {
9015
- rect.style.transition = `width ${durationMs}ms cubic-bezier(.4,0,.2,1)`;
9016
- rect.setAttribute("width", String(bbox.width + 4));
9017
- }));
9018
- // Cleanup after animation
9019
- setTimeout(() => {
9020
- textEl.removeAttribute("clip-path");
9021
- clipPath.remove();
9022
- }, durationMs + 50);
9023
- }, delayMs);
9035
+ clearTextReveal(textEl, clipId);
9036
+ }, delayMs + durationMs + 50);
9024
9037
  }
9025
- function animateNodeDraw(el, strokeDur = ANIMATION.nodeStrokeDur) {
9038
+ function animateNodeDraw(el, strokeDur = ANIMATION.nodeStrokeDur, textOnlyDur = ANIMATION.textRevealMs) {
9026
9039
  showDrawEl(el);
9027
9040
  const guide = nodeGuidePathEl(el);
9028
9041
  if (!guide) {
9029
9042
  const firstPath = el.querySelector("path");
9043
+ const text = nodeText(el);
9044
+ if (!firstPath && el.dataset.nodeShape === "text" && text) {
9045
+ animateTextReveal(text, 0, textOnlyDur);
9046
+ setTimeout(() => {
9047
+ clearNodeDrawStyles(el);
9048
+ }, textOnlyDur + 80);
9049
+ return;
9050
+ }
9030
9051
  if (!firstPath?.style.strokeDasharray)
9031
9052
  prepareForDraw(el);
9032
9053
  animateShapeDraw(el, strokeDur, ANIMATION.nodeStagger);
@@ -9246,11 +9267,13 @@ class AnimationController {
9246
9267
  this._config = _config;
9247
9268
  this._step = -1;
9248
9269
  this._pendingStepTimers = new Set();
9270
+ this._pendingNarrationTimers = new Set();
9249
9271
  this._transforms = new Map();
9250
9272
  this._listeners = [];
9251
9273
  // ── Narration caption ──
9252
9274
  this._captionEl = null;
9253
9275
  this._captionTextEl = null;
9276
+ this._narrationRunId = 0;
9254
9277
  // ── Annotations ──
9255
9278
  this._annotationLayer = null;
9256
9279
  this._annotations = [];
@@ -9513,20 +9536,30 @@ class AnimationController {
9513
9536
  }
9514
9537
  this.emit("step-change");
9515
9538
  }
9539
+ _clearTimerBucket(bucket) {
9540
+ bucket.forEach((id) => window.clearTimeout(id));
9541
+ bucket.clear();
9542
+ }
9516
9543
  _clearPendingStepTimers() {
9517
- this._pendingStepTimers.forEach((id) => window.clearTimeout(id));
9518
- this._pendingStepTimers.clear();
9544
+ this._clearTimerBucket(this._pendingStepTimers);
9519
9545
  }
9520
- _scheduleStep(fn, delayMs) {
9546
+ _cancelNarrationTyping() {
9547
+ this._narrationRunId += 1;
9548
+ this._clearTimerBucket(this._pendingNarrationTimers);
9549
+ }
9550
+ _scheduleTimer(fn, delayMs, bucket = this._pendingStepTimers) {
9521
9551
  if (delayMs <= 0) {
9522
9552
  fn();
9523
9553
  return;
9524
9554
  }
9525
9555
  const id = window.setTimeout(() => {
9526
- this._pendingStepTimers.delete(id);
9556
+ bucket.delete(id);
9527
9557
  fn();
9528
9558
  }, delayMs);
9529
- this._pendingStepTimers.add(id);
9559
+ bucket.add(id);
9560
+ }
9561
+ _scheduleStep(fn, delayMs) {
9562
+ this._scheduleTimer(fn, delayMs, this._pendingStepTimers);
9530
9563
  }
9531
9564
  _stepWaitMs(step, fallbackMs) {
9532
9565
  const delay = Math.max(0, step.delay ?? 0);
@@ -9570,6 +9603,7 @@ class AnimationController {
9570
9603
  }
9571
9604
  _clearAll() {
9572
9605
  this._clearPendingStepTimers();
9606
+ this._cancelNarrationTyping();
9573
9607
  this._cancelSpeech();
9574
9608
  this._transforms.clear();
9575
9609
  // Nodes
@@ -10034,7 +10068,7 @@ class AnimationController {
10034
10068
  if (!nodeGuidePathEl(nodeEl) && !nodeEl.querySelector("path")?.style.strokeDasharray) {
10035
10069
  prepareNodeForDraw(nodeEl);
10036
10070
  }
10037
- animateNodeDraw(nodeEl, step.duration ?? ANIMATION.nodeStrokeDur);
10071
+ animateNodeDraw(nodeEl, step.duration ?? ANIMATION.nodeStrokeDur, step.duration ?? ANIMATION.textRevealMs);
10038
10072
  }
10039
10073
  }
10040
10074
  // ── erase ─────────────────────────────────────────────────
@@ -10124,6 +10158,7 @@ class AnimationController {
10124
10158
  _doNarrate(text, silent) {
10125
10159
  if (!this._captionEl || !this._captionTextEl)
10126
10160
  return;
10161
+ this._cancelNarrationTyping();
10127
10162
  this._captionEl.style.opacity = "1";
10128
10163
  if (silent || !text) {
10129
10164
  this._captionTextEl.textContent = text;
@@ -10134,12 +10169,16 @@ class AnimationController {
10134
10169
  this._speak(text);
10135
10170
  // Typing effect
10136
10171
  this._captionTextEl.textContent = "";
10172
+ const narrationRunId = this._narrationRunId;
10137
10173
  let charIdx = 0;
10138
10174
  const typeNext = () => {
10175
+ if (this._narrationRunId !== narrationRunId || !this._captionTextEl)
10176
+ return;
10139
10177
  if (charIdx < text.length) {
10140
10178
  this._captionTextEl.textContent += text[charIdx++];
10141
- const id = window.setTimeout(typeNext, ANIMATION.narrationTypeMs);
10142
- this._pendingStepTimers.add(id);
10179
+ if (charIdx < text.length) {
10180
+ this._scheduleTimer(typeNext, ANIMATION.narrationTypeMs, this._pendingNarrationTimers);
10181
+ }
10143
10182
  }
10144
10183
  };
10145
10184
  typeNext();
@@ -10249,12 +10288,11 @@ class AnimationController {
10249
10288
  requestAnimationFrame(animate);
10250
10289
  }
10251
10290
  // After guide finishes: reveal rough.js element, remove guide
10252
- const id = window.setTimeout(() => {
10291
+ this._scheduleTimer(() => {
10253
10292
  roughEl.style.transition = `opacity 120ms ease`;
10254
10293
  roughEl.style.opacity = "1";
10255
10294
  guide.remove();
10256
10295
  }, dur + 30);
10257
- this._pendingStepTimers.add(id);
10258
10296
  }));
10259
10297
  }
10260
10298
  _doAnnotationCircle(target, silent) {
@@ -11831,6 +11869,7 @@ const EMBED_CSS = `
11831
11869
 
11832
11870
  .skm-embed__controls {
11833
11871
  display: flex;
11872
+ flex-wrap: wrap;
11834
11873
  align-items: center;
11835
11874
  gap: 8px;
11836
11875
  padding: 10px 12px;
@@ -11848,6 +11887,13 @@ const EMBED_CSS = `
11848
11887
  display: none;
11849
11888
  }
11850
11889
 
11890
+ .skm-embed__controls-group {
11891
+ display: flex;
11892
+ align-items: center;
11893
+ gap: 8px;
11894
+ flex-wrap: wrap;
11895
+ }
11896
+
11851
11897
  .skm-embed__button {
11852
11898
  border: 1px solid #caba98;
11853
11899
  background: #f5eedd;
@@ -11883,6 +11929,17 @@ const EMBED_CSS = `
11883
11929
  cursor: default;
11884
11930
  }
11885
11931
 
11932
+ .skm-embed__zoom {
11933
+ min-width: 48px;
11934
+ text-align: center;
11935
+ color: #8a6040;
11936
+ font-size: 11px;
11937
+ }
11938
+
11939
+ .skm-embed--dark .skm-embed__zoom {
11940
+ color: #d0b176;
11941
+ }
11942
+
11886
11943
  .skm-embed__step {
11887
11944
  margin-left: auto;
11888
11945
  min-width: 96px;
@@ -11901,8 +11958,10 @@ class SketchmarkEmbed {
11901
11958
  this.emitter = new EventEmitter();
11902
11959
  this.animUnsub = null;
11903
11960
  this.playInFlight = false;
11961
+ this.zoom = 1;
11904
11962
  this.offsetX = 0;
11905
11963
  this.offsetY = 0;
11964
+ this.autoFitEnabled = true;
11906
11965
  this.motionFrame = null;
11907
11966
  this.resizeObserver = null;
11908
11967
  this.options = options;
@@ -11923,10 +11982,18 @@ class SketchmarkEmbed {
11923
11982
  </div>
11924
11983
  <div class="skm-embed__error"></div>
11925
11984
  <div class="skm-embed__controls">
11926
- <button type="button" class="skm-embed__button" data-action="reset">Reset</button>
11927
- <button type="button" class="skm-embed__button" data-action="prev">Prev</button>
11928
- <button type="button" class="skm-embed__button" data-action="next">Next</button>
11929
- <button type="button" class="skm-embed__button" data-action="play">Play</button>
11985
+ <div class="skm-embed__controls-group">
11986
+ <button type="button" class="skm-embed__button" data-action="zoom-out">-</button>
11987
+ <span class="skm-embed__zoom">100%</span>
11988
+ <button type="button" class="skm-embed__button" data-action="zoom-in">+</button>
11989
+ <button type="button" class="skm-embed__button" data-action="fit">Reset</button>
11990
+ </div>
11991
+ <div class="skm-embed__controls-group">
11992
+ <button type="button" class="skm-embed__button" data-action="restart">Restart</button>
11993
+ <button type="button" class="skm-embed__button" data-action="prev">Prev</button>
11994
+ <button type="button" class="skm-embed__button" data-action="next">Next</button>
11995
+ <button type="button" class="skm-embed__button" data-action="play">Play</button>
11996
+ </div>
11930
11997
  <span class="skm-embed__step">No steps</span>
11931
11998
  </div>
11932
11999
  `;
@@ -11936,12 +12003,19 @@ class SketchmarkEmbed {
11936
12003
  this.errorElement = this.root.querySelector(".skm-embed__error");
11937
12004
  this.controlsElement = this.root.querySelector(".skm-embed__controls");
11938
12005
  this.stepInfoElement = this.root.querySelector(".skm-embed__step");
11939
- this.btnReset = this.root.querySelector('[data-action="reset"]');
12006
+ this.zoomInfoElement = this.root.querySelector(".skm-embed__zoom");
12007
+ this.btnFit = this.root.querySelector('[data-action="fit"]');
12008
+ this.btnZoomIn = this.root.querySelector('[data-action="zoom-in"]');
12009
+ this.btnZoomOut = this.root.querySelector('[data-action="zoom-out"]');
12010
+ this.btnRestart = this.root.querySelector('[data-action="restart"]');
11940
12011
  this.btnPrev = this.root.querySelector('[data-action="prev"]');
11941
12012
  this.btnNext = this.root.querySelector('[data-action="next"]');
11942
12013
  this.btnPlay = this.root.querySelector('[data-action="play"]');
11943
12014
  this.controlsElement.classList.toggle("is-hidden", options.showControls === false);
11944
- this.btnReset.addEventListener("click", () => this.resetAnimation());
12015
+ this.btnFit.addEventListener("click", () => this.resetView());
12016
+ this.btnZoomIn.addEventListener("click", () => this.zoomIn());
12017
+ this.btnZoomOut.addEventListener("click", () => this.zoomOut());
12018
+ this.btnRestart.addEventListener("click", () => this.resetAnimation());
11945
12019
  this.btnPrev.addEventListener("click", () => this.prevStep());
11946
12020
  this.btnNext.addEventListener("click", () => this.nextStep());
11947
12021
  this.btnPlay.addEventListener("click", () => {
@@ -11987,7 +12061,12 @@ class SketchmarkEmbed {
11987
12061
  this.animUnsub = null;
11988
12062
  this.instance?.anim?.destroy();
11989
12063
  this.instance = null;
12064
+ this.autoFitEnabled = true;
12065
+ this.zoom = 1;
12066
+ this.offsetX = 0;
12067
+ this.offsetY = 0;
11990
12068
  this.diagramWrap.innerHTML = "";
12069
+ this.applyTransform();
11991
12070
  try {
11992
12071
  const instance = render({
11993
12072
  container: this.diagramWrap,
@@ -12068,6 +12147,21 @@ class SketchmarkEmbed {
12068
12147
  this.syncControls();
12069
12148
  this.positionViewport(true);
12070
12149
  }
12150
+ fitToViewport(animated = false) {
12151
+ if (!this.instance?.svg)
12152
+ return;
12153
+ this.autoFitEnabled = true;
12154
+ this.positionViewport(animated);
12155
+ }
12156
+ resetView(animated = false) {
12157
+ this.fitToViewport(animated);
12158
+ }
12159
+ zoomIn() {
12160
+ this.zoomAroundViewportCenter(1.2);
12161
+ }
12162
+ zoomOut() {
12163
+ this.zoomAroundViewportCenter(0.8);
12164
+ }
12071
12165
  exportSVG(filename) {
12072
12166
  this.instance?.exportSVG(filename);
12073
12167
  }
@@ -12090,10 +12184,14 @@ class SketchmarkEmbed {
12090
12184
  return typeof value === "number" ? `${value}px` : value;
12091
12185
  }
12092
12186
  syncControls() {
12187
+ this.syncAnimationControls();
12188
+ this.syncViewControls();
12189
+ }
12190
+ syncAnimationControls() {
12093
12191
  const anim = this.instance?.anim;
12094
12192
  if (!anim || !anim.total) {
12095
12193
  this.stepInfoElement.textContent = "No steps";
12096
- this.btnReset.disabled = true;
12194
+ this.btnRestart.disabled = true;
12097
12195
  this.btnPrev.disabled = true;
12098
12196
  this.btnNext.disabled = true;
12099
12197
  this.btnPlay.disabled = true;
@@ -12101,56 +12199,69 @@ class SketchmarkEmbed {
12101
12199
  }
12102
12200
  this.stepInfoElement.textContent =
12103
12201
  anim.currentStep < 0 ? `${anim.total} steps` : `${anim.currentStep + 1} / ${anim.total}`;
12104
- this.btnReset.disabled = false;
12202
+ this.btnRestart.disabled = false;
12105
12203
  this.btnPrev.disabled = !anim.canPrev;
12106
12204
  this.btnNext.disabled = !anim.canNext;
12107
12205
  this.btnPlay.disabled = this.playInFlight || !anim.canNext;
12108
12206
  }
12207
+ syncViewControls() {
12208
+ const hasView = !!this.instance?.svg;
12209
+ const zoomMin = this.getZoomMin();
12210
+ const zoomMax = this.getZoomMax();
12211
+ this.zoomInfoElement.textContent = `${Math.round(this.zoom * 100)}%`;
12212
+ this.btnFit.disabled = !hasView;
12213
+ this.btnZoomOut.disabled = !hasView || this.zoom <= zoomMin + 0.001;
12214
+ this.btnZoomIn.disabled = !hasView || this.zoom >= zoomMax - 0.001;
12215
+ }
12109
12216
  positionViewport(animated) {
12110
- if (!this.instance?.svg)
12111
- return;
12112
- const svg = this.instance.svg;
12113
- const svgWidth = parseFloat(svg.getAttribute("width") || "0");
12114
- const svgHeight = parseFloat(svg.getAttribute("height") || "0");
12115
- if (!svgWidth || !svgHeight)
12217
+ const size = this.getContentSize();
12218
+ if (!size)
12116
12219
  return;
12220
+ const { width: svgWidth, height: svgHeight } = size;
12117
12221
  const viewportRect = this.viewport.getBoundingClientRect();
12118
12222
  const viewWidth = viewportRect.width || this.viewport.clientWidth;
12119
12223
  const viewHeight = viewportRect.height || this.viewport.clientHeight;
12120
12224
  if (!viewWidth || !viewHeight)
12121
12225
  return;
12122
- const sceneIsLarge = svgWidth > viewWidth || svgHeight > viewHeight;
12226
+ if (this.autoFitEnabled) {
12227
+ this.zoom = this.getFitZoom(svgWidth, svgHeight, viewWidth, viewHeight);
12228
+ }
12229
+ this.syncViewControls();
12230
+ const scaledWidth = svgWidth * this.zoom;
12231
+ const scaledHeight = svgHeight * this.zoom;
12232
+ const focusTarget = this.getFocusTarget();
12233
+ const sceneIsLarge = scaledWidth > viewWidth || scaledHeight > viewHeight;
12123
12234
  const shouldFocus = sceneIsLarge &&
12124
12235
  this.options.autoFocus !== false &&
12125
- !!this.getFocusTarget();
12236
+ !!focusTarget;
12126
12237
  if (!shouldFocus) {
12127
- this.animateTo(svgWidth <= viewWidth ? (viewWidth - svgWidth) / 2 : 0, svgHeight <= viewHeight ? (viewHeight - svgHeight) / 2 : 0, animated);
12238
+ this.animateTo(scaledWidth <= viewWidth ? (viewWidth - scaledWidth) / 2 : 0, scaledHeight <= viewHeight ? (viewHeight - scaledHeight) / 2 : 0, animated);
12128
12239
  return;
12129
12240
  }
12130
- const target = this.findTargetElement(this.getFocusTarget());
12241
+ const target = this.findTargetElement(focusTarget);
12131
12242
  if (!target) {
12132
12243
  this.animateTo(0, 0, animated);
12133
12244
  return;
12134
12245
  }
12135
- const currentRect = target.getBoundingClientRect();
12136
- const sceneX = currentRect.left - viewportRect.left - this.offsetX;
12137
- const sceneY = currentRect.top - viewportRect.top - this.offsetY;
12138
- const targetCenterX = sceneX + currentRect.width / 2;
12139
- const targetCenterY = sceneY + currentRect.height / 2;
12140
- let nextX = viewWidth / 2 - targetCenterX;
12141
- let nextY = viewHeight / 2 - targetCenterY;
12246
+ const targetBox = this.getTargetBox(target, viewportRect);
12247
+ if (!targetBox) {
12248
+ this.animateTo(0, 0, animated);
12249
+ return;
12250
+ }
12251
+ let nextX = viewWidth / 2 - targetBox.centerX;
12252
+ let nextY = viewHeight / 2 - targetBox.centerY;
12142
12253
  const padding = this.options.focusPadding ?? 24;
12143
- if (svgWidth <= viewWidth) {
12144
- nextX = (viewWidth - svgWidth) / 2;
12254
+ if (scaledWidth <= viewWidth) {
12255
+ nextX = (viewWidth - scaledWidth) / 2;
12145
12256
  }
12146
12257
  else {
12147
- nextX = clamp(nextX, viewWidth - svgWidth - padding, padding);
12258
+ nextX = clamp(nextX, viewWidth - scaledWidth - padding, padding);
12148
12259
  }
12149
- if (svgHeight <= viewHeight) {
12150
- nextY = (viewHeight - svgHeight) / 2;
12260
+ if (scaledHeight <= viewHeight) {
12261
+ nextY = (viewHeight - scaledHeight) / 2;
12151
12262
  }
12152
12263
  else {
12153
- nextY = clamp(nextY, viewHeight - svgHeight - padding, padding);
12264
+ nextY = clamp(nextY, viewHeight - scaledHeight - padding, padding);
12154
12265
  }
12155
12266
  this.animateTo(nextX, nextY, animated);
12156
12267
  }
@@ -12182,7 +12293,78 @@ class SketchmarkEmbed {
12182
12293
  this.motionFrame = requestAnimationFrame(frame);
12183
12294
  }
12184
12295
  applyTransform() {
12185
- this.world.style.transform = `translate(${this.offsetX}px, ${this.offsetY}px)`;
12296
+ this.world.style.transform = `translate(${this.offsetX}px, ${this.offsetY}px) scale(${this.zoom})`;
12297
+ this.zoomInfoElement.textContent = `${Math.round(this.zoom * 100)}%`;
12298
+ }
12299
+ getContentSize() {
12300
+ if (!this.instance?.svg)
12301
+ return null;
12302
+ const svg = this.instance.svg;
12303
+ const width = parseFloat(svg.getAttribute("width") || "0");
12304
+ const height = parseFloat(svg.getAttribute("height") || "0");
12305
+ if (!width || !height)
12306
+ return null;
12307
+ return { width, height };
12308
+ }
12309
+ getFitZoom(svgWidth, svgHeight, viewWidth, viewHeight) {
12310
+ const padding = this.getFitPadding(viewWidth, viewHeight);
12311
+ const availableWidth = Math.max(viewWidth - padding * 2, 1);
12312
+ const availableHeight = Math.max(viewHeight - padding * 2, 1);
12313
+ const nextZoom = Math.min(availableWidth / svgWidth, availableHeight / svgHeight, 1);
12314
+ return clamp(nextZoom || 1, this.getZoomMin(), this.getZoomMax());
12315
+ }
12316
+ getFitPadding(viewWidth, viewHeight) {
12317
+ if (typeof this.options.fitPadding === "number") {
12318
+ return Math.max(0, this.options.fitPadding);
12319
+ }
12320
+ return Math.max(16, Math.min(40, Math.round(Math.min(viewWidth, viewHeight) * 0.08)));
12321
+ }
12322
+ getZoomMin() {
12323
+ return this.options.zoomMin ?? 0.08;
12324
+ }
12325
+ getZoomMax() {
12326
+ return this.options.zoomMax ?? 4;
12327
+ }
12328
+ zoomAroundViewportCenter(factor) {
12329
+ if (!this.instance?.svg)
12330
+ return;
12331
+ const pivotX = this.viewport.clientWidth / 2;
12332
+ const pivotY = this.viewport.clientHeight / 2;
12333
+ this.zoomTo(this.zoom * factor, pivotX, pivotY);
12334
+ }
12335
+ zoomTo(nextZoom, pivotX, pivotY) {
12336
+ const clampedZoom = clamp(nextZoom, this.getZoomMin(), this.getZoomMax());
12337
+ const ratio = clampedZoom / this.zoom;
12338
+ if (!Number.isFinite(ratio) || ratio === 1) {
12339
+ this.syncViewControls();
12340
+ return;
12341
+ }
12342
+ this.stopMotion();
12343
+ this.autoFitEnabled = false;
12344
+ this.offsetX = pivotX - (pivotX - this.offsetX) * ratio;
12345
+ this.offsetY = pivotY - (pivotY - this.offsetY) * ratio;
12346
+ this.zoom = clampedZoom;
12347
+ this.applyTransform();
12348
+ this.syncViewControls();
12349
+ }
12350
+ getTargetBox(target, viewportRect) {
12351
+ if (target instanceof SVGGraphicsElement) {
12352
+ try {
12353
+ const bounds = target.getBBox();
12354
+ return {
12355
+ centerX: (bounds.x + bounds.width / 2) * this.zoom,
12356
+ centerY: (bounds.y + bounds.height / 2) * this.zoom,
12357
+ };
12358
+ }
12359
+ catch {
12360
+ // Ignore and fall back to layout-based bounds below.
12361
+ }
12362
+ }
12363
+ const currentRect = target.getBoundingClientRect();
12364
+ return {
12365
+ centerX: currentRect.left - viewportRect.left - this.offsetX + currentRect.width / 2,
12366
+ centerY: currentRect.top - viewportRect.top - this.offsetY + currentRect.height / 2,
12367
+ };
12186
12368
  }
12187
12369
  getFocusTarget() {
12188
12370
  const anim = this.instance?.anim;