sketchmark 1.3.5 → 1.3.6

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
@@ -9989,8 +9989,12 @@ class AnimationController {
9989
9989
  this._rc = _rc;
9990
9990
  this._config = _config;
9991
9991
  this._step = -1;
9992
+ this._isPlaying = false;
9993
+ this._playRunId = 0;
9992
9994
  this._pendingStepTimers = new Set();
9993
9995
  this._pendingNarrationTimers = new Set();
9996
+ this._playbackDelayTimerId = null;
9997
+ this._resolvePlaybackDelay = null;
9994
9998
  this._transforms = new Map();
9995
9999
  this._listeners = [];
9996
10000
  // ── Narration caption ──
@@ -10006,6 +10010,7 @@ class AnimationController {
10006
10010
  // ── TTS ──
10007
10011
  this._tts = false;
10008
10012
  this._speechDone = null;
10013
+ this._resolveSpeechDone = null;
10009
10014
  this.drawTargetEdges = getDrawTargetEdgeIds(steps);
10010
10015
  this.drawTargetNodes = getDrawTargetNodeIds(steps);
10011
10016
  // Groups: non-edge draw steps whose target has a #group-{id} element in the SVG.
@@ -10229,6 +10234,9 @@ class AnimationController {
10229
10234
  get atEnd() {
10230
10235
  return this._step === this.steps.length - 1;
10231
10236
  }
10237
+ get isPlaying() {
10238
+ return this._isPlaying;
10239
+ }
10232
10240
  on(listener) {
10233
10241
  this._listeners.push(listener);
10234
10242
  return () => {
@@ -10246,12 +10254,14 @@ class AnimationController {
10246
10254
  l(e);
10247
10255
  }
10248
10256
  reset() {
10257
+ this.stop();
10249
10258
  this._step = -1;
10250
10259
  this._clearAll();
10251
10260
  this.emit("animation-reset");
10252
10261
  }
10253
10262
  /** Remove caption and annotation layer from the DOM */
10254
10263
  destroy() {
10264
+ this.stop();
10255
10265
  this._clearAll();
10256
10266
  this._captionEl?.remove();
10257
10267
  this._captionEl = null;
@@ -10262,16 +10272,11 @@ class AnimationController {
10262
10272
  this._pointerEl = null;
10263
10273
  }
10264
10274
  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;
10275
+ this.stop();
10276
+ return this._advanceNext();
10273
10277
  }
10274
10278
  prev() {
10279
+ this.stop();
10275
10280
  if (!this.canPrev)
10276
10281
  return false;
10277
10282
  this._step--;
@@ -10282,18 +10287,33 @@ class AnimationController {
10282
10287
  return true;
10283
10288
  }
10284
10289
  async play(msPerStep = 900) {
10290
+ if (this._isPlaying || !this.canNext)
10291
+ return;
10292
+ const runId = ++this._playRunId;
10293
+ this._isPlaying = true;
10285
10294
  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
- ]);
10295
+ try {
10296
+ while (this.canNext && this._playRunId === runId) {
10297
+ const nextStep = this.steps[this._step + 1];
10298
+ if (!this._advanceNext())
10299
+ break;
10300
+ if (this._playRunId !== runId)
10301
+ break;
10302
+ await Promise.all([
10303
+ this._waitForPlaybackDelay(this._playbackWaitMs(nextStep, msPerStep)),
10304
+ this._speechDone ?? Promise.resolve(),
10305
+ ]);
10306
+ }
10307
+ }
10308
+ finally {
10309
+ if (this._playRunId === runId) {
10310
+ this._isPlaying = false;
10311
+ this._cancelPlaybackDelay();
10312
+ }
10294
10313
  }
10295
10314
  }
10296
10315
  goTo(index) {
10316
+ this.stop();
10297
10317
  index = Math.max(-1, Math.min(this.steps.length - 1, index));
10298
10318
  if (index === this._step)
10299
10319
  return;
@@ -10307,6 +10327,30 @@ class AnimationController {
10307
10327
  }
10308
10328
  this.emit("step-change");
10309
10329
  }
10330
+ stop() {
10331
+ if (!this._isPlaying && !this._resolvePlaybackDelay) {
10332
+ this._clearPendingStepTimers();
10333
+ this._cancelNarrationTyping();
10334
+ this._cancelSpeech();
10335
+ return;
10336
+ }
10337
+ this._isPlaying = false;
10338
+ this._playRunId += 1;
10339
+ this._cancelPlaybackDelay();
10340
+ this._clearPendingStepTimers();
10341
+ this._cancelNarrationTyping();
10342
+ this._cancelSpeech();
10343
+ }
10344
+ _advanceNext() {
10345
+ if (!this.canNext)
10346
+ return false;
10347
+ this._step++;
10348
+ this._applyStep(this._step, false);
10349
+ this.emit("step-change");
10350
+ if (!this.canNext)
10351
+ this.emit("animation-end");
10352
+ return true;
10353
+ }
10310
10354
  _clearTimerBucket(bucket) {
10311
10355
  bucket.forEach((id) => window.clearTimeout(id));
10312
10356
  bucket.clear();
@@ -10332,6 +10376,34 @@ class AnimationController {
10332
10376
  _scheduleStep(fn, delayMs) {
10333
10377
  this._scheduleTimer(fn, delayMs, this._pendingStepTimers);
10334
10378
  }
10379
+ _waitForPlaybackDelay(delayMs) {
10380
+ this._cancelPlaybackDelay();
10381
+ return new Promise((resolve) => {
10382
+ let settled = false;
10383
+ const finish = () => {
10384
+ if (settled)
10385
+ return;
10386
+ settled = true;
10387
+ if (this._playbackDelayTimerId !== null) {
10388
+ window.clearTimeout(this._playbackDelayTimerId);
10389
+ this._playbackDelayTimerId = null;
10390
+ }
10391
+ if (this._resolvePlaybackDelay === finish) {
10392
+ this._resolvePlaybackDelay = null;
10393
+ }
10394
+ resolve();
10395
+ };
10396
+ this._resolvePlaybackDelay = finish;
10397
+ if (delayMs <= 0) {
10398
+ finish();
10399
+ return;
10400
+ }
10401
+ this._playbackDelayTimerId = window.setTimeout(finish, delayMs);
10402
+ });
10403
+ }
10404
+ _cancelPlaybackDelay() {
10405
+ this._resolvePlaybackDelay?.();
10406
+ }
10335
10407
  _stepWaitMs(step, fallbackMs) {
10336
10408
  const delay = Math.max(0, step.delay ?? 0);
10337
10409
  const duration = Math.max(0, step.duration ?? 0);
@@ -10373,6 +10445,7 @@ class AnimationController {
10373
10445
  return this._stepWaitMs(step, fallbackMs);
10374
10446
  }
10375
10447
  _clearAll() {
10448
+ this._cancelPlaybackDelay();
10376
10449
  this._clearPendingStepTimers();
10377
10450
  this._cancelNarrationTyping();
10378
10451
  this._cancelSpeech();
@@ -10984,16 +11057,30 @@ class AnimationController {
10984
11057
  utter.rate = 0.95;
10985
11058
  utter.pitch = 1;
10986
11059
  utter.lang = "en-US";
10987
- // Track when speech actually finishes
11060
+ // Track when speech actually finishes so play() can block until the utterance ends.
10988
11061
  this._speechDone = new Promise((resolve) => {
10989
- utter.onend = () => resolve();
10990
- utter.onerror = () => resolve();
11062
+ let settled = false;
11063
+ const finish = () => {
11064
+ if (settled)
11065
+ return;
11066
+ settled = true;
11067
+ if (this._resolveSpeechDone === finish) {
11068
+ this._resolveSpeechDone = null;
11069
+ this._speechDone = null;
11070
+ }
11071
+ resolve();
11072
+ };
11073
+ this._resolveSpeechDone = finish;
11074
+ utter.onend = finish;
11075
+ utter.onerror = finish;
10991
11076
  });
10992
11077
  speechSynthesis.speak(utter);
10993
11078
  }
10994
11079
  _cancelSpeech() {
10995
11080
  if (typeof speechSynthesis !== "undefined")
10996
11081
  speechSynthesis.cancel();
11082
+ this._resolveSpeechDone?.();
11083
+ this._resolveSpeechDone = null;
10997
11084
  this._speechDone = null;
10998
11085
  }
10999
11086
  /** Pre-warm the speech engine with a silent utterance to eliminate cold-start delay */
@@ -11802,7 +11889,13 @@ class SketchmarkCanvas {
11802
11889
  this.resetButton.addEventListener("click", () => this.resetAnimation());
11803
11890
  this.prevButton.addEventListener("click", () => this.prevStep());
11804
11891
  this.nextButton.addEventListener("click", () => this.nextStep());
11805
- this.playButton.addEventListener("click", () => void this.play());
11892
+ this.playButton.addEventListener("click", () => {
11893
+ if (this.playInFlight) {
11894
+ this.stopPlayback();
11895
+ return;
11896
+ }
11897
+ void this.play();
11898
+ });
11806
11899
  this.captionButton.addEventListener("click", () => this.setCaptionVisible(!this.showCaption));
11807
11900
  this.ttsButton.addEventListener("click", () => this.setTtsEnabled(!this.getTtsEnabled()));
11808
11901
  this.viewport.addEventListener("pointerdown", this.onPointerDown);
@@ -11866,6 +11959,7 @@ class SketchmarkCanvas {
11866
11959
  this.dsl = normalizeNewlines(nextDsl);
11867
11960
  this.clearError();
11868
11961
  this.mirroredEditor?.clearError();
11962
+ this.playInFlight = false;
11869
11963
  this.animUnsub?.();
11870
11964
  this.animUnsub = null;
11871
11965
  this.instance?.anim?.destroy();
@@ -11936,9 +12030,16 @@ class SketchmarkCanvas {
11936
12030
  this.syncAnimationUi();
11937
12031
  }
11938
12032
  }
12033
+ stopPlayback() {
12034
+ this.playInFlight = false;
12035
+ if (this.renderer === "svg")
12036
+ this.instance?.anim.stop();
12037
+ this.syncAnimationUi();
12038
+ }
11939
12039
  nextStep() {
11940
12040
  if (!this.instance || this.renderer !== "svg")
11941
12041
  return;
12042
+ this.playInFlight = false;
11942
12043
  this.instance.anim.next();
11943
12044
  this.syncAnimationUi();
11944
12045
  this.focusCurrentStep();
@@ -11946,6 +12047,7 @@ class SketchmarkCanvas {
11946
12047
  prevStep() {
11947
12048
  if (!this.instance || this.renderer !== "svg")
11948
12049
  return;
12050
+ this.playInFlight = false;
11949
12051
  this.instance.anim.prev();
11950
12052
  this.syncAnimationUi();
11951
12053
  this.focusCurrentStep();
@@ -11953,6 +12055,7 @@ class SketchmarkCanvas {
11953
12055
  resetAnimation() {
11954
12056
  if (!this.instance || this.renderer !== "svg")
11955
12057
  return;
12058
+ this.playInFlight = false;
11956
12059
  this.instance.anim.reset();
11957
12060
  this.syncAnimationUi();
11958
12061
  }
@@ -11981,6 +12084,7 @@ class SketchmarkCanvas {
11981
12084
  this.render();
11982
12085
  }
11983
12086
  destroy() {
12087
+ this.playInFlight = false;
11984
12088
  this.editorCleanup?.();
11985
12089
  this.animUnsub?.();
11986
12090
  this.instance?.anim?.destroy();
@@ -12055,6 +12159,9 @@ class SketchmarkCanvas {
12055
12159
  this.prevButton.disabled = true;
12056
12160
  this.nextButton.disabled = true;
12057
12161
  this.resetButton.disabled = true;
12162
+ this.playButton.textContent = "Play";
12163
+ this.playButton.classList.remove("is-active");
12164
+ this.playButton.setAttribute("aria-pressed", "false");
12058
12165
  this.playButton.disabled = true;
12059
12166
  this.syncToggleUi();
12060
12167
  return;
@@ -12064,7 +12171,10 @@ class SketchmarkCanvas {
12064
12171
  this.prevButton.disabled = !anim.canPrev;
12065
12172
  this.nextButton.disabled = !anim.canNext;
12066
12173
  this.resetButton.disabled = false;
12067
- this.playButton.disabled = this.playInFlight || !anim.canNext;
12174
+ this.playButton.textContent = this.playInFlight ? "Stop" : "Play";
12175
+ this.playButton.classList.toggle("is-active", this.playInFlight);
12176
+ this.playButton.setAttribute("aria-pressed", this.playInFlight ? "true" : "false");
12177
+ this.playButton.disabled = this.playInFlight ? false : !anim.canNext;
12068
12178
  this.syncToggleUi();
12069
12179
  }
12070
12180
  getStepTarget(stepItem) {
@@ -12970,6 +13080,10 @@ class SketchmarkEmbed {
12970
13080
  this.btnPrev.addEventListener("click", () => this.prevStep());
12971
13081
  this.btnNext.addEventListener("click", () => this.nextStep());
12972
13082
  this.btnPlay.addEventListener("click", () => {
13083
+ if (this.playInFlight) {
13084
+ this.stopPlayback();
13085
+ return;
13086
+ }
12973
13087
  void this.play();
12974
13088
  });
12975
13089
  this.btnCaption.addEventListener("click", () => this.setCaptionVisible(!this.showCaption));
@@ -13027,6 +13141,7 @@ class SketchmarkEmbed {
13027
13141
  }
13028
13142
  this.clearError();
13029
13143
  this.stopMotion();
13144
+ this.playInFlight = false;
13030
13145
  this.animUnsub?.();
13031
13146
  this.animUnsub = null;
13032
13147
  this.instance?.anim?.destroy();
@@ -13099,9 +13214,15 @@ class SketchmarkEmbed {
13099
13214
  this.syncControls();
13100
13215
  }
13101
13216
  }
13217
+ stopPlayback() {
13218
+ this.playInFlight = false;
13219
+ this.instance?.anim.stop();
13220
+ this.syncControls();
13221
+ }
13102
13222
  nextStep() {
13103
13223
  if (!this.instance)
13104
13224
  return;
13225
+ this.playInFlight = false;
13105
13226
  this.instance.anim.next();
13106
13227
  this.syncControls();
13107
13228
  if (this.options.autoFocus !== false && this.options.autoFocusOnStep !== false) {
@@ -13111,6 +13232,7 @@ class SketchmarkEmbed {
13111
13232
  prevStep() {
13112
13233
  if (!this.instance)
13113
13234
  return;
13235
+ this.playInFlight = false;
13114
13236
  this.instance.anim.prev();
13115
13237
  this.syncControls();
13116
13238
  if (this.options.autoFocus !== false && this.options.autoFocusOnStep !== false) {
@@ -13120,6 +13242,7 @@ class SketchmarkEmbed {
13120
13242
  resetAnimation() {
13121
13243
  if (!this.instance)
13122
13244
  return;
13245
+ this.playInFlight = false;
13123
13246
  this.instance.anim.reset();
13124
13247
  this.syncControls();
13125
13248
  }
@@ -13146,6 +13269,7 @@ class SketchmarkEmbed {
13146
13269
  }
13147
13270
  destroy() {
13148
13271
  this.stopMotion();
13272
+ this.playInFlight = false;
13149
13273
  this.animUnsub?.();
13150
13274
  this.instance?.anim?.destroy();
13151
13275
  this.instance = null;
@@ -13178,6 +13302,9 @@ class SketchmarkEmbed {
13178
13302
  this.btnRestart.disabled = true;
13179
13303
  this.btnPrev.disabled = true;
13180
13304
  this.btnNext.disabled = true;
13305
+ this.btnPlay.textContent = "Play";
13306
+ this.btnPlay.classList.remove("is-active");
13307
+ this.btnPlay.setAttribute("aria-pressed", "false");
13181
13308
  this.btnPlay.disabled = true;
13182
13309
  return;
13183
13310
  }
@@ -13186,7 +13313,10 @@ class SketchmarkEmbed {
13186
13313
  this.btnRestart.disabled = false;
13187
13314
  this.btnPrev.disabled = !anim.canPrev;
13188
13315
  this.btnNext.disabled = !anim.canNext;
13189
- this.btnPlay.disabled = this.playInFlight || !anim.canNext;
13316
+ this.btnPlay.textContent = this.playInFlight ? "Stop" : "Play";
13317
+ this.btnPlay.classList.toggle("is-active", this.playInFlight);
13318
+ this.btnPlay.setAttribute("aria-pressed", this.playInFlight ? "true" : "false");
13319
+ this.btnPlay.disabled = this.playInFlight ? false : !anim.canNext;
13190
13320
  }
13191
13321
  syncViewControls() {
13192
13322
  const hasView = !!this.instance?.svg;