sketchmark 1.1.3 → 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);
@@ -10047,7 +10068,7 @@ class AnimationController {
10047
10068
  if (!nodeGuidePathEl(nodeEl) && !nodeEl.querySelector("path")?.style.strokeDasharray) {
10048
10069
  prepareNodeForDraw(nodeEl);
10049
10070
  }
10050
- animateNodeDraw(nodeEl, step.duration ?? ANIMATION.nodeStrokeDur);
10071
+ animateNodeDraw(nodeEl, step.duration ?? ANIMATION.nodeStrokeDur, step.duration ?? ANIMATION.textRevealMs);
10051
10072
  }
10052
10073
  }
10053
10074
  // ── erase ─────────────────────────────────────────────────
@@ -11848,6 +11869,7 @@ const EMBED_CSS = `
11848
11869
 
11849
11870
  .skm-embed__controls {
11850
11871
  display: flex;
11872
+ flex-wrap: wrap;
11851
11873
  align-items: center;
11852
11874
  gap: 8px;
11853
11875
  padding: 10px 12px;
@@ -11865,6 +11887,13 @@ const EMBED_CSS = `
11865
11887
  display: none;
11866
11888
  }
11867
11889
 
11890
+ .skm-embed__controls-group {
11891
+ display: flex;
11892
+ align-items: center;
11893
+ gap: 8px;
11894
+ flex-wrap: wrap;
11895
+ }
11896
+
11868
11897
  .skm-embed__button {
11869
11898
  border: 1px solid #caba98;
11870
11899
  background: #f5eedd;
@@ -11900,6 +11929,17 @@ const EMBED_CSS = `
11900
11929
  cursor: default;
11901
11930
  }
11902
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
+
11903
11943
  .skm-embed__step {
11904
11944
  margin-left: auto;
11905
11945
  min-width: 96px;
@@ -11918,8 +11958,10 @@ class SketchmarkEmbed {
11918
11958
  this.emitter = new EventEmitter();
11919
11959
  this.animUnsub = null;
11920
11960
  this.playInFlight = false;
11961
+ this.zoom = 1;
11921
11962
  this.offsetX = 0;
11922
11963
  this.offsetY = 0;
11964
+ this.autoFitEnabled = true;
11923
11965
  this.motionFrame = null;
11924
11966
  this.resizeObserver = null;
11925
11967
  this.options = options;
@@ -11940,10 +11982,18 @@ class SketchmarkEmbed {
11940
11982
  </div>
11941
11983
  <div class="skm-embed__error"></div>
11942
11984
  <div class="skm-embed__controls">
11943
- <button type="button" class="skm-embed__button" data-action="reset">Reset</button>
11944
- <button type="button" class="skm-embed__button" data-action="prev">Prev</button>
11945
- <button type="button" class="skm-embed__button" data-action="next">Next</button>
11946
- <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>
11947
11997
  <span class="skm-embed__step">No steps</span>
11948
11998
  </div>
11949
11999
  `;
@@ -11953,12 +12003,19 @@ class SketchmarkEmbed {
11953
12003
  this.errorElement = this.root.querySelector(".skm-embed__error");
11954
12004
  this.controlsElement = this.root.querySelector(".skm-embed__controls");
11955
12005
  this.stepInfoElement = this.root.querySelector(".skm-embed__step");
11956
- 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"]');
11957
12011
  this.btnPrev = this.root.querySelector('[data-action="prev"]');
11958
12012
  this.btnNext = this.root.querySelector('[data-action="next"]');
11959
12013
  this.btnPlay = this.root.querySelector('[data-action="play"]');
11960
12014
  this.controlsElement.classList.toggle("is-hidden", options.showControls === false);
11961
- 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());
11962
12019
  this.btnPrev.addEventListener("click", () => this.prevStep());
11963
12020
  this.btnNext.addEventListener("click", () => this.nextStep());
11964
12021
  this.btnPlay.addEventListener("click", () => {
@@ -12004,7 +12061,12 @@ class SketchmarkEmbed {
12004
12061
  this.animUnsub = null;
12005
12062
  this.instance?.anim?.destroy();
12006
12063
  this.instance = null;
12064
+ this.autoFitEnabled = true;
12065
+ this.zoom = 1;
12066
+ this.offsetX = 0;
12067
+ this.offsetY = 0;
12007
12068
  this.diagramWrap.innerHTML = "";
12069
+ this.applyTransform();
12008
12070
  try {
12009
12071
  const instance = render({
12010
12072
  container: this.diagramWrap,
@@ -12085,6 +12147,21 @@ class SketchmarkEmbed {
12085
12147
  this.syncControls();
12086
12148
  this.positionViewport(true);
12087
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
+ }
12088
12165
  exportSVG(filename) {
12089
12166
  this.instance?.exportSVG(filename);
12090
12167
  }
@@ -12107,10 +12184,14 @@ class SketchmarkEmbed {
12107
12184
  return typeof value === "number" ? `${value}px` : value;
12108
12185
  }
12109
12186
  syncControls() {
12187
+ this.syncAnimationControls();
12188
+ this.syncViewControls();
12189
+ }
12190
+ syncAnimationControls() {
12110
12191
  const anim = this.instance?.anim;
12111
12192
  if (!anim || !anim.total) {
12112
12193
  this.stepInfoElement.textContent = "No steps";
12113
- this.btnReset.disabled = true;
12194
+ this.btnRestart.disabled = true;
12114
12195
  this.btnPrev.disabled = true;
12115
12196
  this.btnNext.disabled = true;
12116
12197
  this.btnPlay.disabled = true;
@@ -12118,56 +12199,69 @@ class SketchmarkEmbed {
12118
12199
  }
12119
12200
  this.stepInfoElement.textContent =
12120
12201
  anim.currentStep < 0 ? `${anim.total} steps` : `${anim.currentStep + 1} / ${anim.total}`;
12121
- this.btnReset.disabled = false;
12202
+ this.btnRestart.disabled = false;
12122
12203
  this.btnPrev.disabled = !anim.canPrev;
12123
12204
  this.btnNext.disabled = !anim.canNext;
12124
12205
  this.btnPlay.disabled = this.playInFlight || !anim.canNext;
12125
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
+ }
12126
12216
  positionViewport(animated) {
12127
- if (!this.instance?.svg)
12128
- return;
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)
12217
+ const size = this.getContentSize();
12218
+ if (!size)
12133
12219
  return;
12220
+ const { width: svgWidth, height: svgHeight } = size;
12134
12221
  const viewportRect = this.viewport.getBoundingClientRect();
12135
12222
  const viewWidth = viewportRect.width || this.viewport.clientWidth;
12136
12223
  const viewHeight = viewportRect.height || this.viewport.clientHeight;
12137
12224
  if (!viewWidth || !viewHeight)
12138
12225
  return;
12139
- 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;
12140
12234
  const shouldFocus = sceneIsLarge &&
12141
12235
  this.options.autoFocus !== false &&
12142
- !!this.getFocusTarget();
12236
+ !!focusTarget;
12143
12237
  if (!shouldFocus) {
12144
- 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);
12145
12239
  return;
12146
12240
  }
12147
- const target = this.findTargetElement(this.getFocusTarget());
12241
+ const target = this.findTargetElement(focusTarget);
12148
12242
  if (!target) {
12149
12243
  this.animateTo(0, 0, animated);
12150
12244
  return;
12151
12245
  }
12152
- const currentRect = target.getBoundingClientRect();
12153
- const sceneX = currentRect.left - viewportRect.left - this.offsetX;
12154
- const sceneY = currentRect.top - viewportRect.top - this.offsetY;
12155
- const targetCenterX = sceneX + currentRect.width / 2;
12156
- const targetCenterY = sceneY + currentRect.height / 2;
12157
- let nextX = viewWidth / 2 - targetCenterX;
12158
- 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;
12159
12253
  const padding = this.options.focusPadding ?? 24;
12160
- if (svgWidth <= viewWidth) {
12161
- nextX = (viewWidth - svgWidth) / 2;
12254
+ if (scaledWidth <= viewWidth) {
12255
+ nextX = (viewWidth - scaledWidth) / 2;
12162
12256
  }
12163
12257
  else {
12164
- nextX = clamp(nextX, viewWidth - svgWidth - padding, padding);
12258
+ nextX = clamp(nextX, viewWidth - scaledWidth - padding, padding);
12165
12259
  }
12166
- if (svgHeight <= viewHeight) {
12167
- nextY = (viewHeight - svgHeight) / 2;
12260
+ if (scaledHeight <= viewHeight) {
12261
+ nextY = (viewHeight - scaledHeight) / 2;
12168
12262
  }
12169
12263
  else {
12170
- nextY = clamp(nextY, viewHeight - svgHeight - padding, padding);
12264
+ nextY = clamp(nextY, viewHeight - scaledHeight - padding, padding);
12171
12265
  }
12172
12266
  this.animateTo(nextX, nextY, animated);
12173
12267
  }
@@ -12199,7 +12293,78 @@ class SketchmarkEmbed {
12199
12293
  this.motionFrame = requestAnimationFrame(frame);
12200
12294
  }
12201
12295
  applyTransform() {
12202
- 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
+ };
12203
12368
  }
12204
12369
  getFocusTarget() {
12205
12370
  const anim = this.instance?.anim;