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/README.md CHANGED
@@ -149,7 +149,7 @@ const embed = new SketchmarkEmbed({
149
149
  });
150
150
  ```
151
151
 
152
- Use `SketchmarkCanvas` for the full playground-style surface, and `SketchmarkEmbed` for fixed-size embeds that clip overflow, auto-fit large diagrams, support drag-to-pan plus wheel/trackpad zoom, and expose built-in zoom, playback, caption, and TTS controls.
152
+ Use `SketchmarkCanvas` for the full playground-style surface, and `SketchmarkEmbed` for fixed-size embeds that clip overflow, auto-fit large diagrams, support drag-to-pan plus wheel/trackpad zoom, and expose built-in zoom, playback, caption, and TTS controls. While autoplay is running, their built-in `Play` control switches to `Stop` so you can hard-stop the sequence immediately.
153
153
 
154
154
  ---
155
155
 
@@ -854,17 +854,19 @@ anim.total // number of steps
854
854
  anim.currentStep // current step index (-1 = before start)
855
855
  anim.canNext // boolean
856
856
  anim.canPrev // boolean
857
- anim.atEnd // boolean
857
+ anim.atEnd // boolean
858
+ anim.isPlaying // boolean
858
859
  anim.captionElement // HTMLDivElement | null — the narration caption element
859
860
  anim.tts // boolean — text-to-speech enabled/disabled
860
861
 
861
862
  // Methods
862
- anim.next() // advance one step (returns bool)
863
- anim.prev() // go back one step (returns bool)
864
- anim.reset() // reset to before step 0
865
- anim.goTo(index) // jump to step N
866
- await anim.play(700) // play all remaining steps (700ms between)
867
- anim.destroy() // remove caption, annotations, pointer from DOM
863
+ anim.next() // advance one step (returns bool)
864
+ anim.prev() // go back one step (returns bool)
865
+ anim.reset() // reset to before step 0
866
+ anim.goTo(index) // jump to step N
867
+ await anim.play(700) // play all remaining steps (700ms between)
868
+ anim.stop() // hard-stop autoplay without resetting the current step
869
+ anim.destroy() // remove caption, annotations, pointer from DOM
868
870
 
869
871
  // Toggle TTS programmatically
870
872
  anim.tts = true; // enable browser speech
@@ -19,8 +19,12 @@ export declare class AnimationController {
19
19
  private _rc?;
20
20
  private _config?;
21
21
  private _step;
22
+ private _isPlaying;
23
+ private _playRunId;
22
24
  private _pendingStepTimers;
23
25
  private _pendingNarrationTimers;
26
+ private _playbackDelayTimerId;
27
+ private _resolvePlaybackDelay;
24
28
  private _transforms;
25
29
  private _listeners;
26
30
  readonly drawTargetEdges: Set<string>;
@@ -43,6 +47,7 @@ export declare class AnimationController {
43
47
  private _pointerType;
44
48
  private _tts;
45
49
  private _speechDone;
50
+ private _resolveSpeechDone;
46
51
  get drawTargets(): Set<string>;
47
52
  constructor(svg: SVGSVGElement, steps: ASTStepItem[], _container?: HTMLElement | undefined, _rc?: any | undefined, _config?: Record<string, string | number | boolean> | undefined);
48
53
  private _buildDrawStepIndex;
@@ -62,6 +67,7 @@ export declare class AnimationController {
62
67
  get canNext(): boolean;
63
68
  get canPrev(): boolean;
64
69
  get atEnd(): boolean;
70
+ get isPlaying(): boolean;
65
71
  on(listener: AnimationListener): () => void;
66
72
  private emit;
67
73
  reset(): void;
@@ -71,11 +77,15 @@ export declare class AnimationController {
71
77
  prev(): boolean;
72
78
  play(msPerStep?: number): Promise<void>;
73
79
  goTo(index: number): void;
80
+ stop(): void;
81
+ private _advanceNext;
74
82
  private _clearTimerBucket;
75
83
  private _clearPendingStepTimers;
76
84
  private _cancelNarrationTyping;
77
85
  private _scheduleTimer;
78
86
  private _scheduleStep;
87
+ private _waitForPlaybackDelay;
88
+ private _cancelPlaybackDelay;
79
89
  private _stepWaitMs;
80
90
  private _playbackWaitMs;
81
91
  private _clearAll;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/animation/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/animation/index.ts"],"names":[],"mappings":"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"}
package/dist/index.cjs CHANGED
@@ -9991,8 +9991,12 @@ class AnimationController {
9991
9991
  this._rc = _rc;
9992
9992
  this._config = _config;
9993
9993
  this._step = -1;
9994
+ this._isPlaying = false;
9995
+ this._playRunId = 0;
9994
9996
  this._pendingStepTimers = new Set();
9995
9997
  this._pendingNarrationTimers = new Set();
9998
+ this._playbackDelayTimerId = null;
9999
+ this._resolvePlaybackDelay = null;
9996
10000
  this._transforms = new Map();
9997
10001
  this._listeners = [];
9998
10002
  // ── Narration caption ──
@@ -10008,6 +10012,7 @@ class AnimationController {
10008
10012
  // ── TTS ──
10009
10013
  this._tts = false;
10010
10014
  this._speechDone = null;
10015
+ this._resolveSpeechDone = null;
10011
10016
  this.drawTargetEdges = getDrawTargetEdgeIds(steps);
10012
10017
  this.drawTargetNodes = getDrawTargetNodeIds(steps);
10013
10018
  // Groups: non-edge draw steps whose target has a #group-{id} element in the SVG.
@@ -10231,6 +10236,9 @@ class AnimationController {
10231
10236
  get atEnd() {
10232
10237
  return this._step === this.steps.length - 1;
10233
10238
  }
10239
+ get isPlaying() {
10240
+ return this._isPlaying;
10241
+ }
10234
10242
  on(listener) {
10235
10243
  this._listeners.push(listener);
10236
10244
  return () => {
@@ -10248,12 +10256,14 @@ class AnimationController {
10248
10256
  l(e);
10249
10257
  }
10250
10258
  reset() {
10259
+ this.stop();
10251
10260
  this._step = -1;
10252
10261
  this._clearAll();
10253
10262
  this.emit("animation-reset");
10254
10263
  }
10255
10264
  /** Remove caption and annotation layer from the DOM */
10256
10265
  destroy() {
10266
+ this.stop();
10257
10267
  this._clearAll();
10258
10268
  this._captionEl?.remove();
10259
10269
  this._captionEl = null;
@@ -10264,16 +10274,11 @@ class AnimationController {
10264
10274
  this._pointerEl = null;
10265
10275
  }
10266
10276
  next() {
10267
- if (!this.canNext)
10268
- return false;
10269
- this._step++;
10270
- this._applyStep(this._step, false);
10271
- this.emit("step-change");
10272
- if (!this.canNext)
10273
- this.emit("animation-end");
10274
- return true;
10277
+ this.stop();
10278
+ return this._advanceNext();
10275
10279
  }
10276
10280
  prev() {
10281
+ this.stop();
10277
10282
  if (!this.canPrev)
10278
10283
  return false;
10279
10284
  this._step--;
@@ -10284,18 +10289,33 @@ class AnimationController {
10284
10289
  return true;
10285
10290
  }
10286
10291
  async play(msPerStep = 900) {
10292
+ if (this._isPlaying || !this.canNext)
10293
+ return;
10294
+ const runId = ++this._playRunId;
10295
+ this._isPlaying = true;
10287
10296
  this.emit("animation-start");
10288
- while (this.canNext) {
10289
- const nextStep = this.steps[this._step + 1];
10290
- this.next();
10291
- // Wait for timer AND speech to finish (whichever is longer)
10292
- await Promise.all([
10293
- new Promise((r) => setTimeout(r, this._playbackWaitMs(nextStep, msPerStep))),
10294
- this._speechDone ?? Promise.resolve(),
10295
- ]);
10297
+ try {
10298
+ while (this.canNext && this._playRunId === runId) {
10299
+ const nextStep = this.steps[this._step + 1];
10300
+ if (!this._advanceNext())
10301
+ break;
10302
+ if (this._playRunId !== runId)
10303
+ break;
10304
+ await Promise.all([
10305
+ this._waitForPlaybackDelay(this._playbackWaitMs(nextStep, msPerStep)),
10306
+ this._speechDone ?? Promise.resolve(),
10307
+ ]);
10308
+ }
10309
+ }
10310
+ finally {
10311
+ if (this._playRunId === runId) {
10312
+ this._isPlaying = false;
10313
+ this._cancelPlaybackDelay();
10314
+ }
10296
10315
  }
10297
10316
  }
10298
10317
  goTo(index) {
10318
+ this.stop();
10299
10319
  index = Math.max(-1, Math.min(this.steps.length - 1, index));
10300
10320
  if (index === this._step)
10301
10321
  return;
@@ -10309,6 +10329,30 @@ class AnimationController {
10309
10329
  }
10310
10330
  this.emit("step-change");
10311
10331
  }
10332
+ stop() {
10333
+ if (!this._isPlaying && !this._resolvePlaybackDelay) {
10334
+ this._clearPendingStepTimers();
10335
+ this._cancelNarrationTyping();
10336
+ this._cancelSpeech();
10337
+ return;
10338
+ }
10339
+ this._isPlaying = false;
10340
+ this._playRunId += 1;
10341
+ this._cancelPlaybackDelay();
10342
+ this._clearPendingStepTimers();
10343
+ this._cancelNarrationTyping();
10344
+ this._cancelSpeech();
10345
+ }
10346
+ _advanceNext() {
10347
+ if (!this.canNext)
10348
+ return false;
10349
+ this._step++;
10350
+ this._applyStep(this._step, false);
10351
+ this.emit("step-change");
10352
+ if (!this.canNext)
10353
+ this.emit("animation-end");
10354
+ return true;
10355
+ }
10312
10356
  _clearTimerBucket(bucket) {
10313
10357
  bucket.forEach((id) => window.clearTimeout(id));
10314
10358
  bucket.clear();
@@ -10334,6 +10378,34 @@ class AnimationController {
10334
10378
  _scheduleStep(fn, delayMs) {
10335
10379
  this._scheduleTimer(fn, delayMs, this._pendingStepTimers);
10336
10380
  }
10381
+ _waitForPlaybackDelay(delayMs) {
10382
+ this._cancelPlaybackDelay();
10383
+ return new Promise((resolve) => {
10384
+ let settled = false;
10385
+ const finish = () => {
10386
+ if (settled)
10387
+ return;
10388
+ settled = true;
10389
+ if (this._playbackDelayTimerId !== null) {
10390
+ window.clearTimeout(this._playbackDelayTimerId);
10391
+ this._playbackDelayTimerId = null;
10392
+ }
10393
+ if (this._resolvePlaybackDelay === finish) {
10394
+ this._resolvePlaybackDelay = null;
10395
+ }
10396
+ resolve();
10397
+ };
10398
+ this._resolvePlaybackDelay = finish;
10399
+ if (delayMs <= 0) {
10400
+ finish();
10401
+ return;
10402
+ }
10403
+ this._playbackDelayTimerId = window.setTimeout(finish, delayMs);
10404
+ });
10405
+ }
10406
+ _cancelPlaybackDelay() {
10407
+ this._resolvePlaybackDelay?.();
10408
+ }
10337
10409
  _stepWaitMs(step, fallbackMs) {
10338
10410
  const delay = Math.max(0, step.delay ?? 0);
10339
10411
  const duration = Math.max(0, step.duration ?? 0);
@@ -10375,6 +10447,7 @@ class AnimationController {
10375
10447
  return this._stepWaitMs(step, fallbackMs);
10376
10448
  }
10377
10449
  _clearAll() {
10450
+ this._cancelPlaybackDelay();
10378
10451
  this._clearPendingStepTimers();
10379
10452
  this._cancelNarrationTyping();
10380
10453
  this._cancelSpeech();
@@ -10986,16 +11059,30 @@ class AnimationController {
10986
11059
  utter.rate = 0.95;
10987
11060
  utter.pitch = 1;
10988
11061
  utter.lang = "en-US";
10989
- // Track when speech actually finishes
11062
+ // Track when speech actually finishes so play() can block until the utterance ends.
10990
11063
  this._speechDone = new Promise((resolve) => {
10991
- utter.onend = () => resolve();
10992
- utter.onerror = () => resolve();
11064
+ let settled = false;
11065
+ const finish = () => {
11066
+ if (settled)
11067
+ return;
11068
+ settled = true;
11069
+ if (this._resolveSpeechDone === finish) {
11070
+ this._resolveSpeechDone = null;
11071
+ this._speechDone = null;
11072
+ }
11073
+ resolve();
11074
+ };
11075
+ this._resolveSpeechDone = finish;
11076
+ utter.onend = finish;
11077
+ utter.onerror = finish;
10993
11078
  });
10994
11079
  speechSynthesis.speak(utter);
10995
11080
  }
10996
11081
  _cancelSpeech() {
10997
11082
  if (typeof speechSynthesis !== "undefined")
10998
11083
  speechSynthesis.cancel();
11084
+ this._resolveSpeechDone?.();
11085
+ this._resolveSpeechDone = null;
10999
11086
  this._speechDone = null;
11000
11087
  }
11001
11088
  /** Pre-warm the speech engine with a silent utterance to eliminate cold-start delay */
@@ -11804,7 +11891,13 @@ class SketchmarkCanvas {
11804
11891
  this.resetButton.addEventListener("click", () => this.resetAnimation());
11805
11892
  this.prevButton.addEventListener("click", () => this.prevStep());
11806
11893
  this.nextButton.addEventListener("click", () => this.nextStep());
11807
- this.playButton.addEventListener("click", () => void this.play());
11894
+ this.playButton.addEventListener("click", () => {
11895
+ if (this.playInFlight) {
11896
+ this.stopPlayback();
11897
+ return;
11898
+ }
11899
+ void this.play();
11900
+ });
11808
11901
  this.captionButton.addEventListener("click", () => this.setCaptionVisible(!this.showCaption));
11809
11902
  this.ttsButton.addEventListener("click", () => this.setTtsEnabled(!this.getTtsEnabled()));
11810
11903
  this.viewport.addEventListener("pointerdown", this.onPointerDown);
@@ -11868,6 +11961,7 @@ class SketchmarkCanvas {
11868
11961
  this.dsl = normalizeNewlines(nextDsl);
11869
11962
  this.clearError();
11870
11963
  this.mirroredEditor?.clearError();
11964
+ this.playInFlight = false;
11871
11965
  this.animUnsub?.();
11872
11966
  this.animUnsub = null;
11873
11967
  this.instance?.anim?.destroy();
@@ -11938,9 +12032,16 @@ class SketchmarkCanvas {
11938
12032
  this.syncAnimationUi();
11939
12033
  }
11940
12034
  }
12035
+ stopPlayback() {
12036
+ this.playInFlight = false;
12037
+ if (this.renderer === "svg")
12038
+ this.instance?.anim.stop();
12039
+ this.syncAnimationUi();
12040
+ }
11941
12041
  nextStep() {
11942
12042
  if (!this.instance || this.renderer !== "svg")
11943
12043
  return;
12044
+ this.playInFlight = false;
11944
12045
  this.instance.anim.next();
11945
12046
  this.syncAnimationUi();
11946
12047
  this.focusCurrentStep();
@@ -11948,6 +12049,7 @@ class SketchmarkCanvas {
11948
12049
  prevStep() {
11949
12050
  if (!this.instance || this.renderer !== "svg")
11950
12051
  return;
12052
+ this.playInFlight = false;
11951
12053
  this.instance.anim.prev();
11952
12054
  this.syncAnimationUi();
11953
12055
  this.focusCurrentStep();
@@ -11955,6 +12057,7 @@ class SketchmarkCanvas {
11955
12057
  resetAnimation() {
11956
12058
  if (!this.instance || this.renderer !== "svg")
11957
12059
  return;
12060
+ this.playInFlight = false;
11958
12061
  this.instance.anim.reset();
11959
12062
  this.syncAnimationUi();
11960
12063
  }
@@ -11983,6 +12086,7 @@ class SketchmarkCanvas {
11983
12086
  this.render();
11984
12087
  }
11985
12088
  destroy() {
12089
+ this.playInFlight = false;
11986
12090
  this.editorCleanup?.();
11987
12091
  this.animUnsub?.();
11988
12092
  this.instance?.anim?.destroy();
@@ -12057,6 +12161,9 @@ class SketchmarkCanvas {
12057
12161
  this.prevButton.disabled = true;
12058
12162
  this.nextButton.disabled = true;
12059
12163
  this.resetButton.disabled = true;
12164
+ this.playButton.textContent = "Play";
12165
+ this.playButton.classList.remove("is-active");
12166
+ this.playButton.setAttribute("aria-pressed", "false");
12060
12167
  this.playButton.disabled = true;
12061
12168
  this.syncToggleUi();
12062
12169
  return;
@@ -12066,7 +12173,10 @@ class SketchmarkCanvas {
12066
12173
  this.prevButton.disabled = !anim.canPrev;
12067
12174
  this.nextButton.disabled = !anim.canNext;
12068
12175
  this.resetButton.disabled = false;
12069
- this.playButton.disabled = this.playInFlight || !anim.canNext;
12176
+ this.playButton.textContent = this.playInFlight ? "Stop" : "Play";
12177
+ this.playButton.classList.toggle("is-active", this.playInFlight);
12178
+ this.playButton.setAttribute("aria-pressed", this.playInFlight ? "true" : "false");
12179
+ this.playButton.disabled = this.playInFlight ? false : !anim.canNext;
12070
12180
  this.syncToggleUi();
12071
12181
  }
12072
12182
  getStepTarget(stepItem) {
@@ -12972,6 +13082,10 @@ class SketchmarkEmbed {
12972
13082
  this.btnPrev.addEventListener("click", () => this.prevStep());
12973
13083
  this.btnNext.addEventListener("click", () => this.nextStep());
12974
13084
  this.btnPlay.addEventListener("click", () => {
13085
+ if (this.playInFlight) {
13086
+ this.stopPlayback();
13087
+ return;
13088
+ }
12975
13089
  void this.play();
12976
13090
  });
12977
13091
  this.btnCaption.addEventListener("click", () => this.setCaptionVisible(!this.showCaption));
@@ -13029,6 +13143,7 @@ class SketchmarkEmbed {
13029
13143
  }
13030
13144
  this.clearError();
13031
13145
  this.stopMotion();
13146
+ this.playInFlight = false;
13032
13147
  this.animUnsub?.();
13033
13148
  this.animUnsub = null;
13034
13149
  this.instance?.anim?.destroy();
@@ -13101,9 +13216,15 @@ class SketchmarkEmbed {
13101
13216
  this.syncControls();
13102
13217
  }
13103
13218
  }
13219
+ stopPlayback() {
13220
+ this.playInFlight = false;
13221
+ this.instance?.anim.stop();
13222
+ this.syncControls();
13223
+ }
13104
13224
  nextStep() {
13105
13225
  if (!this.instance)
13106
13226
  return;
13227
+ this.playInFlight = false;
13107
13228
  this.instance.anim.next();
13108
13229
  this.syncControls();
13109
13230
  if (this.options.autoFocus !== false && this.options.autoFocusOnStep !== false) {
@@ -13113,6 +13234,7 @@ class SketchmarkEmbed {
13113
13234
  prevStep() {
13114
13235
  if (!this.instance)
13115
13236
  return;
13237
+ this.playInFlight = false;
13116
13238
  this.instance.anim.prev();
13117
13239
  this.syncControls();
13118
13240
  if (this.options.autoFocus !== false && this.options.autoFocusOnStep !== false) {
@@ -13122,6 +13244,7 @@ class SketchmarkEmbed {
13122
13244
  resetAnimation() {
13123
13245
  if (!this.instance)
13124
13246
  return;
13247
+ this.playInFlight = false;
13125
13248
  this.instance.anim.reset();
13126
13249
  this.syncControls();
13127
13250
  }
@@ -13148,6 +13271,7 @@ class SketchmarkEmbed {
13148
13271
  }
13149
13272
  destroy() {
13150
13273
  this.stopMotion();
13274
+ this.playInFlight = false;
13151
13275
  this.animUnsub?.();
13152
13276
  this.instance?.anim?.destroy();
13153
13277
  this.instance = null;
@@ -13180,6 +13304,9 @@ class SketchmarkEmbed {
13180
13304
  this.btnRestart.disabled = true;
13181
13305
  this.btnPrev.disabled = true;
13182
13306
  this.btnNext.disabled = true;
13307
+ this.btnPlay.textContent = "Play";
13308
+ this.btnPlay.classList.remove("is-active");
13309
+ this.btnPlay.setAttribute("aria-pressed", "false");
13183
13310
  this.btnPlay.disabled = true;
13184
13311
  return;
13185
13312
  }
@@ -13188,7 +13315,10 @@ class SketchmarkEmbed {
13188
13315
  this.btnRestart.disabled = false;
13189
13316
  this.btnPrev.disabled = !anim.canPrev;
13190
13317
  this.btnNext.disabled = !anim.canNext;
13191
- this.btnPlay.disabled = this.playInFlight || !anim.canNext;
13318
+ this.btnPlay.textContent = this.playInFlight ? "Stop" : "Play";
13319
+ this.btnPlay.classList.toggle("is-active", this.playInFlight);
13320
+ this.btnPlay.setAttribute("aria-pressed", this.playInFlight ? "true" : "false");
13321
+ this.btnPlay.disabled = this.playInFlight ? false : !anim.canNext;
13192
13322
  }
13193
13323
  syncViewControls() {
13194
13324
  const hasView = !!this.instance?.svg;