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