maestro-effects 0.1.0

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.cjs ADDED
@@ -0,0 +1,1457 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ clamp: () => clamp,
24
+ createAnnotationHighlight: () => createAnnotationHighlight,
25
+ createDiagramDrawer: () => createDiagramDrawer,
26
+ createHorizontalScroll: () => createHorizontalScroll,
27
+ createScrollOrganizer: () => createScrollOrganizer,
28
+ createWaveformVisualizer: () => createWaveformVisualizer,
29
+ ease: () => ease,
30
+ lerp: () => lerp
31
+ });
32
+ module.exports = __toCommonJS(index_exports);
33
+
34
+ // src/utils.ts
35
+ function lerp(a, b, t) {
36
+ return a + (b - a) * t;
37
+ }
38
+ function clamp(value, min, max) {
39
+ return Math.min(Math.max(value, min), max);
40
+ }
41
+ var ease = {
42
+ outCubic: (t) => 1 - Math.pow(1 - t, 3),
43
+ inCubic: (t) => t * t * t,
44
+ outQuart: (t) => 1 - Math.pow(1 - t, 4),
45
+ inOutCubic: (t) => t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2
46
+ };
47
+ function seededRandom(seed) {
48
+ let s = seed | 0;
49
+ return () => {
50
+ s = s + 1831565813 | 0;
51
+ let t = Math.imul(s ^ s >>> 15, 1 | s);
52
+ t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
53
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
54
+ };
55
+ }
56
+ var PERM = new Uint8Array(512);
57
+ (() => {
58
+ const rng = seededRandom(42);
59
+ const p = new Uint8Array(256);
60
+ for (let i = 0; i < 256; i++) p[i] = i;
61
+ for (let i = 255; i > 0; i--) {
62
+ const j = rng() * (i + 1) | 0;
63
+ const tmp = p[i];
64
+ p[i] = p[j];
65
+ p[j] = tmp;
66
+ }
67
+ for (let i = 0; i < 512; i++) PERM[i] = p[i & 255];
68
+ })();
69
+ function fade(t) {
70
+ return t * t * t * (t * (t * 6 - 15) + 10);
71
+ }
72
+ function grad(hash, x, y) {
73
+ const h = hash & 3;
74
+ const u = h < 2 ? x : y;
75
+ const v = h < 2 ? y : x;
76
+ return ((h & 1) === 0 ? u : -u) + ((h & 2) === 0 ? v : -v);
77
+ }
78
+ function noise2D(x, y) {
79
+ const X = Math.floor(x) & 255;
80
+ const Y = Math.floor(y) & 255;
81
+ const xf = x - Math.floor(x);
82
+ const yf = y - Math.floor(y);
83
+ const u = fade(xf);
84
+ const v = fade(yf);
85
+ const aa = PERM[PERM[X] + Y];
86
+ const ab = PERM[PERM[X] + Y + 1];
87
+ const ba = PERM[PERM[X + 1] + Y];
88
+ const bb = PERM[PERM[X + 1] + Y + 1];
89
+ return lerp(
90
+ lerp(grad(aa, xf, yf), grad(ba, xf - 1, yf), u),
91
+ lerp(grad(ab, xf, yf - 1), grad(bb, xf - 1, yf - 1), u),
92
+ v
93
+ );
94
+ }
95
+
96
+ // src/waveform.ts
97
+ var BASELINE = "#92400E";
98
+ var MID_PEAK = "#D97706";
99
+ var FULL_PEAK = "#F59E0B";
100
+ function defaultColorFn(amp, _x) {
101
+ if (amp < 0.5) {
102
+ const t2 = amp / 0.5;
103
+ return lerpColor(BASELINE, MID_PEAK, t2);
104
+ }
105
+ const t = (amp - 0.5) / 0.5;
106
+ return lerpColor(MID_PEAK, FULL_PEAK, t);
107
+ }
108
+ function hexToRgb(hex) {
109
+ const v = parseInt(hex.slice(1), 16);
110
+ return [v >> 16 & 255, v >> 8 & 255, v & 255];
111
+ }
112
+ function lerpColor(a, b, t) {
113
+ const [r1, g1, b1] = hexToRgb(a);
114
+ const [r2, g2, b2] = hexToRgb(b);
115
+ const r = Math.round(lerp(r1, r2, t));
116
+ const g = Math.round(lerp(g1, g2, t));
117
+ const bl = Math.round(lerp(b1, b2, t));
118
+ return `rgb(${r},${g},${bl})`;
119
+ }
120
+ function createWaveformVisualizer(options) {
121
+ const {
122
+ canvas,
123
+ colorFn = defaultColorFn,
124
+ amplitude: baseAmplitude = 0.6,
125
+ frequency = 0.02,
126
+ noiseScale = 1,
127
+ parallaxStrength = 6,
128
+ reactToAudio = null,
129
+ barWidth = 3,
130
+ barGap = 1,
131
+ fadeEdges = true
132
+ } = options;
133
+ let mode = options.mode ?? "idle";
134
+ let animId = 0;
135
+ let disposed = false;
136
+ let mouseX = 0.5;
137
+ const ctx = canvas.getContext("2d");
138
+ let audioCtx = null;
139
+ let analyser = null;
140
+ let audioData = null;
141
+ if (reactToAudio) {
142
+ audioCtx = new AudioContext();
143
+ analyser = audioCtx.createAnalyser();
144
+ analyser.fftSize = 256;
145
+ const source = audioCtx.createMediaStreamSource(reactToAudio);
146
+ source.connect(analyser);
147
+ audioData = new Uint8Array(analyser.frequencyBinCount);
148
+ }
149
+ function handleMouseMove(e) {
150
+ const rect = canvas.getBoundingClientRect();
151
+ mouseX = clamp((e.clientX - rect.left) / rect.width, 0, 1);
152
+ }
153
+ canvas.addEventListener("mousemove", handleMouseMove);
154
+ function resizeCanvas() {
155
+ const dpr = Math.min(window.devicePixelRatio, 2);
156
+ const rect = canvas.getBoundingClientRect();
157
+ canvas.width = rect.width * dpr;
158
+ canvas.height = rect.height * dpr;
159
+ ctx.scale(dpr, dpr);
160
+ }
161
+ const ro = new ResizeObserver(resizeCanvas);
162
+ ro.observe(canvas);
163
+ resizeCanvas();
164
+ const startTime = performance.now();
165
+ function render() {
166
+ if (disposed) return;
167
+ const elapsed = (performance.now() - startTime) / 1e3;
168
+ const rect = canvas.getBoundingClientRect();
169
+ const w = rect.width;
170
+ const h = rect.height;
171
+ ctx.clearRect(0, 0, w, h);
172
+ const modeMultiplier = mode === "active" ? 2.5 : mode === "listening" ? 0.3 : 1;
173
+ const modeNoise = mode === "active" ? noiseScale * 2 : noiseScale;
174
+ const breathe = Math.sin(elapsed * Math.PI * 0.5) * 0.3 + 0.7;
175
+ const amp = baseAmplitude * modeMultiplier * breathe;
176
+ const step = barWidth + barGap;
177
+ const barCount = Math.ceil(w / step);
178
+ const parallaxOffset = (mouseX - 0.5) * parallaxStrength;
179
+ if (analyser && audioData) {
180
+ analyser.getByteFrequencyData(audioData);
181
+ }
182
+ for (let i = 0; i < barCount; i++) {
183
+ const x = i * step + parallaxOffset;
184
+ const nx = i / barCount;
185
+ let barAmp;
186
+ if (audioData && analyser && mode === "active") {
187
+ const audioIndex = Math.floor(nx * audioData.length);
188
+ barAmp = (audioData[audioIndex] ?? 0) / 255;
189
+ } else {
190
+ const sine = Math.sin(nx * Math.PI * 8 + elapsed * 2) * 0.5 + 0.5;
191
+ const perlin = (noise2D(nx * 10 * frequency * 100, elapsed * 0.8) + 1) * 0.5;
192
+ barAmp = lerp(sine, perlin, clamp(modeNoise, 0, 1));
193
+ }
194
+ barAmp *= amp;
195
+ if (mode === "listening") {
196
+ const centerPull = 1 - Math.abs(nx - 0.5) * 2;
197
+ barAmp *= centerPull;
198
+ }
199
+ let edgeFade = 1;
200
+ if (fadeEdges) {
201
+ const edgeDist = Math.min(nx, 1 - nx);
202
+ edgeFade = clamp(edgeDist / 0.15, 0, 1);
203
+ }
204
+ const barHeight = barAmp * h * 0.4 * edgeFade;
205
+ const cy = h / 2;
206
+ const color = colorFn(barAmp, nx);
207
+ ctx.fillStyle = color;
208
+ ctx.globalAlpha = edgeFade;
209
+ ctx.fillRect(x, cy - barHeight, barWidth, barHeight * 2);
210
+ ctx.globalAlpha = 1;
211
+ }
212
+ animId = requestAnimationFrame(render);
213
+ }
214
+ animId = requestAnimationFrame(render);
215
+ const cleanup = () => {
216
+ if (disposed) return;
217
+ disposed = true;
218
+ cancelAnimationFrame(animId);
219
+ canvas.removeEventListener("mousemove", handleMouseMove);
220
+ ro.disconnect();
221
+ if (audioCtx) {
222
+ audioCtx.close().catch(() => {
223
+ });
224
+ }
225
+ };
226
+ cleanup.setMode = (m) => {
227
+ mode = m;
228
+ };
229
+ return cleanup;
230
+ }
231
+
232
+ // src/scroll-organizer.ts
233
+ function defaultScatteredState(_el, index) {
234
+ const rng = seededRandom(index * 7 + 31);
235
+ return {
236
+ x: (rng() - 0.5) * 600,
237
+ y: (rng() - 0.5) * 400,
238
+ rotation: (rng() - 0.5) * 30,
239
+ // −15° to +15°
240
+ opacity: rng() * 0.4 + 0.3,
241
+ // 0.3 to 0.7
242
+ scale: rng() * 0.3 + 0.8
243
+ // 0.8 to 1.1
244
+ };
245
+ }
246
+ function defaultOrganizedState(_el, _index) {
247
+ return { x: 0, y: 0, rotation: 0, opacity: 1, scale: 1 };
248
+ }
249
+ function lerpState(a, b, t) {
250
+ return {
251
+ x: a.x + (b.x - a.x) * t,
252
+ y: a.y + (b.y - a.y) * t,
253
+ rotation: a.rotation + (b.rotation - a.rotation) * t,
254
+ opacity: a.opacity + (b.opacity - a.opacity) * t,
255
+ scale: a.scale + (b.scale - a.scale) * t
256
+ };
257
+ }
258
+ function createScrollOrganizer(options) {
259
+ const {
260
+ elements,
261
+ getScatteredState = defaultScatteredState,
262
+ getOrganizedState = defaultOrganizedState,
263
+ staggerFactor = 0.05,
264
+ easing = ease.outCubic
265
+ } = options;
266
+ let currentProgress = options.progress ?? 0;
267
+ let disposed = false;
268
+ const scatteredStates = elements.map((el, i) => getScatteredState(el, i));
269
+ const organizedStates = elements.map((el, i) => getOrganizedState(el, i));
270
+ function applyTransforms() {
271
+ if (disposed) return;
272
+ const count = elements.length;
273
+ for (let i = 0; i < count; i++) {
274
+ const el = elements[i];
275
+ const offset = i * staggerFactor;
276
+ const maxRange = 1 - offset;
277
+ const localT = clamp((currentProgress - offset) / maxRange, 0, 1);
278
+ let easedT;
279
+ if (localT > 0.8) {
280
+ const snapT = (localT - 0.8) / 0.2;
281
+ const baseEased = easing(0.8);
282
+ easedT = baseEased + (1 - baseEased) * ease.outQuart(snapT);
283
+ } else {
284
+ easedT = easing(localT);
285
+ }
286
+ const state = lerpState(scatteredStates[i], organizedStates[i], easedT);
287
+ el.style.transform = `translate(${state.x}px, ${state.y}px) rotate(${state.rotation}deg) scale(${state.scale})`;
288
+ el.style.opacity = String(state.opacity);
289
+ }
290
+ }
291
+ applyTransforms();
292
+ const setProgress = (value) => {
293
+ currentProgress = clamp(value, 0, 1);
294
+ applyTransforms();
295
+ };
296
+ const cleanup = () => {
297
+ if (disposed) return;
298
+ disposed = true;
299
+ for (const el of elements) {
300
+ el.style.transform = "";
301
+ el.style.opacity = "";
302
+ }
303
+ };
304
+ cleanup.setProgress = setProgress;
305
+ return cleanup;
306
+ }
307
+
308
+ // src/diagram-drawer.ts
309
+ var DEFAULT_COLORS = {
310
+ node: "#261F1B",
311
+ nodeAlt: "#2C241F",
312
+ nodeBorder: "#6B3F18",
313
+ nodeAccent: "#D97706",
314
+ nodeAccentSoft: "#B45309",
315
+ nodeHighlight: "#FFF4D6",
316
+ nodeText: "#F7F1E8",
317
+ nodeSubtext: "#C9B8A6",
318
+ nodeMutedText: "#9F8C79",
319
+ connection: "#F59E0B",
320
+ connectionSoft: "#C26A12",
321
+ glow: "#F59E0B",
322
+ bracket: "#D97706",
323
+ bracketText: "#D6C1AB",
324
+ group: "#2A211C",
325
+ groupBorder: "#A65B12",
326
+ io: "#F59E0B"
327
+ };
328
+ var SVG_NS = "http://www.w3.org/2000/svg";
329
+ var NODE_W = 132;
330
+ var NODE_H = 68;
331
+ var STACK_H = 78;
332
+ var NETWORK_W = 96;
333
+ var NETWORK_H = 152;
334
+ var DIAMOND_W = 112;
335
+ var DIAMOND_H = 78;
336
+ var IO_W = 150;
337
+ function nodeSize(type) {
338
+ switch (type) {
339
+ case "network":
340
+ return { w: NETWORK_W, h: NETWORK_H };
341
+ case "stack":
342
+ return { w: NODE_W, h: STACK_H };
343
+ case "diamond":
344
+ return { w: DIAMOND_W, h: DIAMOND_H };
345
+ case "io":
346
+ return { w: IO_W, h: NODE_H };
347
+ case "group":
348
+ return { w: 0, h: 0 };
349
+ default:
350
+ return { w: NODE_W, h: NODE_H };
351
+ }
352
+ }
353
+ function alphaHex(a) {
354
+ return Math.round(clamp(a, 0, 1) * 255).toString(16).padStart(2, "0");
355
+ }
356
+ function setAttrs(el, attrs) {
357
+ for (const [key, value] of Object.entries(attrs)) {
358
+ el.setAttribute(key, value);
359
+ }
360
+ return el;
361
+ }
362
+ function createSvgEl(tag, attrs = {}) {
363
+ return setAttrs(document.createElementNS(SVG_NS, tag), attrs);
364
+ }
365
+ function titleCaseType(type) {
366
+ switch (type) {
367
+ case "io":
368
+ return "Input / Output";
369
+ case "stack":
370
+ return "Feature Maps";
371
+ case "network":
372
+ return "Dense Layer";
373
+ default:
374
+ return type.charAt(0).toUpperCase() + type.slice(1);
375
+ }
376
+ }
377
+ function splitNodeLabel(label) {
378
+ const lines = label.split("\n").map((line) => line.trim()).filter(Boolean);
379
+ return {
380
+ primary: lines[0] ?? "",
381
+ secondary: lines.slice(1)
382
+ };
383
+ }
384
+ function splitSubtitle(subtitle) {
385
+ return subtitle?.split("\n").map((line) => line.trim()).filter(Boolean) ?? [];
386
+ }
387
+ function roundedRectPath(x, y, w, h, r) {
388
+ const path = new Path2D();
389
+ path.roundRect(x, y, w, h, r);
390
+ return path;
391
+ }
392
+ function createDiagramDrawer(options) {
393
+ const {
394
+ container,
395
+ nodes: nodeDefs,
396
+ direction = "horizontal",
397
+ drawSpeed = 600,
398
+ connectionStyle = "stepped",
399
+ connectionArrows = true,
400
+ colorScheme: colorOverrides,
401
+ onNodeDrawn,
402
+ interactive = false,
403
+ glowIntensity = 0.4,
404
+ audioElement = null,
405
+ scrollFallback = true,
406
+ padding = 24,
407
+ nodeSpacing = { x: 80, y: 40 }
408
+ } = options;
409
+ const colors = { ...DEFAULT_COLORS, ...colorOverrides };
410
+ let disposed = false;
411
+ let animId = 0;
412
+ const timers = [];
413
+ container.style.position = "relative";
414
+ const canvas = document.createElement("canvas");
415
+ canvas.style.cssText = "position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;";
416
+ container.appendChild(canvas);
417
+ const ctx = canvas.getContext("2d");
418
+ const svg = document.createElementNS(SVG_NS, "svg");
419
+ svg.style.cssText = "position:absolute;top:0;left:0;width:100%;height:100%;overflow:visible;";
420
+ container.appendChild(svg);
421
+ const defs = createSvgEl("defs");
422
+ defs.appendChild(
423
+ createSvgEl("filter", {
424
+ id: "maestro-node-shadow",
425
+ x: "-40%",
426
+ y: "-40%",
427
+ width: "180%",
428
+ height: "180%",
429
+ colorInterpolationFilters: "sRGB"
430
+ })
431
+ );
432
+ const nodeShadow = defs.lastChild;
433
+ nodeShadow.appendChild(createSvgEl("feDropShadow", {
434
+ dx: "0",
435
+ dy: "8",
436
+ stdDeviation: "10",
437
+ "flood-color": colors.glow,
438
+ "flood-opacity": "0.08"
439
+ }));
440
+ svg.appendChild(defs);
441
+ function resize() {
442
+ const dpr = Math.min(window.devicePixelRatio, 2);
443
+ const rect = container.getBoundingClientRect();
444
+ canvas.width = rect.width * dpr;
445
+ canvas.height = rect.height * dpr;
446
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
447
+ }
448
+ const ro = new ResizeObserver(resize);
449
+ ro.observe(container);
450
+ resize();
451
+ const flowDefs = nodeDefs.filter((n) => n.type !== "group");
452
+ const groupDefs = nodeDefs.filter((n) => n.type === "group");
453
+ const defMap = new Map(nodeDefs.map((n) => [n.id, n]));
454
+ const depthMap = /* @__PURE__ */ new Map();
455
+ const roots = flowDefs.filter((n) => !n.parentId || !defMap.has(n.parentId) || defMap.get(n.parentId)?.type === "group");
456
+ const queue = roots.map((n) => ({ def: n, depth: 0 }));
457
+ while (queue.length > 0) {
458
+ const { def, depth } = queue.shift();
459
+ if (depthMap.has(def.id)) continue;
460
+ depthMap.set(def.id, depth);
461
+ for (const child of flowDefs.filter((n) => n.parentId === def.id)) {
462
+ queue.push({ def: child, depth: depth + 1 });
463
+ }
464
+ }
465
+ const maxDepth = Math.max(0, ...depthMap.values());
466
+ const byDepth = Array.from({ length: maxDepth + 1 }, () => []);
467
+ for (const def of flowDefs) {
468
+ const d = depthMap.get(def.id) ?? 0;
469
+ byDepth[d].push(def);
470
+ }
471
+ const colSizes = byDepth.map((defs2) => {
472
+ const sizes = defs2.map((d) => nodeSize(d.type));
473
+ return {
474
+ w: Math.max(NODE_W, ...sizes.map((s) => s.w)),
475
+ h: Math.max(NODE_H, ...sizes.map((s) => s.h))
476
+ };
477
+ });
478
+ const layoutMap = /* @__PURE__ */ new Map();
479
+ if (direction === "horizontal") {
480
+ let xCursor = padding;
481
+ for (let d = 0; d <= maxDepth; d++) {
482
+ const defs2 = byDepth[d];
483
+ const colW = colSizes[d].w;
484
+ const totalH = defs2.reduce((sum, def) => sum + nodeSize(def.type).h, 0) + (defs2.length - 1) * nodeSpacing.y;
485
+ let yCursor = padding + Math.max(0, (container.getBoundingClientRect().height - totalH) / 2);
486
+ for (const def of defs2) {
487
+ const { w, h } = nodeSize(def.type);
488
+ const cx = xCursor + colW / 2;
489
+ const cy = yCursor + h / 2;
490
+ const lnode = {
491
+ def,
492
+ x: def.position?.x ?? cx,
493
+ y: def.position?.y ?? cy,
494
+ w,
495
+ h,
496
+ depth: d,
497
+ visible: false,
498
+ glowAlpha: 0
499
+ };
500
+ layoutMap.set(def.id, lnode);
501
+ yCursor += h + nodeSpacing.y;
502
+ }
503
+ xCursor += colW + nodeSpacing.x;
504
+ }
505
+ } else {
506
+ let yCursor = container.getBoundingClientRect().height - padding;
507
+ for (let d = 0; d <= maxDepth; d++) {
508
+ const defs2 = byDepth[d];
509
+ const rowH = colSizes[d].h;
510
+ const totalW = defs2.reduce((sum, def) => sum + nodeSize(def.type).w, 0) + (defs2.length - 1) * nodeSpacing.x;
511
+ let xCursor = padding + Math.max(0, (container.getBoundingClientRect().width - totalW) / 2);
512
+ for (const def of defs2) {
513
+ const { w, h } = nodeSize(def.type);
514
+ const cx = xCursor + w / 2;
515
+ const cy = yCursor - rowH / 2;
516
+ const lnode = {
517
+ def,
518
+ x: def.position?.x ?? cx,
519
+ y: def.position?.y ?? cy,
520
+ w,
521
+ h,
522
+ depth: d,
523
+ visible: false,
524
+ glowAlpha: 0
525
+ };
526
+ layoutMap.set(def.id, lnode);
527
+ xCursor += w + nodeSpacing.x;
528
+ }
529
+ yCursor -= rowH + nodeSpacing.y;
530
+ }
531
+ }
532
+ const containerRect = container.getBoundingClientRect();
533
+ const cWidth = containerRect.width;
534
+ const cHeight = containerRect.height;
535
+ let minX = padding, minY = padding, maxX = padding, maxY = padding;
536
+ if ([...layoutMap.values()].length > 0) {
537
+ minX = Math.min(...[...layoutMap.values()].map((n) => n.x - n.w / 2)) - padding;
538
+ minY = Math.min(...[...layoutMap.values()].map((n) => n.y - n.h / 2)) - padding;
539
+ maxX = Math.max(...[...layoutMap.values()].map((n) => n.x + n.w / 2)) + padding;
540
+ maxY = Math.max(...[...layoutMap.values()].map((n) => n.y + n.h / 2)) + padding;
541
+ }
542
+ const diagramW = Math.max(1, maxX - minX);
543
+ const diagramH = Math.max(1, maxY - minY);
544
+ const zoomScale = Math.min(1, cWidth / diagramW, cHeight / diagramH);
545
+ const svgGroup = document.createElementNS(SVG_NS, "g");
546
+ if (zoomScale < 1) {
547
+ const tx = cWidth / 2 * (1 - zoomScale) / zoomScale;
548
+ const ty = cHeight / 2 * (1 - zoomScale) / zoomScale;
549
+ svgGroup.setAttribute("transform", `scale(${zoomScale}) translate(${tx}, ${ty})`);
550
+ }
551
+ svg.appendChild(svgGroup);
552
+ function drawArrowhead(x, y, angle) {
553
+ const size = 10;
554
+ ctx.save();
555
+ ctx.translate(x, y);
556
+ ctx.rotate(angle);
557
+ ctx.shadowColor = colors.glow + alphaHex(0.3);
558
+ ctx.shadowBlur = 14;
559
+ ctx.beginPath();
560
+ ctx.moveTo(0, 0);
561
+ ctx.lineTo(-size, -size * 0.55);
562
+ ctx.lineTo(-size * 0.9, 0);
563
+ ctx.lineTo(-size, size * 0.55);
564
+ ctx.closePath();
565
+ ctx.fillStyle = colors.connection;
566
+ ctx.fill();
567
+ ctx.restore();
568
+ }
569
+ function drawConnection(from, to, alpha) {
570
+ ctx.save();
571
+ ctx.globalAlpha = alpha;
572
+ ctx.lineCap = "round";
573
+ ctx.lineJoin = "round";
574
+ let endX, endY, arrowAngle;
575
+ const path = new Path2D();
576
+ if (direction === "horizontal") {
577
+ const startX = from.x + from.w / 2;
578
+ const startY = from.y;
579
+ endX = to.x - to.w / 2;
580
+ endY = to.y;
581
+ arrowAngle = 0;
582
+ if (connectionStyle === "stepped") {
583
+ const mx = (startX + endX) / 2;
584
+ path.moveTo(startX, startY);
585
+ path.lineTo(mx, startY);
586
+ path.lineTo(mx, endY);
587
+ path.lineTo(endX, endY);
588
+ } else if (connectionStyle === "bezier") {
589
+ const mx = (startX + endX) / 2;
590
+ path.moveTo(startX, startY);
591
+ path.bezierCurveTo(mx, startY, mx, endY, endX, endY);
592
+ } else {
593
+ path.moveTo(startX, startY);
594
+ path.lineTo(endX, endY);
595
+ }
596
+ } else {
597
+ const startX = from.x;
598
+ const startY = from.y - from.h / 2;
599
+ endX = to.x;
600
+ endY = to.y + to.h / 2;
601
+ arrowAngle = -Math.PI / 2;
602
+ if (connectionStyle === "stepped") {
603
+ const my = (startY + endY) / 2;
604
+ path.moveTo(startX, startY);
605
+ path.lineTo(startX, my);
606
+ path.lineTo(endX, my);
607
+ path.lineTo(endX, endY);
608
+ } else if (connectionStyle === "bezier") {
609
+ const my = (startY + endY) / 2;
610
+ path.moveTo(startX, startY);
611
+ path.bezierCurveTo(startX, my, endX, my, endX, endY);
612
+ } else {
613
+ path.moveTo(startX, startY);
614
+ path.lineTo(endX, endY);
615
+ }
616
+ }
617
+ ctx.strokeStyle = colors.connectionSoft + alphaHex(0.28);
618
+ ctx.lineWidth = 5;
619
+ ctx.shadowColor = colors.glow + alphaHex(0.2);
620
+ ctx.shadowBlur = 14;
621
+ ctx.stroke(path);
622
+ ctx.shadowBlur = 0;
623
+ ctx.strokeStyle = colors.connection;
624
+ ctx.lineWidth = 2.2;
625
+ ctx.stroke(path);
626
+ ctx.restore();
627
+ if (connectionArrows) {
628
+ drawArrowhead(endX, endY, arrowAngle);
629
+ }
630
+ }
631
+ function drawGroupEnclosure(groupDef, alpha) {
632
+ if (!groupDef.children || groupDef.children.length === 0) return;
633
+ const childNodes = groupDef.children.map((id) => layoutMap.get(id)).filter(Boolean);
634
+ if (childNodes.length === 0) return;
635
+ const pad = 16;
636
+ const minX2 = Math.min(...childNodes.map((n) => n.x - n.w / 2)) - pad;
637
+ const minY2 = Math.min(...childNodes.map((n) => n.y - n.h / 2)) - pad;
638
+ const maxX2 = Math.max(...childNodes.map((n) => n.x + n.w / 2)) + pad;
639
+ const maxY2 = Math.max(...childNodes.map((n) => n.y + n.h / 2)) + pad;
640
+ const gw = maxX2 - minX2;
641
+ const gh = maxY2 - minY2;
642
+ ctx.save();
643
+ const rectPath = roundedRectPath(minX2, minY2, gw, gh, 14);
644
+ const fill = ctx.createLinearGradient(minX2, minY2, minX2, maxY2);
645
+ fill.addColorStop(0, colors.group + alphaHex(0.12 * alpha));
646
+ fill.addColorStop(1, colors.node + alphaHex(0.04 * alpha));
647
+ ctx.fillStyle = fill;
648
+ ctx.shadowColor = colors.glow + alphaHex(0.08);
649
+ ctx.shadowBlur = 18;
650
+ ctx.fill(rectPath);
651
+ ctx.shadowBlur = 0;
652
+ ctx.strokeStyle = colors.groupBorder + alphaHex(0.5 * alpha);
653
+ ctx.lineWidth = 1.25;
654
+ ctx.setLineDash([10, 7]);
655
+ ctx.stroke(rectPath);
656
+ ctx.setLineDash([]);
657
+ const corner = 18;
658
+ ctx.strokeStyle = colors.connection + alphaHex(0.5 * alpha);
659
+ ctx.lineWidth = 1.2;
660
+ ctx.beginPath();
661
+ ctx.moveTo(minX2 + 10, minY2);
662
+ ctx.lineTo(minX2 + corner, minY2);
663
+ ctx.moveTo(minX2, minY2 + 10);
664
+ ctx.lineTo(minX2, minY2 + corner);
665
+ ctx.moveTo(maxX2 - 10, maxY2);
666
+ ctx.lineTo(maxX2 - corner, maxY2);
667
+ ctx.moveTo(maxX2, maxY2 - 10);
668
+ ctx.lineTo(maxX2, maxY2 - corner);
669
+ ctx.stroke();
670
+ ctx.restore();
671
+ if (groupDef.label) {
672
+ ctx.save();
673
+ ctx.fillStyle = colors.bracketText;
674
+ ctx.font = "600 11px ui-sans-serif, system-ui, sans-serif";
675
+ ctx.textBaseline = "top";
676
+ ctx.textAlign = "left";
677
+ ctx.fillText(groupDef.label.toUpperCase(), minX2 + 14, minY2 + 12);
678
+ ctx.restore();
679
+ }
680
+ }
681
+ function drawBracket(node, alpha) {
682
+ const { bracket } = node.def;
683
+ if (!bracket) return;
684
+ const { label, position } = bracket;
685
+ const pad = 12;
686
+ const labelFont = "600 10px ui-sans-serif, system-ui, sans-serif";
687
+ ctx.save();
688
+ ctx.globalAlpha = alpha;
689
+ ctx.strokeStyle = colors.bracket;
690
+ ctx.fillStyle = colors.bracketText;
691
+ ctx.lineWidth = 1.25;
692
+ ctx.font = labelFont;
693
+ ctx.textAlign = "center";
694
+ ctx.textBaseline = "middle";
695
+ if (position === "top" || position === "bottom") {
696
+ const y = position === "top" ? node.y - node.h / 2 - pad : node.y + node.h / 2 + pad;
697
+ const armLen = 8;
698
+ const hw = node.w / 2;
699
+ ctx.beginPath();
700
+ ctx.moveTo(node.x - hw, y + (position === "top" ? armLen : -armLen));
701
+ ctx.lineTo(node.x - hw, y);
702
+ ctx.lineTo(node.x + hw, y);
703
+ ctx.lineTo(node.x + hw, y + (position === "top" ? armLen : -armLen));
704
+ ctx.stroke();
705
+ const textY = position === "top" ? y - 6 : y + 16;
706
+ ctx.fillText(label.toUpperCase(), node.x, textY);
707
+ } else {
708
+ const x = position === "left" ? node.x - node.w / 2 - pad : node.x + node.w / 2 + pad;
709
+ const armLen = 6;
710
+ const hh = node.h / 2;
711
+ ctx.beginPath();
712
+ ctx.moveTo(x + (position === "left" ? armLen : -armLen), node.y - hh);
713
+ ctx.lineTo(x, node.y - hh);
714
+ ctx.lineTo(x, node.y + hh);
715
+ ctx.lineTo(x + (position === "left" ? armLen : -armLen), node.y + hh);
716
+ ctx.stroke();
717
+ ctx.save();
718
+ ctx.translate(x + (position === "left" ? -14 : 14), node.y);
719
+ ctx.rotate(position === "left" ? -Math.PI / 2 : Math.PI / 2);
720
+ ctx.fillText(label.toUpperCase(), 0, 0);
721
+ ctx.restore();
722
+ }
723
+ ctx.restore();
724
+ }
725
+ function drawStackDepth(node) {
726
+ const depth = node.def.stackDepth ?? 6;
727
+ const offsetStep = 4;
728
+ for (let i = depth - 1; i >= 1; i--) {
729
+ const ox = i * offsetStep;
730
+ const oy = -i * offsetStep;
731
+ const alpha = 0.16 + (1 - i / depth) * 0.24;
732
+ const x = node.x - node.w / 2 + ox;
733
+ const y = node.y - node.h / 2 + oy;
734
+ const path = roundedRectPath(x, y, node.w, node.h, 12);
735
+ const fill = ctx.createLinearGradient(x, y, x, y + node.h);
736
+ fill.addColorStop(0, colors.nodeAlt + alphaHex(alpha));
737
+ fill.addColorStop(1, colors.node + alphaHex(alpha * 0.9));
738
+ ctx.fillStyle = fill;
739
+ ctx.strokeStyle = colors.connectionSoft + alphaHex(alpha);
740
+ ctx.lineWidth = 1.15;
741
+ ctx.fill(path);
742
+ ctx.stroke(path);
743
+ }
744
+ ctx.globalAlpha = 1;
745
+ }
746
+ function drawNetworkNeurons(node) {
747
+ const neuronCount = 5;
748
+ const r = 8;
749
+ const spacing = node.h / (neuronCount + 1);
750
+ ctx.save();
751
+ ctx.strokeStyle = colors.connectionSoft + alphaHex(0.55);
752
+ ctx.lineWidth = 1.35;
753
+ ctx.globalAlpha = 0.6;
754
+ const railOffset = 10;
755
+ ctx.beginPath();
756
+ ctx.moveTo(node.x - railOffset, node.y - node.h / 2 + 16);
757
+ ctx.lineTo(node.x - railOffset, node.y + node.h / 2 - 16);
758
+ ctx.moveTo(node.x + railOffset, node.y - node.h / 2 + 16);
759
+ ctx.lineTo(node.x + railOffset, node.y + node.h / 2 - 16);
760
+ ctx.stroke();
761
+ if (node.def.parentId) {
762
+ const parent = layoutMap.get(node.def.parentId);
763
+ if (parent) {
764
+ const parentCount = 5;
765
+ const parentSpacing = parent.h / (parentCount + 1);
766
+ for (let i = 1; i <= neuronCount; i++) {
767
+ const ny = node.y - node.h / 2 + i * spacing;
768
+ for (let j = 1; j <= parentCount; j++) {
769
+ if (Math.abs((i * 13 + j * 7) % 5) < 2) continue;
770
+ const py = parent.y - parent.h / 2 + j * parentSpacing;
771
+ const px = parent.x + parent.w / 2;
772
+ const strandAlpha = 0.28 + (1 - Math.abs(i - j) / neuronCount) * 0.26;
773
+ ctx.strokeStyle = colors.connectionSoft + alphaHex(strandAlpha);
774
+ ctx.lineWidth = 1.15;
775
+ ctx.beginPath();
776
+ ctx.moveTo(px, py);
777
+ ctx.lineTo(node.x - node.w / 2, ny);
778
+ ctx.stroke();
779
+ }
780
+ }
781
+ }
782
+ }
783
+ for (let i = 1; i <= neuronCount; i++) {
784
+ const ny = node.y - node.h / 2 + i * spacing;
785
+ const glow = ctx.createRadialGradient(node.x, ny, 0, node.x, ny, r * 2.5);
786
+ glow.addColorStop(0, colors.glow + alphaHex(0.18));
787
+ glow.addColorStop(1, colors.glow + "00");
788
+ ctx.fillStyle = glow;
789
+ ctx.beginPath();
790
+ ctx.arc(node.x, ny, r * 2.1, 0, Math.PI * 2);
791
+ ctx.fill();
792
+ ctx.beginPath();
793
+ ctx.arc(node.x, ny, r, 0, Math.PI * 2);
794
+ ctx.fillStyle = colors.nodeText;
795
+ ctx.globalAlpha = 0.1;
796
+ ctx.fill();
797
+ ctx.beginPath();
798
+ ctx.arc(node.x, ny, r - 1.5, 0, Math.PI * 2);
799
+ ctx.globalAlpha = 1;
800
+ ctx.fillStyle = colors.nodeAlt;
801
+ ctx.fill();
802
+ ctx.strokeStyle = colors.connection + alphaHex(0.7);
803
+ ctx.lineWidth = 1.4;
804
+ ctx.stroke();
805
+ }
806
+ ctx.restore();
807
+ }
808
+ function drawGlow(node) {
809
+ if (glowIntensity <= 0 || node.glowAlpha <= 0) return;
810
+ const r = Math.max(node.w, node.h) * 0.95;
811
+ const grad2 = ctx.createRadialGradient(node.x, node.y, 0, node.x, node.y, r);
812
+ grad2.addColorStop(0, colors.glow + alphaHex(glowIntensity * 0.65 * node.glowAlpha));
813
+ grad2.addColorStop(1, colors.glow + "00");
814
+ ctx.fillStyle = grad2;
815
+ ctx.globalAlpha = 1;
816
+ ctx.fillRect(node.x - r, node.y - r, r * 2, r * 2);
817
+ }
818
+ function renderCanvas() {
819
+ if (disposed) return;
820
+ const rect = container.getBoundingClientRect();
821
+ ctx.clearRect(0, 0, rect.width, rect.height);
822
+ ctx.save();
823
+ ctx.translate(rect.width / 2, rect.height / 2);
824
+ ctx.scale(zoomScale, zoomScale);
825
+ ctx.translate(-cWidth / 2, -cHeight / 2);
826
+ for (const gDef of groupDefs) {
827
+ const anyVisible = (gDef.children ?? []).some((id) => layoutMap.get(id)?.visible);
828
+ if (anyVisible) drawGroupEnclosure(gDef, 1);
829
+ }
830
+ for (const node of layoutMap.values()) {
831
+ if (!node.visible) continue;
832
+ if (node.def.type === "stack") drawStackDepth(node);
833
+ }
834
+ for (const node of layoutMap.values()) {
835
+ if (!node.visible) continue;
836
+ drawGlow(node);
837
+ if (node.glowAlpha > 0) node.glowAlpha = Math.max(0, node.glowAlpha - 8e-3);
838
+ }
839
+ for (const node of layoutMap.values()) {
840
+ if (!node.visible || !node.def.parentId) continue;
841
+ const parent = layoutMap.get(node.def.parentId);
842
+ if (parent?.visible) drawConnection(parent, node, 1);
843
+ }
844
+ for (const node of layoutMap.values()) {
845
+ if (!node.visible || node.def.type !== "network") continue;
846
+ drawNetworkNeurons(node);
847
+ }
848
+ for (const node of layoutMap.values()) {
849
+ if (!node.visible || !node.def.bracket) continue;
850
+ drawBracket(node, 1);
851
+ }
852
+ ctx.restore();
853
+ animId = requestAnimationFrame(renderCanvas);
854
+ }
855
+ function createSvgNode(node) {
856
+ const g = document.createElementNS(SVG_NS, "g");
857
+ const fillColor = node.def.color ?? colors.node;
858
+ const gradientId = `maestro-node-fill-${node.def.id}`;
859
+ const nodeGradient = createSvgEl("linearGradient", {
860
+ id: gradientId,
861
+ x1: "0%",
862
+ y1: "0%",
863
+ x2: "0%",
864
+ y2: "100%"
865
+ });
866
+ nodeGradient.appendChild(createSvgEl("stop", {
867
+ offset: "0%",
868
+ "stop-color": node.def.type === "io" ? colors.nodeAlt : node.def.type === "stack" ? colors.nodeHighlight + alphaHex(0.14) : fillColor
869
+ }));
870
+ nodeGradient.appendChild(createSvgEl("stop", {
871
+ offset: "100%",
872
+ "stop-color": node.def.type === "stack" ? colors.nodeAlt : colors.node
873
+ }));
874
+ defs.appendChild(nodeGradient);
875
+ g.setAttribute("filter", "url(#maestro-node-shadow)");
876
+ if (node.def.type === "diamond") {
877
+ const poly = createSvgEl("polygon");
878
+ const hw = node.w / 2;
879
+ const hh = node.h / 2;
880
+ poly.setAttribute("points", `${node.x},${node.y - hh} ${node.x + hw},${node.y} ${node.x},${node.y + hh} ${node.x - hw},${node.y}`);
881
+ poly.setAttribute("fill", `url(#${gradientId})`);
882
+ poly.setAttribute("stroke", colors.nodeBorder + alphaHex(0.7));
883
+ poly.setAttribute("stroke-width", "1.4");
884
+ g.appendChild(poly);
885
+ } else if (node.def.type === "network") {
886
+ const rect = createSvgEl("rect", {
887
+ x: String(node.x - node.w / 2),
888
+ y: String(node.y - node.h / 2),
889
+ width: String(node.w),
890
+ height: String(node.h),
891
+ rx: "16",
892
+ fill: `url(#${gradientId})`,
893
+ "fill-opacity": "0.96",
894
+ stroke: colors.connectionSoft + alphaHex(0.72),
895
+ "stroke-width": "1.6"
896
+ });
897
+ g.appendChild(rect);
898
+ const leftRail = createSvgEl("rect", {
899
+ x: String(node.x - node.w / 2 + 16),
900
+ y: String(node.y - node.h / 2 + 14),
901
+ width: "2",
902
+ height: String(node.h - 28),
903
+ rx: "1",
904
+ fill: colors.connectionSoft,
905
+ "fill-opacity": "0.5"
906
+ });
907
+ const rightRail = createSvgEl("rect", {
908
+ x: String(node.x + node.w / 2 - 18),
909
+ y: String(node.y - node.h / 2 + 14),
910
+ width: "2",
911
+ height: String(node.h - 28),
912
+ rx: "1",
913
+ fill: colors.connectionSoft,
914
+ "fill-opacity": "0.5"
915
+ });
916
+ g.appendChild(leftRail);
917
+ g.appendChild(rightRail);
918
+ } else {
919
+ const rx = node.def.type === "io" ? "28" : "14";
920
+ const rect = createSvgEl("rect", {
921
+ x: String(node.x - node.w / 2),
922
+ y: String(node.y - node.h / 2),
923
+ width: String(node.w),
924
+ height: String(node.h),
925
+ rx,
926
+ fill: `url(#${gradientId})`,
927
+ "fill-opacity": node.def.type === "io" ? "0.96" : "1",
928
+ stroke: colors.nodeBorder + alphaHex(node.def.type === "io" ? 0.85 : 0.65),
929
+ "stroke-width": node.def.type === "io" ? "1.6" : "1.4"
930
+ });
931
+ g.appendChild(rect);
932
+ }
933
+ if (node.def.type !== "diamond") {
934
+ const accentInset = 6;
935
+ const accentWidth = node.def.type === "io" ? 6 : node.def.type === "stack" ? 6 : 4;
936
+ const accent = createSvgEl("rect", {
937
+ x: String(node.x - node.w / 2 + accentInset),
938
+ y: String(node.y - node.h / 2 + accentInset),
939
+ width: String(accentWidth),
940
+ height: String(node.h - accentInset * 2),
941
+ rx: "3",
942
+ fill: node.def.type === "io" ? colors.connection : colors.nodeAccent,
943
+ "fill-opacity": node.def.type === "io" ? "1" : "0.9"
944
+ });
945
+ g.appendChild(accent);
946
+ const highlight = createSvgEl("rect", {
947
+ x: String(node.x - node.w / 2 + 10),
948
+ y: String(node.y - node.h / 2 + 8),
949
+ width: String(node.w - 20),
950
+ height: "1.5",
951
+ rx: "1",
952
+ fill: colors.nodeHighlight,
953
+ "fill-opacity": node.def.type === "stack" ? "0.14" : "0.08"
954
+ });
955
+ g.appendChild(highlight);
956
+ }
957
+ const { primary, secondary } = splitNodeLabel(node.def.label);
958
+ const subtitleLines = splitSubtitle(node.def.subtitle);
959
+ const textAnchor = node.def.type === "diamond" ? "middle" : "start";
960
+ const textX = node.def.type === "diamond" ? node.x : node.x - node.w / 2 + 18;
961
+ let textY = node.y - node.h / 2 + 18;
962
+ if (node.def.type !== "diamond") {
963
+ const eyebrow = createSvgEl("text", {
964
+ x: String(textX),
965
+ y: String(textY),
966
+ "text-anchor": textAnchor,
967
+ fill: colors.nodeMutedText,
968
+ "font-size": "8.5",
969
+ "font-family": "ui-sans-serif, system-ui, sans-serif",
970
+ "font-weight": "700",
971
+ "letter-spacing": "0.18em"
972
+ });
973
+ eyebrow.textContent = titleCaseType(node.def.type).toUpperCase();
974
+ g.appendChild(eyebrow);
975
+ textY += 16;
976
+ }
977
+ const primaryText = createSvgEl("text", {
978
+ x: String(textX),
979
+ y: String(textY),
980
+ "text-anchor": textAnchor,
981
+ fill: colors.nodeText,
982
+ "font-size": node.def.type === "diamond" ? "12.5" : "13",
983
+ "font-family": "ui-sans-serif, system-ui, sans-serif",
984
+ "font-weight": "600",
985
+ "letter-spacing": "0.01em"
986
+ });
987
+ primaryText.textContent = primary;
988
+ g.appendChild(primaryText);
989
+ textY += 16;
990
+ for (const line of secondary) {
991
+ const t = createSvgEl("text", {
992
+ x: String(textX),
993
+ y: String(textY),
994
+ "text-anchor": textAnchor,
995
+ fill: colors.nodeSubtext,
996
+ "font-size": "10.5",
997
+ "font-family": "ui-sans-serif, system-ui, sans-serif",
998
+ "font-weight": "500"
999
+ });
1000
+ t.textContent = line;
1001
+ g.appendChild(t);
1002
+ textY += 13;
1003
+ }
1004
+ for (const line of subtitleLines) {
1005
+ const t = createSvgEl("text", {
1006
+ x: String(textX),
1007
+ y: String(textY),
1008
+ "text-anchor": textAnchor,
1009
+ fill: colors.nodeMutedText,
1010
+ "font-size": "9",
1011
+ "font-family": "ui-sans-serif, system-ui, sans-serif",
1012
+ "font-weight": "500"
1013
+ });
1014
+ t.textContent = line;
1015
+ g.appendChild(t);
1016
+ textY += 11.5;
1017
+ }
1018
+ g.style.opacity = "0";
1019
+ g.style.transformOrigin = `${node.x}px ${node.y}px`;
1020
+ g.style.transform = "translateY(6px) scale(0.97)";
1021
+ g.style.transition = "opacity 360ms ease-out, transform 360ms ease-out";
1022
+ if (interactive) {
1023
+ g.style.cursor = "pointer";
1024
+ g.style.pointerEvents = "all";
1025
+ }
1026
+ svgGroup.appendChild(g);
1027
+ node.svgGroup = g;
1028
+ requestAnimationFrame(() => {
1029
+ g.style.opacity = "1";
1030
+ g.style.transform = "translateY(0) scale(1)";
1031
+ });
1032
+ }
1033
+ function revealNode(node) {
1034
+ if (node.visible || disposed) return;
1035
+ node.visible = true;
1036
+ node.glowAlpha = 1;
1037
+ createSvgNode(node);
1038
+ onNodeDrawn?.(node.def.id);
1039
+ }
1040
+ const orderedNodes = [...layoutMap.values()].sort((a, b) => a.depth - b.depth || 0);
1041
+ let audioListenerAdded = false;
1042
+ function onTimeUpdate() {
1043
+ if (disposed || !audioElement) return;
1044
+ const t = audioElement.currentTime;
1045
+ for (const node of orderedNodes) {
1046
+ if (!node.visible && node.def.audioTimestamp !== void 0 && t >= node.def.audioTimestamp) {
1047
+ revealNode(node);
1048
+ }
1049
+ }
1050
+ }
1051
+ function onSeeked() {
1052
+ if (disposed || !audioElement) return;
1053
+ const t = audioElement.currentTime;
1054
+ for (const node of orderedNodes) {
1055
+ if (!node.visible && node.def.audioTimestamp !== void 0 && t >= node.def.audioTimestamp) {
1056
+ node.visible = true;
1057
+ node.glowAlpha = 0;
1058
+ if (!node.svgGroup) createSvgNode(node);
1059
+ if (node.svgGroup) {
1060
+ node.svgGroup.style.transition = "none";
1061
+ node.svgGroup.style.opacity = "1";
1062
+ node.svgGroup.style.transform = "scale(1)";
1063
+ }
1064
+ }
1065
+ }
1066
+ }
1067
+ if (audioElement) {
1068
+ audioElement.addEventListener("timeupdate", onTimeUpdate);
1069
+ audioElement.addEventListener("seeked", onSeeked);
1070
+ audioListenerAdded = true;
1071
+ } else if (scrollFallback) {
1072
+ let setupScrollFallback2 = function() {
1073
+ const totalScrollH = Math.max(1, document.documentElement.scrollHeight - window.innerHeight);
1074
+ const nodeCount = orderedNodes.length;
1075
+ let lastDrawnIndex = -1;
1076
+ scrollHandler = () => {
1077
+ if (disposed) return;
1078
+ const progress = clamp(window.scrollY / totalScrollH, 0, 1);
1079
+ const targetIndex = Math.floor(progress * nodeCount);
1080
+ for (let i = lastDrawnIndex + 1; i <= targetIndex && i < nodeCount; i++) {
1081
+ revealNode(orderedNodes[i]);
1082
+ lastDrawnIndex = i;
1083
+ }
1084
+ };
1085
+ window.addEventListener("scroll", scrollHandler, { passive: true });
1086
+ };
1087
+ var setupScrollFallback = setupScrollFallback2;
1088
+ let scrollHandler = null;
1089
+ setupScrollFallback2();
1090
+ } else {
1091
+ orderedNodes.forEach((node, i) => {
1092
+ const timer = setTimeout(() => {
1093
+ if (disposed) return;
1094
+ revealNode(node);
1095
+ }, i * drawSpeed);
1096
+ timers.push(timer);
1097
+ });
1098
+ }
1099
+ animId = requestAnimationFrame(renderCanvas);
1100
+ function drawAll() {
1101
+ for (const node of orderedNodes) {
1102
+ if (!node.visible) {
1103
+ node.visible = true;
1104
+ node.glowAlpha = 0;
1105
+ if (!node.svgGroup) {
1106
+ createSvgNode(node);
1107
+ }
1108
+ const g = node.svgGroup;
1109
+ if (g) {
1110
+ g.style.transition = "none";
1111
+ g.style.opacity = "1";
1112
+ g.style.transform = "scale(1)";
1113
+ }
1114
+ }
1115
+ }
1116
+ }
1117
+ const cleanup = () => {
1118
+ if (disposed) return;
1119
+ disposed = true;
1120
+ cancelAnimationFrame(animId);
1121
+ for (const t of timers) clearTimeout(t);
1122
+ ro.disconnect();
1123
+ if (audioListenerAdded && audioElement) {
1124
+ audioElement.removeEventListener("timeupdate", onTimeUpdate);
1125
+ audioElement.removeEventListener("seeked", onSeeked);
1126
+ }
1127
+ canvas.remove();
1128
+ svg.remove();
1129
+ };
1130
+ cleanup.drawAll = drawAll;
1131
+ return cleanup;
1132
+ }
1133
+
1134
+ // src/horizontal-scroll.ts
1135
+ function createHorizontalScroll(options) {
1136
+ const {
1137
+ container,
1138
+ panels: panelsOpt = ".panel",
1139
+ lerp: lerpFactor = 0.08,
1140
+ snap = true,
1141
+ snapThreshold = 0.5,
1142
+ onProgress,
1143
+ mobileBreakpoint = 768,
1144
+ keyboard = true
1145
+ } = options;
1146
+ let disposed = false;
1147
+ let animId = 0;
1148
+ let isMobile = false;
1149
+ const panels = typeof panelsOpt === "string" ? Array.from(container.querySelectorAll(panelsOpt)) : panelsOpt;
1150
+ const panelCount = panels.length;
1151
+ if (panelCount === 0) return () => {
1152
+ };
1153
+ let targetScroll = 0;
1154
+ let currentScroll = 0;
1155
+ let velocity = 0;
1156
+ let prevScroll = 0;
1157
+ const spacer = document.createElement("div");
1158
+ spacer.style.cssText = "width:1px;pointer-events:none;";
1159
+ function measureLayout() {
1160
+ const header = document.querySelector("header, [data-maestro-header]");
1161
+ const footer = document.querySelector("footer, [data-maestro-footer]");
1162
+ const headerH = header ? header.getBoundingClientRect().height : 0;
1163
+ const footerH = footer ? footer.getBoundingClientRect().height : 0;
1164
+ document.documentElement.style.setProperty("--maestro-header-h", `${headerH}px`);
1165
+ document.documentElement.style.setProperty("--maestro-footer-h", `${footerH}px`);
1166
+ }
1167
+ function setupDesktop() {
1168
+ measureLayout();
1169
+ const vw = window.innerWidth;
1170
+ const totalWidth = panelCount * vw;
1171
+ spacer.style.height = `${totalWidth}px`;
1172
+ document.body.appendChild(spacer);
1173
+ container.style.position = "fixed";
1174
+ container.style.top = "var(--maestro-header-h, 0px)";
1175
+ container.style.left = "0";
1176
+ container.style.width = "100%";
1177
+ container.style.height = `calc(100vh - var(--maestro-header-h, 0px) - var(--maestro-footer-h, 0px))`;
1178
+ container.style.overflow = "hidden";
1179
+ container.style.display = "flex";
1180
+ container.style.flexWrap = "nowrap";
1181
+ }
1182
+ function teardownDesktop() {
1183
+ spacer.remove();
1184
+ container.style.position = "";
1185
+ container.style.top = "";
1186
+ container.style.left = "";
1187
+ container.style.width = "";
1188
+ container.style.height = "";
1189
+ container.style.overflow = "";
1190
+ container.style.display = "";
1191
+ container.style.flexWrap = "";
1192
+ container.style.transform = "";
1193
+ }
1194
+ function setupMobile() {
1195
+ teardownDesktop();
1196
+ container.style.display = "flex";
1197
+ container.style.flexDirection = "column";
1198
+ }
1199
+ let mobileObserver = null;
1200
+ function setupMobileObserver() {
1201
+ mobileObserver = new IntersectionObserver(
1202
+ (entries) => {
1203
+ for (const entry of entries) {
1204
+ const idx = panels.indexOf(entry.target);
1205
+ if (idx !== -1) {
1206
+ onProgress?.(idx, entry.intersectionRatio);
1207
+ }
1208
+ }
1209
+ },
1210
+ { threshold: Array.from({ length: 20 }, (_, i) => i / 19) }
1211
+ );
1212
+ for (const p of panels) mobileObserver.observe(p);
1213
+ }
1214
+ function handleResize() {
1215
+ const wasMobile = isMobile;
1216
+ isMobile = window.innerWidth < mobileBreakpoint;
1217
+ if (isMobile && !wasMobile) {
1218
+ cancelAnimationFrame(animId);
1219
+ setupMobile();
1220
+ setupMobileObserver();
1221
+ } else if (!isMobile && wasMobile) {
1222
+ mobileObserver?.disconnect();
1223
+ mobileObserver = null;
1224
+ setupDesktop();
1225
+ tick();
1226
+ } else if (!isMobile) {
1227
+ const vw = window.innerWidth;
1228
+ spacer.style.height = `${panelCount * vw}px`;
1229
+ measureLayout();
1230
+ }
1231
+ }
1232
+ function handleScroll() {
1233
+ if (isMobile || disposed) return;
1234
+ targetScroll = window.scrollY;
1235
+ }
1236
+ function handleKeydown(e) {
1237
+ if (!keyboard || isMobile || disposed) return;
1238
+ const vw = window.innerWidth;
1239
+ if (e.key === "ArrowRight" || e.key === "ArrowDown") {
1240
+ e.preventDefault();
1241
+ targetScroll = Math.min(targetScroll + vw, (panelCount - 1) * vw);
1242
+ window.scrollTo(0, targetScroll);
1243
+ } else if (e.key === "ArrowLeft" || e.key === "ArrowUp") {
1244
+ e.preventDefault();
1245
+ targetScroll = Math.max(targetScroll - vw, 0);
1246
+ window.scrollTo(0, targetScroll);
1247
+ }
1248
+ }
1249
+ function tick() {
1250
+ if (disposed || isMobile) return;
1251
+ prevScroll = currentScroll;
1252
+ currentScroll = lerp(currentScroll, targetScroll, lerpFactor);
1253
+ velocity = Math.abs(currentScroll - prevScroll);
1254
+ if (snap && velocity < snapThreshold && velocity > 0.01) {
1255
+ const vw = window.innerWidth;
1256
+ const nearestPanel = Math.round(currentScroll / vw);
1257
+ targetScroll = nearestPanel * vw;
1258
+ }
1259
+ container.style.transform = `translateX(${-currentScroll}px)`;
1260
+ if (onProgress) {
1261
+ const vw = window.innerWidth;
1262
+ const panelIndex = Math.floor(currentScroll / vw);
1263
+ const localProgress = clamp(currentScroll / vw - panelIndex, 0, 1);
1264
+ onProgress(clamp(panelIndex, 0, panelCount - 1), localProgress);
1265
+ }
1266
+ animId = requestAnimationFrame(tick);
1267
+ }
1268
+ isMobile = window.innerWidth < mobileBreakpoint;
1269
+ if (isMobile) {
1270
+ setupMobile();
1271
+ setupMobileObserver();
1272
+ } else {
1273
+ setupDesktop();
1274
+ animId = requestAnimationFrame(tick);
1275
+ }
1276
+ window.addEventListener("scroll", handleScroll, { passive: true });
1277
+ window.addEventListener("resize", handleResize);
1278
+ window.addEventListener("keydown", handleKeydown);
1279
+ return () => {
1280
+ if (disposed) return;
1281
+ disposed = true;
1282
+ cancelAnimationFrame(animId);
1283
+ window.removeEventListener("scroll", handleScroll);
1284
+ window.removeEventListener("resize", handleResize);
1285
+ window.removeEventListener("keydown", handleKeydown);
1286
+ mobileObserver?.disconnect();
1287
+ teardownDesktop();
1288
+ document.documentElement.style.removeProperty("--maestro-header-h");
1289
+ document.documentElement.style.removeProperty("--maestro-footer-h");
1290
+ };
1291
+ }
1292
+
1293
+ // src/annotation.ts
1294
+ var SVG_NS2 = "http://www.w3.org/2000/svg";
1295
+ function createAnnotationHighlight(options) {
1296
+ const {
1297
+ target,
1298
+ style = "underline",
1299
+ color = "#D97706",
1300
+ wobble = 0.3,
1301
+ strokeWidth = 2.5,
1302
+ duration = 600,
1303
+ delay = 0,
1304
+ trigger = "scroll",
1305
+ scrollOffset = 0.3
1306
+ } = options;
1307
+ let disposed = false;
1308
+ let observer = null;
1309
+ let animated = false;
1310
+ const seed = hashString(target.textContent ?? "" + target.offsetLeft);
1311
+ const rng = seededRandom(seed);
1312
+ const svg = document.createElementNS(SVG_NS2, "svg");
1313
+ svg.style.cssText = "position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;overflow:visible;z-index:1;";
1314
+ target.style.position = "relative";
1315
+ target.appendChild(svg);
1316
+ function wobbleOffset() {
1317
+ return (rng() - 0.5) * wobble * 12;
1318
+ }
1319
+ function getRect() {
1320
+ return target.getBoundingClientRect();
1321
+ }
1322
+ function createPath() {
1323
+ const rect = getRect();
1324
+ const w = rect.width;
1325
+ const h = rect.height;
1326
+ if (style === "underline") {
1327
+ const path2 = document.createElementNS(SVG_NS2, "path");
1328
+ const y = h + 4;
1329
+ const cp1y = y + wobbleOffset();
1330
+ const cp2y = y + wobbleOffset();
1331
+ const d2 = `M 0 ${y + wobbleOffset()} Q ${w * 0.3} ${cp1y}, ${w * 0.5} ${y + wobbleOffset()} Q ${w * 0.7} ${cp2y}, ${w} ${y + wobbleOffset()}`;
1332
+ path2.setAttribute("d", d2);
1333
+ path2.setAttribute("fill", "none");
1334
+ path2.setAttribute("stroke", color);
1335
+ path2.setAttribute("stroke-width", String(strokeWidth));
1336
+ path2.setAttribute("stroke-linecap", "round");
1337
+ return path2;
1338
+ }
1339
+ if (style === "highlight") {
1340
+ const rectEl = document.createElementNS(SVG_NS2, "rect");
1341
+ const pad = 2;
1342
+ rectEl.setAttribute("x", String(-pad + wobbleOffset() * 0.3));
1343
+ rectEl.setAttribute("y", String(-pad + wobbleOffset() * 0.3));
1344
+ rectEl.setAttribute("width", String(w + pad * 2 + wobbleOffset() * 0.2));
1345
+ rectEl.setAttribute("height", String(h + pad * 2 + wobbleOffset() * 0.2));
1346
+ rectEl.setAttribute("rx", String(3 + Math.abs(wobbleOffset() * 0.5)));
1347
+ rectEl.setAttribute("fill", color);
1348
+ rectEl.setAttribute("opacity", "0.2");
1349
+ return rectEl;
1350
+ }
1351
+ if (style === "circle") {
1352
+ const pad = 12;
1353
+ const cx = w / 2;
1354
+ const cy = h / 2;
1355
+ const rx = w / 2 + pad;
1356
+ const ry = h / 2 + pad;
1357
+ const path2 = document.createElementNS(SVG_NS2, "path");
1358
+ let d2 = "";
1359
+ for (let stroke = 0; stroke < 2; stroke++) {
1360
+ const segments = 8;
1361
+ for (let i = 0; i <= segments; i++) {
1362
+ const angle = i / segments * Math.PI * 2;
1363
+ const x = cx + (rx + wobbleOffset()) * Math.cos(angle);
1364
+ const y = cy + (ry + wobbleOffset()) * Math.sin(angle);
1365
+ d2 += i === 0 && stroke === 0 ? `M ${x} ${y}` : ` L ${x} ${y}`;
1366
+ }
1367
+ }
1368
+ path2.setAttribute("d", d2);
1369
+ path2.setAttribute("fill", "none");
1370
+ path2.setAttribute("stroke", color);
1371
+ path2.setAttribute("stroke-width", String(strokeWidth));
1372
+ path2.setAttribute("stroke-linecap", "round");
1373
+ path2.setAttribute("stroke-linejoin", "round");
1374
+ return path2;
1375
+ }
1376
+ const path = document.createElementNS(SVG_NS2, "path");
1377
+ const bx = -10;
1378
+ const midY = h / 2;
1379
+ const d = `M ${bx + 6 + wobbleOffset()} ${wobbleOffset()} Q ${bx + wobbleOffset()} ${wobbleOffset()}, ${bx + wobbleOffset()} ${midY * 0.3} L ${bx + wobbleOffset()} ${midY - 5} Q ${bx - 4 + wobbleOffset()} ${midY}, ${bx + wobbleOffset()} ${midY + 5} L ${bx + wobbleOffset()} ${h * 0.7} Q ${bx + wobbleOffset()} ${h}, ${bx + 6 + wobbleOffset()} ${h + wobbleOffset()}`;
1380
+ path.setAttribute("d", d);
1381
+ path.setAttribute("fill", "none");
1382
+ path.setAttribute("stroke", color);
1383
+ path.setAttribute("stroke-width", String(strokeWidth));
1384
+ path.setAttribute("stroke-linecap", "round");
1385
+ return path;
1386
+ }
1387
+ function animate() {
1388
+ if (animated || disposed) return;
1389
+ animated = true;
1390
+ const el = createPath();
1391
+ svg.appendChild(el);
1392
+ if (style === "highlight") {
1393
+ el.style.opacity = "0";
1394
+ el.style.transition = `opacity ${duration}ms ease-out ${delay}ms`;
1395
+ requestAnimationFrame(() => {
1396
+ el.style.opacity = "0.2";
1397
+ });
1398
+ return;
1399
+ }
1400
+ if (el instanceof SVGGeometryElement) {
1401
+ const length = el.getTotalLength();
1402
+ el.setAttribute("stroke-dasharray", String(length));
1403
+ el.setAttribute("stroke-dashoffset", String(length));
1404
+ el.style.transition = `stroke-dashoffset ${duration}ms ease-out ${delay}ms`;
1405
+ requestAnimationFrame(() => {
1406
+ el.setAttribute("stroke-dashoffset", "0");
1407
+ });
1408
+ }
1409
+ }
1410
+ if (trigger === "immediate") {
1411
+ requestAnimationFrame(animate);
1412
+ } else if (trigger === "hover") {
1413
+ const onEnter = () => {
1414
+ animate();
1415
+ target.removeEventListener("mouseenter", onEnter);
1416
+ };
1417
+ target.addEventListener("mouseenter", onEnter);
1418
+ } else {
1419
+ observer = new IntersectionObserver(
1420
+ (entries) => {
1421
+ for (const entry of entries) {
1422
+ if (entry.isIntersecting) {
1423
+ animate();
1424
+ observer?.disconnect();
1425
+ }
1426
+ }
1427
+ },
1428
+ { rootMargin: `0px 0px -${Math.round(scrollOffset * 100)}% 0px` }
1429
+ );
1430
+ observer.observe(target);
1431
+ }
1432
+ return () => {
1433
+ if (disposed) return;
1434
+ disposed = true;
1435
+ observer?.disconnect();
1436
+ svg.remove();
1437
+ };
1438
+ }
1439
+ function hashString(str) {
1440
+ let hash = 0;
1441
+ for (let i = 0; i < str.length; i++) {
1442
+ const ch = str.charCodeAt(i);
1443
+ hash = (hash << 5) - hash + ch | 0;
1444
+ }
1445
+ return Math.abs(hash);
1446
+ }
1447
+ // Annotate the CommonJS export names for ESM import in node:
1448
+ 0 && (module.exports = {
1449
+ clamp,
1450
+ createAnnotationHighlight,
1451
+ createDiagramDrawer,
1452
+ createHorizontalScroll,
1453
+ createScrollOrganizer,
1454
+ createWaveformVisualizer,
1455
+ ease,
1456
+ lerp
1457
+ });