@neo-ui-kit/react 1.3.9 → 1.3.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  PianoRoll,
4
4
  isBlackKey,
5
5
  noteName
6
- } from "./chunks/chunk-VK6GHRLV.js";
6
+ } from "./chunks/chunk-RM7YNNMZ.js";
7
7
  import {
8
8
  Router,
9
9
  matchRoute,
@@ -2642,12 +2642,12 @@ function DataTable({
2642
2642
  icon: "download",
2643
2643
  label: tr2("\u042D\u043A\u0441\u043F\u043E\u0440\u0442 CSV"),
2644
2644
  onClick: () => {
2645
- const url = URL.createObjectURL(new Blob(["\uFEFF", dataTableCsv(visibleCols, sorted.map(({ row }) => row))], { type: "text/csv;charset=utf-8" }));
2645
+ const url2 = URL.createObjectURL(new Blob(["\uFEFF", dataTableCsv(visibleCols, sorted.map(({ row }) => row))], { type: "text/csv;charset=utf-8" }));
2646
2646
  const link = document.createElement("a");
2647
- link.href = url;
2647
+ link.href = url2;
2648
2648
  link.download = "data-table.csv";
2649
2649
  link.click();
2650
- window.setTimeout(() => URL.revokeObjectURL(url), 0);
2650
+ window.setTimeout(() => URL.revokeObjectURL(url2), 0);
2651
2651
  }
2652
2652
  }
2653
2653
  )
@@ -4938,7 +4938,7 @@ function createQuantumFieldAudio() {
4938
4938
  analyser.fftSize = 1024;
4939
4939
  let source = null;
4940
4940
  let media = null;
4941
- let url = null;
4941
+ let url2 = null;
4942
4942
  let ownedStream = null;
4943
4943
  let disposed = false;
4944
4944
  const disconnect = () => {
@@ -4946,10 +4946,10 @@ function createQuantumFieldAudio() {
4946
4946
  source?.disconnect();
4947
4947
  analyser.disconnect();
4948
4948
  ownedStream?.getTracks().forEach((track) => track.stop());
4949
- if (url) URL.revokeObjectURL(url);
4949
+ if (url2) URL.revokeObjectURL(url2);
4950
4950
  media = null;
4951
4951
  source = null;
4952
- url = null;
4952
+ url2 = null;
4953
4953
  ownedStream = null;
4954
4954
  };
4955
4955
  const assertOpen = () => {
@@ -4970,8 +4970,8 @@ function createQuantumFieldAudio() {
4970
4970
  loadFile(file) {
4971
4971
  assertOpen();
4972
4972
  disconnect();
4973
- url = URL.createObjectURL(file);
4974
- media = new Audio(url);
4973
+ url2 = URL.createObjectURL(file);
4974
+ media = new Audio(url2);
4975
4975
  source = context.createMediaElementSource(media);
4976
4976
  source.connect(analyser);
4977
4977
  analyser.connect(context.destination);
@@ -5322,7 +5322,7 @@ var rackLines = rackRows.map((row) => `M33 ${50 + row * 3.35} 90 ${38.8 + row *
5322
5322
  var rackHatches = rackRows.flatMap((row) => Array.from({ length: 9 }, (_, col) => `M${34 + col * 6.15} ${49.8 + row * 3.35 - col * 1.205}l2.6-.51`)).join(" ");
5323
5323
  function ServerArt({ upload = false, label, ...p }) {
5324
5324
  const id = useSvgId();
5325
- const url = (name) => `url(#${id}-${name})`;
5325
+ const url2 = (name) => `url(#${id}-${name})`;
5326
5326
  const box = "0 0 180 170";
5327
5327
  const lift = upload ? "translate(0 15)" : void 0;
5328
5328
  const dy = upload ? 15 : 0;
@@ -5379,14 +5379,14 @@ function ServerArt({ upload = false, label, ...p }) {
5379
5379
  ),
5380
5380
  /* @__PURE__ */ jsx45("path", { d: "M80 37V18m-7 7 7-7 7 7", stroke: "var(--ad-secondary)", strokeWidth: "1.5" })
5381
5381
  ] }),
5382
- /* @__PURE__ */ jsx45(ArtLayer, { viewBox: box, className: "ad-art-aura-layer", origin: [89, 124 + dy], children: /* @__PURE__ */ jsx45("ellipse", { cx: "89", cy: 124 + dy, rx: "76", ry: "22", fill: url("aura") }) }),
5382
+ /* @__PURE__ */ jsx45(ArtLayer, { viewBox: box, className: "ad-art-aura-layer", origin: [89, 124 + dy], children: /* @__PURE__ */ jsx45("ellipse", { cx: "89", cy: 124 + dy, rx: "76", ry: "22", fill: url2("aura") }) }),
5383
5383
  /* @__PURE__ */ jsx45(ArtLayer, { viewBox: box, children: /* @__PURE__ */ jsxs36("g", { transform: lift, children: [
5384
5384
  /* @__PURE__ */ jsx45(
5385
5385
  "path",
5386
5386
  {
5387
5387
  d: "M26 43 98 29 150 45 75 61Z",
5388
- fill: url("top"),
5389
- stroke: url("edge"),
5388
+ fill: url2("top"),
5389
+ stroke: url2("edge"),
5390
5390
  strokeWidth: ".75"
5391
5391
  }
5392
5392
  ),
@@ -5394,7 +5394,7 @@ function ServerArt({ upload = false, label, ...p }) {
5394
5394
  "path",
5395
5395
  {
5396
5396
  d: "M98 29 150 45 150 119 98 107Z",
5397
- fill: url("side"),
5397
+ fill: url2("side"),
5398
5398
  stroke: "var(--ad-primary-700)",
5399
5399
  strokeWidth: ".7"
5400
5400
  }
@@ -5403,8 +5403,8 @@ function ServerArt({ upload = false, label, ...p }) {
5403
5403
  "path",
5404
5404
  {
5405
5405
  d: "M26 43 98 29 98 107 26 121Z",
5406
- fill: url("front"),
5407
- stroke: url("edge"),
5406
+ fill: url2("front"),
5407
+ stroke: url2("edge"),
5408
5408
  strokeWidth: "1"
5409
5409
  }
5410
5410
  ),
@@ -5427,7 +5427,7 @@ function ServerArt({ upload = false, label, ...p }) {
5427
5427
  stroke: "var(--ad-secondary-200)",
5428
5428
  strokeWidth: "3",
5429
5429
  opacity: ".35",
5430
- filter: url("bloom")
5430
+ filter: url2("bloom")
5431
5431
  }
5432
5432
  ),
5433
5433
  /* @__PURE__ */ jsx45("path", { d: "M27 44 98 30 147 45", stroke: "var(--ad-secondary-200)", strokeWidth: ".75" }),
@@ -5454,7 +5454,7 @@ function ServerArt({ upload = false, label, ...p }) {
5454
5454
  "path",
5455
5455
  {
5456
5456
  d: "M26 125 98 112 150 126v14l-73 10-51-10Z",
5457
- fill: url("side"),
5457
+ fill: url2("side"),
5458
5458
  stroke: "var(--ad-primary-700)",
5459
5459
  strokeWidth: ".6"
5460
5460
  }
@@ -5463,7 +5463,7 @@ function ServerArt({ upload = false, label, ...p }) {
5463
5463
  "path",
5464
5464
  {
5465
5465
  d: "M26 125 98 112v15l-72 13Z",
5466
- fill: url("front"),
5466
+ fill: url2("front"),
5467
5467
  stroke: "var(--ad-primary-600)",
5468
5468
  strokeWidth: ".65"
5469
5469
  }
@@ -6040,7 +6040,7 @@ function LevelMeter({
6040
6040
  samples.push(envelope);
6041
6041
  shape.setAttribute("d", wavePath2(samples));
6042
6042
  }
6043
- if (!analyser && envelope === 0 && samples.every((sample) => sample === 0)) {
6043
+ if (!analyser && input === 0 && envelope === 0 && samples.every((sample) => sample === 0)) {
6044
6044
  shape.setAttribute("transform", "translate(0 0)");
6045
6045
  stop?.();
6046
6046
  stop = void 0;
@@ -6147,7 +6147,982 @@ var KaraokeLyrics = ({
6147
6147
  );
6148
6148
 
6149
6149
  // src/components/media/MelodyRoll/MelodyRoll.tsx
6150
- import { useMemo as useMemo5, useRef as useRef28 } from "react";
6150
+ import { useEffect as useEffect14, useLayoutEffect as useLayoutEffect5, useMemo as useMemo5, useRef as useRef28, useState as useState13 } from "react";
6151
+
6152
+ // src/components/media/MelodyRoll/melody-glow.js
6153
+ function renderGlow() {
6154
+ const scenes = /* @__PURE__ */ new Map();
6155
+ const fonts = /* @__PURE__ */ new Map();
6156
+ let frame = null;
6157
+ function ease(x, out = false) {
6158
+ if (x <= 0) return 0;
6159
+ if (x >= 1) return 1;
6160
+ let lo = 0, hi = 1;
6161
+ for (let i = 0; i < 18; i++) {
6162
+ const t2 = (lo + hi) / 2;
6163
+ const at = out ? 1.74 * (1 - t2) * t2 * t2 + t2 ** 3 : 0.75 * (1 - t2) * t2 + t2 ** 3;
6164
+ if (at < x) lo = t2;
6165
+ else hi = t2;
6166
+ }
6167
+ const t = (lo + hi) / 2;
6168
+ return (out ? 0 : 0.3 * (1 - t) ** 2 * t) + 3 * (1 - t) * t * t + t ** 3;
6169
+ }
6170
+ function value(scene, now) {
6171
+ return animated(scene.beat, now);
6172
+ }
6173
+ function mix(ctx, from, to, progress2, space = "oklab") {
6174
+ ctx.fillStyle = `color-mix(in ${space}, ${from}, ${to} ${progress2 * 100}%)`;
6175
+ return ctx.fillStyle;
6176
+ }
6177
+ function progress(channel, now) {
6178
+ const fraction = channel.duration > 0 ? Math.min(1, Math.max(0, (now - channel.start) / channel.duration)) : 1;
6179
+ return channel.linear ? fraction : ease(fraction);
6180
+ }
6181
+ function animated(channel, now, ctx) {
6182
+ const amount = progress(channel, now);
6183
+ if (amount === 1) return channel.to;
6184
+ if (amount === 0) return channel.from;
6185
+ if (Array.isArray(channel.to)) return Array.from({ length: Math.max(channel.from.length, channel.to.length) }, (_, i) => {
6186
+ const model = channel.to[i] ?? channel.from[i];
6187
+ const empty = { kind: model.kind, value: 1, x: 0, y: 0, blur: 0, spread: 0, color: "transparent", inset: model.inset };
6188
+ const a = channel.from[i] ?? empty, b = channel.to[i] ?? empty;
6189
+ if (model.kind === "brightness") return { kind: model.kind, value: a.value + (b.value - a.value) * amount };
6190
+ return { kind: model.kind, inset: b.inset, ...Object.fromEntries(["x", "y", "blur", "spread"].map((key) => [key, a[key] + (b[key] - a[key]) * amount])), color: mix(ctx, a.color, b.color, amount) };
6191
+ });
6192
+ return typeof channel.to === "number" ? channel.from + (channel.to - channel.from) * amount : mix(ctx, channel.from, channel.to, amount);
6193
+ }
6194
+ function transition(previous, to, now, duration, ctx, linear = false) {
6195
+ if (Array.isArray(to) && previous?.to.some((shadow, i) => to[i] && (shadow.inset !== to[i].inset || shadow.kind !== to[i].kind))) previous = void 0;
6196
+ if (!previous || duration <= 0) return { from: to, to, start: now, duration: 0, reverse: to, factor: 1, linear };
6197
+ if (previous.to === to) return previous;
6198
+ const from = animated(previous, now, ctx);
6199
+ const reversing = previous.reverse === to;
6200
+ const factor = reversing ? Math.min(1, Math.abs(progress(previous, now) * previous.factor + 1 - previous.factor)) : 1;
6201
+ return { from, to, start: now, duration: duration * factor, reverse: reversing ? previous.to : from, factor, linear };
6202
+ }
6203
+ function box(ctx, style, x, y, width, height, shadows = style.shadows) {
6204
+ const radius2 = style.radius.map((r) => Math.min(r, width / 2, height / 2));
6205
+ const [top, right, bottom, left] = style.borders.map((edge) => edge.width);
6206
+ const innerX = x + left, innerY = y + top, innerW = width - left - right, innerH = height - top - bottom;
6207
+ const innerR = radius2.map((r, i) => ({ x: Math.max(0, r - ([1, 2].includes(i) ? right : left)), y: Math.max(0, r - (i < 2 ? top : bottom)) }));
6208
+ const shape = () => {
6209
+ ctx.beginPath();
6210
+ ctx.roundRect(x, y, width, height, radius2);
6211
+ };
6212
+ const dpr = ctx.getTransform().a;
6213
+ for (const shadow of [...shadows].reverse()) {
6214
+ if (shadow.inset) continue;
6215
+ ctx.save();
6216
+ ctx.beginPath();
6217
+ ctx.rect(x - 200, y - 200, width + 400, height + 400);
6218
+ ctx.roundRect(x, y, width, height, radius2);
6219
+ ctx.clip("evenodd");
6220
+ const offset = width + 1e3;
6221
+ ctx.shadowOffsetX = (offset + shadow.x) * dpr;
6222
+ ctx.shadowOffsetY = shadow.y * dpr;
6223
+ ctx.shadowBlur = shadow.blur * dpr;
6224
+ ctx.shadowColor = shadow.color;
6225
+ ctx.fillStyle = "#000";
6226
+ ctx.beginPath();
6227
+ ctx.roundRect(x - offset - shadow.spread, y - shadow.spread, Math.max(0, width + 2 * shadow.spread), Math.max(0, height + 2 * shadow.spread), radius2.map((r) => Math.max(0, r + shadow.spread)));
6228
+ ctx.fill();
6229
+ ctx.restore();
6230
+ }
6231
+ ctx.save();
6232
+ shape();
6233
+ ctx.clip();
6234
+ ctx.fillStyle = style.backgroundColor;
6235
+ ctx.fillRect(x, y, width, height);
6236
+ if (style.gradient.length) {
6237
+ const gradient = ctx.createLinearGradient(innerX, y, innerX + innerW, y);
6238
+ for (const stop of style.gradient) gradient.addColorStop(stop.at, stop.color);
6239
+ ctx.fillStyle = gradient;
6240
+ ctx.fillRect(x, y, width, height);
6241
+ }
6242
+ for (const shadow of [...shadows].reverse()) {
6243
+ if (!shadow.inset) continue;
6244
+ ctx.save();
6245
+ ctx.beginPath();
6246
+ ctx.roundRect(innerX, innerY, innerW, innerH, innerR);
6247
+ ctx.clip();
6248
+ const offset = width + 1e3;
6249
+ ctx.shadowOffsetX = (offset + shadow.x) * dpr;
6250
+ ctx.shadowOffsetY = shadow.y * dpr;
6251
+ ctx.shadowBlur = shadow.blur * dpr;
6252
+ ctx.shadowColor = shadow.color;
6253
+ ctx.fillStyle = "#000";
6254
+ ctx.beginPath();
6255
+ ctx.rect(innerX - offset - 200, innerY - 200, innerW + 400, innerH + 400);
6256
+ ctx.roundRect(innerX - offset + shadow.spread, innerY + shadow.spread, Math.max(0, innerW - 2 * shadow.spread), Math.max(0, innerH - 2 * shadow.spread), innerR.map((r) => ({ x: Math.max(0, r.x - shadow.spread), y: Math.max(0, r.y - shadow.spread) })));
6257
+ ctx.fill("evenodd");
6258
+ ctx.restore();
6259
+ }
6260
+ const border = style.borders.find((edge) => edge.width > 0);
6261
+ if (border && style.borders.every((edge) => !edge.width || edge.color === border.color)) {
6262
+ ctx.fillStyle = border.color;
6263
+ ctx.beginPath();
6264
+ ctx.roundRect(x, y, width, height, radius2);
6265
+ ctx.roundRect(innerX, innerY, innerW, innerH, innerR);
6266
+ ctx.fill("evenodd");
6267
+ } else for (let i = 0; i < 4; i++) {
6268
+ const edge = style.borders[i];
6269
+ ctx.fillStyle = edge.color;
6270
+ const rect = [[x, y, width, edge.width], [x + width - edge.width, y, edge.width, height], [x, y + height - edge.width, width, edge.width], [x, y, edge.width, height]][i];
6271
+ ctx.fillRect(...rect);
6272
+ }
6273
+ ctx.restore();
6274
+ }
6275
+ function text(ctx, style, value2, x, y, width, height, color3 = style.color, right = false) {
6276
+ ctx.save();
6277
+ if (!right) {
6278
+ ctx.beginPath();
6279
+ ctx.rect(x, y, width, height);
6280
+ ctx.clip();
6281
+ }
6282
+ ctx.font = style.font;
6283
+ ctx.textBaseline = "alphabetic";
6284
+ ctx.textAlign = "left";
6285
+ ctx.letterSpacing = style.letterSpacing;
6286
+ ctx.fillStyle = color3;
6287
+ const metrics = ctx.measureText(value2);
6288
+ const baseline = y + (style.baseline ?? (height - metrics.fontBoundingBoxAscent - metrics.fontBoundingBoxDescent) / 2 + metrics.fontBoundingBoxAscent);
6289
+ const start = x + (right ? style.offset : style.padding);
6290
+ for (const shadow of [...style.textShadows].reverse()) {
6291
+ ctx.save();
6292
+ ctx.shadowColor = shadow.color;
6293
+ ctx.shadowBlur = shadow.blur * ctx.getTransform().a;
6294
+ ctx.shadowOffsetX = (width + 1e3 + shadow.x) * ctx.getTransform().a;
6295
+ ctx.shadowOffsetY = shadow.y * ctx.getTransform().a;
6296
+ ctx.fillText(value2, start - width - 1e3, baseline);
6297
+ ctx.restore();
6298
+ }
6299
+ ctx.fillText(value2, start, baseline);
6300
+ ctx.restore();
6301
+ }
6302
+ function retargetForeground(scene, now) {
6303
+ if (!scene.foreground) return;
6304
+ const { state, foreground: style, noteCtx: ctx } = scene;
6305
+ scene.noteStates ??= /* @__PURE__ */ new Map();
6306
+ const kept = /* @__PURE__ */ new Map();
6307
+ const from = state.position - state.span * state.lead;
6308
+ let until = 0;
6309
+ for (const [i, note] of (state.notes ?? []).entries()) {
6310
+ const previous = scene.noteStates.get(note.id);
6311
+ const visible = note.end >= from && note.start <= from + state.span;
6312
+ const hit = visible && state.hits?.[i] === "h";
6313
+ const current = visible && state.position >= note.start && state.position <= note.end;
6314
+ const target = hit ? style.note.hit : current ? style.note.current : style.note.base;
6315
+ const item = {
6316
+ note,
6317
+ visible,
6318
+ hit,
6319
+ buffer: previous?.buffer,
6320
+ opacity: transition(previous?.opacity, visible && !hit && state.position > note.end ? style.note.pastOpacity : 1, now, style.note.opacityDuration),
6321
+ filter: transition(previous?.filter, target.filter, now, style.note.filterDuration, ctx),
6322
+ hitStart: hit && !previous?.hit ? previous ? now : scene.noteTimes?.[note.id] ?? now : previous?.hitStart ?? -Infinity
6323
+ };
6324
+ kept.set(note.id, item);
6325
+ until = Math.max(until, item.opacity.start + item.opacity.duration, item.filter.start + item.filter.duration, hit ? item.hitStart + style.note.hitDuration : 0);
6326
+ }
6327
+ scene.noteStates = kept;
6328
+ scene.keyStates ??= /* @__PURE__ */ new Map();
6329
+ const keyStates = /* @__PURE__ */ new Map();
6330
+ for (const key of style.keys) {
6331
+ const previous = scene.keyStates.get(key.midi);
6332
+ const active = state.pitch !== void 0 && key.midi === Math.round(state.pitch);
6333
+ const hit = active && state.hit;
6334
+ const pressed = scene.pressed?.includes(key.midi);
6335
+ const variant = hit ? "hit" : active ? "active" : pressed ? "pressed" : "base";
6336
+ const target = key[variant];
6337
+ const item = {
6338
+ key,
6339
+ target,
6340
+ hit,
6341
+ variant,
6342
+ buffer: previous?.buffer,
6343
+ color: transition(previous?.color, target.color, now, key.duration, ctx),
6344
+ shadows: transition(previous?.shadows, target.shadows, now, key.duration, ctx),
6345
+ shift: transition(previous?.shift, pressed ? key.pressShift : 0, now, key.pressDuration),
6346
+ hitStart: hit && !previous?.hit ? previous ? now : scene.keyTimes?.[key.midi] ?? now : previous?.hitStart ?? now
6347
+ };
6348
+ keyStates.set(key.midi, item);
6349
+ until = Math.max(until, ...["color", "shadows", "shift"].map((name) => item[name].start + item[name].duration), hit && key.hitDuration > 0 ? item.hitStart + key.hitDuration * key.hitIterations : 0);
6350
+ }
6351
+ scene.keyStates = keyStates;
6352
+ scene.noteTimes = scene.keyTimes = void 0;
6353
+ scene.foregroundUntil = until;
6354
+ }
6355
+ function drawForeground(scene, now) {
6356
+ if (!scene.foreground) return;
6357
+ const { noteCtx: ctx, keyCtx, foreground: style, state, dpr, laneWidth, laneHeight } = scene;
6358
+ ctx.clearRect(0, 0, laneWidth, laneHeight);
6359
+ scene.sprites ??= /* @__PURE__ */ new Map();
6360
+ scene.filteredSprites ??= /* @__PURE__ */ new Map();
6361
+ scene.filteredPixels ??= 0;
6362
+ const from = state.position - state.span * state.lead;
6363
+ ctx.fillStyle = style.grid.color;
6364
+ const spacing2 = laneWidth / state.span;
6365
+ const shift = -((from % 1 + 1) % 1) * spacing2;
6366
+ for (let x = shift; x < laneWidth; x += spacing2) ctx.fillRect(x, 0, style.grid.width, laneHeight);
6367
+ for (const item of scene.noteStates.values()) {
6368
+ if (!item.visible) continue;
6369
+ const { note } = item;
6370
+ const origin = Math.floor(from / state.span) * state.span;
6371
+ const left = Math.floor((note.start - origin) / state.span * laneWidth * 64) / 64;
6372
+ const width = Math.floor(Math.max(6e-3, (note.end - note.start) / state.span) * laneWidth * 64) / 64;
6373
+ const height = Math.round(style.note.height * 64) / 64;
6374
+ const top = Math.floor((state.high - note.pitch) / state.rows * laneHeight * 64) / 64 + height * 0.158;
6375
+ const offsetX = left - Math.floor(left * dpr) / dpr, offsetY = top - Math.floor(top * dpr) / dpr;
6376
+ const lyric = state.showLyrics ? note.lyric ?? "" : "";
6377
+ const spriteKey = JSON.stringify([width, height, lyric, item.hit, offsetX, offsetY]);
6378
+ let sprite = scene.sprites.get(spriteKey);
6379
+ if (!sprite) {
6380
+ sprite = new OffscreenCanvas(Math.ceil((width + offsetX) * dpr), Math.ceil((height + offsetY) * dpr));
6381
+ const paint = sprite.getContext("2d");
6382
+ paint.setTransform(dpr, 0, 0, dpr, 0, 0);
6383
+ const base = item.hit ? style.note.hit : style.note.base;
6384
+ paint.save();
6385
+ paint.beginPath();
6386
+ paint.roundRect(offsetX, offsetY, width, height, base.radius.map((radius2) => Math.min(radius2, width / 2, height / 2)));
6387
+ paint.clip();
6388
+ box(paint, base, offsetX, offsetY, width, height);
6389
+ text(paint, style.note.text, lyric, offsetX, offsetY, width, height);
6390
+ paint.restore();
6391
+ scene.sprites.set(spriteKey, sprite);
6392
+ if (scene.sprites.size > 512) scene.sprites.delete(scene.sprites.keys().next().value);
6393
+ }
6394
+ let filter = animated(item.filter, now, ctx);
6395
+ const phase = style.note.hitDuration > 0 ? (now - item.hitStart) / style.note.hitDuration : 1;
6396
+ let scale = 1;
6397
+ if (item.hit && phase < 1) {
6398
+ const amount = phase < 0.28 ? ease(phase / 0.28, true) : 1 - ease((phase - 0.28) / 0.72, true);
6399
+ scale += 0.6 * amount;
6400
+ const a = style.note.hit.filter, b = style.note.peakFilter;
6401
+ filter = a.some((entry, i) => b[i] && entry.kind !== b[i].kind) ? amount < 0.5 ? a : b : animated({ from: a, to: b, start: 0, duration: 1, linear: true }, amount, ctx);
6402
+ }
6403
+ const filterValue = filter.map((part2) => part2.kind === "brightness" ? `brightness(${part2.value})` : `drop-shadow(${part2.x * dpr}px ${part2.y * dpr}px ${part2.blur * dpr}px ${part2.color})`).join(" ") || "none";
6404
+ let painted = sprite, padding = 0;
6405
+ if (filter.length) {
6406
+ padding = Math.ceil(style.note.filterPad * dpr);
6407
+ const filteredKey = spriteKey + filterValue;
6408
+ const settled = progress(item.filter, now) === 1 && (!item.hit || phase >= 1);
6409
+ painted = settled ? scene.filteredSprites.get(filteredKey) : null;
6410
+ if (!painted) {
6411
+ const w = sprite.width + padding * 2, h = sprite.height + padding * 2;
6412
+ if (!item.buffer || item.buffer.width !== w || item.buffer.height !== h) item.buffer = new OffscreenCanvas(w, h);
6413
+ painted = settled ? new OffscreenCanvas(w, h) : item.buffer;
6414
+ const paint = painted.getContext("2d");
6415
+ paint.clearRect(0, 0, w, h);
6416
+ paint.filter = filterValue;
6417
+ paint.drawImage(sprite, padding, padding);
6418
+ if (settled && w * h <= 8 * 1024 * 1024) {
6419
+ scene.filteredSprites.set(filteredKey, painted);
6420
+ scene.filteredPixels += w * h;
6421
+ while (scene.filteredPixels > 8 * 1024 * 1024) {
6422
+ const oldest = scene.filteredSprites.keys().next().value;
6423
+ const old = scene.filteredSprites.get(oldest);
6424
+ scene.filteredPixels -= old.width * old.height;
6425
+ scene.filteredSprites.delete(oldest);
6426
+ }
6427
+ }
6428
+ }
6429
+ }
6430
+ ctx.save();
6431
+ ctx.globalAlpha = animated(item.opacity, now);
6432
+ const x = left - offsetX + Number(((origin - from) / state.span * 100).toFixed(3)) / 100 * laneWidth;
6433
+ const y = top - offsetY;
6434
+ ctx.drawImage(painted, x - padding / dpr, top + height / 2 + (y - padding / dpr - top - height / 2) * scale, painted.width / dpr, painted.height / dpr * scale);
6435
+ ctx.restore();
6436
+ }
6437
+ keyCtx.clearRect(0, 0, style.keyWidth, style.keyHeight);
6438
+ scene.keySprites ??= /* @__PURE__ */ new Map();
6439
+ for (const item of scene.keyStates.values()) {
6440
+ const { key, target } = item;
6441
+ keyCtx.save();
6442
+ const phase = key.hitDuration > 0 ? (now - item.hitStart) / key.hitDuration % 1 : 0;
6443
+ const fraction = phase < 0.5 ? phase * 2 : (1 - phase) * 2;
6444
+ let lo = 0, hi = 1;
6445
+ for (let i = 0; i < 18; i++) {
6446
+ const t2 = (lo + hi) / 2;
6447
+ if (1.26 * (1 - t2) ** 2 * t2 + 1.74 * (1 - t2) * t2 * t2 + t2 ** 3 < fraction) lo = t2;
6448
+ else hi = t2;
6449
+ }
6450
+ const t = (lo + hi) / 2;
6451
+ const baseBrightness = scene.hovered === key.midi ? 1.08 : 1;
6452
+ const brightness = item.hit && key.hitDuration > 0 && now < item.hitStart + key.hitDuration * key.hitIterations ? baseBrightness + (1.25 - baseBrightness) * (3 * (1 - t) * t * t + t ** 3) : baseBrightness;
6453
+ keyCtx.filter = `brightness(${brightness})`;
6454
+ const settled = progress(item.shadows, now) === 1 && progress(item.color, now) === 1;
6455
+ const cacheKey = `${key.midi}:${item.variant}`;
6456
+ let sprite = settled ? scene.keySprites.get(cacheKey) : null;
6457
+ const paintTop = Math.round(key.top * dpr) / dpr, paintWidth = Math.round(key.width * dpr) / dpr;
6458
+ const paintHeight = Math.round((key.top + key.height) * dpr) / dpr - paintTop;
6459
+ const pad = key.pad, width = paintWidth + pad * 2, height = paintHeight + pad * 2;
6460
+ if (!sprite) {
6461
+ sprite = settled ? new OffscreenCanvas(Math.ceil(width * dpr), Math.ceil(height * dpr)) : item.buffer ??= new OffscreenCanvas(Math.ceil(width * dpr), Math.ceil(height * dpr));
6462
+ const paint = sprite.getContext("2d");
6463
+ paint.setTransform(dpr, 0, 0, dpr, 0, 0);
6464
+ paint.clearRect(0, 0, width, height);
6465
+ box(paint, target, pad, pad, paintWidth, paintHeight, animated(item.shadows, now, paint));
6466
+ const [top, right, bottom, left] = target.borders.map((edge) => edge.width);
6467
+ text(paint, key.text, key.label, pad + left, pad + top + key.top - paintTop, key.width - left - right, key.height - top - bottom, animated(item.color, now, paint), true);
6468
+ if (settled) scene.keySprites.set(cacheKey, sprite);
6469
+ }
6470
+ keyCtx.drawImage(sprite, 0, 0, width * dpr, height * dpr, animated(item.shift, now) - pad, paintTop - pad, width, height);
6471
+ keyCtx.restore();
6472
+ }
6473
+ }
6474
+ function voiceValue(scene, now) {
6475
+ return Object.fromEntries(["y", "size", "color"].map((key) => [key, animated(scene.voice[key], now, scene.voiceCtx)]));
6476
+ }
6477
+ function retargetVoice(scene, now, previous) {
6478
+ const { state, voiceStyle: style, voiceCtx: ctx, laneHeight } = scene;
6479
+ const to = {
6480
+ y: (state.high - (state.pitch ?? 0) + 0.5) / state.rows * laneHeight,
6481
+ size: style.size[0] + (style.size[1] - style.size[0]) * state.level,
6482
+ color: state.hit ? style.hitColor : mix(ctx, style.color[0], style.color[1], state.accuracy, "oklch")
6483
+ };
6484
+ const hitStart = state.hit && !previous?.hit ? now : scene.voice?.hitStart ?? -Infinity;
6485
+ scene.voice = { ...Object.fromEntries(Object.entries(to).map(([key, target]) => [
6486
+ key,
6487
+ transition(previous?.pitch !== void 0 ? scene.voice?.[key] : void 0, target, now, style.duration, ctx, key === "y")
6488
+ ])), hitStart };
6489
+ }
6490
+ function drawVoice(scene, now) {
6491
+ const { voiceCtx: ctx, laneWidth, laneHeight, state, voiceStyle: style, dpr } = scene;
6492
+ ctx.clearRect(0, 0, laneWidth, laneHeight);
6493
+ ctx.save();
6494
+ const head = state.lead * laneWidth - scene.headWidth / 2;
6495
+ const offset = laneWidth + 200;
6496
+ ctx.beginPath();
6497
+ ctx.rect(0, 0, laneWidth, laneHeight);
6498
+ ctx.rect(head, 0, scene.headWidth, laneHeight);
6499
+ ctx.clip("evenodd");
6500
+ ctx.globalAlpha = state.beat;
6501
+ ctx.shadowOffsetX = offset * dpr;
6502
+ for (const shadow of [...scene.headShadows].reverse()) {
6503
+ ctx.shadowBlur = shadow.blur * dpr;
6504
+ ctx.shadowColor = shadow.color;
6505
+ ctx.fillRect(head - offset, 0, scene.headWidth, laneHeight);
6506
+ }
6507
+ ctx.restore();
6508
+ if (state.pitch === void 0) return;
6509
+ const voice = voiceValue(scene, now);
6510
+ const phase = style.hitDuration > 0 ? (now - scene.voice.hitStart) / style.hitDuration : 1;
6511
+ const scale = state.hit && phase < 1 ? 1 + 0.35 * (phase < 0.45 ? ease(phase / 0.45, true) : 1 - ease((phase - 0.45) / 0.55, true)) : 1;
6512
+ const x = state.lead * laneWidth, r = voice.size * scale / 2;
6513
+ for (let i = style.shadows[0].length - 1; i >= 0; i--) {
6514
+ const a = style.shadows[0][i], b = style.shadows[1][i];
6515
+ const blur = (a.blur + (b.blur - a.blur) * state.accuracy) * scale;
6516
+ const spread = (a.spread + (b.spread - a.spread) * state.accuracy) * scale;
6517
+ ctx.save();
6518
+ const offset2 = laneWidth + 100;
6519
+ ctx.shadowOffsetX = offset2 * dpr;
6520
+ ctx.shadowBlur = blur * dpr;
6521
+ ctx.shadowColor = mix(ctx, a.color, b.color, state.accuracy, "srgb");
6522
+ ctx.fillStyle = "#000";
6523
+ ctx.beginPath();
6524
+ ctx.arc(x - offset2, voice.y, r + spread, 0, Math.PI * 2);
6525
+ ctx.fill();
6526
+ ctx.restore();
6527
+ }
6528
+ ctx.fillStyle = style.borderColor;
6529
+ ctx.beginPath();
6530
+ ctx.arc(x, voice.y, r, 0, Math.PI * 2);
6531
+ ctx.fill();
6532
+ ctx.fillStyle = voice.color;
6533
+ ctx.beginPath();
6534
+ ctx.arc(x, voice.y, Math.max(0, r - style.border * scale), 0, Math.PI * 2);
6535
+ ctx.fill();
6536
+ }
6537
+ function updateTrail(scene) {
6538
+ const { position, pitch } = scene.state;
6539
+ if (scene.trail.at(-1)?.t !== position)
6540
+ scene.trail = [...scene.trail.filter((point) => point.t > position - 3 && point.t < position), { t: position, pitch }];
6541
+ }
6542
+ function drawTrail(scene) {
6543
+ const { trailCtx: ctx, laneWidth, laneHeight, state, trailStyle } = scene;
6544
+ ctx.clearRect(0, 0, laneWidth, laneHeight);
6545
+ const paths = [];
6546
+ let path = null;
6547
+ for (const point of scene.trail) {
6548
+ if (point.pitch === void 0) {
6549
+ path = null;
6550
+ continue;
6551
+ }
6552
+ const x = Number(((point.t - state.position + state.span * state.lead) / state.span * 100).toFixed(2)) / 100 * laneWidth;
6553
+ const y = Number(((state.high - point.pitch + 0.5) / state.rows * 100).toFixed(2)) / 100 * laneHeight;
6554
+ if (!path) {
6555
+ path = new Path2D();
6556
+ path.moveTo(x, y);
6557
+ paths.push(path);
6558
+ } else path.lineTo(x, y);
6559
+ }
6560
+ ctx.lineCap = ctx.lineJoin = "round";
6561
+ for (const path2 of paths) for (const layer of trailStyle) {
6562
+ ctx.strokeStyle = mix(ctx, layer.color[0], layer.color[1], state.accuracy, "oklch");
6563
+ ctx.lineWidth = layer.width;
6564
+ ctx.globalAlpha = layer.opacity;
6565
+ ctx.stroke(path2);
6566
+ }
6567
+ ctx.globalAlpha = 1;
6568
+ }
6569
+ function draw(scene, now) {
6570
+ const { ctx, width, height, pad, radius: radius2, blur, color: color3, dpr } = scene;
6571
+ ctx.clearRect(0, 0, width + pad * 2, height + pad * 2);
6572
+ const amount = value(scene, now);
6573
+ if (amount > 0) {
6574
+ ctx.save();
6575
+ ctx.beginPath();
6576
+ ctx.rect(0, 0, width + pad * 2, height + pad * 2);
6577
+ ctx.roundRect(pad, pad, width, height, radius2);
6578
+ ctx.clip("evenodd");
6579
+ const offset = width + pad * 4;
6580
+ ctx.shadowOffsetX = offset * dpr;
6581
+ ctx.shadowBlur = blur * amount * dpr;
6582
+ ctx.shadowColor = color3;
6583
+ ctx.fillStyle = "#000";
6584
+ ctx.beginPath();
6585
+ ctx.roundRect(pad - offset, pad, width, height, radius2);
6586
+ ctx.fill();
6587
+ ctx.restore();
6588
+ ctx.save();
6589
+ ctx.globalAlpha = amount;
6590
+ ctx.translate(pad, pad);
6591
+ ctx.beginPath();
6592
+ ctx.roundRect(0, 0, width, height, radius2);
6593
+ ctx.clip();
6594
+ ctx.save();
6595
+ ctx.translate(width * 0.25, height * 0.5);
6596
+ ctx.scale(width * 0.8, height * 1.2);
6597
+ const light = ctx.createRadialGradient(0, 0, 0, 0, 0, 1);
6598
+ light.addColorStop(0, scene.lightColors[0]);
6599
+ light.addColorStop(0.7, `rgb(from ${scene.lightColors[0]} r g b / 0)`);
6600
+ ctx.fillStyle = light;
6601
+ ctx.fillRect(-0.25 / 0.8, -0.5 / 1.2, 1 / 0.8, 1 / 1.2);
6602
+ ctx.restore();
6603
+ ctx.strokeStyle = scene.lightBorder;
6604
+ ctx.lineWidth = scene.lightWidth;
6605
+ const half = scene.lightWidth / 2;
6606
+ ctx.beginPath();
6607
+ ctx.roundRect(half, half, width - scene.lightWidth, height - scene.lightWidth, radius2.map((r) => Math.max(0, r - half)));
6608
+ ctx.stroke();
6609
+ ctx.restore();
6610
+ }
6611
+ drawTrail(scene);
6612
+ drawForeground(scene, now);
6613
+ drawVoice(scene, now);
6614
+ }
6615
+ function tick(now) {
6616
+ now += performance.timeOrigin;
6617
+ frame = null;
6618
+ for (const scene of scenes.values()) if (scene.visible && !scene.loading) draw(scene, now);
6619
+ if ([...scenes.values()].some((scene) => scene.visible && !scene.loading && now < Math.max(scene.foregroundUntil ?? 0, scene.beat.start + scene.beat.duration, ...["y", "size", "color"].map((key) => scene.voice[key].start + scene.voice[key].duration), scene.state.hit ? scene.voice.hitStart + scene.voiceStyle.hitDuration : 0)))
6620
+ frame = requestAnimationFrame(tick);
6621
+ }
6622
+ onmessage = async ({ data }) => {
6623
+ try {
6624
+ let scene = scenes.get(data.id);
6625
+ const now = data.time ?? performance.timeOrigin + performance.now();
6626
+ if (data.type === "remove") scenes.delete(data.id);
6627
+ else if (data.type === "configure") {
6628
+ const canvas = scene?.canvas ?? data.canvas;
6629
+ const previous = scene?.state;
6630
+ if (scene?.voice && scene.laneHeight > 0) {
6631
+ scene.voice.y.from *= data.laneHeight / scene.laneHeight;
6632
+ scene.voice.y.to *= data.laneHeight / scene.laneHeight;
6633
+ scene.voice.y.reverse *= data.laneHeight / scene.laneHeight;
6634
+ }
6635
+ const voiceCanvas = scene?.voiceCanvas ?? data.voiceCanvas;
6636
+ const trailCanvas = scene?.trailCanvas ?? data.trailCanvas;
6637
+ scene = { ...scene, ...data, canvas, voiceCanvas, trailCanvas, beat: transition(scene?.beat, data.state.beat, now, data.duration), trail: scene?.trail ?? data.state.trail ?? [] };
6638
+ canvas.width = Math.ceil((data.width + data.pad * 2) * data.dpr);
6639
+ canvas.height = Math.ceil((data.height + data.pad * 2) * data.dpr);
6640
+ scene.ctx = canvas.getContext("2d");
6641
+ scene.ctx.setTransform(data.dpr, 0, 0, data.dpr, 0, 0);
6642
+ voiceCanvas.width = Math.ceil(data.laneWidth * data.dpr);
6643
+ voiceCanvas.height = Math.ceil(data.laneHeight * data.dpr);
6644
+ scene.voiceCtx = voiceCanvas.getContext("2d");
6645
+ scene.voiceCtx.fillStyle = "#010203";
6646
+ scene.voiceCtx.fillStyle = "color-mix(in oklch, white, #2ee886 50%)";
6647
+ if (scene.voiceCtx.fillStyle === "#010203") throw new Error("Canvas color interpolation is unavailable");
6648
+ scene.voiceCtx.setTransform(data.dpr, 0, 0, data.dpr, 0, 0);
6649
+ trailCanvas.width = Math.ceil(data.laneWidth * data.dpr);
6650
+ trailCanvas.height = Math.ceil(data.laneHeight * data.dpr);
6651
+ scene.trailCtx = trailCanvas.getContext("2d");
6652
+ scene.trailCtx.setTransform(data.dpr, 0, 0, data.dpr, 0, 0);
6653
+ if (data.foreground) {
6654
+ scene.noteCanvas ??= data.noteCanvas;
6655
+ scene.keyCanvas ??= data.keyCanvas;
6656
+ scene.noteCanvas.width = Math.ceil(data.laneWidth * data.dpr);
6657
+ scene.noteCanvas.height = Math.ceil(data.laneHeight * data.dpr);
6658
+ scene.keyCanvas.width = Math.ceil(data.foreground.keyWidth * data.dpr);
6659
+ scene.keyCanvas.height = Math.ceil(data.foreground.keyHeight * data.dpr);
6660
+ scene.noteCtx = scene.noteCanvas.getContext("2d");
6661
+ scene.keyCtx = scene.keyCanvas.getContext("2d");
6662
+ scene.noteCtx.setTransform(data.dpr, 0, 0, data.dpr, 0, 0);
6663
+ scene.keyCtx.setTransform(data.dpr, 0, 0, data.dpr, 0, 0);
6664
+ scene.sprites?.clear();
6665
+ scene.keySprites?.clear();
6666
+ scene.filteredSprites?.clear();
6667
+ scene.filteredPixels = 0;
6668
+ for (const item of scene.noteStates?.values() ?? []) item.buffer = void 0;
6669
+ for (const item of scene.keyStates?.values() ?? []) item.buffer = void 0;
6670
+ retargetForeground(scene, now);
6671
+ }
6672
+ updateTrail(scene);
6673
+ retargetVoice(scene, now, previous);
6674
+ if (data.hitStartedAt !== void 0) scene.voice.hitStart = data.hitStartedAt;
6675
+ scenes.set(data.id, scene);
6676
+ if (scene.foreground?.fonts.length) {
6677
+ scene.loading = true;
6678
+ try {
6679
+ await Promise.all(scene.foreground.fonts.map((definition) => {
6680
+ const key = JSON.stringify(definition);
6681
+ if (!fonts.has(key)) fonts.set(key, new FontFace(definition.family, definition.src, definition.descriptors).load().then((face) => self.fonts.add(face)));
6682
+ return fonts.get(key);
6683
+ }));
6684
+ } catch {
6685
+ scenes.delete(data.id);
6686
+ postMessage({ id: data.id, failed: true });
6687
+ return;
6688
+ }
6689
+ if (scenes.get(data.id) !== scene) return;
6690
+ scene.loading = false;
6691
+ }
6692
+ draw(scene, performance.timeOrigin + performance.now());
6693
+ postMessage({ id: data.id, revision: data.revision });
6694
+ } else if (scene && data.type === "state") {
6695
+ const previous = scene.state;
6696
+ scene.beat = transition(scene.beat, data.state.beat, now, scene.duration);
6697
+ scene.state = { ...scene.state, ...data.state };
6698
+ updateTrail(scene);
6699
+ retargetVoice(scene, now, previous);
6700
+ retargetForeground(scene, now);
6701
+ } else if (scene && data.type === "keys") {
6702
+ scene.pressed = data.pressed;
6703
+ scene.hovered = data.hovered;
6704
+ retargetForeground(scene, now);
6705
+ } else if (scene && data.type === "visible") scene.visible = data.visible;
6706
+ if (frame === null && [...scenes.values()].some((scene2) => scene2.visible && !scene2.loading)) frame = requestAnimationFrame(tick);
6707
+ } catch {
6708
+ scenes.delete(data.id);
6709
+ postMessage({ id: data.id, failed: true });
6710
+ }
6711
+ };
6712
+ }
6713
+ var thread = null;
6714
+ var url = null;
6715
+ var serial = 0;
6716
+ var unavailable = false;
6717
+ var clients = /* @__PURE__ */ new Map();
6718
+ function createMelodyGlow(root, initialState, onReady) {
6719
+ if (unavailable || typeof Worker === "undefined" || typeof OffscreenCanvas === "undefined" || typeof ResizeObserver === "undefined" || typeof IntersectionObserver === "undefined" || typeof MutationObserver === "undefined" || typeof HTMLCanvasElement === "undefined" || typeof matchMedia === "undefined" || !HTMLCanvasElement.prototype.transferControlToOffscreen) return null;
6720
+ try {
6721
+ if (!thread) {
6722
+ url = URL.createObjectURL(new Blob([`(${renderGlow.toString()})()`], { type: "text/javascript" }));
6723
+ thread = new Worker(url);
6724
+ thread.onmessage = ({ data }) => data.failed ? clients.get(data.id)?.dispose() : clients.get(data.id)?.ready(data);
6725
+ thread.onerror = () => {
6726
+ unavailable = true;
6727
+ for (const client of [...clients.values()]) client.dispose();
6728
+ };
6729
+ }
6730
+ } catch {
6731
+ unavailable = true;
6732
+ if (url) URL.revokeObjectURL(url);
6733
+ thread = url = null;
6734
+ return null;
6735
+ }
6736
+ const worker = thread, id = ++serial;
6737
+ const canvas = document.createElement("canvas");
6738
+ canvas.className = "ad-melody-glow";
6739
+ canvas.style.visibility = "hidden";
6740
+ root.append(canvas);
6741
+ const lane = root.querySelector(".ad-melody-roll-lane");
6742
+ const voiceCanvas = document.createElement("canvas");
6743
+ voiceCanvas.className = "ad-melody-voice-canvas";
6744
+ voiceCanvas.style.visibility = "hidden";
6745
+ lane.append(voiceCanvas);
6746
+ const trailCanvas = document.createElement("canvas");
6747
+ trailCanvas.className = "ad-melody-trail";
6748
+ trailCanvas.style.visibility = "hidden";
6749
+ lane.insertBefore(trailCanvas, lane.querySelector(".ad-melody-playhead"));
6750
+ const noteCanvas = document.createElement("canvas"), keyCanvas = document.createElement("canvas");
6751
+ noteCanvas.className = "ad-melody-note-canvas";
6752
+ keyCanvas.className = "ad-melody-key-canvas";
6753
+ noteCanvas.style.visibility = keyCanvas.style.visibility = "hidden";
6754
+ lane.insertBefore(noteCanvas, trailCanvas);
6755
+ const keyboard = root.querySelector(".ad-melody-roll-keys");
6756
+ keyboard.append(keyCanvas);
6757
+ for (const layer of [canvas, voiceCanvas, trailCanvas, noteCanvas, keyCanvas]) layer.setAttribute("aria-hidden", "true");
6758
+ let state = initialState, disposed = false, transferred = false, inView = true, revision = 0, signature = "";
6759
+ const visible = () => inView && !document.hidden && !root.closest("[data-ad-paused], [data-ad-offscreen]");
6760
+ const visibility = () => worker.postMessage({ type: "visible", id, visible: visible() });
6761
+ const resize = new ResizeObserver(configure);
6762
+ const intersection = new IntersectionObserver(([entry]) => {
6763
+ inView = entry.isIntersecting;
6764
+ visibility();
6765
+ });
6766
+ const tokens = new MutationObserver((records) => {
6767
+ visibility();
6768
+ if (records.some((record) => !["data-ad-paused", "data-ad-offscreen"].includes(record.attributeName))) configure();
6769
+ });
6770
+ for (let node = root; node; node = node.parentElement)
6771
+ tokens.observe(node, { attributes: true, attributeFilter: ["style", "class", "data-ad-theme", "data-ad-color-scheme", "data-ad-motion", "data-ad-paused", "data-ad-offscreen"] });
6772
+ resize.observe(root);
6773
+ resize.observe(lane);
6774
+ intersection.observe(root);
6775
+ document.addEventListener("visibilitychange", visibility);
6776
+ window.addEventListener("resize", configure);
6777
+ const reduced = matchMedia("(prefers-reduced-motion: reduce)");
6778
+ reduced.addEventListener("change", configure);
6779
+ document.fonts?.addEventListener("loadingdone", configure);
6780
+ const keysChanged = () => worker.postMessage({
6781
+ type: "keys",
6782
+ id,
6783
+ time: performance.timeOrigin + performance.now(),
6784
+ pressed: [...keyboard.querySelectorAll("[data-pressed]")].map((node) => Number(node.dataset.midi)),
6785
+ hovered: keyboard.querySelector("button:hover") ? Number(keyboard.querySelector("button:hover").dataset.midi) : void 0
6786
+ });
6787
+ const keyObserver = new MutationObserver((records) => {
6788
+ if (records.some((record) => record.type === "childList")) configure();
6789
+ keysChanged();
6790
+ });
6791
+ keyObserver.observe(keyboard, { subtree: true, childList: true, attributes: true, attributeFilter: ["data-pressed"] });
6792
+ keyboard.addEventListener("pointerover", keysChanged);
6793
+ keyboard.addEventListener("pointerout", keysChanged);
6794
+ const split = (value) => value.split(/,\s*(?![^()]*\))/);
6795
+ const shadows = (value, text = false) => value === "none" ? [] : split(value).map((part2) => {
6796
+ const inset = part2.includes("inset");
6797
+ const match = part2.replace(/\s*inset\s*/, "").match(text ? /^(.*) (-?[\d.]+)px (-?[\d.]+)px ([\d.]+)px$/ : /^(.*) (-?[\d.]+)px (-?[\d.]+)px ([\d.]+)px (-?[\d.]+)px$/);
6798
+ if (!match) throw new Error("Unsupported foreground shadow");
6799
+ return { color: match[1], x: Number(match[2]), y: Number(match[3]), blur: Number(match[4]), spread: Number(match[5] ?? 0), inset };
6800
+ });
6801
+ const filters = (value) => {
6802
+ const result = [];
6803
+ while (value && value !== "none") {
6804
+ const kind = value.slice(0, value.indexOf("("));
6805
+ let depth = 1, end = kind.length + 1;
6806
+ for (; depth && end < value.length; end++) {
6807
+ if (value[end] === "(") depth++;
6808
+ if (value[end] === ")") depth--;
6809
+ }
6810
+ const argument = value.slice(kind.length + 1, end - 1);
6811
+ if (kind === "brightness") result.push({ kind, value: Number(argument) });
6812
+ else if (kind === "drop-shadow") result.push({ kind, ...shadows(argument, true)[0] });
6813
+ else throw new Error("Unsupported note filter");
6814
+ value = value.slice(end).trim();
6815
+ }
6816
+ return result;
6817
+ };
6818
+ const surface = (node) => {
6819
+ const style = getComputedStyle(node);
6820
+ if ([style.borderTopLeftRadius, style.borderTopRightRadius, style.borderBottomRightRadius, style.borderBottomLeftRadius].some((value) => !/^[\d.]+px$/.test(value)) || ["Top", "Right", "Bottom", "Left"].some((side) => !["none", "solid"].includes(style[`border${side}Style`]))) throw new Error("Unsupported foreground border");
6821
+ const gradient = style.backgroundImage === "none" ? [] : split(style.backgroundImage.slice(16, -1)).slice(1).map((stop) => {
6822
+ const match = stop.match(/^(.*?)(?: ([\d.]+)%)?$/);
6823
+ return { color: match[1], at: match[2] === void 0 ? void 0 : Number(match[2]) / 100 };
6824
+ });
6825
+ if (style.backgroundImage !== "none" && !style.backgroundImage.startsWith("linear-gradient(90deg,")) throw new Error("Unsupported note background");
6826
+ if (gradient.length) {
6827
+ gradient[0].at ??= 0;
6828
+ gradient.at(-1).at ??= 1;
6829
+ for (let i = 1; i < gradient.length - 1; i++) if (gradient[i].at === void 0) {
6830
+ let end = i;
6831
+ while (gradient[end].at === void 0) end++;
6832
+ const start = gradient[i - 1].at, count = end - i + 1;
6833
+ for (let j = i; j < end; j++) gradient[j].at = start + (gradient[end].at - start) * (j - i + 1) / count;
6834
+ }
6835
+ }
6836
+ return {
6837
+ gradient,
6838
+ backgroundColor: style.backgroundColor,
6839
+ color: style.color,
6840
+ filter: filters(style.filter),
6841
+ shadows: shadows(style.boxShadow),
6842
+ radius: [style.borderTopLeftRadius, style.borderTopRightRadius, style.borderBottomRightRadius, style.borderBottomLeftRadius].map(parseFloat),
6843
+ borders: ["Top", "Right", "Bottom", "Left"].map((side) => ({ width: parseFloat(style[`border${side}Width`]), color: style[`border${side}Color`] }))
6844
+ };
6845
+ };
6846
+ const lettering = (node, right = false) => {
6847
+ const style = getComputedStyle(node);
6848
+ if (style.fontFeatureSettings !== "normal" || style.fontVariationSettings !== "normal" || style.fontVariant !== "normal" || style.textTransform !== "none" || style.direction !== "ltr" || !["normal", "100%"].includes(style.fontStretch)) throw new Error("Unsupported foreground typography");
6849
+ const parent = node.parentElement, parentStyle = getComputedStyle(parent);
6850
+ const marker = document.createElement("i");
6851
+ Object.assign(marker.style, { display: "inline-block", width: "0", height: "0", padding: "0", border: "0", margin: "0", verticalAlign: "baseline" });
6852
+ node.append(marker);
6853
+ const parentBox = parent.getBoundingClientRect(), labelBox = node.getBoundingClientRect();
6854
+ const baseline = marker.getBoundingClientRect().top - parentBox.top - parseFloat(parentStyle.borderTopWidth);
6855
+ const offset = labelBox.left - parentBox.left - parseFloat(parentStyle.borderLeftWidth);
6856
+ marker.remove();
6857
+ return {
6858
+ font: `${style.fontStyle} ${style.fontWeight} ${style.fontSize} ${style.fontFamily}`,
6859
+ color: style.color,
6860
+ baseline,
6861
+ offset,
6862
+ letterSpacing: style.letterSpacing === "normal" ? "0px" : style.letterSpacing,
6863
+ padding: parseFloat(right ? getComputedStyle(node.parentElement).paddingRight : style.paddingLeft),
6864
+ textShadows: shadows(style.textShadow, true)
6865
+ };
6866
+ };
6867
+ function configure() {
6868
+ if (disposed || !root.clientWidth || !root.clientHeight) return;
6869
+ const temporary = [];
6870
+ try {
6871
+ const probe = document.createElement("div");
6872
+ temporary.push(probe);
6873
+ probe.className = root.className;
6874
+ probe.style.cssText = root.style.cssText;
6875
+ probe.style.setProperty("--ad-roll-beat", "1");
6876
+ Object.assign(probe.style, { position: "absolute", visibility: "hidden", pointerEvents: "none" });
6877
+ root.append(probe);
6878
+ const style = getComputedStyle(probe);
6879
+ const shadow = style.boxShadow.match(/^(.*) 0px 0px ([\d.]+)px 0px$/);
6880
+ const radius2 = [style.borderTopLeftRadius, style.borderTopRightRadius, style.borderBottomRightRadius, style.borderBottomLeftRadius];
6881
+ const duration = parseFloat(style.transitionDuration) * 1e3;
6882
+ const rootStyle = getComputedStyle(root);
6883
+ const width = parseFloat(rootStyle.width), height = parseFloat(rootStyle.height);
6884
+ probe.remove();
6885
+ if (!shadow || radius2.some((value) => !/^[\d.]+px$/.test(value)) || !Number.isFinite(width + height)) {
6886
+ dispose();
6887
+ return;
6888
+ }
6889
+ const blur = Number(shadow[2]), pad = Math.ceil(blur * 2);
6890
+ Object.assign(canvas.style, { left: `${-pad}px`, top: `${-pad}px`, width: `${width + pad * 2}px`, height: `${height + pad * 2}px` });
6891
+ const lightStyle = getComputedStyle(root.querySelector(".ad-melody-roll-light"));
6892
+ if (!lightStyle.backgroundImage.startsWith("radial-gradient(80% 120% at 25% 50%,")) throw new Error("Unsupported melody light");
6893
+ const lightColors = lightStyle.backgroundImage.slice(lightStyle.backgroundImage.indexOf(",") + 1, -1).split(/,\s*(?![^()]*\))/).map((color3) => color3.trim().replace(/\s+[\d.]+%$/, ""));
6894
+ const lightBorder = lightStyle.borderTopColor, lightWidth = parseFloat(lightStyle.borderTopWidth);
6895
+ const headStyle = getComputedStyle(lane.querySelector(".ad-melody-playhead-light"));
6896
+ const headShadows = headStyle.boxShadow.split(/,\s*(?![^()]*\))/).map((shadow2) => {
6897
+ const match = shadow2.match(/^(.*) 0px 0px ([\d.]+)px 0px$/);
6898
+ return { color: match[1], blur: Number(match[2]) };
6899
+ });
6900
+ const headWidth = parseFloat(getComputedStyle(lane.querySelector(".ad-melody-playhead")).width);
6901
+ const marker = document.createElement("span");
6902
+ temporary.push(marker);
6903
+ marker.className = "ad-melody-voice";
6904
+ marker.style.visibility = "hidden";
6905
+ lane.append(marker);
6906
+ const voiceDuration = parseFloat(getComputedStyle(marker).transitionDuration) * 1e3;
6907
+ marker.style.transition = "none";
6908
+ marker.setAttribute("data-hit", "");
6909
+ const hitStyle = getComputedStyle(marker);
6910
+ const hitColor = hitStyle.backgroundColor, hitDuration = parseFloat(hitStyle.animationDuration) * 1e3;
6911
+ marker.removeAttribute("data-hit");
6912
+ marker.style.animation = "none";
6913
+ const samples = [0, 1].map((value) => {
6914
+ marker.style.setProperty("--ad-roll-level", value);
6915
+ marker.style.setProperty("--ad-roll-accuracy", value);
6916
+ const computed = getComputedStyle(marker);
6917
+ if (computed.borderRadius !== "50%" || computed.width !== computed.height || computed.backgroundImage !== "none") throw new Error("Unsupported melody marker");
6918
+ const shadows2 = computed.boxShadow.split(/,\s*(?![^()]*\))/).map((shadow2) => {
6919
+ const match = shadow2.match(/^(.*) 0px 0px ([\d.]+)px ([\d.]+)px$/);
6920
+ if (!match) throw new Error("Unsupported melody shadow");
6921
+ return { color: match[1], blur: Number(match[2]), spread: Number(match[3]) };
6922
+ });
6923
+ return { size: parseFloat(computed.width), color: computed.backgroundColor, shadows: shadows2, border: parseFloat(computed.borderTopWidth), borderColor: computed.borderTopColor };
6924
+ });
6925
+ marker.remove();
6926
+ const voiceStyle = { size: samples.map((sample) => sample.size), color: samples.map((sample) => sample.color), shadows: samples.map((sample) => sample.shadows), border: samples[0].border, borderColor: samples[0].borderColor, hitColor, hitDuration, duration: voiceDuration };
6927
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
6928
+ temporary.push(svg);
6929
+ svg.setAttribute("class", "ad-melody-trail");
6930
+ svg.style.visibility = "hidden";
6931
+ lane.append(svg);
6932
+ const trailStyle = ["wide", "near", ""].map((halo) => {
6933
+ const path = document.createElementNS(svg.namespaceURI, "path");
6934
+ if (halo) path.setAttribute("data-halo", halo);
6935
+ svg.append(path);
6936
+ const color3 = [0, 1].map((value) => {
6937
+ svg.style.setProperty("--ad-roll-accuracy", value);
6938
+ return getComputedStyle(path).stroke;
6939
+ });
6940
+ const computed = getComputedStyle(path);
6941
+ return { color: color3, width: parseFloat(computed.strokeWidth), opacity: Number(computed.opacity) };
6942
+ });
6943
+ svg.remove();
6944
+ const noteProbe = document.createElement("span");
6945
+ temporary.push(noteProbe);
6946
+ noteProbe.className = "ad-melody-note";
6947
+ noteProbe.dataset.renderProbe = "";
6948
+ noteProbe.style.width = "100px";
6949
+ noteProbe.style.visibility = "hidden";
6950
+ const label = document.createElement("span");
6951
+ label.textContent = "M";
6952
+ noteProbe.append(label);
6953
+ root.querySelector(".ad-melody-roll-strip").append(noteProbe);
6954
+ const noteDurations = getComputedStyle(noteProbe).transitionDuration.split(",").map((value) => parseFloat(value) * 1e3);
6955
+ noteProbe.style.transition = "none";
6956
+ noteProbe.style.animation = "none";
6957
+ const noteStyle = { base: surface(noteProbe), height: parseFloat(getComputedStyle(noteProbe).height), text: lettering(label), filterDuration: noteDurations[0], opacityDuration: noteDurations[1] ?? noteDurations[0] };
6958
+ noteProbe.dataset.now = "";
6959
+ noteStyle.current = surface(noteProbe);
6960
+ delete noteProbe.dataset.now;
6961
+ noteProbe.dataset.past = "";
6962
+ noteStyle.pastOpacity = Number(getComputedStyle(noteProbe).opacity);
6963
+ delete noteProbe.dataset.past;
6964
+ noteProbe.dataset.hit = "";
6965
+ noteStyle.hit = surface(noteProbe);
6966
+ noteProbe.style.animation = "";
6967
+ noteStyle.hitDuration = parseFloat(getComputedStyle(noteProbe).animationDuration) * 1e3;
6968
+ const noteAnimation = noteProbe.getAnimations().find((animation2) => animation2.animationName === "ad-melody-hit");
6969
+ if (noteAnimation) {
6970
+ noteAnimation.pause();
6971
+ noteAnimation.currentTime = noteStyle.hitDuration * 0.28;
6972
+ }
6973
+ noteStyle.peakFilter = filters(getComputedStyle(noteProbe).filter);
6974
+ noteStyle.filterPad = Math.ceil(Math.max(0, ...[noteStyle.base.filter, noteStyle.current.filter, noteStyle.hit.filter, noteStyle.peakFilter].map((filter) => filter.reduce((pad2, part2) => pad2 + (part2.kind === "drop-shadow" ? Math.max(Math.abs(part2.x), Math.abs(part2.y)) + part2.blur * 3 : 0), 0))));
6975
+ noteProbe.remove();
6976
+ const board = keyboard.cloneNode(true);
6977
+ temporary.push(board);
6978
+ board.dataset.renderProbe = "";
6979
+ for (const layer of board.querySelectorAll("canvas")) layer.remove();
6980
+ Object.assign(board.style, { position: "absolute", width: `${keyboard.clientWidth}px`, height: `${keyboard.clientHeight}px`, visibility: "hidden", pointerEvents: "none" });
6981
+ root.append(board);
6982
+ const boardTop = board.getBoundingClientRect().top;
6983
+ const keys = [...board.querySelectorAll(".ad-piano-key")].map((node) => {
6984
+ delete node.dataset.active;
6985
+ delete node.dataset.hit;
6986
+ delete node.dataset.pressed;
6987
+ const css = getComputedStyle(node), durations = css.transitionDuration.split(",").map((value) => parseFloat(value) * 1e3);
6988
+ node.style.transition = "none";
6989
+ node.style.animation = "none";
6990
+ const bounds = node.getBoundingClientRect();
6991
+ const key = { midi: Number(node.dataset.midi), label: node.textContent, top: bounds.top - boardTop, width: bounds.width, height: bounds.height, text: lettering(node.querySelector("span") ?? node, true), duration: durations[0], pressDuration: durations[3] ?? durations[0] };
6992
+ key.base = surface(node);
6993
+ key.text.color = key.base.color;
6994
+ node.dataset.pressed = "";
6995
+ key.pressed = surface(node);
6996
+ key.pressShift = parseFloat(getComputedStyle(node).translate);
6997
+ delete node.dataset.pressed;
6998
+ node.dataset.active = "";
6999
+ key.active = surface(node);
7000
+ node.dataset.hit = "";
7001
+ key.hit = surface(node);
7002
+ node.style.animation = "";
7003
+ const hitCSS = getComputedStyle(node);
7004
+ key.hitDuration = parseFloat(hitCSS.animationDuration) * 1e3;
7005
+ key.hitIterations = hitCSS.animationIterationCount === "infinite" ? Infinity : Number(hitCSS.animationIterationCount);
7006
+ node.style.animation = "none";
7007
+ key.pad = Math.ceil(Math.max(1, ...[key.base, key.pressed, key.active, key.hit].flatMap((style2) => style2.shadows.map((shadow2) => Math.max(Math.abs(shadow2.x), Math.abs(shadow2.y)) + shadow2.blur * 2 + Math.abs(shadow2.spread)))));
7008
+ return key;
7009
+ });
7010
+ board.remove();
7011
+ const fonts = [];
7012
+ const fontNames = [noteStyle.text.font, ...keys.map((key) => key.text.font)].join(",");
7013
+ const readFonts = (sheet) => {
7014
+ let rules;
7015
+ try {
7016
+ rules = sheet.cssRules;
7017
+ } catch {
7018
+ return;
7019
+ }
7020
+ for (const rule of rules) {
7021
+ if (rule.styleSheet) readFonts(rule.styleSheet);
7022
+ if (rule.type !== 5) continue;
7023
+ const family = rule.style.fontFamily.replaceAll('"', "").replaceAll("'", "");
7024
+ if (!fontNames.includes(family)) continue;
7025
+ fonts.push({
7026
+ family,
7027
+ src: rule.style.getPropertyValue("src").replace(/url\(["']?([^"')]+)["']?\)/g, (_, url2) => `url("${new URL(url2, sheet.href ?? document.baseURI)}")`),
7028
+ descriptors: { ...Object.fromEntries(["weight", "style", "stretch"].map((key) => [key, rule.style.getPropertyValue(`font-${key}`) || "normal"])), unicodeRange: rule.style.getPropertyValue("unicode-range") || "U+0-10FFFF" }
7029
+ });
7030
+ }
7031
+ };
7032
+ for (const sheet of document.styleSheets) readFonts(sheet);
7033
+ for (const face of document.fonts ?? []) if (fontNames.includes(face.family.replaceAll('"', "").replaceAll("'", "")) && !fonts.some((font) => font.family === face.family.replaceAll('"', "").replaceAll("'", ""))) throw new Error("Font source is unavailable to the worker");
7034
+ const gridCSS = getComputedStyle(lane.querySelector(".ad-melody-roll-seconds")).backgroundImage;
7035
+ const gridStop = split(gridCSS.slice(16, -1))[1].match(/^(.*) ([\d.]+)px$/);
7036
+ if (!gridStop) throw new Error("Unsupported melody grid");
7037
+ const foreground = { note: noteStyle, keys, fonts, grid: { color: gridStop[1].trim(), width: Number(gridStop[2]) }, keyWidth: keyboard.clientWidth, keyHeight: keyboard.clientHeight };
7038
+ const config = { type: "configure", id, width, height, pad, blur, color: shadow[1], radius: radius2.map(parseFloat), dpr: devicePixelRatio || 1, duration, voiceStyle, trailStyle, lightColors, lightBorder, lightWidth, headShadows, headWidth, foreground, laneWidth: lane.clientWidth, laneHeight: lane.clientHeight };
7039
+ const nextSignature = JSON.stringify(config);
7040
+ if (nextSignature === signature) return;
7041
+ signature = nextSignature;
7042
+ const time = performance.timeOrigin + performance.now();
7043
+ const started = (node, name, duration2) => time - Number(node?.getAnimations().find((animation2) => animation2.animationName === name)?.currentTime ?? duration2);
7044
+ const noteTimes = !transferred ? Object.fromEntries([...root.querySelectorAll(".ad-melody-note")].map((node, i) => [state.notes?.[i]?.id, started(node, "ad-melody-hit", noteStyle.hitDuration)])) : void 0;
7045
+ const keyTimes = !transferred ? Object.fromEntries([...keyboard.querySelectorAll(".ad-piano-key")].map((node) => [node.dataset.midi, started(node, "ad-piano-key-hit", 0)])) : void 0;
7046
+ const animation = !transferred && state.hit ? lane.querySelector(".ad-melody-voice")?.getAnimations().find((animation2) => animation2.animationName === "ad-melody-voice-hit") : void 0;
7047
+ const message = {
7048
+ ...config,
7049
+ state,
7050
+ visible: visible(),
7051
+ revision: ++revision,
7052
+ time,
7053
+ noteTimes,
7054
+ keyTimes,
7055
+ hitStartedAt: !transferred && state.hit ? time - Number(animation?.currentTime ?? hitDuration) : void 0
7056
+ };
7057
+ if (!transferred) {
7058
+ const offscreen = canvas.transferControlToOffscreen();
7059
+ const voiceOffscreen = voiceCanvas.transferControlToOffscreen();
7060
+ const trailOffscreen = trailCanvas.transferControlToOffscreen();
7061
+ const noteOffscreen = noteCanvas.transferControlToOffscreen(), keyOffscreen = keyCanvas.transferControlToOffscreen();
7062
+ worker.postMessage({ ...message, canvas: offscreen, voiceCanvas: voiceOffscreen, trailCanvas: trailOffscreen, noteCanvas: noteOffscreen, keyCanvas: keyOffscreen }, [offscreen, voiceOffscreen, trailOffscreen, noteOffscreen, keyOffscreen]);
7063
+ transferred = true;
7064
+ } else worker.postMessage(message);
7065
+ } catch {
7066
+ dispose();
7067
+ } finally {
7068
+ temporary.forEach((node) => node.remove());
7069
+ }
7070
+ }
7071
+ function dispose() {
7072
+ if (disposed) return;
7073
+ disposed = true;
7074
+ resize.disconnect();
7075
+ intersection.disconnect();
7076
+ tokens.disconnect();
7077
+ keyObserver.disconnect();
7078
+ keyboard.removeEventListener("pointerover", keysChanged);
7079
+ keyboard.removeEventListener("pointerout", keysChanged);
7080
+ document.removeEventListener("visibilitychange", visibility);
7081
+ window.removeEventListener("resize", configure);
7082
+ reduced.removeEventListener("change", configure);
7083
+ document.fonts?.removeEventListener("loadingdone", configure);
7084
+ root.removeAttribute("data-ad-glow");
7085
+ root.removeAttribute("data-ad-foreground");
7086
+ onReady(false);
7087
+ canvas.remove();
7088
+ voiceCanvas.remove();
7089
+ trailCanvas.remove();
7090
+ noteCanvas.remove();
7091
+ keyCanvas.remove();
7092
+ worker.postMessage({ type: "remove", id });
7093
+ clients.delete(id);
7094
+ if (!clients.size) {
7095
+ worker.terminate();
7096
+ URL.revokeObjectURL(url);
7097
+ thread = url = null;
7098
+ }
7099
+ }
7100
+ const renderer = {
7101
+ update(next) {
7102
+ if (!disposed) {
7103
+ const sent = { ...next };
7104
+ if (next.notes === state.notes) delete sent.notes;
7105
+ if (next.hits === state.hits) delete sent.hits;
7106
+ state = next;
7107
+ worker.postMessage({ type: "state", id, state: sent, time: performance.timeOrigin + performance.now() });
7108
+ }
7109
+ },
7110
+ ready(data) {
7111
+ if (!disposed && data.revision === revision) {
7112
+ canvas.style.visibility = voiceCanvas.style.visibility = trailCanvas.style.visibility = noteCanvas.style.visibility = keyCanvas.style.visibility = "";
7113
+ root.setAttribute("data-ad-glow", "");
7114
+ root.setAttribute("data-ad-foreground", "");
7115
+ onReady(true);
7116
+ }
7117
+ },
7118
+ dispose
7119
+ };
7120
+ clients.set(id, renderer);
7121
+ configure();
7122
+ return renderer;
7123
+ }
7124
+
7125
+ // src/components/media/MelodyRoll/MelodyRoll.tsx
6151
7126
  import { jsx as jsx50, jsxs as jsxs41 } from "react/jsx-runtime";
6152
7127
  var DEMO2 = [
6153
7128
  { id: "1", start: 0.4, end: 1.1, pitch: 64, lyric: "\u041D\u043E\u0447\u044C" },
@@ -6181,6 +7156,9 @@ var MelodyRoll = ({
6181
7156
  ...p
6182
7157
  }) => {
6183
7158
  const tr2 = useTr();
7159
+ const root = useRef28(null);
7160
+ const glow3 = useRef28(null);
7161
+ const [glowReady, setGlowReady] = useState13(false);
6184
7162
  const [span, setSpan] = useControllable(windowProp, defaultWindow, onWindowChange);
6185
7163
  const indexed = useMemo5(() => {
6186
7164
  let low2 = 60, high2 = 64;
@@ -6204,14 +7182,52 @@ var MelodyRoll = ({
6204
7182
  const from = position - span * lead;
6205
7183
  const origin = Math.floor(from / span) * span;
6206
7184
  const x = (seconds) => (seconds - from) / span * 100;
6207
- const y = (pitch) => (high - pitch) / rows * 100;
6208
- const visible = indexed.blocks.filter((block) => block.end >= from && block.start <= from + span).flatMap((block) => block.notes.filter((note) => note.end >= from && note.start <= from + span));
7185
+ const rangeStart = glowReady ? origin : from;
7186
+ const rangeEnd = glowReady ? origin + span * 2 : from + span;
7187
+ const visible = useMemo5(() => indexed.blocks.filter((block) => block.end >= rangeStart && block.start <= rangeEnd).flatMap((block) => block.notes.filter((note) => note.end >= rangeStart && note.start <= rangeEnd)), [indexed, rangeStart, rangeEnd]);
7188
+ const phases = glowReady ? "" : visible.map((note) => note.end < from || note.start > from + span ? "x" : position >= note.start && position <= note.end ? "n" : position > note.end ? "p" : "-").join("");
7189
+ const hits = hitIds?.size ? visible.map((note) => hitIds.has(note.id) ? "h" : "-").join("") : "";
7190
+ const glowState = { beat: clamp(beat, 0, 1), pitch: livePitch, high, rows, lead, accuracy: clamp(accuracy, 0, 1), level: clamp(level, 0, 1), hit, position, span, notes: visible, hits, showLyrics };
7191
+ useEffect14(() => {
7192
+ if (!root.current) return;
7193
+ glow3.current = createMelodyGlow(root.current, { ...glowState, trail: trail.current }, setGlowReady);
7194
+ return () => {
7195
+ glow3.current?.dispose();
7196
+ glow3.current = null;
7197
+ };
7198
+ }, []);
7199
+ useLayoutEffect5(() => {
7200
+ glow3.current?.update(glowState);
7201
+ }, [beat, livePitch, high, rows, lead, accuracy, level, hit, position, span, visible, hits, showLyrics]);
7202
+ const nativeHits = glowReady ? "" : hits;
7203
+ const noteElements = useMemo5(() => glowReady ? void 0 : visible.map((note, i) => {
7204
+ const hidden = phases[i] === "x";
7205
+ return /* @__PURE__ */ jsx50(
7206
+ "span",
7207
+ {
7208
+ className: "ad-melody-note",
7209
+ "data-hit": !hidden && nativeHits[i] === "h" || void 0,
7210
+ "data-now": phases[i] === "n" || void 0,
7211
+ "data-past": phases[i] === "p" || void 0,
7212
+ style: {
7213
+ visibility: hidden ? "hidden" : void 0,
7214
+ left: `${(note.start - origin) / span * 100}%`,
7215
+ width: `${Math.max(0.6, (note.end - note.start) / span * 100)}%`,
7216
+ top: `${(high - note.pitch) / rows * 100}%`
7217
+ },
7218
+ children: showLyrics && note.lyric && /* @__PURE__ */ jsx50("span", { children: note.lyric })
7219
+ },
7220
+ note.id
7221
+ );
7222
+ }), [glowReady, visible, phases, nativeHits, showLyrics, origin, span, high, rows]);
6209
7223
  const trail = useRef28([]);
6210
7224
  if (trail.current.at(-1)?.t !== position) {
6211
- const kept = trail.current.filter((point) => point.t > position - TRAIL && point.t < position);
6212
- trail.current = [...kept, { t: position, pitch: livePitch }];
7225
+ if (position < (trail.current.at(-1)?.t ?? position)) trail.current = trail.current.filter((point) => point.t < position);
7226
+ const expired = trail.current.findIndex((point) => point.t > position - TRAIL);
7227
+ trail.current.splice(0, expired < 0 ? trail.current.length : expired);
7228
+ trail.current.push({ t: position, pitch: livePitch });
6213
7229
  }
6214
- const segments = trail.current.reduce((paths, point, i, all) => {
7230
+ const segments = glowReady ? [] : trail.current.reduce((paths, point, i, all) => {
6215
7231
  if (point.pitch === void 0) return paths;
6216
7232
  const joined = i > 0 && all[i - 1]?.pitch !== void 0;
6217
7233
  const at = `${x(point.t).toFixed(2)},${((high - point.pitch + 0.5) / rows * 100).toFixed(2)}`;
@@ -6223,65 +7239,56 @@ var MelodyRoll = ({
6223
7239
  if (!event.deltaY) return;
6224
7240
  setSpan(clamp(span * (event.deltaY > 0 ? 1.12 : 1 / 1.12), 2, 20));
6225
7241
  };
7242
+ const nativeState = glowReady ? null : glowState;
7243
+ const content = useMemo5(() => /* @__PURE__ */ jsxs41("div", { className: "ad-melody-roll-content", children: [
7244
+ /* @__PURE__ */ jsx50(
7245
+ PianoKeyboard,
7246
+ {
7247
+ className: "ad-melody-roll-keys",
7248
+ minMidi: low,
7249
+ maxMidi: high,
7250
+ activeMidi: glowReady || livePitch === void 0 ? void 0 : Math.round(livePitch),
7251
+ hit: !glowReady && hit,
7252
+ labels: rows > 30 ? "octaves" : "all",
7253
+ onKeyPress
7254
+ }
7255
+ ),
7256
+ /* @__PURE__ */ jsxs41("div", { className: "ad-melody-roll-lane", "aria-hidden": "true", children: [
7257
+ /* @__PURE__ */ jsx50("span", { className: "ad-melody-roll-seconds", style: glowReady ? void 0 : { "--ad-roll-shift": `${-((from % 1 + 1) % 1) / span * 100}cqw` } }),
7258
+ /* @__PURE__ */ jsx50("div", { className: "ad-melody-roll-strip", style: glowReady ? void 0 : { transform: `translateX(${((origin - from) / span * 100).toFixed(3)}%)` }, children: noteElements }),
7259
+ !glowReady && /* @__PURE__ */ jsx50("svg", { className: "ad-melody-trail", viewBox: "0 0 100 100", preserveAspectRatio: "none", style: { "--ad-roll-accuracy": clamp(accuracy, 0, 1) }, children: segments.flatMap((d, i) => [
7260
+ /* @__PURE__ */ jsx50("path", { "data-halo": "wide", d, vectorEffect: "non-scaling-stroke" }, `${i}-wide`),
7261
+ /* @__PURE__ */ jsx50("path", { "data-halo": "near", d, vectorEffect: "non-scaling-stroke" }, `${i}-near`),
7262
+ /* @__PURE__ */ jsx50("path", { d, vectorEffect: "non-scaling-stroke" }, i)
7263
+ ]) }),
7264
+ /* @__PURE__ */ jsx50("span", { className: "ad-melody-playhead", style: { left: `${lead * 100}%` }, children: /* @__PURE__ */ jsx50("span", { className: "ad-melody-playhead-light", style: { opacity: glowReady ? 0 : clamp(beat, 0, 1) } }) }),
7265
+ !glowReady && livePitch !== void 0 && /* @__PURE__ */ jsx50(
7266
+ "span",
7267
+ {
7268
+ className: "ad-melody-voice",
7269
+ "data-hit": hit || void 0,
7270
+ style: { left: `${lead * 100}%`, top: `${(high - livePitch + 0.5) / rows * 100}%`, "--ad-roll-level": clamp(level, 0, 1), "--ad-roll-accuracy": clamp(accuracy, 0, 1) }
7271
+ }
7272
+ )
7273
+ ] })
7274
+ ] }), [glowReady, noteElements, low, high, rows, onKeyPress, lead, nativeState]);
6226
7275
  return /* @__PURE__ */ jsxs41(
6227
7276
  "div",
6228
7277
  {
6229
7278
  ...mark("MelodyRoll", p),
7279
+ ref: root,
6230
7280
  role: "img",
6231
7281
  "aria-label": label ?? tr2("\u041C\u0435\u043B\u043E\u0434\u0438\u044F"),
6232
7282
  onWheel: zoom,
6233
7283
  style: {
6234
7284
  ...p.style,
6235
7285
  "--ad-roll-rows": rows,
6236
- "--ad-roll-beat": clamp(beat, 0, 1),
7286
+ "--ad-roll-beat": glowReady ? 0 : clamp(beat, 0, 1),
6237
7287
  "--ad-roll-second": `${100 / span}cqw`
6238
7288
  },
6239
7289
  children: [
6240
- /* @__PURE__ */ jsx50(
6241
- PianoKeyboard,
6242
- {
6243
- className: "ad-melody-roll-keys",
6244
- minMidi: low,
6245
- maxMidi: high,
6246
- activeMidi: livePitch === void 0 ? void 0 : Math.round(livePitch),
6247
- hit,
6248
- labels: rows > 30 ? "octaves" : "all",
6249
- onKeyPress
6250
- }
6251
- ),
6252
- /* @__PURE__ */ jsxs41("div", { className: "ad-melody-roll-lane", "aria-hidden": "true", children: [
6253
- /* @__PURE__ */ jsx50("span", { className: "ad-melody-roll-seconds", style: { "--ad-roll-shift": `${-((from % 1 + 1) % 1) / span * 100}cqw` } }),
6254
- /* @__PURE__ */ jsx50("div", { className: "ad-melody-roll-strip", style: { transform: `translateX(${((origin - from) / span * 100).toFixed(3)}%)` }, children: visible.map((note) => /* @__PURE__ */ jsx50(
6255
- "span",
6256
- {
6257
- className: "ad-melody-note",
6258
- "data-hit": hitIds?.has(note.id) || void 0,
6259
- "data-now": position >= note.start && position <= note.end || void 0,
6260
- "data-past": position > note.end || void 0,
6261
- style: {
6262
- left: `${(note.start - origin) / span * 100}%`,
6263
- width: `${Math.max(0.6, (note.end - note.start) / span * 100)}%`,
6264
- top: `${y(note.pitch)}%`
6265
- },
6266
- children: showLyrics && note.lyric && /* @__PURE__ */ jsx50("span", { children: note.lyric })
6267
- },
6268
- note.id
6269
- )) }),
6270
- /* @__PURE__ */ jsx50("svg", { className: "ad-melody-trail", viewBox: "0 0 100 100", preserveAspectRatio: "none", style: { "--ad-roll-accuracy": clamp(accuracy, 0, 1) }, children: segments.flatMap((d, i) => [
6271
- /* @__PURE__ */ jsx50("path", { "data-halo": "wide", d, vectorEffect: "non-scaling-stroke" }, `${i}-wide`),
6272
- /* @__PURE__ */ jsx50("path", { "data-halo": "near", d, vectorEffect: "non-scaling-stroke" }, `${i}-near`),
6273
- /* @__PURE__ */ jsx50("path", { d, vectorEffect: "non-scaling-stroke" }, i)
6274
- ]) }),
6275
- /* @__PURE__ */ jsx50("span", { className: "ad-melody-playhead", style: { left: `${lead * 100}%`, "--ad-roll-beat": clamp(beat, 0, 1) } }),
6276
- livePitch !== void 0 && /* @__PURE__ */ jsx50(
6277
- "span",
6278
- {
6279
- className: "ad-melody-voice",
6280
- "data-hit": hit || void 0,
6281
- style: { left: `${lead * 100}%`, top: `${(high - livePitch + 0.5) / rows * 100}%`, "--ad-roll-level": clamp(level, 0, 1), "--ad-roll-accuracy": clamp(accuracy, 0, 1) }
6282
- }
6283
- )
6284
- ] })
7290
+ /* @__PURE__ */ jsx50("span", { className: "ad-melody-roll-light", "aria-hidden": "true", style: { opacity: glowReady ? 0 : clamp(beat, 0, 1) } }),
7291
+ content
6285
7292
  ]
6286
7293
  }
6287
7294
  );
@@ -6291,24 +7298,24 @@ var MelodyRoll = ({
6291
7298
  import { useRef as useRef30 } from "react";
6292
7299
 
6293
7300
  // src/components/layout/FloatingPanel/useFloatingPanel.ts
6294
- import { useCallback as useCallback2, useEffect as useEffect14, useLayoutEffect as useLayoutEffect5, useRef as useRef29, useState as useState13 } from "react";
7301
+ import { useCallback as useCallback2, useEffect as useEffect15, useLayoutEffect as useLayoutEffect6, useRef as useRef29, useState as useState14 } from "react";
6295
7302
  var resizeEdges = ["n", "s", "e", "w", "ne", "nw", "se", "sw"];
6296
7303
  var threshold = 3;
6297
7304
  var panelControls = "button, a, input, select, textarea, [role=slider], [role=button], [role=switch], [role=listbox], [contenteditable=true], .ad-rotary-knob";
6298
7305
  var useFloatingPanel = (frameRef, options = {}) => {
6299
- const [active, setActive] = useState13(false);
6300
- const [live, setLive] = useState13(null);
7306
+ const [active, setActive] = useState14(false);
7307
+ const [live, setLive] = useState14(null);
6301
7308
  const drag = useRef29(null);
6302
7309
  const moved = useRef29(false);
6303
7310
  const latest = useRef29(options);
6304
7311
  latest.current = options;
6305
7312
  const saved = options.layout;
6306
- useLayoutEffect5(() => {
7313
+ useLayoutEffect6(() => {
6307
7314
  drag.current = null;
6308
7315
  setLive(null);
6309
7316
  }, [saved?.left, saved?.top, saved?.width, saved?.height]);
6310
- const [, setViewport] = useState13(0);
6311
- useEffect14(() => {
7317
+ const [, setViewport] = useState14(0);
7318
+ useEffect15(() => {
6312
7319
  const onResize = () => setViewport((n) => n + 1);
6313
7320
  window.addEventListener("resize", onResize);
6314
7321
  onResize();
@@ -6418,7 +7425,7 @@ var useFloatingPanel = (frameRef, options = {}) => {
6418
7425
  latest.current.onLayoutChange?.(next);
6419
7426
  };
6420
7427
  const deactivate = useCallback2(() => setActive(false), []);
6421
- useEffect14(() => {
7428
+ useEffect15(() => {
6422
7429
  if (!active) return;
6423
7430
  const onPointerDown = (event) => {
6424
7431
  if (!(event.target instanceof Node) || !frameRef.current?.contains(event.target)) deactivate();