sketchmark 1.3.5 → 1.3.7

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.
package/dist/index.js CHANGED
@@ -8278,6 +8278,36 @@ function setParentGroupData(el, groupId) {
8278
8278
  if (groupId)
8279
8279
  el.dataset.parentGroup = groupId;
8280
8280
  }
8281
+ function resolveEdgeEndpointKind(id, nm, tm, gm, cm) {
8282
+ if (nm.has(id))
8283
+ return "node";
8284
+ if (gm.has(id))
8285
+ return "group";
8286
+ if (tm.has(id))
8287
+ return "table";
8288
+ if (cm.has(id))
8289
+ return "chart";
8290
+ return null;
8291
+ }
8292
+ function collectEdgeGroupLineage(endpointId, endpointKind, parentGroups) {
8293
+ const lineage = [];
8294
+ let groupId = endpointKind === "group"
8295
+ ? endpointId
8296
+ : parentGroups.get(`${endpointKind}:${endpointId}`);
8297
+ while (groupId) {
8298
+ lineage.push(groupId);
8299
+ groupId = parentGroups.get(`group:${groupId}`);
8300
+ }
8301
+ return lineage;
8302
+ }
8303
+ function resolveEdgeParentGroupId(fromId, toId, nm, tm, gm, cm, parentGroups) {
8304
+ const fromKind = resolveEdgeEndpointKind(fromId, nm, tm, gm, cm);
8305
+ const toKind = resolveEdgeEndpointKind(toId, nm, tm, gm, cm);
8306
+ if (!fromKind || !toKind)
8307
+ return undefined;
8308
+ const toLineage = new Set(collectEdgeGroupLineage(toId, toKind, parentGroups));
8309
+ return collectEdgeGroupLineage(fromId, fromKind, parentGroups).find((groupId) => toLineage.has(groupId));
8310
+ }
8281
8311
  // ── Node shapes ───────────────────────────────────────────────────────────
8282
8312
  function renderShape$1(rc, n, palette) {
8283
8313
  const s = n.style ?? {};
@@ -8435,6 +8465,7 @@ function renderToSVG(sg, container, options = {}) {
8435
8465
  const [x1, y1] = getConnPoint(src, dstCX, dstCY, e.fromAnchor);
8436
8466
  const [x2, y2] = getConnPoint(dst, srcCX, srcCY, e.toAnchor);
8437
8467
  const eg = mkGroup(`edge-${e.from}-${e.to}`, "eg");
8468
+ setParentGroupData(eg, resolveEdgeParentGroupId(e.from, e.to, nm, tm, gmMap, cm, parentGroups));
8438
8469
  if (e.style?.opacity != null)
8439
8470
  eg.setAttribute("opacity", String(e.style.opacity));
8440
8471
  const len = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2) || 1;
@@ -9481,7 +9512,7 @@ const getTableEl = (svg, id) => getEl(svg, `table-${id}`);
9481
9512
  const getNoteEl = (svg, id) => getEl(svg, `note-${id}`);
9482
9513
  const getChartEl = (svg, id) => getEl(svg, `chart-${id}`);
9483
9514
  const getMarkdownEl = (svg, id) => getEl(svg, `markdown-${id}`);
9484
- const POSITIONABLE_SELECTOR = ".ng, .gg, .tg, .ntg, .cg, .mdg";
9515
+ const POSITIONABLE_SELECTOR = ".ng, .gg, .tg, .ntg, .cg, .eg, .mdg";
9485
9516
  function resolveNonEdgeDrawEl(svg, target) {
9486
9517
  return (getGroupEl(svg, target) ??
9487
9518
  getTableEl(svg, target) ??
@@ -9989,8 +10020,12 @@ class AnimationController {
9989
10020
  this._rc = _rc;
9990
10021
  this._config = _config;
9991
10022
  this._step = -1;
10023
+ this._isPlaying = false;
10024
+ this._playRunId = 0;
9992
10025
  this._pendingStepTimers = new Set();
9993
10026
  this._pendingNarrationTimers = new Set();
10027
+ this._playbackDelayTimerId = null;
10028
+ this._resolvePlaybackDelay = null;
9994
10029
  this._transforms = new Map();
9995
10030
  this._listeners = [];
9996
10031
  // ── Narration caption ──
@@ -10006,6 +10041,7 @@ class AnimationController {
10006
10041
  // ── TTS ──
10007
10042
  this._tts = false;
10008
10043
  this._speechDone = null;
10044
+ this._resolveSpeechDone = null;
10009
10045
  this.drawTargetEdges = getDrawTargetEdgeIds(steps);
10010
10046
  this.drawTargetNodes = getDrawTargetNodeIds(steps);
10011
10047
  // Groups: non-edge draw steps whose target has a #group-{id} element in the SVG.
@@ -10070,8 +10106,16 @@ class AnimationController {
10070
10106
  _buildDrawStepIndex() {
10071
10107
  const drawStepIndexByElementId = new Map();
10072
10108
  forEachPlaybackStep(this.steps, (step, stepIndex) => {
10073
- if (step.action !== "draw" || parseEdgeTarget(step.target))
10109
+ if (step.action !== "draw")
10110
+ return;
10111
+ const edge = parseEdgeTarget(step.target);
10112
+ if (edge) {
10113
+ const edgeEl = getEdgeEl(this.svg, edge.from, edge.to);
10114
+ if (edgeEl && !drawStepIndexByElementId.has(edgeEl.id)) {
10115
+ drawStepIndexByElementId.set(edgeEl.id, stepIndex);
10116
+ }
10074
10117
  return;
10118
+ }
10075
10119
  const el = resolveNonEdgeDrawEl(this.svg, step.target);
10076
10120
  if (el && !drawStepIndexByElementId.has(el.id)) {
10077
10121
  drawStepIndexByElementId.set(el.id, stepIndex);
@@ -10229,6 +10273,9 @@ class AnimationController {
10229
10273
  get atEnd() {
10230
10274
  return this._step === this.steps.length - 1;
10231
10275
  }
10276
+ get isPlaying() {
10277
+ return this._isPlaying;
10278
+ }
10232
10279
  on(listener) {
10233
10280
  this._listeners.push(listener);
10234
10281
  return () => {
@@ -10246,12 +10293,14 @@ class AnimationController {
10246
10293
  l(e);
10247
10294
  }
10248
10295
  reset() {
10296
+ this.stop();
10249
10297
  this._step = -1;
10250
10298
  this._clearAll();
10251
10299
  this.emit("animation-reset");
10252
10300
  }
10253
10301
  /** Remove caption and annotation layer from the DOM */
10254
10302
  destroy() {
10303
+ this.stop();
10255
10304
  this._clearAll();
10256
10305
  this._captionEl?.remove();
10257
10306
  this._captionEl = null;
@@ -10262,16 +10311,11 @@ class AnimationController {
10262
10311
  this._pointerEl = null;
10263
10312
  }
10264
10313
  next() {
10265
- if (!this.canNext)
10266
- return false;
10267
- this._step++;
10268
- this._applyStep(this._step, false);
10269
- this.emit("step-change");
10270
- if (!this.canNext)
10271
- this.emit("animation-end");
10272
- return true;
10314
+ this.stop();
10315
+ return this._advanceNext();
10273
10316
  }
10274
10317
  prev() {
10318
+ this.stop();
10275
10319
  if (!this.canPrev)
10276
10320
  return false;
10277
10321
  this._step--;
@@ -10282,18 +10326,33 @@ class AnimationController {
10282
10326
  return true;
10283
10327
  }
10284
10328
  async play(msPerStep = 900) {
10329
+ if (this._isPlaying || !this.canNext)
10330
+ return;
10331
+ const runId = ++this._playRunId;
10332
+ this._isPlaying = true;
10285
10333
  this.emit("animation-start");
10286
- while (this.canNext) {
10287
- const nextStep = this.steps[this._step + 1];
10288
- this.next();
10289
- // Wait for timer AND speech to finish (whichever is longer)
10290
- await Promise.all([
10291
- new Promise((r) => setTimeout(r, this._playbackWaitMs(nextStep, msPerStep))),
10292
- this._speechDone ?? Promise.resolve(),
10293
- ]);
10334
+ try {
10335
+ while (this.canNext && this._playRunId === runId) {
10336
+ const nextStep = this.steps[this._step + 1];
10337
+ if (!this._advanceNext())
10338
+ break;
10339
+ if (this._playRunId !== runId)
10340
+ break;
10341
+ await Promise.all([
10342
+ this._waitForPlaybackDelay(this._playbackWaitMs(nextStep, msPerStep)),
10343
+ this._speechDone ?? Promise.resolve(),
10344
+ ]);
10345
+ }
10346
+ }
10347
+ finally {
10348
+ if (this._playRunId === runId) {
10349
+ this._isPlaying = false;
10350
+ this._cancelPlaybackDelay();
10351
+ }
10294
10352
  }
10295
10353
  }
10296
10354
  goTo(index) {
10355
+ this.stop();
10297
10356
  index = Math.max(-1, Math.min(this.steps.length - 1, index));
10298
10357
  if (index === this._step)
10299
10358
  return;
@@ -10307,6 +10366,30 @@ class AnimationController {
10307
10366
  }
10308
10367
  this.emit("step-change");
10309
10368
  }
10369
+ stop() {
10370
+ if (!this._isPlaying && !this._resolvePlaybackDelay) {
10371
+ this._clearPendingStepTimers();
10372
+ this._cancelNarrationTyping();
10373
+ this._cancelSpeech();
10374
+ return;
10375
+ }
10376
+ this._isPlaying = false;
10377
+ this._playRunId += 1;
10378
+ this._cancelPlaybackDelay();
10379
+ this._clearPendingStepTimers();
10380
+ this._cancelNarrationTyping();
10381
+ this._cancelSpeech();
10382
+ }
10383
+ _advanceNext() {
10384
+ if (!this.canNext)
10385
+ return false;
10386
+ this._step++;
10387
+ this._applyStep(this._step, false);
10388
+ this.emit("step-change");
10389
+ if (!this.canNext)
10390
+ this.emit("animation-end");
10391
+ return true;
10392
+ }
10310
10393
  _clearTimerBucket(bucket) {
10311
10394
  bucket.forEach((id) => window.clearTimeout(id));
10312
10395
  bucket.clear();
@@ -10332,6 +10415,34 @@ class AnimationController {
10332
10415
  _scheduleStep(fn, delayMs) {
10333
10416
  this._scheduleTimer(fn, delayMs, this._pendingStepTimers);
10334
10417
  }
10418
+ _waitForPlaybackDelay(delayMs) {
10419
+ this._cancelPlaybackDelay();
10420
+ return new Promise((resolve) => {
10421
+ let settled = false;
10422
+ const finish = () => {
10423
+ if (settled)
10424
+ return;
10425
+ settled = true;
10426
+ if (this._playbackDelayTimerId !== null) {
10427
+ window.clearTimeout(this._playbackDelayTimerId);
10428
+ this._playbackDelayTimerId = null;
10429
+ }
10430
+ if (this._resolvePlaybackDelay === finish) {
10431
+ this._resolvePlaybackDelay = null;
10432
+ }
10433
+ resolve();
10434
+ };
10435
+ this._resolvePlaybackDelay = finish;
10436
+ if (delayMs <= 0) {
10437
+ finish();
10438
+ return;
10439
+ }
10440
+ this._playbackDelayTimerId = window.setTimeout(finish, delayMs);
10441
+ });
10442
+ }
10443
+ _cancelPlaybackDelay() {
10444
+ this._resolvePlaybackDelay?.();
10445
+ }
10335
10446
  _stepWaitMs(step, fallbackMs) {
10336
10447
  const delay = Math.max(0, step.delay ?? 0);
10337
10448
  const duration = Math.max(0, step.duration ?? 0);
@@ -10373,6 +10484,7 @@ class AnimationController {
10373
10484
  return this._stepWaitMs(step, fallbackMs);
10374
10485
  }
10375
10486
  _clearAll() {
10487
+ this._cancelPlaybackDelay();
10376
10488
  this._clearPendingStepTimers();
10377
10489
  this._cancelNarrationTyping();
10378
10490
  this._cancelSpeech();
@@ -10703,6 +10815,7 @@ class AnimationController {
10703
10815
  const el = getEdgeEl(this.svg, edge.from, edge.to);
10704
10816
  if (!el)
10705
10817
  return;
10818
+ showDrawEl(el);
10706
10819
  if (silent) {
10707
10820
  revealEdgeInstant(el);
10708
10821
  requestAnimationFrame(() => requestAnimationFrame(() => {
@@ -10984,16 +11097,30 @@ class AnimationController {
10984
11097
  utter.rate = 0.95;
10985
11098
  utter.pitch = 1;
10986
11099
  utter.lang = "en-US";
10987
- // Track when speech actually finishes
11100
+ // Track when speech actually finishes so play() can block until the utterance ends.
10988
11101
  this._speechDone = new Promise((resolve) => {
10989
- utter.onend = () => resolve();
10990
- utter.onerror = () => resolve();
11102
+ let settled = false;
11103
+ const finish = () => {
11104
+ if (settled)
11105
+ return;
11106
+ settled = true;
11107
+ if (this._resolveSpeechDone === finish) {
11108
+ this._resolveSpeechDone = null;
11109
+ this._speechDone = null;
11110
+ }
11111
+ resolve();
11112
+ };
11113
+ this._resolveSpeechDone = finish;
11114
+ utter.onend = finish;
11115
+ utter.onerror = finish;
10991
11116
  });
10992
11117
  speechSynthesis.speak(utter);
10993
11118
  }
10994
11119
  _cancelSpeech() {
10995
11120
  if (typeof speechSynthesis !== "undefined")
10996
11121
  speechSynthesis.cancel();
11122
+ this._resolveSpeechDone?.();
11123
+ this._resolveSpeechDone = null;
10997
11124
  this._speechDone = null;
10998
11125
  }
10999
11126
  /** Pre-warm the speech engine with a silent utterance to eliminate cold-start delay */
@@ -11302,11 +11429,12 @@ const ANIMATION_CSS = `
11302
11429
  .cg.faded, .eg.faded, .mdg.faded { opacity: 0.22; }
11303
11430
 
11304
11431
  .ng.hidden { opacity: 0; pointer-events: none; }
11305
- .gg.gg-hidden { opacity: 0; }
11306
- .tg.gg-hidden { opacity: 0; }
11307
- .ntg.gg-hidden { opacity: 0; }
11308
- .cg.gg-hidden { opacity: 0; }
11309
- .mdg.gg-hidden { opacity: 0; }
11432
+ .gg.gg-hidden { opacity: 0; }
11433
+ .tg.gg-hidden { opacity: 0; }
11434
+ .ntg.gg-hidden { opacity: 0; }
11435
+ .cg.gg-hidden { opacity: 0; }
11436
+ .eg.gg-hidden { opacity: 0; }
11437
+ .mdg.gg-hidden { opacity: 0; }
11310
11438
 
11311
11439
  /* narration caption */
11312
11440
  .skm-caption { pointer-events: none; user-select: none; }
@@ -11802,7 +11930,13 @@ class SketchmarkCanvas {
11802
11930
  this.resetButton.addEventListener("click", () => this.resetAnimation());
11803
11931
  this.prevButton.addEventListener("click", () => this.prevStep());
11804
11932
  this.nextButton.addEventListener("click", () => this.nextStep());
11805
- this.playButton.addEventListener("click", () => void this.play());
11933
+ this.playButton.addEventListener("click", () => {
11934
+ if (this.playInFlight) {
11935
+ this.stopPlayback();
11936
+ return;
11937
+ }
11938
+ void this.play();
11939
+ });
11806
11940
  this.captionButton.addEventListener("click", () => this.setCaptionVisible(!this.showCaption));
11807
11941
  this.ttsButton.addEventListener("click", () => this.setTtsEnabled(!this.getTtsEnabled()));
11808
11942
  this.viewport.addEventListener("pointerdown", this.onPointerDown);
@@ -11866,6 +12000,7 @@ class SketchmarkCanvas {
11866
12000
  this.dsl = normalizeNewlines(nextDsl);
11867
12001
  this.clearError();
11868
12002
  this.mirroredEditor?.clearError();
12003
+ this.playInFlight = false;
11869
12004
  this.animUnsub?.();
11870
12005
  this.animUnsub = null;
11871
12006
  this.instance?.anim?.destroy();
@@ -11936,9 +12071,16 @@ class SketchmarkCanvas {
11936
12071
  this.syncAnimationUi();
11937
12072
  }
11938
12073
  }
12074
+ stopPlayback() {
12075
+ this.playInFlight = false;
12076
+ if (this.renderer === "svg")
12077
+ this.instance?.anim.stop();
12078
+ this.syncAnimationUi();
12079
+ }
11939
12080
  nextStep() {
11940
12081
  if (!this.instance || this.renderer !== "svg")
11941
12082
  return;
12083
+ this.playInFlight = false;
11942
12084
  this.instance.anim.next();
11943
12085
  this.syncAnimationUi();
11944
12086
  this.focusCurrentStep();
@@ -11946,6 +12088,7 @@ class SketchmarkCanvas {
11946
12088
  prevStep() {
11947
12089
  if (!this.instance || this.renderer !== "svg")
11948
12090
  return;
12091
+ this.playInFlight = false;
11949
12092
  this.instance.anim.prev();
11950
12093
  this.syncAnimationUi();
11951
12094
  this.focusCurrentStep();
@@ -11953,6 +12096,7 @@ class SketchmarkCanvas {
11953
12096
  resetAnimation() {
11954
12097
  if (!this.instance || this.renderer !== "svg")
11955
12098
  return;
12099
+ this.playInFlight = false;
11956
12100
  this.instance.anim.reset();
11957
12101
  this.syncAnimationUi();
11958
12102
  }
@@ -11981,6 +12125,7 @@ class SketchmarkCanvas {
11981
12125
  this.render();
11982
12126
  }
11983
12127
  destroy() {
12128
+ this.playInFlight = false;
11984
12129
  this.editorCleanup?.();
11985
12130
  this.animUnsub?.();
11986
12131
  this.instance?.anim?.destroy();
@@ -12055,6 +12200,9 @@ class SketchmarkCanvas {
12055
12200
  this.prevButton.disabled = true;
12056
12201
  this.nextButton.disabled = true;
12057
12202
  this.resetButton.disabled = true;
12203
+ this.playButton.textContent = "Play";
12204
+ this.playButton.classList.remove("is-active");
12205
+ this.playButton.setAttribute("aria-pressed", "false");
12058
12206
  this.playButton.disabled = true;
12059
12207
  this.syncToggleUi();
12060
12208
  return;
@@ -12064,7 +12212,10 @@ class SketchmarkCanvas {
12064
12212
  this.prevButton.disabled = !anim.canPrev;
12065
12213
  this.nextButton.disabled = !anim.canNext;
12066
12214
  this.resetButton.disabled = false;
12067
- this.playButton.disabled = this.playInFlight || !anim.canNext;
12215
+ this.playButton.textContent = this.playInFlight ? "Stop" : "Play";
12216
+ this.playButton.classList.toggle("is-active", this.playInFlight);
12217
+ this.playButton.setAttribute("aria-pressed", this.playInFlight ? "true" : "false");
12218
+ this.playButton.disabled = this.playInFlight ? false : !anim.canNext;
12068
12219
  this.syncToggleUi();
12069
12220
  }
12070
12221
  getStepTarget(stepItem) {
@@ -12970,6 +13121,10 @@ class SketchmarkEmbed {
12970
13121
  this.btnPrev.addEventListener("click", () => this.prevStep());
12971
13122
  this.btnNext.addEventListener("click", () => this.nextStep());
12972
13123
  this.btnPlay.addEventListener("click", () => {
13124
+ if (this.playInFlight) {
13125
+ this.stopPlayback();
13126
+ return;
13127
+ }
12973
13128
  void this.play();
12974
13129
  });
12975
13130
  this.btnCaption.addEventListener("click", () => this.setCaptionVisible(!this.showCaption));
@@ -13027,6 +13182,7 @@ class SketchmarkEmbed {
13027
13182
  }
13028
13183
  this.clearError();
13029
13184
  this.stopMotion();
13185
+ this.playInFlight = false;
13030
13186
  this.animUnsub?.();
13031
13187
  this.animUnsub = null;
13032
13188
  this.instance?.anim?.destroy();
@@ -13099,9 +13255,15 @@ class SketchmarkEmbed {
13099
13255
  this.syncControls();
13100
13256
  }
13101
13257
  }
13258
+ stopPlayback() {
13259
+ this.playInFlight = false;
13260
+ this.instance?.anim.stop();
13261
+ this.syncControls();
13262
+ }
13102
13263
  nextStep() {
13103
13264
  if (!this.instance)
13104
13265
  return;
13266
+ this.playInFlight = false;
13105
13267
  this.instance.anim.next();
13106
13268
  this.syncControls();
13107
13269
  if (this.options.autoFocus !== false && this.options.autoFocusOnStep !== false) {
@@ -13111,6 +13273,7 @@ class SketchmarkEmbed {
13111
13273
  prevStep() {
13112
13274
  if (!this.instance)
13113
13275
  return;
13276
+ this.playInFlight = false;
13114
13277
  this.instance.anim.prev();
13115
13278
  this.syncControls();
13116
13279
  if (this.options.autoFocus !== false && this.options.autoFocusOnStep !== false) {
@@ -13120,6 +13283,7 @@ class SketchmarkEmbed {
13120
13283
  resetAnimation() {
13121
13284
  if (!this.instance)
13122
13285
  return;
13286
+ this.playInFlight = false;
13123
13287
  this.instance.anim.reset();
13124
13288
  this.syncControls();
13125
13289
  }
@@ -13146,6 +13310,7 @@ class SketchmarkEmbed {
13146
13310
  }
13147
13311
  destroy() {
13148
13312
  this.stopMotion();
13313
+ this.playInFlight = false;
13149
13314
  this.animUnsub?.();
13150
13315
  this.instance?.anim?.destroy();
13151
13316
  this.instance = null;
@@ -13178,6 +13343,9 @@ class SketchmarkEmbed {
13178
13343
  this.btnRestart.disabled = true;
13179
13344
  this.btnPrev.disabled = true;
13180
13345
  this.btnNext.disabled = true;
13346
+ this.btnPlay.textContent = "Play";
13347
+ this.btnPlay.classList.remove("is-active");
13348
+ this.btnPlay.setAttribute("aria-pressed", "false");
13181
13349
  this.btnPlay.disabled = true;
13182
13350
  return;
13183
13351
  }
@@ -13186,7 +13354,10 @@ class SketchmarkEmbed {
13186
13354
  this.btnRestart.disabled = false;
13187
13355
  this.btnPrev.disabled = !anim.canPrev;
13188
13356
  this.btnNext.disabled = !anim.canNext;
13189
- this.btnPlay.disabled = this.playInFlight || !anim.canNext;
13357
+ this.btnPlay.textContent = this.playInFlight ? "Stop" : "Play";
13358
+ this.btnPlay.classList.toggle("is-active", this.playInFlight);
13359
+ this.btnPlay.setAttribute("aria-pressed", this.playInFlight ? "true" : "false");
13360
+ this.btnPlay.disabled = this.playInFlight ? false : !anim.canNext;
13190
13361
  }
13191
13362
  syncViewControls() {
13192
13363
  const hasView = !!this.instance?.svg;