sketchmark 1.1.3 → 1.1.5
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 +149 -132
- package/dist/animation/index.d.ts.map +1 -1
- package/dist/index.cjs +366 -71
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +366 -71
- package/dist/index.js.map +1 -1
- package/dist/render.d.ts +1 -0
- package/dist/render.d.ts.map +1 -1
- package/dist/sketchmark.iife.js +366 -71
- package/dist/ui/canvas.d.ts +12 -0
- package/dist/ui/canvas.d.ts.map +1 -1
- package/dist/ui/embed.d.ts +36 -1
- package/dist/ui/embed.d.ts.map +1 -1
- package/package.json +1 -1
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);
|
|
@@ -9420,8 +9441,17 @@ class AnimationController {
|
|
|
9420
9441
|
}
|
|
9421
9442
|
/** Enable/disable browser text-to-speech for narrate steps */
|
|
9422
9443
|
get tts() { return this._tts; }
|
|
9423
|
-
set tts(on) {
|
|
9424
|
-
|
|
9444
|
+
set tts(on) {
|
|
9445
|
+
const next = !!on;
|
|
9446
|
+
const changed = next !== this._tts;
|
|
9447
|
+
this._tts = next;
|
|
9448
|
+
if (!next) {
|
|
9449
|
+
this._cancelSpeech();
|
|
9450
|
+
return;
|
|
9451
|
+
}
|
|
9452
|
+
if (changed)
|
|
9453
|
+
this._warmUpSpeech();
|
|
9454
|
+
}
|
|
9425
9455
|
get currentStep() {
|
|
9426
9456
|
return this._step;
|
|
9427
9457
|
}
|
|
@@ -10047,7 +10077,7 @@ class AnimationController {
|
|
|
10047
10077
|
if (!nodeGuidePathEl(nodeEl) && !nodeEl.querySelector("path")?.style.strokeDasharray) {
|
|
10048
10078
|
prepareNodeForDraw(nodeEl);
|
|
10049
10079
|
}
|
|
10050
|
-
animateNodeDraw(nodeEl, step.duration ?? ANIMATION.nodeStrokeDur);
|
|
10080
|
+
animateNodeDraw(nodeEl, step.duration ?? ANIMATION.nodeStrokeDur, step.duration ?? ANIMATION.textRevealMs);
|
|
10051
10081
|
}
|
|
10052
10082
|
}
|
|
10053
10083
|
// ── erase ─────────────────────────────────────────────────
|
|
@@ -10700,7 +10730,7 @@ class EventEmitter {
|
|
|
10700
10730
|
}
|
|
10701
10731
|
|
|
10702
10732
|
function render(options) {
|
|
10703
|
-
const { container: rawContainer, dsl, renderer = "svg", injectCSS = true, svgOptions = {}, canvasOptions = {}, onNodeClick, onReady, } = options;
|
|
10733
|
+
const { container: rawContainer, dsl, renderer = "svg", injectCSS = true, tts, svgOptions = {}, canvasOptions = {}, onNodeClick, onReady, } = options;
|
|
10704
10734
|
if (injectCSS && !document.getElementById("ai-diagram-css")) {
|
|
10705
10735
|
const style = document.createElement("style");
|
|
10706
10736
|
style.id = "ai-diagram-css";
|
|
@@ -10749,6 +10779,9 @@ function render(options) {
|
|
|
10749
10779
|
const containerEl = el instanceof SVGSVGElement ? undefined : el;
|
|
10750
10780
|
anim = new AnimationController(svg, ast.steps, containerEl, rc, ast.config);
|
|
10751
10781
|
}
|
|
10782
|
+
if (typeof tts === "boolean") {
|
|
10783
|
+
anim.tts = tts;
|
|
10784
|
+
}
|
|
10752
10785
|
onReady?.(anim, svg);
|
|
10753
10786
|
return {
|
|
10754
10787
|
scene,
|
|
@@ -10800,13 +10833,14 @@ function toError(error) {
|
|
|
10800
10833
|
const CANVAS_STYLE_ID = "sketchmark-canvas-ui";
|
|
10801
10834
|
const CANVAS_CSS = `
|
|
10802
10835
|
.skm-canvas{display:flex;flex-direction:column;width:100%;height:100%;min-height:320px;overflow:hidden;border:1px solid #caba98;border-radius:10px;background:#f8f4ea;color:#3a2010;font-family:"Courier New",monospace}
|
|
10803
|
-
.skm-canvas__animbar{display:flex;align-items:center;gap:6px;padding:6px 10px;background:#eee7d8;border-bottom:1px solid #caba98;flex-shrink:0}
|
|
10836
|
+
.skm-canvas__animbar{display:flex;align-items:center;gap:6px;padding:6px 10px;background:#eee7d8;border-bottom:1px solid #caba98;flex-shrink:0;flex-wrap:wrap}
|
|
10804
10837
|
.skm-canvas__status{min-width:96px;text-align:center;color:#6a4820;font-size:11px}
|
|
10805
10838
|
.skm-canvas__label{color:#8a6040;font-size:11px;font-style:italic}
|
|
10806
10839
|
.skm-canvas__spacer{flex:1}
|
|
10807
10840
|
.skm-canvas__stats{color:#9a7848;font-size:10px}
|
|
10808
10841
|
.skm-canvas__button{border:1px solid #caba98;background:#f5eedd;color:#3a2010;border-radius:6px;padding:4px 9px;font:inherit;font-size:11px;cursor:pointer;transition:background .12s ease,border-color .12s ease,color .12s ease}
|
|
10809
10842
|
.skm-canvas__button:hover:not(:disabled){background:#c8a060;border-color:#c8a060;color:#fff}
|
|
10843
|
+
.skm-canvas__button.is-active{background:#c8a060;border-color:#c8a060;color:#fff}
|
|
10810
10844
|
.skm-canvas__button:disabled{opacity:.45;cursor:default}
|
|
10811
10845
|
.skm-canvas__error{display:none;padding:8px 12px;background:#280a0a;border-bottom:1px solid #5a1818;color:#f07070;font-size:11px;line-height:1.4;white-space:pre-wrap;flex-shrink:0}
|
|
10812
10846
|
.skm-canvas__error.is-visible{display:block}
|
|
@@ -10828,6 +10862,8 @@ class SketchmarkCanvas {
|
|
|
10828
10862
|
this.instance = null;
|
|
10829
10863
|
this.emitter = new EventEmitter();
|
|
10830
10864
|
this.dsl = "";
|
|
10865
|
+
this.showCaption = true;
|
|
10866
|
+
this.ttsOverride = null;
|
|
10831
10867
|
this.panX = 60;
|
|
10832
10868
|
this.panY = 60;
|
|
10833
10869
|
this.zoom = 1;
|
|
@@ -10915,6 +10951,8 @@ class SketchmarkCanvas {
|
|
|
10915
10951
|
this.options = options;
|
|
10916
10952
|
this.renderer = options.renderer ?? "svg";
|
|
10917
10953
|
this.theme = options.theme ?? "light";
|
|
10954
|
+
this.showCaption = options.showCaption !== false;
|
|
10955
|
+
this.ttsOverride = typeof options.tts === "boolean" ? options.tts : null;
|
|
10918
10956
|
this.dsl = normalizeNewlines(options.dsl ?? "");
|
|
10919
10957
|
injectStyleOnce(CANVAS_STYLE_ID, CANVAS_CSS);
|
|
10920
10958
|
const host = resolveContainer(options.container);
|
|
@@ -10933,6 +10971,8 @@ class SketchmarkCanvas {
|
|
|
10933
10971
|
<span class="skm-canvas__status">No steps</span>
|
|
10934
10972
|
<button type="button" class="skm-canvas__button" data-action="next">Next</button>
|
|
10935
10973
|
<button type="button" class="skm-canvas__button" data-action="play">Play</button>
|
|
10974
|
+
<button type="button" class="skm-canvas__button" data-action="toggle-caption">Caption On</button>
|
|
10975
|
+
<button type="button" class="skm-canvas__button" data-action="toggle-tts">TTS Off</button>
|
|
10936
10976
|
<span class="skm-canvas__label"></span>
|
|
10937
10977
|
<span class="skm-canvas__spacer"></span>
|
|
10938
10978
|
<span class="skm-canvas__stats"></span>
|
|
@@ -10967,6 +11007,8 @@ class SketchmarkCanvas {
|
|
|
10967
11007
|
this.prevButton = this.root.querySelector('[data-action="prev"]');
|
|
10968
11008
|
this.nextButton = this.root.querySelector('[data-action="next"]');
|
|
10969
11009
|
this.resetButton = this.root.querySelector('[data-action="reset"]');
|
|
11010
|
+
this.captionButton = this.root.querySelector('[data-action="toggle-caption"]');
|
|
11011
|
+
this.ttsButton = this.root.querySelector('[data-action="toggle-tts"]');
|
|
10970
11012
|
this.gridPattern = this.root.querySelector(`#${patternId}`);
|
|
10971
11013
|
this.gridDot = this.gridPattern.querySelector("circle");
|
|
10972
11014
|
this.root.querySelector('[data-action="fit"]')?.addEventListener("click", () => this.fitContent());
|
|
@@ -10977,6 +11019,8 @@ class SketchmarkCanvas {
|
|
|
10977
11019
|
this.prevButton.addEventListener("click", () => this.prevStep());
|
|
10978
11020
|
this.nextButton.addEventListener("click", () => this.nextStep());
|
|
10979
11021
|
this.playButton.addEventListener("click", () => void this.play());
|
|
11022
|
+
this.captionButton.addEventListener("click", () => this.setCaptionVisible(!this.showCaption));
|
|
11023
|
+
this.ttsButton.addEventListener("click", () => this.setTtsEnabled(!this.getTtsEnabled()));
|
|
10980
11024
|
this.viewport.addEventListener("pointerdown", this.onPointerDown);
|
|
10981
11025
|
this.viewport.addEventListener("pointermove", this.onPointerMove);
|
|
10982
11026
|
this.viewport.addEventListener("pointerup", this.onStopPanning);
|
|
@@ -10998,6 +11042,16 @@ class SketchmarkCanvas {
|
|
|
10998
11042
|
if (renderNow)
|
|
10999
11043
|
this.render();
|
|
11000
11044
|
}
|
|
11045
|
+
setCaptionVisible(visible) {
|
|
11046
|
+
this.showCaption = visible;
|
|
11047
|
+
this.applyCaptionVisibility(this.instance);
|
|
11048
|
+
this.syncToggleUi();
|
|
11049
|
+
}
|
|
11050
|
+
setTtsEnabled(enabled) {
|
|
11051
|
+
this.ttsOverride = enabled;
|
|
11052
|
+
this.applyTtsSetting(this.instance);
|
|
11053
|
+
this.syncToggleUi();
|
|
11054
|
+
}
|
|
11001
11055
|
bindEditor(editor, options = {}) {
|
|
11002
11056
|
this.editorCleanup?.();
|
|
11003
11057
|
const renderOnRun = options.renderOnRun !== false;
|
|
@@ -11042,6 +11096,8 @@ class SketchmarkCanvas {
|
|
|
11042
11096
|
onNodeClick: this.options.onNodeClick,
|
|
11043
11097
|
});
|
|
11044
11098
|
this.instance = instance;
|
|
11099
|
+
this.applyCaptionVisibility(instance);
|
|
11100
|
+
this.applyTtsSetting(instance);
|
|
11045
11101
|
this.statsLabel.textContent = `${instance.scene.nodes.length}n / ${instance.scene.edges.length}e / ${instance.scene.groups.length}g`;
|
|
11046
11102
|
if (this.renderer === "svg") {
|
|
11047
11103
|
this.animUnsub = instance.anim.on((event) => {
|
|
@@ -11174,6 +11230,37 @@ class SketchmarkCanvas {
|
|
|
11174
11230
|
this.zoom = clampedZoom;
|
|
11175
11231
|
this.applyTransform();
|
|
11176
11232
|
}
|
|
11233
|
+
applyCaptionVisibility(instance) {
|
|
11234
|
+
const caption = instance?.anim.captionElement;
|
|
11235
|
+
if (!caption)
|
|
11236
|
+
return;
|
|
11237
|
+
caption.style.display = this.showCaption ? "" : "none";
|
|
11238
|
+
caption.setAttribute("aria-hidden", this.showCaption ? "false" : "true");
|
|
11239
|
+
}
|
|
11240
|
+
applyTtsSetting(instance) {
|
|
11241
|
+
if (!instance || this.ttsOverride === null)
|
|
11242
|
+
return;
|
|
11243
|
+
instance.anim.tts = this.ttsOverride;
|
|
11244
|
+
}
|
|
11245
|
+
getTtsEnabled() {
|
|
11246
|
+
if (this.ttsOverride !== null)
|
|
11247
|
+
return this.ttsOverride;
|
|
11248
|
+
return !!this.instance?.anim.tts;
|
|
11249
|
+
}
|
|
11250
|
+
syncToggleUi() {
|
|
11251
|
+
const canToggleCaption = this.renderer === "svg" && !!this.instance;
|
|
11252
|
+
const canToggleTts = canToggleCaption &&
|
|
11253
|
+
typeof speechSynthesis !== "undefined";
|
|
11254
|
+
const ttsEnabled = this.getTtsEnabled();
|
|
11255
|
+
this.captionButton.textContent = this.showCaption ? "Caption On" : "Caption Off";
|
|
11256
|
+
this.captionButton.classList.toggle("is-active", this.showCaption);
|
|
11257
|
+
this.captionButton.setAttribute("aria-pressed", this.showCaption ? "true" : "false");
|
|
11258
|
+
this.captionButton.disabled = !canToggleCaption;
|
|
11259
|
+
this.ttsButton.textContent = ttsEnabled ? "TTS On" : "TTS Off";
|
|
11260
|
+
this.ttsButton.classList.toggle("is-active", ttsEnabled);
|
|
11261
|
+
this.ttsButton.setAttribute("aria-pressed", ttsEnabled ? "true" : "false");
|
|
11262
|
+
this.ttsButton.disabled = !canToggleTts;
|
|
11263
|
+
}
|
|
11177
11264
|
syncAnimationUi() {
|
|
11178
11265
|
const anim = this.instance?.anim;
|
|
11179
11266
|
const canAnimate = this.renderer === "svg" && !!anim && anim.total > 0;
|
|
@@ -11184,6 +11271,7 @@ class SketchmarkCanvas {
|
|
|
11184
11271
|
this.nextButton.disabled = true;
|
|
11185
11272
|
this.resetButton.disabled = true;
|
|
11186
11273
|
this.playButton.disabled = true;
|
|
11274
|
+
this.syncToggleUi();
|
|
11187
11275
|
return;
|
|
11188
11276
|
}
|
|
11189
11277
|
this.stepDisplay.textContent = anim.currentStep < 0 ? `${anim.total} steps` : `${anim.currentStep + 1} / ${anim.total}`;
|
|
@@ -11192,6 +11280,7 @@ class SketchmarkCanvas {
|
|
|
11192
11280
|
this.nextButton.disabled = !anim.canNext;
|
|
11193
11281
|
this.resetButton.disabled = false;
|
|
11194
11282
|
this.playButton.disabled = this.playInFlight || !anim.canNext;
|
|
11283
|
+
this.syncToggleUi();
|
|
11195
11284
|
}
|
|
11196
11285
|
getStepTarget(stepItem) {
|
|
11197
11286
|
if (!stepItem)
|
|
@@ -11848,6 +11937,7 @@ const EMBED_CSS = `
|
|
|
11848
11937
|
|
|
11849
11938
|
.skm-embed__controls {
|
|
11850
11939
|
display: flex;
|
|
11940
|
+
flex-wrap: wrap;
|
|
11851
11941
|
align-items: center;
|
|
11852
11942
|
gap: 8px;
|
|
11853
11943
|
padding: 10px 12px;
|
|
@@ -11865,6 +11955,13 @@ const EMBED_CSS = `
|
|
|
11865
11955
|
display: none;
|
|
11866
11956
|
}
|
|
11867
11957
|
|
|
11958
|
+
.skm-embed__controls-group {
|
|
11959
|
+
display: flex;
|
|
11960
|
+
align-items: center;
|
|
11961
|
+
gap: 8px;
|
|
11962
|
+
flex-wrap: wrap;
|
|
11963
|
+
}
|
|
11964
|
+
|
|
11868
11965
|
.skm-embed__button {
|
|
11869
11966
|
border: 1px solid #caba98;
|
|
11870
11967
|
background: #f5eedd;
|
|
@@ -11883,6 +11980,12 @@ const EMBED_CSS = `
|
|
|
11883
11980
|
color: #fff;
|
|
11884
11981
|
}
|
|
11885
11982
|
|
|
11983
|
+
.skm-embed__button.is-active {
|
|
11984
|
+
background: #c8a060;
|
|
11985
|
+
border-color: #c8a060;
|
|
11986
|
+
color: #fff;
|
|
11987
|
+
}
|
|
11988
|
+
|
|
11886
11989
|
.skm-embed--dark .skm-embed__button {
|
|
11887
11990
|
border-color: #4a3520;
|
|
11888
11991
|
background: #22190e;
|
|
@@ -11900,6 +12003,17 @@ const EMBED_CSS = `
|
|
|
11900
12003
|
cursor: default;
|
|
11901
12004
|
}
|
|
11902
12005
|
|
|
12006
|
+
.skm-embed__zoom {
|
|
12007
|
+
min-width: 48px;
|
|
12008
|
+
text-align: center;
|
|
12009
|
+
color: #8a6040;
|
|
12010
|
+
font-size: 11px;
|
|
12011
|
+
}
|
|
12012
|
+
|
|
12013
|
+
.skm-embed--dark .skm-embed__zoom {
|
|
12014
|
+
color: #d0b176;
|
|
12015
|
+
}
|
|
12016
|
+
|
|
11903
12017
|
.skm-embed__step {
|
|
11904
12018
|
margin-left: auto;
|
|
11905
12019
|
min-width: 96px;
|
|
@@ -11918,13 +12032,19 @@ class SketchmarkEmbed {
|
|
|
11918
12032
|
this.emitter = new EventEmitter();
|
|
11919
12033
|
this.animUnsub = null;
|
|
11920
12034
|
this.playInFlight = false;
|
|
12035
|
+
this.showCaption = true;
|
|
12036
|
+
this.ttsOverride = null;
|
|
12037
|
+
this.zoom = 1;
|
|
11921
12038
|
this.offsetX = 0;
|
|
11922
12039
|
this.offsetY = 0;
|
|
12040
|
+
this.autoFitEnabled = true;
|
|
11923
12041
|
this.motionFrame = null;
|
|
11924
12042
|
this.resizeObserver = null;
|
|
11925
12043
|
this.options = options;
|
|
11926
12044
|
this.dsl = normalizeNewlines(options.dsl);
|
|
11927
12045
|
this.theme = options.theme ?? "light";
|
|
12046
|
+
this.showCaption = options.showCaption !== false;
|
|
12047
|
+
this.ttsOverride = typeof options.tts === "boolean" ? options.tts : null;
|
|
11928
12048
|
injectStyleOnce(EMBED_STYLE_ID, EMBED_CSS);
|
|
11929
12049
|
const host = resolveContainer(options.container);
|
|
11930
12050
|
host.innerHTML = "";
|
|
@@ -11940,10 +12060,22 @@ class SketchmarkEmbed {
|
|
|
11940
12060
|
</div>
|
|
11941
12061
|
<div class="skm-embed__error"></div>
|
|
11942
12062
|
<div class="skm-embed__controls">
|
|
11943
|
-
<
|
|
11944
|
-
|
|
11945
|
-
|
|
11946
|
-
|
|
12063
|
+
<div class="skm-embed__controls-group">
|
|
12064
|
+
<button type="button" class="skm-embed__button" data-action="zoom-out">-</button>
|
|
12065
|
+
<span class="skm-embed__zoom">100%</span>
|
|
12066
|
+
<button type="button" class="skm-embed__button" data-action="zoom-in">+</button>
|
|
12067
|
+
<button type="button" class="skm-embed__button" data-action="fit">Reset</button>
|
|
12068
|
+
</div>
|
|
12069
|
+
<div class="skm-embed__controls-group">
|
|
12070
|
+
<button type="button" class="skm-embed__button" data-action="restart">Restart</button>
|
|
12071
|
+
<button type="button" class="skm-embed__button" data-action="prev">Prev</button>
|
|
12072
|
+
<button type="button" class="skm-embed__button" data-action="next">Next</button>
|
|
12073
|
+
<button type="button" class="skm-embed__button" data-action="play">Play</button>
|
|
12074
|
+
</div>
|
|
12075
|
+
<div class="skm-embed__controls-group">
|
|
12076
|
+
<button type="button" class="skm-embed__button" data-action="toggle-caption">Caption On</button>
|
|
12077
|
+
<button type="button" class="skm-embed__button" data-action="toggle-tts">TTS Off</button>
|
|
12078
|
+
</div>
|
|
11947
12079
|
<span class="skm-embed__step">No steps</span>
|
|
11948
12080
|
</div>
|
|
11949
12081
|
`;
|
|
@@ -11953,17 +12085,28 @@ class SketchmarkEmbed {
|
|
|
11953
12085
|
this.errorElement = this.root.querySelector(".skm-embed__error");
|
|
11954
12086
|
this.controlsElement = this.root.querySelector(".skm-embed__controls");
|
|
11955
12087
|
this.stepInfoElement = this.root.querySelector(".skm-embed__step");
|
|
11956
|
-
this.
|
|
12088
|
+
this.zoomInfoElement = this.root.querySelector(".skm-embed__zoom");
|
|
12089
|
+
this.btnFit = this.root.querySelector('[data-action="fit"]');
|
|
12090
|
+
this.btnZoomIn = this.root.querySelector('[data-action="zoom-in"]');
|
|
12091
|
+
this.btnZoomOut = this.root.querySelector('[data-action="zoom-out"]');
|
|
12092
|
+
this.btnRestart = this.root.querySelector('[data-action="restart"]');
|
|
11957
12093
|
this.btnPrev = this.root.querySelector('[data-action="prev"]');
|
|
11958
12094
|
this.btnNext = this.root.querySelector('[data-action="next"]');
|
|
11959
12095
|
this.btnPlay = this.root.querySelector('[data-action="play"]');
|
|
12096
|
+
this.btnCaption = this.root.querySelector('[data-action="toggle-caption"]');
|
|
12097
|
+
this.btnTts = this.root.querySelector('[data-action="toggle-tts"]');
|
|
11960
12098
|
this.controlsElement.classList.toggle("is-hidden", options.showControls === false);
|
|
11961
|
-
this.
|
|
12099
|
+
this.btnFit.addEventListener("click", () => this.resetView());
|
|
12100
|
+
this.btnZoomIn.addEventListener("click", () => this.zoomIn());
|
|
12101
|
+
this.btnZoomOut.addEventListener("click", () => this.zoomOut());
|
|
12102
|
+
this.btnRestart.addEventListener("click", () => this.resetAnimation());
|
|
11962
12103
|
this.btnPrev.addEventListener("click", () => this.prevStep());
|
|
11963
12104
|
this.btnNext.addEventListener("click", () => this.nextStep());
|
|
11964
12105
|
this.btnPlay.addEventListener("click", () => {
|
|
11965
12106
|
void this.play();
|
|
11966
12107
|
});
|
|
12108
|
+
this.btnCaption.addEventListener("click", () => this.setCaptionVisible(!this.showCaption));
|
|
12109
|
+
this.btnTts.addEventListener("click", () => this.setTtsEnabled(!this.getTtsEnabled()));
|
|
11967
12110
|
if (typeof ResizeObserver !== "undefined") {
|
|
11968
12111
|
this.resizeObserver = new ResizeObserver(() => {
|
|
11969
12112
|
this.positionViewport(false);
|
|
@@ -11981,6 +12124,16 @@ class SketchmarkEmbed {
|
|
|
11981
12124
|
if (renderNow)
|
|
11982
12125
|
this.render();
|
|
11983
12126
|
}
|
|
12127
|
+
setCaptionVisible(visible) {
|
|
12128
|
+
this.showCaption = visible;
|
|
12129
|
+
this.applyCaptionVisibility(this.instance);
|
|
12130
|
+
this.syncToggleControls();
|
|
12131
|
+
}
|
|
12132
|
+
setTtsEnabled(enabled) {
|
|
12133
|
+
this.ttsOverride = enabled;
|
|
12134
|
+
this.applyTtsSetting(this.instance);
|
|
12135
|
+
this.syncToggleControls();
|
|
12136
|
+
}
|
|
11984
12137
|
setSize(width, height) {
|
|
11985
12138
|
this.applySize(width, height);
|
|
11986
12139
|
this.positionViewport(false);
|
|
@@ -12004,7 +12157,12 @@ class SketchmarkEmbed {
|
|
|
12004
12157
|
this.animUnsub = null;
|
|
12005
12158
|
this.instance?.anim?.destroy();
|
|
12006
12159
|
this.instance = null;
|
|
12160
|
+
this.autoFitEnabled = true;
|
|
12161
|
+
this.zoom = 1;
|
|
12162
|
+
this.offsetX = 0;
|
|
12163
|
+
this.offsetY = 0;
|
|
12007
12164
|
this.diagramWrap.innerHTML = "";
|
|
12165
|
+
this.applyTransform();
|
|
12008
12166
|
try {
|
|
12009
12167
|
const instance = render({
|
|
12010
12168
|
container: this.diagramWrap,
|
|
@@ -12020,6 +12178,8 @@ class SketchmarkEmbed {
|
|
|
12020
12178
|
onNodeClick: this.options.onNodeClick,
|
|
12021
12179
|
});
|
|
12022
12180
|
this.instance = instance;
|
|
12181
|
+
this.applyCaptionVisibility(instance);
|
|
12182
|
+
this.applyTtsSetting(instance);
|
|
12023
12183
|
this.animUnsub = instance.anim.on((event) => {
|
|
12024
12184
|
this.syncControls();
|
|
12025
12185
|
if (event.type === "step-change") {
|
|
@@ -12085,6 +12245,21 @@ class SketchmarkEmbed {
|
|
|
12085
12245
|
this.syncControls();
|
|
12086
12246
|
this.positionViewport(true);
|
|
12087
12247
|
}
|
|
12248
|
+
fitToViewport(animated = false) {
|
|
12249
|
+
if (!this.instance?.svg)
|
|
12250
|
+
return;
|
|
12251
|
+
this.autoFitEnabled = true;
|
|
12252
|
+
this.positionViewport(animated);
|
|
12253
|
+
}
|
|
12254
|
+
resetView(animated = false) {
|
|
12255
|
+
this.fitToViewport(animated);
|
|
12256
|
+
}
|
|
12257
|
+
zoomIn() {
|
|
12258
|
+
this.zoomAroundViewportCenter(1.2);
|
|
12259
|
+
}
|
|
12260
|
+
zoomOut() {
|
|
12261
|
+
this.zoomAroundViewportCenter(0.8);
|
|
12262
|
+
}
|
|
12088
12263
|
exportSVG(filename) {
|
|
12089
12264
|
this.instance?.exportSVG(filename);
|
|
12090
12265
|
}
|
|
@@ -12107,10 +12282,15 @@ class SketchmarkEmbed {
|
|
|
12107
12282
|
return typeof value === "number" ? `${value}px` : value;
|
|
12108
12283
|
}
|
|
12109
12284
|
syncControls() {
|
|
12285
|
+
this.syncAnimationControls();
|
|
12286
|
+
this.syncViewControls();
|
|
12287
|
+
this.syncToggleControls();
|
|
12288
|
+
}
|
|
12289
|
+
syncAnimationControls() {
|
|
12110
12290
|
const anim = this.instance?.anim;
|
|
12111
12291
|
if (!anim || !anim.total) {
|
|
12112
12292
|
this.stepInfoElement.textContent = "No steps";
|
|
12113
|
-
this.
|
|
12293
|
+
this.btnRestart.disabled = true;
|
|
12114
12294
|
this.btnPrev.disabled = true;
|
|
12115
12295
|
this.btnNext.disabled = true;
|
|
12116
12296
|
this.btnPlay.disabled = true;
|
|
@@ -12118,56 +12298,69 @@ class SketchmarkEmbed {
|
|
|
12118
12298
|
}
|
|
12119
12299
|
this.stepInfoElement.textContent =
|
|
12120
12300
|
anim.currentStep < 0 ? `${anim.total} steps` : `${anim.currentStep + 1} / ${anim.total}`;
|
|
12121
|
-
this.
|
|
12301
|
+
this.btnRestart.disabled = false;
|
|
12122
12302
|
this.btnPrev.disabled = !anim.canPrev;
|
|
12123
12303
|
this.btnNext.disabled = !anim.canNext;
|
|
12124
12304
|
this.btnPlay.disabled = this.playInFlight || !anim.canNext;
|
|
12125
12305
|
}
|
|
12306
|
+
syncViewControls() {
|
|
12307
|
+
const hasView = !!this.instance?.svg;
|
|
12308
|
+
const zoomMin = this.getZoomMin();
|
|
12309
|
+
const zoomMax = this.getZoomMax();
|
|
12310
|
+
this.zoomInfoElement.textContent = `${Math.round(this.zoom * 100)}%`;
|
|
12311
|
+
this.btnFit.disabled = !hasView;
|
|
12312
|
+
this.btnZoomOut.disabled = !hasView || this.zoom <= zoomMin + 0.001;
|
|
12313
|
+
this.btnZoomIn.disabled = !hasView || this.zoom >= zoomMax - 0.001;
|
|
12314
|
+
}
|
|
12126
12315
|
positionViewport(animated) {
|
|
12127
|
-
|
|
12128
|
-
|
|
12129
|
-
const svg = this.instance.svg;
|
|
12130
|
-
const svgWidth = parseFloat(svg.getAttribute("width") || "0");
|
|
12131
|
-
const svgHeight = parseFloat(svg.getAttribute("height") || "0");
|
|
12132
|
-
if (!svgWidth || !svgHeight)
|
|
12316
|
+
const size = this.getContentSize();
|
|
12317
|
+
if (!size)
|
|
12133
12318
|
return;
|
|
12319
|
+
const { width: svgWidth, height: svgHeight } = size;
|
|
12134
12320
|
const viewportRect = this.viewport.getBoundingClientRect();
|
|
12135
12321
|
const viewWidth = viewportRect.width || this.viewport.clientWidth;
|
|
12136
12322
|
const viewHeight = viewportRect.height || this.viewport.clientHeight;
|
|
12137
12323
|
if (!viewWidth || !viewHeight)
|
|
12138
12324
|
return;
|
|
12139
|
-
|
|
12325
|
+
if (this.autoFitEnabled) {
|
|
12326
|
+
this.zoom = this.getFitZoom(svgWidth, svgHeight, viewWidth, viewHeight);
|
|
12327
|
+
}
|
|
12328
|
+
this.syncViewControls();
|
|
12329
|
+
const scaledWidth = svgWidth * this.zoom;
|
|
12330
|
+
const scaledHeight = svgHeight * this.zoom;
|
|
12331
|
+
const focusTarget = this.getFocusTarget();
|
|
12332
|
+
const sceneIsLarge = scaledWidth > viewWidth || scaledHeight > viewHeight;
|
|
12140
12333
|
const shouldFocus = sceneIsLarge &&
|
|
12141
12334
|
this.options.autoFocus !== false &&
|
|
12142
|
-
!!
|
|
12335
|
+
!!focusTarget;
|
|
12143
12336
|
if (!shouldFocus) {
|
|
12144
|
-
this.animateTo(
|
|
12337
|
+
this.animateTo(scaledWidth <= viewWidth ? (viewWidth - scaledWidth) / 2 : 0, scaledHeight <= viewHeight ? (viewHeight - scaledHeight) / 2 : 0, animated);
|
|
12145
12338
|
return;
|
|
12146
12339
|
}
|
|
12147
|
-
const target = this.findTargetElement(
|
|
12340
|
+
const target = this.findTargetElement(focusTarget);
|
|
12148
12341
|
if (!target) {
|
|
12149
12342
|
this.animateTo(0, 0, animated);
|
|
12150
12343
|
return;
|
|
12151
12344
|
}
|
|
12152
|
-
const
|
|
12153
|
-
|
|
12154
|
-
|
|
12155
|
-
|
|
12156
|
-
|
|
12157
|
-
let nextX = viewWidth / 2 -
|
|
12158
|
-
let nextY = viewHeight / 2 -
|
|
12345
|
+
const targetBox = this.getTargetBox(target, viewportRect);
|
|
12346
|
+
if (!targetBox) {
|
|
12347
|
+
this.animateTo(0, 0, animated);
|
|
12348
|
+
return;
|
|
12349
|
+
}
|
|
12350
|
+
let nextX = viewWidth / 2 - targetBox.centerX;
|
|
12351
|
+
let nextY = viewHeight / 2 - targetBox.centerY;
|
|
12159
12352
|
const padding = this.options.focusPadding ?? 24;
|
|
12160
|
-
if (
|
|
12161
|
-
nextX = (viewWidth -
|
|
12353
|
+
if (scaledWidth <= viewWidth) {
|
|
12354
|
+
nextX = (viewWidth - scaledWidth) / 2;
|
|
12162
12355
|
}
|
|
12163
12356
|
else {
|
|
12164
|
-
nextX = clamp(nextX, viewWidth -
|
|
12357
|
+
nextX = clamp(nextX, viewWidth - scaledWidth - padding, padding);
|
|
12165
12358
|
}
|
|
12166
|
-
if (
|
|
12167
|
-
nextY = (viewHeight -
|
|
12359
|
+
if (scaledHeight <= viewHeight) {
|
|
12360
|
+
nextY = (viewHeight - scaledHeight) / 2;
|
|
12168
12361
|
}
|
|
12169
12362
|
else {
|
|
12170
|
-
nextY = clamp(nextY, viewHeight -
|
|
12363
|
+
nextY = clamp(nextY, viewHeight - scaledHeight - padding, padding);
|
|
12171
12364
|
}
|
|
12172
12365
|
this.animateTo(nextX, nextY, animated);
|
|
12173
12366
|
}
|
|
@@ -12199,7 +12392,109 @@ class SketchmarkEmbed {
|
|
|
12199
12392
|
this.motionFrame = requestAnimationFrame(frame);
|
|
12200
12393
|
}
|
|
12201
12394
|
applyTransform() {
|
|
12202
|
-
this.world.style.transform = `translate(${this.offsetX}px, ${this.offsetY}px)`;
|
|
12395
|
+
this.world.style.transform = `translate(${this.offsetX}px, ${this.offsetY}px) scale(${this.zoom})`;
|
|
12396
|
+
this.zoomInfoElement.textContent = `${Math.round(this.zoom * 100)}%`;
|
|
12397
|
+
}
|
|
12398
|
+
getContentSize() {
|
|
12399
|
+
if (!this.instance?.svg)
|
|
12400
|
+
return null;
|
|
12401
|
+
const svg = this.instance.svg;
|
|
12402
|
+
const width = parseFloat(svg.getAttribute("width") || "0");
|
|
12403
|
+
const height = parseFloat(svg.getAttribute("height") || "0");
|
|
12404
|
+
if (!width || !height)
|
|
12405
|
+
return null;
|
|
12406
|
+
return { width, height };
|
|
12407
|
+
}
|
|
12408
|
+
getFitZoom(svgWidth, svgHeight, viewWidth, viewHeight) {
|
|
12409
|
+
const padding = this.getFitPadding(viewWidth, viewHeight);
|
|
12410
|
+
const availableWidth = Math.max(viewWidth - padding * 2, 1);
|
|
12411
|
+
const availableHeight = Math.max(viewHeight - padding * 2, 1);
|
|
12412
|
+
const nextZoom = Math.min(availableWidth / svgWidth, availableHeight / svgHeight, 1);
|
|
12413
|
+
return clamp(nextZoom || 1, this.getZoomMin(), this.getZoomMax());
|
|
12414
|
+
}
|
|
12415
|
+
getFitPadding(viewWidth, viewHeight) {
|
|
12416
|
+
if (typeof this.options.fitPadding === "number") {
|
|
12417
|
+
return Math.max(0, this.options.fitPadding);
|
|
12418
|
+
}
|
|
12419
|
+
return Math.max(16, Math.min(40, Math.round(Math.min(viewWidth, viewHeight) * 0.08)));
|
|
12420
|
+
}
|
|
12421
|
+
getZoomMin() {
|
|
12422
|
+
return this.options.zoomMin ?? 0.08;
|
|
12423
|
+
}
|
|
12424
|
+
getZoomMax() {
|
|
12425
|
+
return this.options.zoomMax ?? 4;
|
|
12426
|
+
}
|
|
12427
|
+
zoomAroundViewportCenter(factor) {
|
|
12428
|
+
if (!this.instance?.svg)
|
|
12429
|
+
return;
|
|
12430
|
+
const pivotX = this.viewport.clientWidth / 2;
|
|
12431
|
+
const pivotY = this.viewport.clientHeight / 2;
|
|
12432
|
+
this.zoomTo(this.zoom * factor, pivotX, pivotY);
|
|
12433
|
+
}
|
|
12434
|
+
zoomTo(nextZoom, pivotX, pivotY) {
|
|
12435
|
+
const clampedZoom = clamp(nextZoom, this.getZoomMin(), this.getZoomMax());
|
|
12436
|
+
const ratio = clampedZoom / this.zoom;
|
|
12437
|
+
if (!Number.isFinite(ratio) || ratio === 1) {
|
|
12438
|
+
this.syncViewControls();
|
|
12439
|
+
return;
|
|
12440
|
+
}
|
|
12441
|
+
this.stopMotion();
|
|
12442
|
+
this.autoFitEnabled = false;
|
|
12443
|
+
this.offsetX = pivotX - (pivotX - this.offsetX) * ratio;
|
|
12444
|
+
this.offsetY = pivotY - (pivotY - this.offsetY) * ratio;
|
|
12445
|
+
this.zoom = clampedZoom;
|
|
12446
|
+
this.applyTransform();
|
|
12447
|
+
this.syncViewControls();
|
|
12448
|
+
}
|
|
12449
|
+
applyCaptionVisibility(instance) {
|
|
12450
|
+
const caption = instance?.anim.captionElement;
|
|
12451
|
+
if (!caption)
|
|
12452
|
+
return;
|
|
12453
|
+
caption.style.display = this.showCaption ? "" : "none";
|
|
12454
|
+
caption.setAttribute("aria-hidden", this.showCaption ? "false" : "true");
|
|
12455
|
+
}
|
|
12456
|
+
applyTtsSetting(instance) {
|
|
12457
|
+
if (!instance || this.ttsOverride === null)
|
|
12458
|
+
return;
|
|
12459
|
+
instance.anim.tts = this.ttsOverride;
|
|
12460
|
+
}
|
|
12461
|
+
getTtsEnabled() {
|
|
12462
|
+
if (this.ttsOverride !== null)
|
|
12463
|
+
return this.ttsOverride;
|
|
12464
|
+
return !!this.instance?.anim.tts;
|
|
12465
|
+
}
|
|
12466
|
+
syncToggleControls() {
|
|
12467
|
+
const hasView = !!this.instance?.svg;
|
|
12468
|
+
const canToggleTts = hasView &&
|
|
12469
|
+
typeof speechSynthesis !== "undefined";
|
|
12470
|
+
const ttsEnabled = this.getTtsEnabled();
|
|
12471
|
+
this.btnCaption.textContent = this.showCaption ? "Caption On" : "Caption Off";
|
|
12472
|
+
this.btnCaption.classList.toggle("is-active", this.showCaption);
|
|
12473
|
+
this.btnCaption.setAttribute("aria-pressed", this.showCaption ? "true" : "false");
|
|
12474
|
+
this.btnCaption.disabled = !hasView;
|
|
12475
|
+
this.btnTts.textContent = ttsEnabled ? "TTS On" : "TTS Off";
|
|
12476
|
+
this.btnTts.classList.toggle("is-active", ttsEnabled);
|
|
12477
|
+
this.btnTts.setAttribute("aria-pressed", ttsEnabled ? "true" : "false");
|
|
12478
|
+
this.btnTts.disabled = !canToggleTts;
|
|
12479
|
+
}
|
|
12480
|
+
getTargetBox(target, viewportRect) {
|
|
12481
|
+
if (target instanceof SVGGraphicsElement) {
|
|
12482
|
+
try {
|
|
12483
|
+
const bounds = target.getBBox();
|
|
12484
|
+
return {
|
|
12485
|
+
centerX: (bounds.x + bounds.width / 2) * this.zoom,
|
|
12486
|
+
centerY: (bounds.y + bounds.height / 2) * this.zoom,
|
|
12487
|
+
};
|
|
12488
|
+
}
|
|
12489
|
+
catch {
|
|
12490
|
+
// Ignore and fall back to layout-based bounds below.
|
|
12491
|
+
}
|
|
12492
|
+
}
|
|
12493
|
+
const currentRect = target.getBoundingClientRect();
|
|
12494
|
+
return {
|
|
12495
|
+
centerX: currentRect.left - viewportRect.left - this.offsetX + currentRect.width / 2,
|
|
12496
|
+
centerY: currentRect.top - viewportRect.top - this.offsetY + currentRect.height / 2,
|
|
12497
|
+
};
|
|
12203
12498
|
}
|
|
12204
12499
|
getFocusTarget() {
|
|
12205
12500
|
const anim = this.instance?.anim;
|