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/README.md +101 -104
- package/dist/animation/index.d.ts +5 -0
- package/dist/animation/index.d.ts.map +1 -1
- package/dist/index.cjs +258 -76
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +258 -76
- package/dist/index.js.map +1 -1
- package/dist/sketchmark.iife.js +258 -76
- package/dist/ui/embed.d.ts +24 -1
- package/dist/ui/embed.d.ts.map +1 -1
- package/package.json +70 -72
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
8994
|
-
|
|
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.
|
|
9518
|
-
this._pendingStepTimers.clear();
|
|
9544
|
+
this._clearTimerBucket(this._pendingStepTimers);
|
|
9519
9545
|
}
|
|
9520
|
-
|
|
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
|
-
|
|
9556
|
+
bucket.delete(id);
|
|
9527
9557
|
fn();
|
|
9528
9558
|
}, delayMs);
|
|
9529
|
-
|
|
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
|
-
|
|
10142
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
11927
|
-
|
|
11928
|
-
|
|
11929
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
12111
|
-
|
|
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
|
-
|
|
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
|
-
!!
|
|
12236
|
+
!!focusTarget;
|
|
12126
12237
|
if (!shouldFocus) {
|
|
12127
|
-
this.animateTo(
|
|
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(
|
|
12241
|
+
const target = this.findTargetElement(focusTarget);
|
|
12131
12242
|
if (!target) {
|
|
12132
12243
|
this.animateTo(0, 0, animated);
|
|
12133
12244
|
return;
|
|
12134
12245
|
}
|
|
12135
|
-
const
|
|
12136
|
-
|
|
12137
|
-
|
|
12138
|
-
|
|
12139
|
-
|
|
12140
|
-
let nextX = viewWidth / 2 -
|
|
12141
|
-
let nextY = viewHeight / 2 -
|
|
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 (
|
|
12144
|
-
nextX = (viewWidth -
|
|
12254
|
+
if (scaledWidth <= viewWidth) {
|
|
12255
|
+
nextX = (viewWidth - scaledWidth) / 2;
|
|
12145
12256
|
}
|
|
12146
12257
|
else {
|
|
12147
|
-
nextX = clamp(nextX, viewWidth -
|
|
12258
|
+
nextX = clamp(nextX, viewWidth - scaledWidth - padding, padding);
|
|
12148
12259
|
}
|
|
12149
|
-
if (
|
|
12150
|
-
nextY = (viewHeight -
|
|
12260
|
+
if (scaledHeight <= viewHeight) {
|
|
12261
|
+
nextY = (viewHeight - scaledHeight) / 2;
|
|
12151
12262
|
}
|
|
12152
12263
|
else {
|
|
12153
|
-
nextY = clamp(nextY, viewHeight -
|
|
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;
|