@kieransimkin/dancemoves 3.1.22 → 3.2.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/lib/index.mjs CHANGED
@@ -1032,7 +1032,7 @@ function install2(__dmScope) {
1032
1032
  return;
1033
1033
  var instances = /* @__PURE__ */ new Map();
1034
1034
  var serial2 = 0;
1035
- function finite2(value, fallback) {
1035
+ function finite3(value, fallback) {
1036
1036
  value = Number(value);
1037
1037
  return Number.isFinite(value) ? value : fallback;
1038
1038
  }
@@ -1086,8 +1086,8 @@ function install2(__dmScope) {
1086
1086
  rect = bounds.getBoundingClientRect();
1087
1087
  }
1088
1088
  function apply(x, y, reason) {
1089
- latestX = clamp(finite2(x, 0), -1, 1);
1090
- latestY = clamp(finite2(y, 0), -1, 1);
1089
+ latestX = clamp(finite3(x, 0), -1, 1);
1090
+ latestY = clamp(finite3(y, 0), -1, 1);
1091
1091
  __dmScope.setStyle(state.root.style, "--dance-moves-x", latestX.toFixed(4));
1092
1092
  __dmScope.setStyle(state.root.style, "--dance-moves-y", latestY.toFixed(4));
1093
1093
  __dmScope.dataset(state.root, "danceMovesPointer", reason || "active");
@@ -1178,13 +1178,13 @@ function install2(__dmScope) {
1178
1178
  var audio = playbackElement(options.audio, state.root.querySelector("audio"));
1179
1179
  if (!audio)
1180
1180
  throw new Error("DanceMovesEffects pulse requires an audio element");
1181
- var ticks = Math.max(1, Math.round(finite2(options.ticks, 16)));
1181
+ var ticks = Math.max(1, Math.round(finite3(options.ticks, 16)));
1182
1182
  var className = String(options.className || "dance-moves-playing");
1183
1183
  var prefix = String(options.propertyPrefix || "--dance-moves-pulse").replace(/-+$/, "");
1184
1184
  function sync() {
1185
- var rate = Math.max(0.01, finite2(audio.playbackRate, 1));
1185
+ var rate = Math.max(0.01, finite3(audio.playbackRate, 1));
1186
1186
  var duration = motion.durationMilliseconds(ticks) / 1e3 / rate;
1187
- var phase = -(finite2(audio.currentTime, 0) / rate % duration);
1187
+ var phase = -(finite3(audio.currentTime, 0) / rate % duration);
1188
1188
  __dmScope.setStyle(state.root.style, prefix + "-duration", duration.toFixed(6) + "s");
1189
1189
  __dmScope.setStyle(state.root.style, prefix + "-delay", phase.toFixed(6) + "s");
1190
1190
  __dmScope.dataset(state.root, "danceMovesPulse", state.reduced.matches || state.forced.matches ? "inactive" : audio.paused ? "paused" : "playing");
@@ -1223,7 +1223,7 @@ function install2(__dmScope) {
1223
1223
  var state = commonState(options, "cue-class");
1224
1224
  var cue = String(options.cue || "*");
1225
1225
  var className = String(options.className || "dance-moves-cue-active");
1226
- var durationTicks = Math.max(1, Math.round(finite2(options.durationTicks, 16)));
1226
+ var durationTicks = Math.max(1, Math.round(finite3(options.durationTicks, 16)));
1227
1227
  var timer = 0, count = 0;
1228
1228
  function clear(reason) {
1229
1229
  if (timer)
@@ -1279,16 +1279,16 @@ function install2(__dmScope) {
1279
1279
  if (!audio)
1280
1280
  throw new Error("DanceMovesEffects cueTimeline requires an audio element");
1281
1281
  var cues = (Array.isArray(options.cues) ? options.cues : []).map(function(cue, index) {
1282
- var time = finite2(cue && cue.time, NaN);
1282
+ var time = finite3(cue && cue.time, NaN);
1283
1283
  if (!Number.isFinite(time) || time < 0)
1284
1284
  throw new TypeError("DanceMovesEffects cueTimeline cue time must be a non-negative number");
1285
1285
  return Object.freeze({ id: String(cue.id || "cue-" + index), time, end: Number.isFinite(Number(cue.end)) ? Number(cue.end) : null, data: cue.data });
1286
1286
  }).sort(function(a, b) {
1287
1287
  return a.time - b.time;
1288
1288
  });
1289
- var tolerance = Math.max(0, finite2(options.seekLandingTolerance, 0.05));
1290
- var maxCrossingGap = Math.max(0.05, finite2(options.maxCrossingGap, 1.25));
1291
- var fired = /* @__PURE__ */ new Set(), frame = 0, previous = finite2(audio.currentTime, 0), status = "idle", generation = 0, previousWall = 0;
1289
+ var tolerance = Math.max(0, finite3(options.seekLandingTolerance, 0.05));
1290
+ var maxCrossingGap = Math.max(0.05, finite3(options.maxCrossingGap, 1.25));
1291
+ var fired = /* @__PURE__ */ new Set(), frame = 0, previous = finite3(audio.currentTime, 0), status = "idle", generation = 0, previousWall = 0;
1292
1292
  function blocked() {
1293
1293
  return document2.hidden || state.reduced.matches || state.forced.matches;
1294
1294
  }
@@ -1298,7 +1298,7 @@ function install2(__dmScope) {
1298
1298
  });
1299
1299
  }
1300
1300
  function publish(reason, extra) {
1301
- var time = finite2(audio.currentTime, 0);
1301
+ var time = finite3(audio.currentTime, 0);
1302
1302
  __dmScope.dataset(state.root, "danceMovesCueTimeline", reason);
1303
1303
  if (typeof options.render === "function")
1304
1304
  options.render({ root: state.root, audio, time, previousTime: previous, reason, playing: !audio.paused && !audio.ended && !blocked(), active: activeAt(time), generation, detail: extra || null });
@@ -1308,10 +1308,10 @@ function install2(__dmScope) {
1308
1308
  return;
1309
1309
  fired.add(cue.id);
1310
1310
  if (typeof options.onCue === "function")
1311
- options.onCue({ root: state.root, audio, cue, time: finite2(audio.currentTime, cue.time), reason, generation });
1311
+ options.onCue({ root: state.root, audio, cue, time: finite3(audio.currentTime, cue.time), reason, generation });
1312
1312
  }
1313
1313
  function rebuild(time, reason, allowLanding) {
1314
- time = Math.max(0, finite2(time, 0));
1314
+ time = Math.max(0, finite3(time, 0));
1315
1315
  generation += 1;
1316
1316
  fired.clear();
1317
1317
  cues.forEach(function(cue) {
@@ -1337,7 +1337,7 @@ function install2(__dmScope) {
1337
1337
  frame = 0;
1338
1338
  if (audio.paused || audio.ended || blocked())
1339
1339
  return stop(audio.ended ? "ended" : blocked() ? "inactive" : "paused");
1340
- var now = Math.max(0, finite2(audio.currentTime, previous));
1340
+ var now = Math.max(0, finite3(audio.currentTime, previous));
1341
1341
  var delta = now - previous;
1342
1342
  if (delta < -1e-3 || delta > maxCrossingGap)
1343
1343
  rebuild(now, "discontinuity", false);
@@ -1346,8 +1346,8 @@ function install2(__dmScope) {
1346
1346
  if (previous < cue.time && now >= cue.time)
1347
1347
  fire(cue, "crossing");
1348
1348
  });
1349
- var wallDelta = previousWall ? Math.min(0.25, Math.max(0, (finite2(wallNow, previousWall) - previousWall) / 1e3)) : 0;
1350
- previousWall = finite2(wallNow, previousWall);
1349
+ var wallDelta = previousWall ? Math.min(0.25, Math.max(0, (finite3(wallNow, previousWall) - previousWall) / 1e3)) : 0;
1350
+ previousWall = finite3(wallNow, previousWall);
1351
1351
  previous = now;
1352
1352
  status = "playing";
1353
1353
  publish("frame", { deltaSeconds: wallDelta });
@@ -1356,7 +1356,7 @@ function install2(__dmScope) {
1356
1356
  function start(reason) {
1357
1357
  if (frame || audio.paused || audio.ended || blocked())
1358
1358
  return publish(blocked() ? "inactive" : audio.ended ? "ended" : "paused");
1359
- previous = Math.max(0, finite2(audio.currentTime, 0));
1359
+ previous = Math.max(0, finite3(audio.currentTime, 0));
1360
1360
  previousWall = 0;
1361
1361
  status = reason || "playing";
1362
1362
  publish(status);
@@ -1420,7 +1420,7 @@ function install2(__dmScope) {
1420
1420
  start,
1421
1421
  stop,
1422
1422
  snapshot: function() {
1423
- return { id: state.id, type: "cue-timeline", status, time: finite2(audio.currentTime, 0), previousTime: previous, generation, fired: Array.from(fired), active: activeAt(finite2(audio.currentTime, 0)).map(function(cue) {
1423
+ return { id: state.id, type: "cue-timeline", status, time: finite3(audio.currentTime, 0), previousTime: previous, generation, fired: Array.from(fired), active: activeAt(finite3(audio.currentTime, 0)).map(function(cue) {
1424
1424
  return cue.id;
1425
1425
  }) };
1426
1426
  },
@@ -1443,28 +1443,28 @@ function install2(__dmScope) {
1443
1443
  if (!target)
1444
1444
  throw new Error("DanceMovesEffects spritePlayback requires a target element");
1445
1445
  var frameMap = Array.isArray(options.frameMap) && options.frameMap.length ? options.frameMap.map(function(value) {
1446
- value = Math.floor(finite2(value, -1));
1446
+ value = Math.floor(finite3(value, -1));
1447
1447
  if (value < 0)
1448
1448
  throw new TypeError("DanceMovesEffects spritePlayback frameMap values must be non-negative integers");
1449
1449
  return value;
1450
1450
  }) : null;
1451
- var frameCount = frameMap ? frameMap.length : Math.max(1, Math.round(finite2(options.frameCount, 1)));
1452
- var columns = Math.max(1, Math.round(finite2(options.columns, frameCount)));
1453
- var framesPerRow = clamp(Math.round(finite2(options.framesPerRow, columns)), 1, columns);
1454
- var columnOffset = clamp(Math.round(finite2(options.columnOffset, 0)), 0, columns - framesPerRow);
1455
- var rowOffset = Math.max(0, Math.round(finite2(options.rowOffset, 0)));
1456
- var cycleTicks = Math.max(1, Math.round(finite2(options.cycleTicks, 32)));
1457
- var phaseOffsetSeconds = finite2(options.phaseOffsetSeconds, 0);
1458
- var staticFrame = clamp(Math.round(finite2(options.staticFrame, 0)), 0, frameCount - 1);
1451
+ var frameCount = frameMap ? frameMap.length : Math.max(1, Math.round(finite3(options.frameCount, 1)));
1452
+ var columns = Math.max(1, Math.round(finite3(options.columns, frameCount)));
1453
+ var framesPerRow = clamp(Math.round(finite3(options.framesPerRow, columns)), 1, columns);
1454
+ var columnOffset = clamp(Math.round(finite3(options.columnOffset, 0)), 0, columns - framesPerRow);
1455
+ var rowOffset = Math.max(0, Math.round(finite3(options.rowOffset, 0)));
1456
+ var cycleTicks = Math.max(1, Math.round(finite3(options.cycleTicks, 32)));
1457
+ var phaseOffsetSeconds = finite3(options.phaseOffsetSeconds, 0);
1458
+ var staticFrame = clamp(Math.round(finite3(options.staticFrame, 0)), 0, frameCount - 1);
1459
1459
  var prefix = String(options.propertyPrefix || "--dance-moves-sprite").replace(/-+$/, "");
1460
1460
  if (prefix.slice(0, 2) !== "--")
1461
1461
  prefix = "--" + prefix.replace(/^-+/, "");
1462
1462
  var tiers = Array.isArray(options.qualityTiers) && options.qualityTiers.length ? options.qualityTiers.map(String) : ["full", "constrained", "minimal"];
1463
1463
  var suppliedFps = Array.isArray(options.qualityFramesPerSecond) ? options.qualityFramesPerSecond : [60, 30, 15];
1464
1464
  var qualityFps = tiers.map(function(_, index) {
1465
- return Math.max(1, finite2(suppliedFps[index], suppliedFps[suppliedFps.length - 1] || 15));
1465
+ return Math.max(1, finite3(suppliedFps[index], suppliedFps[suppliedFps.length - 1] || 15));
1466
1466
  });
1467
- var qualityIndex = clamp(Math.round(finite2(options.initialQuality, 0)), 0, tiers.length - 1);
1467
+ var qualityIndex = clamp(Math.round(finite3(options.initialQuality, 0)), 0, tiers.length - 1);
1468
1468
  var frame = 0, status = "idle", lastWall = -Infinity, renders = 0, latest = null;
1469
1469
  function blocked() {
1470
1470
  return document2.hidden || state.reduced.matches || state.forced.matches;
@@ -1474,8 +1474,8 @@ function install2(__dmScope) {
1474
1474
  return { sourceFrame, column: sourceFrame % columns, row: Math.floor(sourceFrame / columns) };
1475
1475
  }
1476
1476
  function calculate(reason, staticFallback) {
1477
- var cycleSeconds = Math.max(1e-3, finite2(motion.durationMilliseconds(cycleTicks), 1) / 1e3);
1478
- var time = Math.max(0, finite2(audio.currentTime, 0));
1477
+ var cycleSeconds = Math.max(1e-3, finite3(motion.durationMilliseconds(cycleTicks), 1) / 1e3);
1478
+ var time = Math.max(0, finite3(audio.currentTime, 0));
1479
1479
  var phase = ((time - phaseOffsetSeconds) % cycleSeconds + cycleSeconds) % cycleSeconds / cycleSeconds;
1480
1480
  var localFrame = staticFallback ? staticFrame : Math.min(frameCount - 1, Math.floor(phase * frameCount + 1e-9));
1481
1481
  var mapped = mappedFrame(localFrame);
@@ -1530,8 +1530,8 @@ function install2(__dmScope) {
1530
1530
  if (audio.paused || audio.ended || blocked())
1531
1531
  return stop(audio.ended ? "ended" : blocked() ? "inactive" : "paused", audio.ended || blocked());
1532
1532
  var minimumInterval = 1e3 / qualityFps[qualityIndex];
1533
- if (!Number.isFinite(lastWall) || finite2(wallNow, lastWall) - lastWall >= minimumInterval - 0.01) {
1534
- lastWall = finite2(wallNow, lastWall);
1533
+ if (!Number.isFinite(lastWall) || finite3(wallNow, lastWall) - lastWall >= minimumInterval - 0.01) {
1534
+ lastWall = finite3(wallNow, lastWall);
1535
1535
  status = "playing";
1536
1536
  publish("frame", false);
1537
1537
  }
@@ -1562,7 +1562,7 @@ function install2(__dmScope) {
1562
1562
  }
1563
1563
  function setQuality(value, reason) {
1564
1564
  var named = typeof value === "string" ? tiers.indexOf(value) : -1;
1565
- qualityIndex = named >= 0 ? named : clamp(Math.round(finite2(value, qualityIndex)), 0, tiers.length - 1);
1565
+ qualityIndex = named >= 0 ? named : clamp(Math.round(finite3(value, qualityIndex)), 0, tiers.length - 1);
1566
1566
  lastWall = -Infinity;
1567
1567
  publish(reason || "quality-change", audio.ended || blocked());
1568
1568
  }
@@ -1616,7 +1616,7 @@ function install2(__dmScope) {
1616
1616
  },
1617
1617
  setQuality,
1618
1618
  snapshot: function() {
1619
- return { id: state.id, type: "sprite-playback", status, time: finite2(audio.currentTime, 0), frame: latest ? latest.frame : null, localFrame: latest ? latest.localFrame : null, column: latest ? latest.column : null, row: latest ? latest.row : null, phase: latest ? latest.phase : null, cycleTicks, cycleSeconds: latest ? latest.cycleSeconds : null, qualityTier: tiers[qualityIndex], qualityIndex, renders, running: Boolean(frame) };
1619
+ return { id: state.id, type: "sprite-playback", status, time: finite3(audio.currentTime, 0), frame: latest ? latest.frame : null, localFrame: latest ? latest.localFrame : null, column: latest ? latest.column : null, row: latest ? latest.row : null, phase: latest ? latest.phase : null, cycleTicks, cycleSeconds: latest ? latest.cycleSeconds : null, qualityTier: tiers[qualityIndex], qualityIndex, renders, running: Boolean(frame) };
1620
1620
  },
1621
1621
  teardown: function() {
1622
1622
  stop("teardown", true);
@@ -1683,8 +1683,8 @@ function install2(__dmScope) {
1683
1683
  popover.appendChild(viewport);
1684
1684
  __dmScope.dataset(popover, "danceMovesLyricStage", "ready");
1685
1685
  __dmScope.setStyle(popover.style, "--dance-moves-lyric-progress", "0");
1686
- var travelTicks = Math.max(1, Math.round(finite2(options.travelTicks, 32)));
1687
- var defaultCueTicks = Math.max(1, Math.round(finite2(options.cueDurationTicks, 32)));
1686
+ var travelTicks = Math.max(1, Math.round(finite3(options.travelTicks, 32)));
1687
+ var defaultCueTicks = Math.max(1, Math.round(finite3(options.cueDurationTicks, 32)));
1688
1688
  var latest = null, phase = "idle", progress = 0, frame = 0, wakeTimer = 0, cueTimer = 0, cueCleanup = null, cueCount = 0;
1689
1689
  var lyricEffects = [];
1690
1690
  var slots = Object.freeze({ previous, current, next, viewport, track });
@@ -1733,7 +1733,7 @@ function install2(__dmScope) {
1733
1733
  publish(reason || value);
1734
1734
  }
1735
1735
  function setProgress(value) {
1736
- progress = clamp(finite2(value, 0), 0, 1);
1736
+ progress = clamp(finite3(value, 0), 0, 1);
1737
1737
  __dmScope.setStyle(popover.style, "--dance-moves-lyric-progress", progress.toFixed(5));
1738
1738
  }
1739
1739
  function copyLyrics(detail) {
@@ -1750,7 +1750,7 @@ function install2(__dmScope) {
1750
1750
  var cleanup = typeof result === "function" ? result : typeof result.cleanup === "function" ? result.cleanup : null;
1751
1751
  if (!cleanup)
1752
1752
  return;
1753
- var durationTicks = result && typeof result === "object" ? Math.max(1, Math.round(finite2(result.durationTicks, 64))) : 64;
1753
+ var durationTicks = result && typeof result === "object" ? Math.max(1, Math.round(finite3(result.durationTicks, 64))) : 64;
1754
1754
  var record = { cleanup, timer: 0 };
1755
1755
  record.timer = window2.setTimeout(function() {
1756
1756
  lyricEffects = lyricEffects.filter(function(item) {
@@ -1776,7 +1776,7 @@ function install2(__dmScope) {
1776
1776
  setProgress(0);
1777
1777
  return setPhase("waiting", "no-next-line");
1778
1778
  }
1779
- var now = Math.max(0, finite2(audio.currentTime, 0));
1779
+ var now = Math.max(0, finite3(audio.currentTime, 0));
1780
1780
  if (now < windowRange.start)
1781
1781
  return reconcile("frame-before-window");
1782
1782
  if (now >= windowRange.end) {
@@ -1806,7 +1806,7 @@ function install2(__dmScope) {
1806
1806
  setProgress(0);
1807
1807
  return setPhase("waiting", reason || "no-next-line");
1808
1808
  }
1809
- var now = Math.max(0, finite2(audio.currentTime, 0));
1809
+ var now = Math.max(0, finite3(audio.currentTime, 0));
1810
1810
  if (now >= windowRange.end) {
1811
1811
  setProgress(1);
1812
1812
  return setPhase("arrived", reason || "arrived");
@@ -1821,7 +1821,7 @@ function install2(__dmScope) {
1821
1821
  wakeTimer = window2.setTimeout(function() {
1822
1822
  wakeTimer = 0;
1823
1823
  tick();
1824
- }, Math.max(0, (windowRange.start - now) * 1e3 / Math.max(0.01, finite2(audio.playbackRate, 1))));
1824
+ }, Math.max(0, (windowRange.start - now) * 1e3 / Math.max(0.01, finite3(audio.playbackRate, 1))));
1825
1825
  }
1826
1826
  function onLyric(detail) {
1827
1827
  var previousDetail = latest;
@@ -1876,7 +1876,7 @@ function install2(__dmScope) {
1876
1876
  else if (result && typeof result === "object") {
1877
1877
  if (typeof result.cleanup === "function")
1878
1878
  cueCleanup = result.cleanup;
1879
- durationTicks = Math.max(1, Math.round(finite2(result.durationTicks, defaultCueTicks)));
1879
+ durationTicks = Math.max(1, Math.round(finite3(result.durationTicks, defaultCueTicks)));
1880
1880
  }
1881
1881
  cueTimer = window2.setTimeout(function() {
1882
1882
  clearCue("complete");
@@ -1948,16 +1948,16 @@ function install2(__dmScope) {
1948
1948
  var hazardSources = Array.isArray(options.hazardSources) ? options.hazardSources.filter(Boolean).map(String) : [];
1949
1949
  if (!shipSources.length || !hazardSources.length)
1950
1950
  throw new Error("DanceMovesEffects cooperativeArena requires ship and hazard sources");
1951
- var shipCount = clamp(Math.round(finite2(options.shipCount, 3)), 2, 12);
1952
- var hazardCount = clamp(Math.round(finite2(options.hazardCount, 8)), 3, 48);
1953
- var speed = clamp(finite2(options.speed, 1), 0.35, 2.4);
1951
+ var shipCount = clamp(Math.round(finite3(options.shipCount, 3)), 2, 12);
1952
+ var hazardCount = clamp(Math.round(finite3(options.hazardCount, 8)), 3, 48);
1953
+ var speed = clamp(finite3(options.speed, 1), 0.35, 2.4);
1954
1954
  var inlineTransform = options.inlineTransform === true;
1955
- var bpm = Math.max(1, finite2(options.bpm, motion.bpm || 120));
1956
- var shotIntervalTicks = clamp(Math.round(finite2(options.shotIntervalTicks, 32)), 4, 256);
1955
+ var bpm = Math.max(1, finite3(options.bpm, motion.bpm || 120));
1956
+ var shotIntervalTicks = clamp(Math.round(finite3(options.shotIntervalTicks, 32)), 4, 256);
1957
1957
  var shotIntervalSeconds = motion.durationMilliseconds ? motion.durationMilliseconds(shotIntervalTicks, bpm) / 1e3 : shotIntervalTicks * 60 / (bpm * 16);
1958
- var burstDurationSeconds = clamp(finite2(options.burstDurationBeats, 1.5), 0.1, 8) * 60 / bpm;
1959
- var shipHitboxScale = clamp(finite2(options.shipHitboxScale, 1), 0.4, 1);
1960
- var hazardHitboxScale = clamp(finite2(options.hazardHitboxScale, 1), 0.4, 1);
1958
+ var burstDurationSeconds = clamp(finite3(options.burstDurationBeats, 1.5), 0.1, 8) * 60 / bpm;
1959
+ var shipHitboxScale = clamp(finite3(options.shipHitboxScale, 1), 0.4, 1);
1960
+ var hazardHitboxScale = clamp(finite3(options.hazardHitboxScale, 1), 0.4, 1);
1961
1961
  var removers = state.removers, ships = [], hazards = [], shots = [], bursts = [], burstTimers = [];
1962
1962
  var activeShips = shipCount, activeHazards = hazardCount;
1963
1963
  var width = 1, height = 1, frame = 0, last = 0, elapsed = 0, nextShot = 0, hits = 0;
@@ -2289,7 +2289,7 @@ function install2(__dmScope) {
2289
2289
  reconcile(reason || "manual");
2290
2290
  },
2291
2291
  setDensity: function(nextShips, nextHazards) {
2292
- var shipsNext = clamp(Math.round(finite2(nextShips, activeShips)), 2, shipCount), hazardsNext = clamp(Math.round(finite2(nextHazards, activeHazards)), 3, hazardCount);
2292
+ var shipsNext = clamp(Math.round(finite3(nextShips, activeShips)), 2, shipCount), hazardsNext = clamp(Math.round(finite3(nextHazards, activeHazards)), 3, hazardCount);
2293
2293
  if (shipsNext === activeShips && hazardsNext === activeHazards)
2294
2294
  return;
2295
2295
  activeShips = shipsNext;
@@ -2332,12 +2332,12 @@ function install2(__dmScope) {
2332
2332
  options = options || {};
2333
2333
  var state = commonState(options, "quality");
2334
2334
  var tiers = Array.isArray(options.tiers) && options.tiers.length ? options.tiers.map(String) : ["full", "constrained", "minimal"];
2335
- var index = clamp(Math.round(finite2(options.initial, 0)), 0, tiers.length - 1);
2336
- var sampleMs = Math.max(800, finite2(options.sampleMilliseconds, 1600));
2337
- var poorWindows = Math.max(2, Math.round(finite2(options.poorWindows, 2)));
2338
- var healthyWindows = Math.max(3, Math.round(finite2(options.healthyWindows, 5)));
2339
- var downgradeRatio = clamp(finite2(options.downgradeRatio, 0.72), 0.2, 0.95);
2340
- var recoveryRatio = clamp(finite2(options.recoveryRatio, 0.9), downgradeRatio, 1);
2335
+ var index = clamp(Math.round(finite3(options.initial, 0)), 0, tiers.length - 1);
2336
+ var sampleMs = Math.max(800, finite3(options.sampleMilliseconds, 1600));
2337
+ var poorWindows = Math.max(2, Math.round(finite3(options.poorWindows, 2)));
2338
+ var healthyWindows = Math.max(3, Math.round(finite3(options.healthyWindows, 5)));
2339
+ var downgradeRatio = clamp(finite3(options.downgradeRatio, 0.72), 0.2, 0.95);
2340
+ var recoveryRatio = clamp(finite3(options.recoveryRatio, 0.9), downgradeRatio, 1);
2341
2341
  var frame = 0, frames = 0, start = 0, reference = 0, poor = 0, healthy = 0, lastShift = 0, running = true;
2342
2342
  var preferenceLimited = false, tierBeforePreference = index;
2343
2343
  function publish(reason, sample) {
@@ -2381,12 +2381,12 @@ function install2(__dmScope) {
2381
2381
  poor = 0;
2382
2382
  healthy = 0;
2383
2383
  }
2384
- if (poor >= poorWindows && index < tiers.length - 1 && now - lastShift >= finite2(options.downgradeCooldownMilliseconds, 5e3)) {
2384
+ if (poor >= poorWindows && index < tiers.length - 1 && now - lastShift >= finite3(options.downgradeCooldownMilliseconds, 5e3)) {
2385
2385
  index += 1;
2386
2386
  poor = 0;
2387
2387
  lastShift = now;
2388
2388
  publish("sustained-low-fps", { fps });
2389
- } else if (healthy >= healthyWindows && index > 0 && now - lastShift >= finite2(options.recoveryCooldownMilliseconds, 12e3)) {
2389
+ } else if (healthy >= healthyWindows && index > 0 && now - lastShift >= finite3(options.recoveryCooldownMilliseconds, 12e3)) {
2390
2390
  index -= 1;
2391
2391
  healthy = 0;
2392
2392
  lastShift = now;
@@ -2425,7 +2425,7 @@ function install2(__dmScope) {
2425
2425
  id: state.id,
2426
2426
  type: "quality",
2427
2427
  setTier: function(value, reason) {
2428
- index = clamp(Math.round(finite2(value, index)), 0, tiers.length - 1);
2428
+ index = clamp(Math.round(finite3(value, index)), 0, tiers.length - 1);
2429
2429
  publish(reason || "manual");
2430
2430
  },
2431
2431
  snapshot: function() {
@@ -6403,6 +6403,184 @@ function installPlayback(scope, api, host = scope.document.defaultView) {
6403
6403
  });
6404
6404
  }
6405
6405
 
6406
+ // .build/modules/recordings.mjs
6407
+ var WASM = "AGFzbQEAAAABDwJgAAF/YAZ/f39/f38BfAMDAgABBQUBAQKAQAYIAX8BQYCIBAsHKgMGbWVtb3J5AgAKdGFibGVfYmFzZQAAEHNhbXBsZV9yZWNvcmRpbmcAAQpvAggAQYCIhIAAC2QCAXwBfkQAAAAAAAD4fyEGAkAgAEUNACABQQFIDQAgAkEBSA0AIAVBAEgNACAFIAJPDQAgBKwgA6x9IgdCAFMNACAHIAGtWg0AIAAgB6cgAmxBA3RqIAVBA3RqKwMAIQYLIAYL";
6408
+ var MAX_VALUES = 8388608;
6409
+ var MAX_BYTES = 64 * 1024 * 1024;
6410
+ var modulePromise;
6411
+ var finite2 = (n, label) => {
6412
+ if (typeof n !== "number" || !Number.isFinite(n)) throw new TypeError(label + " must be finite");
6413
+ return n;
6414
+ };
6415
+ function validateRecording(input) {
6416
+ if (!input || input.format !== "dancerudiments.recording" || input.schema_version !== 1 || input.pips_per_beat !== 64 || input.sample_dtype !== "float64") throw new TypeError("Expected dancerudiments.recording v1, float64, 64 pips per beat");
6417
+ if (typeof input.name !== "string" || !input.name.length || input.name.length > 128) throw new TypeError("Recording name is required");
6418
+ const dimensions = input.dimensions;
6419
+ if (!Array.isArray(dimensions) || !dimensions.length || dimensions.length > 64 || new Set(dimensions).size !== dimensions.length || dimensions.some((d) => typeof d !== "string" || !/^[a-z][a-z0-9_]{0,127}$/.test(d))) throw new TypeError("Recording requires 1..64 unique named dimensions");
6420
+ const c = input.clock;
6421
+ if (!c || finite2(c.bpm, "Recording BPM") <= 0 || !["confirmed", "estimated"].includes(c.bpm_status) || !["unverified", "estimated", "supplied"].includes(c.phase_status) || typeof c.bpm_source !== "string" || !c.bpm_source.trim()) throw new TypeError("Recording clock requires BPM and provenance");
6422
+ finite2(c.beat_zero_seconds, "Beat zero");
6423
+ if (!Number.isInteger(c.start_pip) || c.start_pip < -2147483648) throw new RangeError("Invalid recording start pip");
6424
+ const rows = input.samples;
6425
+ if (!Array.isArray(rows) || !rows.length || rows.length * dimensions.length > MAX_VALUES || c.start_pip + rows.length > 2147483647) throw new RangeError("Recording exceeds bounded sample limits");
6426
+ const values = new Float64Array(rows.length * dimensions.length);
6427
+ rows.forEach((row, i) => {
6428
+ if (!Array.isArray(row) || row.length !== dimensions.length) throw new TypeError("Recording sample dimensions differ");
6429
+ row.forEach((v, j) => {
6430
+ finite2(v, "Sample");
6431
+ if (Math.abs(v) > 1) throw new RangeError("Recording samples must be in [-1,1]");
6432
+ values[i * dimensions.length + j] = v;
6433
+ });
6434
+ });
6435
+ return Object.freeze({ name: input.name, dimensions: Object.freeze([...dimensions]), count: rows.length, clock: Object.freeze({ ...c }), values });
6436
+ }
6437
+ async function samplerFromData(data) {
6438
+ if (!modulePromise) modulePromise = WebAssembly.compile(Uint8Array.from(atob(WASM), (c) => c.charCodeAt(0))).catch((error) => {
6439
+ modulePromise = null;
6440
+ throw error;
6441
+ });
6442
+ const api = (await WebAssembly.instantiate(await modulePromise, {})).exports;
6443
+ const base = api.table_base(), pages = Math.ceil((base + data.values.byteLength - api.memory.buffer.byteLength) / 65536);
6444
+ if (pages > 0) api.memory.grow(pages);
6445
+ new Float64Array(api.memory.buffer, base, data.values.length).set(data.values);
6446
+ return Object.freeze({ ...data, sample(pip) {
6447
+ if (!Number.isInteger(pip) || pip < data.clock.start_pip || pip >= data.clock.start_pip + data.count) throw new RangeError("Position is outside the non-looping recording");
6448
+ return data.dimensions.map((_, axis) => api.sample_recording(base, data.count, data.dimensions.length, data.clock.start_pip, pip, axis));
6449
+ } });
6450
+ }
6451
+ function createRecordingService(scope, rudiments, config) {
6452
+ const handles = /* @__PURE__ */ new Map();
6453
+ async function load(url2) {
6454
+ if (scope.disposed) throw new Error("DanceMoves mount is destroyed");
6455
+ const response = await scope.window.fetch(url2);
6456
+ if (!response.ok) throw new Error("Recording request failed: " + response.status);
6457
+ if (Number(response.headers?.get("content-length")) > MAX_BYTES) throw new RangeError("Recording exceeds 64 MiB");
6458
+ let text;
6459
+ if (response.body?.getReader) {
6460
+ const reader = response.body.getReader(), chunks = [];
6461
+ let size = 0;
6462
+ for (; ; ) {
6463
+ const { done, value } = await reader.read();
6464
+ if (done) break;
6465
+ size += value.byteLength;
6466
+ if (size > MAX_BYTES) {
6467
+ await reader.cancel();
6468
+ throw new RangeError("Recording exceeds 64 MiB");
6469
+ }
6470
+ chunks.push(value);
6471
+ }
6472
+ const bytes = new Uint8Array(size);
6473
+ let offset = 0;
6474
+ for (const chunk of chunks) {
6475
+ bytes.set(chunk, offset);
6476
+ offset += chunk.length;
6477
+ }
6478
+ text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
6479
+ } else {
6480
+ text = await response.text();
6481
+ if (new TextEncoder().encode(text).length > MAX_BYTES) throw new RangeError("Recording exceeds 64 MiB");
6482
+ }
6483
+ const input = JSON.parse(text);
6484
+ validateRecording(input);
6485
+ return input;
6486
+ }
6487
+ function animate(options = {}) {
6488
+ if (scope.disposed) throw new Error("DanceMoves mount is destroyed");
6489
+ if (typeof options.id !== "string" || !options.id.trim() || handles.has(options.id)) throw new TypeError("Recording controller requires a unique id");
6490
+ if (typeof options.render !== "function") throw new TypeError("Recording render callback is required");
6491
+ if (!options.audio) throw new TypeError("Recording playback requires an audio/player source");
6492
+ if (["rate", "phasePips", "clock", "bpm"].some((k) => Object.hasOwn(options, k))) throw new TypeError("Recording clock, rate and origin come from its capture");
6493
+ const data = validateRecording(options.recording);
6494
+ if (Math.abs(data.clock.bpm - config.bpm) > 1e-9) throw new RangeError("Recording BPM does not match the authoritative page BPM");
6495
+ let inner = null, sampler = null, destroyed = false, enabled = options.enabled !== false, paused = false, status = "loading", error = null, last = null;
6496
+ const output = (frame, values, pip, reason) => {
6497
+ if (destroyed) return;
6498
+ last = { ...frame, rudiment: null, recording: data.name, dimensions: data.dimensions, values, position: { x: values[0] || 0, y: values[1] || 0, z: values[2] || 0 }, pip, positionPips: pip, loopProgress: null, reason };
6499
+ options.render(last);
6500
+ };
6501
+ const handle = {
6502
+ id: options.id,
6503
+ ready: null,
6504
+ setEnabled(value) {
6505
+ if (typeof value !== "boolean") throw new TypeError("enabled must be boolean");
6506
+ enabled = value;
6507
+ inner?.setEnabled(value);
6508
+ },
6509
+ pause() {
6510
+ paused = true;
6511
+ inner?.pause();
6512
+ },
6513
+ resume() {
6514
+ paused = false;
6515
+ inner?.resume();
6516
+ },
6517
+ refresh() {
6518
+ inner?.refresh();
6519
+ },
6520
+ destroy() {
6521
+ if (destroyed) return;
6522
+ inner?.destroy();
6523
+ inner = null;
6524
+ sampler = null;
6525
+ destroyed = true;
6526
+ status = "destroyed";
6527
+ handles.delete(options.id);
6528
+ },
6529
+ snapshot() {
6530
+ const state = inner?.snapshot() ?? null;
6531
+ return { id: options.id, type: "recording", recording: data.name, status: !destroyed && ["hidden", "offscreen", "paused", "seeking"].includes(state?.status) ? state.status : status, error, destroyed, enabled, paused, pip: last?.pip ?? null, dimensions: data.dimensions, count: data.count, clock: data.clock, inner: state };
6532
+ }
6533
+ };
6534
+ handle.teardown = handle.destroy;
6535
+ handles.set(options.id, handle);
6536
+ handle.ready = samplerFromData(data).then(async (native) => {
6537
+ if (destroyed || scope.disposed) return handle;
6538
+ sampler = native;
6539
+ inner = rudiments.animate({
6540
+ id: options.id,
6541
+ root: options.root,
6542
+ target: options.target,
6543
+ audio: options.audio,
6544
+ clock: "audio",
6545
+ rudiment: "circle",
6546
+ rate: 1,
6547
+ phasePips: 0,
6548
+ amplitude: 0,
6549
+ css: false,
6550
+ renderEveryFrame: true,
6551
+ offscreen: options.offscreen !== false,
6552
+ enabled,
6553
+ render(frame) {
6554
+ if (destroyed) return;
6555
+ const neutral = ["disabled", "reduced-motion", "forced-colors"].includes(frame.reason), precise = (frame.sourceBeats - data.clock.beat_zero_seconds * data.clock.bpm / 60) * 64;
6556
+ const pip = Math.floor(precise + 1e-8), outside = pip < data.clock.start_pip || pip >= data.clock.start_pip + data.count;
6557
+ status = neutral ? frame.reason : outside ? "outside-recording" : frame.reason;
6558
+ const values = neutral || outside ? data.dimensions.map(() => 0) : sampler.sample(pip);
6559
+ output(frame, values, pip, status);
6560
+ }
6561
+ });
6562
+ if (paused) inner.pause();
6563
+ await inner.ready;
6564
+ return handle;
6565
+ }).catch((failure) => {
6566
+ error = String(failure.message || failure);
6567
+ status = "error";
6568
+ inner?.destroy();
6569
+ inner = null;
6570
+ sampler = null;
6571
+ if (handles.get(options.id) === handle) handles.delete(options.id);
6572
+ throw failure;
6573
+ });
6574
+ return handle;
6575
+ }
6576
+ scope.onDispose(() => {
6577
+ for (const handle of [...handles.values()]) handle.destroy();
6578
+ });
6579
+ return Object.freeze({ load, animate, get: (id) => handles.get(id) || null, destroyAll: () => {
6580
+ for (const h of [...handles.values()]) h.destroy();
6581
+ }, snapshot: () => ({ native: "DanceRudiments 0.3.0 recording C++/WASM", instances: [...handles.values()].map((h) => h.snapshot()) }) });
6582
+ }
6583
+
6406
6584
  // .build/modules/child-playback.mjs
6407
6585
  var PROTOCOL = "dancemoves.child-playback.v1";
6408
6586
  var EVENTS = /* @__PURE__ */ new Set(["snapshot", "loadedmetadata", "durationchange", "play", "playing", "pause", "ended", "seeking", "seeked", "ratechange", "timeupdate", "waiting", "error"]);
@@ -6827,7 +7005,7 @@ function installChildPlayback(scope, api, host) {
6827
7005
  }
6828
7006
 
6829
7007
  // .build/modules/version.mjs
6830
- var VERSION = "3.1.22";
7008
+ var VERSION = "3.2.0";
6831
7009
 
6832
7010
  // .build/modules/index.mjs
6833
7011
  var mounted = /* @__PURE__ */ new WeakMap();
@@ -6880,6 +7058,7 @@ function createDanceMoves(options = {}) {
6880
7058
  config,
6881
7059
  effects: w.DanceMovesEffects,
6882
7060
  rudiments: w.DanceMovesRudiments,
7061
+ recordings: createRecordingService(scope, w.DanceMovesRudiments, config),
6883
7062
  orientationCore: w.KSEpkOrientationCore,
6884
7063
  getOrientation: () => w.__ksEpkOrientationRuntime || null,
6885
7064
  getClay: () => w.DanceMovesClayStars || null,