sketchmark 0.2.8 → 1.0.1
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 +207 -53
- package/dist/animation/index.d.ts +50 -10
- package/dist/animation/index.d.ts.map +1 -1
- package/dist/ast/types.d.ts +10 -2
- package/dist/ast/types.d.ts.map +1 -1
- package/dist/config.d.ts +12 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/index.cjs +603 -50
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +603 -50
- package/dist/index.js.map +1 -1
- package/dist/parser/index.d.ts.map +1 -1
- package/dist/parser/tokenizer.d.ts.map +1 -1
- package/dist/scene/index.d.ts +2 -2
- package/dist/scene/index.d.ts.map +1 -1
- package/dist/sketchmark.iife.js +604 -51
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -66,6 +66,15 @@ const KEYWORDS = new Set([
|
|
|
66
66
|
"tree",
|
|
67
67
|
"force",
|
|
68
68
|
"markdown",
|
|
69
|
+
"narrate",
|
|
70
|
+
"pace",
|
|
71
|
+
"slow",
|
|
72
|
+
"fast",
|
|
73
|
+
"pause",
|
|
74
|
+
"beat",
|
|
75
|
+
"underline",
|
|
76
|
+
"crossout",
|
|
77
|
+
"bracket",
|
|
69
78
|
]);
|
|
70
79
|
const ARROW_PATTERNS = ["<-->", "<->", "-->", "<--", "->", "<-", "---", "--"];
|
|
71
80
|
// Characters that can start an arrow pattern — used to decide whether a '-'
|
|
@@ -675,7 +684,18 @@ function parse(src) {
|
|
|
675
684
|
target = `${toks[1].value}${toks[2].value}${toks[3].value}`;
|
|
676
685
|
}
|
|
677
686
|
const step = { kind: "step", action, target };
|
|
678
|
-
|
|
687
|
+
// narrate: text is the value, not a target
|
|
688
|
+
if (action === "narrate") {
|
|
689
|
+
step.target = "";
|
|
690
|
+
step.value = toks[1]?.value ?? "";
|
|
691
|
+
}
|
|
692
|
+
// bracket: needs two targets
|
|
693
|
+
if (action === "bracket" && toks.length >= 3) {
|
|
694
|
+
step.target = toks[1]?.value ?? "";
|
|
695
|
+
step.target2 = toks[2]?.value ?? "";
|
|
696
|
+
}
|
|
697
|
+
const kvStart = action === "bracket" ? 3 : 2;
|
|
698
|
+
for (let j = kvStart; j < toks.length; j++) {
|
|
679
699
|
const k = toks[j]?.value;
|
|
680
700
|
const eq = toks[j + 1];
|
|
681
701
|
const vt = toks[j + 2];
|
|
@@ -721,6 +741,11 @@ function parse(src) {
|
|
|
721
741
|
j += 2;
|
|
722
742
|
continue;
|
|
723
743
|
}
|
|
744
|
+
if (k === "pace") {
|
|
745
|
+
step.pace = vt.value;
|
|
746
|
+
j += 2;
|
|
747
|
+
continue;
|
|
748
|
+
}
|
|
724
749
|
}
|
|
725
750
|
// bare key value (legacy)
|
|
726
751
|
if (k === "delay" && eq?.type === "NUMBER") {
|
|
@@ -1116,6 +1141,31 @@ function parse(src) {
|
|
|
1116
1141
|
ast.rootOrder.push({ kind: "node", id: note.id });
|
|
1117
1142
|
continue;
|
|
1118
1143
|
}
|
|
1144
|
+
// beat { ... } — parallel steps
|
|
1145
|
+
if (v === "beat") {
|
|
1146
|
+
skip(); // 'beat'
|
|
1147
|
+
skipNL();
|
|
1148
|
+
if (cur().type === "LBRACE") {
|
|
1149
|
+
skip();
|
|
1150
|
+
skipNL();
|
|
1151
|
+
}
|
|
1152
|
+
const children = [];
|
|
1153
|
+
while (cur().type !== "RBRACE" && cur().value !== "end" && cur().type !== "EOF") {
|
|
1154
|
+
skipNL();
|
|
1155
|
+
if (cur().type === "RBRACE")
|
|
1156
|
+
break;
|
|
1157
|
+
if (cur().value === "step") {
|
|
1158
|
+
children.push(parseStep());
|
|
1159
|
+
}
|
|
1160
|
+
else {
|
|
1161
|
+
skip();
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
if (cur().type === "RBRACE")
|
|
1165
|
+
skip();
|
|
1166
|
+
ast.steps.push({ kind: "beat", children });
|
|
1167
|
+
continue;
|
|
1168
|
+
}
|
|
1119
1169
|
// step
|
|
1120
1170
|
if (v === "step") {
|
|
1121
1171
|
ast.steps.push(parseStep());
|
|
@@ -1307,6 +1357,20 @@ const ANIMATION = {
|
|
|
1307
1357
|
fillFadeOffset: -60, // fill-opacity start relative to stroke end (ms)
|
|
1308
1358
|
textDelay: 80, // extra buffer before text reveals (ms)
|
|
1309
1359
|
chartFade: 500, // chart/markdown opacity transition (ms)
|
|
1360
|
+
// Pace
|
|
1361
|
+
paceSlowMul: 2.0, // slow pace duration multiplier
|
|
1362
|
+
paceFastMul: 0.5, // fast pace duration multiplier
|
|
1363
|
+
pauseHoldMs: 1500, // extra hold time for pause pace (ms)
|
|
1364
|
+
// Narration
|
|
1365
|
+
narrationFadeMs: 300, // caption fade-in/out duration (ms)
|
|
1366
|
+
narrationTypeMs: 30, // per-character typing speed for narration (ms)
|
|
1367
|
+
// Text writing reveal
|
|
1368
|
+
textRevealMs: 400, // text clip-reveal duration (ms)
|
|
1369
|
+
// Annotations
|
|
1370
|
+
annotationStrokeDur: 300, // annotation draw-in duration (ms)
|
|
1371
|
+
annotationColor: '#c85428', // default annotation color
|
|
1372
|
+
annotationStrokeW: 2.5, // annotation stroke width
|
|
1373
|
+
pointerSize: 8, // default pointer dot radius
|
|
1310
1374
|
};
|
|
1311
1375
|
// ── Export defaults ────────────────────────────────────────
|
|
1312
1376
|
const EXPORT = {
|
|
@@ -6842,6 +6906,51 @@ function prepareNodeForDraw(el) {
|
|
|
6842
6906
|
function revealNodeInstant(el) {
|
|
6843
6907
|
clearNodeDrawStyles(el);
|
|
6844
6908
|
}
|
|
6909
|
+
// ── Text writing reveal (clipPath) ───────────────────────
|
|
6910
|
+
function animateTextReveal(textEl, delayMs, durationMs = ANIMATION.textRevealMs) {
|
|
6911
|
+
const ownerSvg = textEl.ownerSVGElement;
|
|
6912
|
+
if (!ownerSvg) {
|
|
6913
|
+
// fallback: just fade
|
|
6914
|
+
textEl.style.transition = `opacity ${ANIMATION.textFade}ms ease ${delayMs}ms`;
|
|
6915
|
+
textEl.style.opacity = "1";
|
|
6916
|
+
return;
|
|
6917
|
+
}
|
|
6918
|
+
// Make text visible but clipped to zero width
|
|
6919
|
+
textEl.style.opacity = "1";
|
|
6920
|
+
// We need to wait for text to be visible before we can measure it
|
|
6921
|
+
setTimeout(() => {
|
|
6922
|
+
const bbox = textEl.getBBox?.();
|
|
6923
|
+
if (!bbox || bbox.width === 0) {
|
|
6924
|
+
// fallback if can't measure
|
|
6925
|
+
return;
|
|
6926
|
+
}
|
|
6927
|
+
let defs = ownerSvg.querySelector("defs");
|
|
6928
|
+
if (!defs) {
|
|
6929
|
+
defs = document.createElementNS(SVG_NS$1, "defs");
|
|
6930
|
+
ownerSvg.insertBefore(defs, ownerSvg.firstChild);
|
|
6931
|
+
}
|
|
6932
|
+
const clipId = `skm-clip-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
6933
|
+
const clipPath = document.createElementNS(SVG_NS$1, "clipPath");
|
|
6934
|
+
clipPath.setAttribute("id", clipId);
|
|
6935
|
+
const rect = document.createElementNS(SVG_NS$1, "rect");
|
|
6936
|
+
rect.setAttribute("x", String(bbox.x - 2));
|
|
6937
|
+
rect.setAttribute("y", String(bbox.y - 2));
|
|
6938
|
+
rect.setAttribute("width", "0");
|
|
6939
|
+
rect.setAttribute("height", String(bbox.height + 4));
|
|
6940
|
+
clipPath.appendChild(rect);
|
|
6941
|
+
defs.appendChild(clipPath);
|
|
6942
|
+
textEl.setAttribute("clip-path", `url(#${clipId})`);
|
|
6943
|
+
requestAnimationFrame(() => requestAnimationFrame(() => {
|
|
6944
|
+
rect.style.transition = `width ${durationMs}ms cubic-bezier(.4,0,.2,1)`;
|
|
6945
|
+
rect.setAttribute("width", String(bbox.width + 4));
|
|
6946
|
+
}));
|
|
6947
|
+
// Cleanup after animation
|
|
6948
|
+
setTimeout(() => {
|
|
6949
|
+
textEl.removeAttribute("clip-path");
|
|
6950
|
+
clipPath.remove();
|
|
6951
|
+
}, durationMs + 50);
|
|
6952
|
+
}, delayMs);
|
|
6953
|
+
}
|
|
6845
6954
|
function animateNodeDraw(el, strokeDur = ANIMATION.nodeStrokeDur) {
|
|
6846
6955
|
const guide = nodeGuidePathEl(el);
|
|
6847
6956
|
if (!guide) {
|
|
@@ -6865,12 +6974,11 @@ function animateNodeDraw(el, strokeDur = ANIMATION.nodeStrokeDur) {
|
|
|
6865
6974
|
p.style.opacity = "1";
|
|
6866
6975
|
});
|
|
6867
6976
|
if (text) {
|
|
6868
|
-
text
|
|
6869
|
-
text.style.opacity = "1";
|
|
6977
|
+
animateTextReveal(text, textDelay);
|
|
6870
6978
|
}
|
|
6871
6979
|
setTimeout(() => {
|
|
6872
6980
|
clearNodeDrawStyles(el);
|
|
6873
|
-
}, textDelay + ANIMATION.
|
|
6981
|
+
}, textDelay + ANIMATION.textRevealMs + 80);
|
|
6874
6982
|
}));
|
|
6875
6983
|
}
|
|
6876
6984
|
// ── Arrow connector parser ────────────────────────────────
|
|
@@ -6887,10 +6995,21 @@ function parseEdgeTarget(target) {
|
|
|
6887
6995
|
}
|
|
6888
6996
|
return null;
|
|
6889
6997
|
}
|
|
6998
|
+
// ── Step flattening helper ────────────────────────────────
|
|
6999
|
+
function flattenSteps(items) {
|
|
7000
|
+
const out = [];
|
|
7001
|
+
for (const item of items) {
|
|
7002
|
+
if (item.kind === "beat")
|
|
7003
|
+
out.push(...item.children);
|
|
7004
|
+
else
|
|
7005
|
+
out.push(item);
|
|
7006
|
+
}
|
|
7007
|
+
return out;
|
|
7008
|
+
}
|
|
6890
7009
|
// ── Draw target helpers ───────────────────────────────────
|
|
6891
7010
|
function getDrawTargetEdgeIds(steps) {
|
|
6892
7011
|
const ids = new Set();
|
|
6893
|
-
for (const s of steps) {
|
|
7012
|
+
for (const s of flattenSteps(steps)) {
|
|
6894
7013
|
if (s.action !== "draw")
|
|
6895
7014
|
continue;
|
|
6896
7015
|
const e = parseEdgeTarget(s.target);
|
|
@@ -6901,7 +7020,7 @@ function getDrawTargetEdgeIds(steps) {
|
|
|
6901
7020
|
}
|
|
6902
7021
|
function getDrawTargetNodeIds(steps) {
|
|
6903
7022
|
const ids = new Set();
|
|
6904
|
-
for (const s of steps) {
|
|
7023
|
+
for (const s of flattenSteps(steps)) {
|
|
6905
7024
|
if (s.action !== "draw" || parseEdgeTarget(s.target))
|
|
6906
7025
|
continue;
|
|
6907
7026
|
ids.add(`node-${s.target}`);
|
|
@@ -7038,29 +7157,41 @@ class AnimationController {
|
|
|
7038
7157
|
get drawTargets() {
|
|
7039
7158
|
return this.drawTargetEdges;
|
|
7040
7159
|
}
|
|
7041
|
-
constructor(svg, steps) {
|
|
7160
|
+
constructor(svg, steps, _container, _rc, _config) {
|
|
7042
7161
|
this.svg = svg;
|
|
7043
7162
|
this.steps = steps;
|
|
7163
|
+
this._container = _container;
|
|
7164
|
+
this._rc = _rc;
|
|
7165
|
+
this._config = _config;
|
|
7044
7166
|
this._step = -1;
|
|
7045
7167
|
this._pendingStepTimers = new Set();
|
|
7046
7168
|
this._transforms = new Map();
|
|
7047
7169
|
this._listeners = [];
|
|
7170
|
+
// ── Narration caption ──
|
|
7171
|
+
this._captionEl = null;
|
|
7172
|
+
this._captionTextEl = null;
|
|
7173
|
+
// ── Annotations ──
|
|
7174
|
+
this._annotationLayer = null;
|
|
7175
|
+
this._annotations = [];
|
|
7176
|
+
// ── Pointer ──
|
|
7177
|
+
this._pointerEl = null;
|
|
7178
|
+
this._pointerType = 'none';
|
|
7179
|
+
// ── TTS ──
|
|
7180
|
+
this._tts = false;
|
|
7181
|
+
this._speechDone = null;
|
|
7048
7182
|
this.drawTargetEdges = getDrawTargetEdgeIds(steps);
|
|
7049
7183
|
this.drawTargetNodes = getDrawTargetNodeIds(steps);
|
|
7050
7184
|
// Groups: non-edge draw steps whose target has a #group-{id} element in the SVG.
|
|
7051
|
-
// We detect this at construction time (after render) so we correctly distinguish
|
|
7052
|
-
// a group ID from a node ID without needing extra metadata.
|
|
7053
7185
|
this.drawTargetGroups = new Set();
|
|
7054
7186
|
this.drawTargetTables = new Set();
|
|
7055
7187
|
this.drawTargetNotes = new Set();
|
|
7056
7188
|
this.drawTargetCharts = new Set();
|
|
7057
7189
|
this.drawTargetMarkdowns = new Set();
|
|
7058
|
-
for (const s of steps) {
|
|
7190
|
+
for (const s of flattenSteps(steps)) {
|
|
7059
7191
|
if (s.action !== "draw" || parseEdgeTarget(s.target))
|
|
7060
7192
|
continue;
|
|
7061
7193
|
if (svg.querySelector(`#group-${s.target}`)) {
|
|
7062
7194
|
this.drawTargetGroups.add(`group-${s.target}`);
|
|
7063
|
-
// Remove from node targets if it was accidentally added
|
|
7064
7195
|
this.drawTargetNodes.delete(`node-${s.target}`);
|
|
7065
7196
|
}
|
|
7066
7197
|
if (svg.querySelector(`#table-${s.target}`)) {
|
|
@@ -7081,7 +7212,31 @@ class AnimationController {
|
|
|
7081
7212
|
}
|
|
7082
7213
|
}
|
|
7083
7214
|
this._clearAll();
|
|
7084
|
-
|
|
7215
|
+
// Init narration caption
|
|
7216
|
+
if (this._container)
|
|
7217
|
+
this._initCaption();
|
|
7218
|
+
// Init annotation layer
|
|
7219
|
+
this._annotationLayer = document.createElementNS(SVG_NS$1, "g");
|
|
7220
|
+
this._annotationLayer.setAttribute("id", "annotation-layer");
|
|
7221
|
+
this._annotationLayer.style.pointerEvents = "none";
|
|
7222
|
+
this.svg.appendChild(this._annotationLayer);
|
|
7223
|
+
// Init pointer
|
|
7224
|
+
this._pointerType = (this._config?.pointer ?? "none");
|
|
7225
|
+
if (this._pointerType !== "none")
|
|
7226
|
+
this._initPointer();
|
|
7227
|
+
// Init TTS from config: `config tts=on`
|
|
7228
|
+
this._tts = this._config?.tts === true || this._config?.tts === "on";
|
|
7229
|
+
if (this._tts)
|
|
7230
|
+
this._warmUpSpeech();
|
|
7231
|
+
}
|
|
7232
|
+
/** The narration caption element — mount it anywhere via `yourContainer.appendChild(anim.captionElement)` */
|
|
7233
|
+
get captionElement() {
|
|
7234
|
+
return this._captionEl;
|
|
7235
|
+
}
|
|
7236
|
+
/** Enable/disable browser text-to-speech for narrate steps */
|
|
7237
|
+
get tts() { return this._tts; }
|
|
7238
|
+
set tts(on) { this._tts = on; if (!on)
|
|
7239
|
+
this._cancelSpeech(); }
|
|
7085
7240
|
get currentStep() {
|
|
7086
7241
|
return this._step;
|
|
7087
7242
|
}
|
|
@@ -7118,6 +7273,17 @@ class AnimationController {
|
|
|
7118
7273
|
this._clearAll();
|
|
7119
7274
|
this.emit("animation-reset");
|
|
7120
7275
|
}
|
|
7276
|
+
/** Remove caption and annotation layer from the DOM */
|
|
7277
|
+
destroy() {
|
|
7278
|
+
this._clearAll();
|
|
7279
|
+
this._captionEl?.remove();
|
|
7280
|
+
this._captionEl = null;
|
|
7281
|
+
this._captionTextEl = null;
|
|
7282
|
+
this._annotationLayer?.remove();
|
|
7283
|
+
this._annotationLayer = null;
|
|
7284
|
+
this._pointerEl?.remove();
|
|
7285
|
+
this._pointerEl = null;
|
|
7286
|
+
}
|
|
7121
7287
|
next() {
|
|
7122
7288
|
if (!this.canNext)
|
|
7123
7289
|
return false;
|
|
@@ -7143,7 +7309,11 @@ class AnimationController {
|
|
|
7143
7309
|
while (this.canNext) {
|
|
7144
7310
|
const nextStep = this.steps[this._step + 1];
|
|
7145
7311
|
this.next();
|
|
7146
|
-
|
|
7312
|
+
// Wait for timer AND speech to finish (whichever is longer)
|
|
7313
|
+
await Promise.all([
|
|
7314
|
+
new Promise((r) => setTimeout(r, this._playbackWaitMs(nextStep, msPerStep))),
|
|
7315
|
+
this._speechDone ?? Promise.resolve(),
|
|
7316
|
+
]);
|
|
7147
7317
|
}
|
|
7148
7318
|
}
|
|
7149
7319
|
goTo(index) {
|
|
@@ -7175,15 +7345,48 @@ class AnimationController {
|
|
|
7175
7345
|
}, delayMs);
|
|
7176
7346
|
this._pendingStepTimers.add(id);
|
|
7177
7347
|
}
|
|
7348
|
+
_stepWaitMs(step, fallbackMs) {
|
|
7349
|
+
const delay = Math.max(0, step.delay ?? 0);
|
|
7350
|
+
const duration = Math.max(0, step.duration ?? 0);
|
|
7351
|
+
// Compute minimum time the step actually needs to finish
|
|
7352
|
+
let minNeeded = 0;
|
|
7353
|
+
if (step.action === "narrate") {
|
|
7354
|
+
const text = step.value ?? "";
|
|
7355
|
+
// Typing effect: chars × typeMs + fade buffer
|
|
7356
|
+
const typingMs = text.length * ANIMATION.narrationTypeMs + ANIMATION.narrationFadeMs;
|
|
7357
|
+
// TTS estimate: ~150ms per word + 500ms buffer for engine latency
|
|
7358
|
+
const wordCount = text.split(/\s+/).filter(Boolean).length;
|
|
7359
|
+
const ttsMs = this._tts ? wordCount * 150 + 500 : 0;
|
|
7360
|
+
minNeeded = Math.max(typingMs, ttsMs);
|
|
7361
|
+
}
|
|
7362
|
+
else if (step.action === "circle" || step.action === "underline" ||
|
|
7363
|
+
step.action === "crossout" || step.action === "bracket") {
|
|
7364
|
+
// Annotation guide draw + rough reveal + pointer fade
|
|
7365
|
+
minNeeded = ANIMATION.annotationStrokeDur + 120 + 200;
|
|
7366
|
+
}
|
|
7367
|
+
else if (step.action === "draw") {
|
|
7368
|
+
minNeeded = ANIMATION.nodeStrokeDur + ANIMATION.textRevealMs + 80;
|
|
7369
|
+
}
|
|
7370
|
+
let wait = delay + Math.max(fallbackMs, duration, minNeeded);
|
|
7371
|
+
if (step.pace === "slow")
|
|
7372
|
+
wait *= ANIMATION.paceSlowMul;
|
|
7373
|
+
else if (step.pace === "fast")
|
|
7374
|
+
wait *= ANIMATION.paceFastMul;
|
|
7375
|
+
else if (step.pace === "pause")
|
|
7376
|
+
wait += ANIMATION.pauseHoldMs;
|
|
7377
|
+
return wait;
|
|
7378
|
+
}
|
|
7178
7379
|
_playbackWaitMs(step, fallbackMs) {
|
|
7179
7380
|
if (!step)
|
|
7180
7381
|
return fallbackMs;
|
|
7181
|
-
|
|
7182
|
-
|
|
7183
|
-
|
|
7382
|
+
if (step.kind === "beat") {
|
|
7383
|
+
return Math.max(fallbackMs, ...step.children.map((c) => this._stepWaitMs(c, fallbackMs)));
|
|
7384
|
+
}
|
|
7385
|
+
return this._stepWaitMs(step, fallbackMs);
|
|
7184
7386
|
}
|
|
7185
7387
|
_clearAll() {
|
|
7186
7388
|
this._clearPendingStepTimers();
|
|
7389
|
+
this._cancelSpeech();
|
|
7187
7390
|
this._transforms.clear();
|
|
7188
7391
|
// Nodes
|
|
7189
7392
|
this.svg.querySelectorAll(".ng").forEach((el) => {
|
|
@@ -7294,17 +7497,52 @@ class AnimationController {
|
|
|
7294
7497
|
el.style.opacity = "";
|
|
7295
7498
|
el.classList.remove("hl", "faded");
|
|
7296
7499
|
});
|
|
7500
|
+
// Clear narration caption
|
|
7501
|
+
if (this._captionEl) {
|
|
7502
|
+
this._captionEl.style.opacity = "0";
|
|
7503
|
+
if (this._captionTextEl)
|
|
7504
|
+
this._captionTextEl.textContent = "";
|
|
7505
|
+
}
|
|
7506
|
+
// Clear annotations
|
|
7507
|
+
this._annotations.forEach((a) => a.remove());
|
|
7508
|
+
this._annotations = [];
|
|
7509
|
+
// Clear pointer
|
|
7510
|
+
if (this._pointerEl) {
|
|
7511
|
+
this._pointerEl.setAttribute("opacity", "0");
|
|
7512
|
+
this._pointerEl.style.transition = "none";
|
|
7513
|
+
}
|
|
7297
7514
|
}
|
|
7298
7515
|
_applyStep(i, silent) {
|
|
7299
|
-
const
|
|
7300
|
-
if (!
|
|
7516
|
+
const item = this.steps[i];
|
|
7517
|
+
if (!item)
|
|
7301
7518
|
return;
|
|
7302
|
-
const run = () => this._runStep(s, silent);
|
|
7303
7519
|
if (silent) {
|
|
7304
|
-
|
|
7520
|
+
this._runStepItem(item, true);
|
|
7305
7521
|
return;
|
|
7306
7522
|
}
|
|
7307
|
-
|
|
7523
|
+
if (item.kind === "beat") {
|
|
7524
|
+
for (const child of item.children) {
|
|
7525
|
+
const run = () => this._runStep(child, false);
|
|
7526
|
+
this._scheduleStep(run, Math.max(0, child.delay ?? 0));
|
|
7527
|
+
}
|
|
7528
|
+
}
|
|
7529
|
+
else {
|
|
7530
|
+
let delayMs = Math.max(0, item.delay ?? 0);
|
|
7531
|
+
if (item.pace === "slow")
|
|
7532
|
+
delayMs *= ANIMATION.paceSlowMul;
|
|
7533
|
+
else if (item.pace === "fast")
|
|
7534
|
+
delayMs *= ANIMATION.paceFastMul;
|
|
7535
|
+
this._scheduleStep(() => this._runStep(item, false), delayMs);
|
|
7536
|
+
}
|
|
7537
|
+
}
|
|
7538
|
+
_runStepItem(item, silent) {
|
|
7539
|
+
if (item.kind === "beat") {
|
|
7540
|
+
for (const child of item.children)
|
|
7541
|
+
this._runStep(child, silent);
|
|
7542
|
+
}
|
|
7543
|
+
else {
|
|
7544
|
+
this._runStep(item, silent);
|
|
7545
|
+
}
|
|
7308
7546
|
}
|
|
7309
7547
|
_runStep(s, silent) {
|
|
7310
7548
|
switch (s.action) {
|
|
@@ -7345,6 +7583,21 @@ class AnimationController {
|
|
|
7345
7583
|
case "rotate":
|
|
7346
7584
|
this._doRotate(s.target, s, silent);
|
|
7347
7585
|
break;
|
|
7586
|
+
case "narrate":
|
|
7587
|
+
this._doNarrate(s.value ?? "", silent);
|
|
7588
|
+
break;
|
|
7589
|
+
case "circle":
|
|
7590
|
+
this._doAnnotationCircle(s.target, silent);
|
|
7591
|
+
break;
|
|
7592
|
+
case "underline":
|
|
7593
|
+
this._doAnnotationUnderline(s.target, silent);
|
|
7594
|
+
break;
|
|
7595
|
+
case "crossout":
|
|
7596
|
+
this._doAnnotationCrossout(s.target, silent);
|
|
7597
|
+
break;
|
|
7598
|
+
case "bracket":
|
|
7599
|
+
this._doAnnotationBracket(s.target, s.target2 ?? "", silent);
|
|
7600
|
+
break;
|
|
7348
7601
|
}
|
|
7349
7602
|
}
|
|
7350
7603
|
// ── highlight ────────────────────────────────────────────
|
|
@@ -7647,40 +7900,333 @@ class AnimationController {
|
|
|
7647
7900
|
});
|
|
7648
7901
|
}
|
|
7649
7902
|
}
|
|
7903
|
+
// ── narration ───────────────────────────────────────────
|
|
7904
|
+
_initCaption() {
|
|
7905
|
+
// Remove any leftover caption from a previous instance
|
|
7906
|
+
document.querySelector('.skm-caption')?.remove();
|
|
7907
|
+
const cap = document.createElement("div");
|
|
7908
|
+
cap.className = "skm-caption";
|
|
7909
|
+
cap.style.cssText = `
|
|
7910
|
+
position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%);
|
|
7911
|
+
z-index: 9999; max-width: 600px; width: max-content;
|
|
7912
|
+
padding: 10px 24px; box-sizing: border-box;
|
|
7913
|
+
font-family: var(--font-sans, system-ui, sans-serif);
|
|
7914
|
+
font-size: 15px; line-height: 1.5;
|
|
7915
|
+
color: #fde68a; background: #1a1208;
|
|
7916
|
+
border-radius: 8px;
|
|
7917
|
+
box-shadow: 0 4px 24px rgba(0,0,0,0.35);
|
|
7918
|
+
opacity: 0; transition: opacity ${ANIMATION.narrationFadeMs}ms ease;
|
|
7919
|
+
pointer-events: none; user-select: none;
|
|
7920
|
+
text-align: center;
|
|
7921
|
+
`;
|
|
7922
|
+
const span = document.createElement("span");
|
|
7923
|
+
cap.appendChild(span);
|
|
7924
|
+
document.body.appendChild(cap);
|
|
7925
|
+
this._captionEl = cap;
|
|
7926
|
+
this._captionTextEl = span;
|
|
7927
|
+
}
|
|
7928
|
+
_doNarrate(text, silent) {
|
|
7929
|
+
if (!this._captionEl || !this._captionTextEl)
|
|
7930
|
+
return;
|
|
7931
|
+
this._captionEl.style.opacity = "1";
|
|
7932
|
+
if (silent || !text) {
|
|
7933
|
+
this._captionTextEl.textContent = text;
|
|
7934
|
+
return;
|
|
7935
|
+
}
|
|
7936
|
+
// Fire TTS as full sentence — play() waits for _speechDone
|
|
7937
|
+
if (this._tts && text)
|
|
7938
|
+
this._speak(text);
|
|
7939
|
+
// Typing effect
|
|
7940
|
+
this._captionTextEl.textContent = "";
|
|
7941
|
+
let charIdx = 0;
|
|
7942
|
+
const typeNext = () => {
|
|
7943
|
+
if (charIdx < text.length) {
|
|
7944
|
+
this._captionTextEl.textContent += text[charIdx++];
|
|
7945
|
+
const id = window.setTimeout(typeNext, ANIMATION.narrationTypeMs);
|
|
7946
|
+
this._pendingStepTimers.add(id);
|
|
7947
|
+
}
|
|
7948
|
+
};
|
|
7949
|
+
typeNext();
|
|
7950
|
+
}
|
|
7951
|
+
_speak(text) {
|
|
7952
|
+
if (typeof speechSynthesis === "undefined")
|
|
7953
|
+
return;
|
|
7954
|
+
this._cancelSpeech();
|
|
7955
|
+
const utter = new SpeechSynthesisUtterance(text);
|
|
7956
|
+
utter.rate = 0.95;
|
|
7957
|
+
utter.pitch = 1;
|
|
7958
|
+
utter.lang = "en-US";
|
|
7959
|
+
// Track when speech actually finishes
|
|
7960
|
+
this._speechDone = new Promise((resolve) => {
|
|
7961
|
+
utter.onend = () => resolve();
|
|
7962
|
+
utter.onerror = () => resolve();
|
|
7963
|
+
});
|
|
7964
|
+
speechSynthesis.speak(utter);
|
|
7965
|
+
}
|
|
7966
|
+
_cancelSpeech() {
|
|
7967
|
+
if (typeof speechSynthesis !== "undefined")
|
|
7968
|
+
speechSynthesis.cancel();
|
|
7969
|
+
this._speechDone = null;
|
|
7970
|
+
}
|
|
7971
|
+
/** Pre-warm the speech engine with a silent utterance to eliminate cold-start delay */
|
|
7972
|
+
_warmUpSpeech() {
|
|
7973
|
+
if (typeof speechSynthesis === "undefined")
|
|
7974
|
+
return;
|
|
7975
|
+
const warm = new SpeechSynthesisUtterance("");
|
|
7976
|
+
warm.volume = 0;
|
|
7977
|
+
speechSynthesis.speak(warm);
|
|
7978
|
+
}
|
|
7979
|
+
// ── annotations ─────────────────────────────────────────
|
|
7980
|
+
_nodeMetrics(el) {
|
|
7981
|
+
const x = parseFloat(el.dataset.x ?? "");
|
|
7982
|
+
const y = parseFloat(el.dataset.y ?? "");
|
|
7983
|
+
const w = parseFloat(el.dataset.w ?? "");
|
|
7984
|
+
const h = parseFloat(el.dataset.h ?? "");
|
|
7985
|
+
if (isNaN(x) || isNaN(y) || isNaN(w) || isNaN(h))
|
|
7986
|
+
return null;
|
|
7987
|
+
return { x, y, w, h };
|
|
7988
|
+
}
|
|
7989
|
+
/**
|
|
7990
|
+
* Animate an annotation using the same guide-path approach as node draw:
|
|
7991
|
+
* 1. Hide the rough.js element (opacity=0)
|
|
7992
|
+
* 2. Create a clean single guide path and animate it with stroke-dashoffset
|
|
7993
|
+
* 3. Pointer follows the guide path
|
|
7994
|
+
* 4. After guide finishes → fade in rough.js element, remove guide
|
|
7995
|
+
*/
|
|
7996
|
+
_animateAnnotation(roughEl, guideD, silent) {
|
|
7997
|
+
if (silent)
|
|
7998
|
+
return;
|
|
7999
|
+
// Hide rough.js element — will be revealed after guide draws
|
|
8000
|
+
roughEl.style.opacity = "0";
|
|
8001
|
+
roughEl.style.transition = "none";
|
|
8002
|
+
// Create a clean guide path
|
|
8003
|
+
const guide = document.createElementNS(SVG_NS$1, "path");
|
|
8004
|
+
guide.setAttribute("d", guideD);
|
|
8005
|
+
guide.setAttribute("fill", "none");
|
|
8006
|
+
guide.setAttribute("stroke", ANIMATION.annotationColor);
|
|
8007
|
+
guide.setAttribute("stroke-width", String(ANIMATION.annotationStrokeW));
|
|
8008
|
+
guide.setAttribute("stroke-linecap", "round");
|
|
8009
|
+
guide.setAttribute("stroke-linejoin", "round");
|
|
8010
|
+
guide.style.pointerEvents = "none";
|
|
8011
|
+
this._annotationLayer.appendChild(guide);
|
|
8012
|
+
const len = pathLength(guide);
|
|
8013
|
+
guide.style.strokeDasharray = `${len}`;
|
|
8014
|
+
guide.style.strokeDashoffset = `${len}`;
|
|
8015
|
+
guide.style.transition = "none";
|
|
8016
|
+
// Pre-position pointer at the start of the guide
|
|
8017
|
+
const hasPointer = !!this._pointerEl;
|
|
8018
|
+
if (hasPointer) {
|
|
8019
|
+
try {
|
|
8020
|
+
const startPt = guide.getPointAtLength(0);
|
|
8021
|
+
this._pointerEl.setAttribute("transform", `translate(${startPt.x},${startPt.y})`);
|
|
8022
|
+
}
|
|
8023
|
+
catch { /* ignore */ }
|
|
8024
|
+
this._pointerEl.setAttribute("opacity", "1");
|
|
8025
|
+
this._pointerEl.style.transition = "none";
|
|
8026
|
+
}
|
|
8027
|
+
const dur = ANIMATION.annotationStrokeDur;
|
|
8028
|
+
requestAnimationFrame(() => requestAnimationFrame(() => {
|
|
8029
|
+
// Animate guide stroke-dashoffset
|
|
8030
|
+
guide.style.transition = `stroke-dashoffset ${dur}ms cubic-bezier(.4,0,.2,1)`;
|
|
8031
|
+
guide.style.strokeDashoffset = "0";
|
|
8032
|
+
// Animate pointer along guide path
|
|
8033
|
+
if (hasPointer) {
|
|
8034
|
+
const startTime = performance.now();
|
|
8035
|
+
const pointerRef = this._pointerEl;
|
|
8036
|
+
const animate = () => {
|
|
8037
|
+
const elapsed = performance.now() - startTime;
|
|
8038
|
+
const t = Math.min(elapsed / dur, 1);
|
|
8039
|
+
const eased = t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
|
|
8040
|
+
try {
|
|
8041
|
+
const pt = guide.getPointAtLength(eased * len);
|
|
8042
|
+
pointerRef.setAttribute("transform", `translate(${pt.x},${pt.y})`);
|
|
8043
|
+
}
|
|
8044
|
+
catch { /* ignore */ }
|
|
8045
|
+
if (t < 1) {
|
|
8046
|
+
requestAnimationFrame(animate);
|
|
8047
|
+
}
|
|
8048
|
+
else {
|
|
8049
|
+
pointerRef.style.transition = `opacity 200ms ease`;
|
|
8050
|
+
pointerRef.setAttribute("opacity", "0");
|
|
8051
|
+
}
|
|
8052
|
+
};
|
|
8053
|
+
requestAnimationFrame(animate);
|
|
8054
|
+
}
|
|
8055
|
+
// After guide finishes: reveal rough.js element, remove guide
|
|
8056
|
+
const id = window.setTimeout(() => {
|
|
8057
|
+
roughEl.style.transition = `opacity 120ms ease`;
|
|
8058
|
+
roughEl.style.opacity = "1";
|
|
8059
|
+
guide.remove();
|
|
8060
|
+
}, dur + 30);
|
|
8061
|
+
this._pendingStepTimers.add(id);
|
|
8062
|
+
}));
|
|
8063
|
+
}
|
|
8064
|
+
_doAnnotationCircle(target, silent) {
|
|
8065
|
+
const el = resolveEl(this.svg, target);
|
|
8066
|
+
if (!el || !this._rc || !this._annotationLayer)
|
|
8067
|
+
return;
|
|
8068
|
+
const m = this._nodeMetrics(el);
|
|
8069
|
+
if (!m)
|
|
8070
|
+
return;
|
|
8071
|
+
const cx = m.x + m.w / 2, cy = m.y + m.h / 2;
|
|
8072
|
+
const rx = m.w * 0.65, ry = m.h * 0.65;
|
|
8073
|
+
const roughEl = this._rc.ellipse(cx, cy, rx * 2, ry * 2, {
|
|
8074
|
+
roughness: 2.0, stroke: ANIMATION.annotationColor,
|
|
8075
|
+
strokeWidth: ANIMATION.annotationStrokeW, fill: "none",
|
|
8076
|
+
seed: Date.now(),
|
|
8077
|
+
});
|
|
8078
|
+
this._annotationLayer.appendChild(roughEl);
|
|
8079
|
+
this._annotations.push(roughEl);
|
|
8080
|
+
// Clean guide path for draw-in animation
|
|
8081
|
+
const guideD = ellipsePath(cx, cy, rx, ry);
|
|
8082
|
+
this._animateAnnotation(roughEl, guideD, silent);
|
|
8083
|
+
}
|
|
8084
|
+
_doAnnotationUnderline(target, silent) {
|
|
8085
|
+
const el = resolveEl(this.svg, target);
|
|
8086
|
+
if (!el || !this._rc || !this._annotationLayer)
|
|
8087
|
+
return;
|
|
8088
|
+
const m = this._nodeMetrics(el);
|
|
8089
|
+
if (!m)
|
|
8090
|
+
return;
|
|
8091
|
+
const lineY = m.y + m.h + 4;
|
|
8092
|
+
const roughEl = this._rc.line(m.x, lineY, m.x + m.w, lineY, {
|
|
8093
|
+
roughness: 1.5, stroke: ANIMATION.annotationColor,
|
|
8094
|
+
strokeWidth: ANIMATION.annotationStrokeW, seed: Date.now(),
|
|
8095
|
+
});
|
|
8096
|
+
this._annotationLayer.appendChild(roughEl);
|
|
8097
|
+
this._annotations.push(roughEl);
|
|
8098
|
+
// Clean guide path
|
|
8099
|
+
const guideD = `M ${m.x} ${lineY} L ${m.x + m.w} ${lineY}`;
|
|
8100
|
+
this._animateAnnotation(roughEl, guideD, silent);
|
|
8101
|
+
}
|
|
8102
|
+
_doAnnotationCrossout(target, silent) {
|
|
8103
|
+
const el = resolveEl(this.svg, target);
|
|
8104
|
+
if (!el || !this._rc || !this._annotationLayer)
|
|
8105
|
+
return;
|
|
8106
|
+
const m = this._nodeMetrics(el);
|
|
8107
|
+
if (!m)
|
|
8108
|
+
return;
|
|
8109
|
+
const pad = 4;
|
|
8110
|
+
const roughG = document.createElementNS(SVG_NS$1, "g");
|
|
8111
|
+
const line1 = this._rc.line(m.x - pad, m.y - pad, m.x + m.w + pad, m.y + m.h + pad, {
|
|
8112
|
+
roughness: 1.5, stroke: ANIMATION.annotationColor,
|
|
8113
|
+
strokeWidth: ANIMATION.annotationStrokeW, seed: Date.now(),
|
|
8114
|
+
});
|
|
8115
|
+
const line2 = this._rc.line(m.x + m.w + pad, m.y - pad, m.x - pad, m.y + m.h + pad, {
|
|
8116
|
+
roughness: 1.5, stroke: ANIMATION.annotationColor,
|
|
8117
|
+
strokeWidth: ANIMATION.annotationStrokeW, seed: Date.now() + 1,
|
|
8118
|
+
});
|
|
8119
|
+
roughG.appendChild(line1);
|
|
8120
|
+
roughG.appendChild(line2);
|
|
8121
|
+
this._annotationLayer.appendChild(roughG);
|
|
8122
|
+
this._annotations.push(roughG);
|
|
8123
|
+
// Clean guide: two diagonal lines in a single path (pointer draws both)
|
|
8124
|
+
const guideD = `M ${m.x - pad} ${m.y - pad} L ${m.x + m.w + pad} ${m.y + m.h + pad} ` +
|
|
8125
|
+
`M ${m.x + m.w + pad} ${m.y - pad} L ${m.x - pad} ${m.y + m.h + pad}`;
|
|
8126
|
+
this._animateAnnotation(roughG, guideD, silent);
|
|
8127
|
+
}
|
|
8128
|
+
_doAnnotationBracket(target1, target2, silent) {
|
|
8129
|
+
const el1 = resolveEl(this.svg, target1);
|
|
8130
|
+
const el2 = resolveEl(this.svg, target2);
|
|
8131
|
+
if (!el1 || !el2 || !this._rc || !this._annotationLayer)
|
|
8132
|
+
return;
|
|
8133
|
+
const m1 = this._nodeMetrics(el1);
|
|
8134
|
+
const m2 = this._nodeMetrics(el2);
|
|
8135
|
+
if (!m1 || !m2)
|
|
8136
|
+
return;
|
|
8137
|
+
// Bracket on the right side spanning both elements
|
|
8138
|
+
const rightX = Math.max(m1.x + m1.w, m2.x + m2.w) + 12;
|
|
8139
|
+
const topY = Math.min(m1.y, m2.y);
|
|
8140
|
+
const botY = Math.max(m1.y + m1.h, m2.y + m2.h);
|
|
8141
|
+
const midY = (topY + botY) / 2;
|
|
8142
|
+
const bulge = 16;
|
|
8143
|
+
// Draw a curly brace using path
|
|
8144
|
+
const guideD = `M ${rightX} ${topY} Q ${rightX + bulge} ${topY} ${rightX + bulge} ${midY - 4} ` +
|
|
8145
|
+
`L ${rightX + bulge} ${midY} L ${rightX + bulge * 1.5} ${midY} ` +
|
|
8146
|
+
`M ${rightX + bulge} ${midY} L ${rightX + bulge} ${midY + 4} ` +
|
|
8147
|
+
`Q ${rightX + bulge} ${botY} ${rightX} ${botY}`;
|
|
8148
|
+
const roughEl = this._rc.path(guideD, {
|
|
8149
|
+
roughness: 1.2, stroke: ANIMATION.annotationColor,
|
|
8150
|
+
strokeWidth: ANIMATION.annotationStrokeW, fill: "none",
|
|
8151
|
+
seed: Date.now(),
|
|
8152
|
+
});
|
|
8153
|
+
this._annotationLayer.appendChild(roughEl);
|
|
8154
|
+
this._annotations.push(roughEl);
|
|
8155
|
+
this._animateAnnotation(roughEl, guideD, silent);
|
|
8156
|
+
}
|
|
8157
|
+
// ── pointer ─────────────────────────────────────────────
|
|
8158
|
+
_initPointer() {
|
|
8159
|
+
if (this._pointerType === "dot") {
|
|
8160
|
+
const circle = document.createElementNS(SVG_NS$1, "circle");
|
|
8161
|
+
circle.setAttribute("r", String(ANIMATION.pointerSize));
|
|
8162
|
+
circle.setAttribute("fill", ANIMATION.annotationColor);
|
|
8163
|
+
circle.setAttribute("opacity", "0");
|
|
8164
|
+
circle.style.pointerEvents = "none";
|
|
8165
|
+
this.svg.appendChild(circle);
|
|
8166
|
+
this._pointerEl = circle;
|
|
8167
|
+
}
|
|
8168
|
+
else if (this._pointerType === "chalk") {
|
|
8169
|
+
const g = document.createElementNS(SVG_NS$1, "g");
|
|
8170
|
+
const circle = document.createElementNS(SVG_NS$1, "circle");
|
|
8171
|
+
circle.setAttribute("r", "5");
|
|
8172
|
+
circle.setAttribute("fill", "#fff");
|
|
8173
|
+
circle.setAttribute("stroke", "#1a1208");
|
|
8174
|
+
circle.setAttribute("stroke-width", "1.5");
|
|
8175
|
+
g.appendChild(circle);
|
|
8176
|
+
g.setAttribute("opacity", "0");
|
|
8177
|
+
g.style.pointerEvents = "none";
|
|
8178
|
+
this.svg.appendChild(g);
|
|
8179
|
+
this._pointerEl = g;
|
|
8180
|
+
}
|
|
8181
|
+
else if (this._pointerType === "hand") {
|
|
8182
|
+
const g = document.createElementNS(SVG_NS$1, "g");
|
|
8183
|
+
const path = document.createElementNS(SVG_NS$1, "path");
|
|
8184
|
+
path.setAttribute("d", "M5,0 L5,12 L8,9 L11,16 L13,15 L10,8 L14,8 Z");
|
|
8185
|
+
path.setAttribute("fill", "#1a1208");
|
|
8186
|
+
g.appendChild(path);
|
|
8187
|
+
g.setAttribute("opacity", "0");
|
|
8188
|
+
g.style.pointerEvents = "none";
|
|
8189
|
+
this.svg.appendChild(g);
|
|
8190
|
+
this._pointerEl = g;
|
|
8191
|
+
}
|
|
8192
|
+
}
|
|
8193
|
+
}
|
|
8194
|
+
const ANIMATION_CSS = `
|
|
8195
|
+
.ng, .gg, .tg, .ntg, .cg, .eg, .mdg {
|
|
8196
|
+
transform-box: fill-box;
|
|
8197
|
+
transform-origin: center;
|
|
8198
|
+
transition: filter 0.3s, opacity 0.35s;
|
|
8199
|
+
}
|
|
8200
|
+
|
|
8201
|
+
/* highlight */
|
|
8202
|
+
.ng.hl path, .ng.hl rect, .ng.hl ellipse, .ng.hl polygon,
|
|
8203
|
+
.tg.hl path, .tg.hl rect,
|
|
8204
|
+
.ntg.hl path, .ntg.hl polygon,
|
|
8205
|
+
.cg.hl path, .cg.hl rect,
|
|
8206
|
+
.mdg.hl text,
|
|
8207
|
+
.eg.hl path, .eg.hl line, .eg.hl polygon { stroke-width: 2.8 !important; }
|
|
8208
|
+
|
|
8209
|
+
.ng.hl, .tg.hl, .ntg.hl, .cg.hl, .mdg.hl, .eg.hl {
|
|
8210
|
+
animation: ng-pulse 1.4s ease-in-out infinite;
|
|
8211
|
+
}
|
|
8212
|
+
@keyframes ng-pulse {
|
|
8213
|
+
0%, 100% { filter: drop-shadow(0 0 7px rgba(200,84,40,.6)); }
|
|
8214
|
+
50% { filter: drop-shadow(0 0 14px rgba(200,84,40,.9)); }
|
|
7650
8215
|
}
|
|
7651
|
-
|
|
7652
|
-
|
|
7653
|
-
|
|
7654
|
-
transform-origin: center;
|
|
7655
|
-
transition: filter 0.3s, opacity 0.35s;
|
|
7656
|
-
}
|
|
7657
|
-
|
|
7658
|
-
/* highlight */
|
|
7659
|
-
.ng.hl path, .ng.hl rect, .ng.hl ellipse, .ng.hl polygon,
|
|
7660
|
-
.tg.hl path, .tg.hl rect,
|
|
7661
|
-
.ntg.hl path, .ntg.hl polygon,
|
|
7662
|
-
.cg.hl path, .cg.hl rect,
|
|
7663
|
-
.mdg.hl text,
|
|
7664
|
-
.eg.hl path, .eg.hl line, .eg.hl polygon { stroke-width: 2.8 !important; }
|
|
7665
|
-
|
|
7666
|
-
.ng.hl, .tg.hl, .ntg.hl, .cg.hl, .mdg.hl, .eg.hl {
|
|
7667
|
-
animation: ng-pulse 1.4s ease-in-out infinite;
|
|
7668
|
-
}
|
|
7669
|
-
@keyframes ng-pulse {
|
|
7670
|
-
0%, 100% { filter: drop-shadow(0 0 7px rgba(200,84,40,.6)); }
|
|
7671
|
-
50% { filter: drop-shadow(0 0 14px rgba(200,84,40,.9)); }
|
|
7672
|
-
}
|
|
7673
|
-
|
|
7674
|
-
/* fade */
|
|
7675
|
-
.ng.faded, .gg.faded, .tg.faded, .ntg.faded,
|
|
8216
|
+
|
|
8217
|
+
/* fade */
|
|
8218
|
+
.ng.faded, .gg.faded, .tg.faded, .ntg.faded,
|
|
7676
8219
|
.cg.faded, .eg.faded, .mdg.faded { opacity: 0.22; }
|
|
7677
8220
|
|
|
7678
8221
|
.ng.hidden { opacity: 0; pointer-events: none; }
|
|
7679
8222
|
.gg.gg-hidden { opacity: 0; }
|
|
7680
8223
|
.tg.gg-hidden { opacity: 0; }
|
|
7681
8224
|
.ntg.gg-hidden { opacity: 0; }
|
|
7682
|
-
.cg.gg-hidden { opacity: 0; }
|
|
7683
|
-
.mdg.gg-hidden { opacity: 0; }
|
|
8225
|
+
.cg.gg-hidden { opacity: 0; }
|
|
8226
|
+
.mdg.gg-hidden { opacity: 0; }
|
|
8227
|
+
|
|
8228
|
+
/* narration caption */
|
|
8229
|
+
.skm-caption { pointer-events: none; user-select: none; }
|
|
7684
8230
|
`;
|
|
7685
8231
|
|
|
7686
8232
|
// ============================================================
|
|
@@ -7927,12 +8473,19 @@ function render(options) {
|
|
|
7927
8473
|
interactive: true,
|
|
7928
8474
|
onNodeClick,
|
|
7929
8475
|
});
|
|
7930
|
-
|
|
8476
|
+
// Create rough.js instance for annotations (same import as SVG renderer)
|
|
8477
|
+
let rc = null;
|
|
8478
|
+
try {
|
|
8479
|
+
rc = rough.svg(svg);
|
|
8480
|
+
}
|
|
8481
|
+
catch { /* rough.js not available — annotations disabled */ }
|
|
8482
|
+
const containerEl = el instanceof SVGSVGElement ? undefined : el;
|
|
8483
|
+
anim = new AnimationController(svg, ast.steps, containerEl, rc, ast.config);
|
|
7931
8484
|
}
|
|
7932
8485
|
onReady?.(anim, svg);
|
|
7933
8486
|
const instance = {
|
|
7934
8487
|
scene, anim, svg, canvas,
|
|
7935
|
-
update: (newDsl) => render({ ...options, dsl: newDsl }),
|
|
8488
|
+
update: (newDsl) => { anim?.destroy(); return render({ ...options, dsl: newDsl }); },
|
|
7936
8489
|
exportSVG: (filename = 'diagram.svg') => {
|
|
7937
8490
|
if (svg) {
|
|
7938
8491
|
Promise.resolve().then(function () { return index; }).then(m => m.exportSVG(svg, { filename }));
|