@kieransimkin/dancemoves 3.1.23 → 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.cjs CHANGED
@@ -1065,7 +1065,7 @@ function install2(__dmScope) {
1065
1065
  return;
1066
1066
  var instances = /* @__PURE__ */ new Map();
1067
1067
  var serial2 = 0;
1068
- function finite2(value, fallback) {
1068
+ function finite3(value, fallback) {
1069
1069
  value = Number(value);
1070
1070
  return Number.isFinite(value) ? value : fallback;
1071
1071
  }
@@ -1119,8 +1119,8 @@ function install2(__dmScope) {
1119
1119
  rect = bounds.getBoundingClientRect();
1120
1120
  }
1121
1121
  function apply(x, y, reason) {
1122
- latestX = clamp(finite2(x, 0), -1, 1);
1123
- latestY = clamp(finite2(y, 0), -1, 1);
1122
+ latestX = clamp(finite3(x, 0), -1, 1);
1123
+ latestY = clamp(finite3(y, 0), -1, 1);
1124
1124
  __dmScope.setStyle(state.root.style, "--dance-moves-x", latestX.toFixed(4));
1125
1125
  __dmScope.setStyle(state.root.style, "--dance-moves-y", latestY.toFixed(4));
1126
1126
  __dmScope.dataset(state.root, "danceMovesPointer", reason || "active");
@@ -1211,13 +1211,13 @@ function install2(__dmScope) {
1211
1211
  var audio = playbackElement(options.audio, state.root.querySelector("audio"));
1212
1212
  if (!audio)
1213
1213
  throw new Error("DanceMovesEffects pulse requires an audio element");
1214
- var ticks = Math.max(1, Math.round(finite2(options.ticks, 16)));
1214
+ var ticks = Math.max(1, Math.round(finite3(options.ticks, 16)));
1215
1215
  var className = String(options.className || "dance-moves-playing");
1216
1216
  var prefix = String(options.propertyPrefix || "--dance-moves-pulse").replace(/-+$/, "");
1217
1217
  function sync() {
1218
- var rate = Math.max(0.01, finite2(audio.playbackRate, 1));
1218
+ var rate = Math.max(0.01, finite3(audio.playbackRate, 1));
1219
1219
  var duration = motion.durationMilliseconds(ticks) / 1e3 / rate;
1220
- var phase = -(finite2(audio.currentTime, 0) / rate % duration);
1220
+ var phase = -(finite3(audio.currentTime, 0) / rate % duration);
1221
1221
  __dmScope.setStyle(state.root.style, prefix + "-duration", duration.toFixed(6) + "s");
1222
1222
  __dmScope.setStyle(state.root.style, prefix + "-delay", phase.toFixed(6) + "s");
1223
1223
  __dmScope.dataset(state.root, "danceMovesPulse", state.reduced.matches || state.forced.matches ? "inactive" : audio.paused ? "paused" : "playing");
@@ -1256,7 +1256,7 @@ function install2(__dmScope) {
1256
1256
  var state = commonState(options, "cue-class");
1257
1257
  var cue = String(options.cue || "*");
1258
1258
  var className = String(options.className || "dance-moves-cue-active");
1259
- var durationTicks = Math.max(1, Math.round(finite2(options.durationTicks, 16)));
1259
+ var durationTicks = Math.max(1, Math.round(finite3(options.durationTicks, 16)));
1260
1260
  var timer = 0, count = 0;
1261
1261
  function clear(reason) {
1262
1262
  if (timer)
@@ -1312,16 +1312,16 @@ function install2(__dmScope) {
1312
1312
  if (!audio)
1313
1313
  throw new Error("DanceMovesEffects cueTimeline requires an audio element");
1314
1314
  var cues = (Array.isArray(options.cues) ? options.cues : []).map(function(cue, index) {
1315
- var time = finite2(cue && cue.time, NaN);
1315
+ var time = finite3(cue && cue.time, NaN);
1316
1316
  if (!Number.isFinite(time) || time < 0)
1317
1317
  throw new TypeError("DanceMovesEffects cueTimeline cue time must be a non-negative number");
1318
1318
  return Object.freeze({ id: String(cue.id || "cue-" + index), time, end: Number.isFinite(Number(cue.end)) ? Number(cue.end) : null, data: cue.data });
1319
1319
  }).sort(function(a, b) {
1320
1320
  return a.time - b.time;
1321
1321
  });
1322
- var tolerance = Math.max(0, finite2(options.seekLandingTolerance, 0.05));
1323
- var maxCrossingGap = Math.max(0.05, finite2(options.maxCrossingGap, 1.25));
1324
- var fired = /* @__PURE__ */ new Set(), frame = 0, previous = finite2(audio.currentTime, 0), status = "idle", generation = 0, previousWall = 0;
1322
+ var tolerance = Math.max(0, finite3(options.seekLandingTolerance, 0.05));
1323
+ var maxCrossingGap = Math.max(0.05, finite3(options.maxCrossingGap, 1.25));
1324
+ var fired = /* @__PURE__ */ new Set(), frame = 0, previous = finite3(audio.currentTime, 0), status = "idle", generation = 0, previousWall = 0;
1325
1325
  function blocked() {
1326
1326
  return document2.hidden || state.reduced.matches || state.forced.matches;
1327
1327
  }
@@ -1331,7 +1331,7 @@ function install2(__dmScope) {
1331
1331
  });
1332
1332
  }
1333
1333
  function publish(reason, extra) {
1334
- var time = finite2(audio.currentTime, 0);
1334
+ var time = finite3(audio.currentTime, 0);
1335
1335
  __dmScope.dataset(state.root, "danceMovesCueTimeline", reason);
1336
1336
  if (typeof options.render === "function")
1337
1337
  options.render({ root: state.root, audio, time, previousTime: previous, reason, playing: !audio.paused && !audio.ended && !blocked(), active: activeAt(time), generation, detail: extra || null });
@@ -1341,10 +1341,10 @@ function install2(__dmScope) {
1341
1341
  return;
1342
1342
  fired.add(cue.id);
1343
1343
  if (typeof options.onCue === "function")
1344
- options.onCue({ root: state.root, audio, cue, time: finite2(audio.currentTime, cue.time), reason, generation });
1344
+ options.onCue({ root: state.root, audio, cue, time: finite3(audio.currentTime, cue.time), reason, generation });
1345
1345
  }
1346
1346
  function rebuild(time, reason, allowLanding) {
1347
- time = Math.max(0, finite2(time, 0));
1347
+ time = Math.max(0, finite3(time, 0));
1348
1348
  generation += 1;
1349
1349
  fired.clear();
1350
1350
  cues.forEach(function(cue) {
@@ -1370,7 +1370,7 @@ function install2(__dmScope) {
1370
1370
  frame = 0;
1371
1371
  if (audio.paused || audio.ended || blocked())
1372
1372
  return stop(audio.ended ? "ended" : blocked() ? "inactive" : "paused");
1373
- var now = Math.max(0, finite2(audio.currentTime, previous));
1373
+ var now = Math.max(0, finite3(audio.currentTime, previous));
1374
1374
  var delta = now - previous;
1375
1375
  if (delta < -1e-3 || delta > maxCrossingGap)
1376
1376
  rebuild(now, "discontinuity", false);
@@ -1379,8 +1379,8 @@ function install2(__dmScope) {
1379
1379
  if (previous < cue.time && now >= cue.time)
1380
1380
  fire(cue, "crossing");
1381
1381
  });
1382
- var wallDelta = previousWall ? Math.min(0.25, Math.max(0, (finite2(wallNow, previousWall) - previousWall) / 1e3)) : 0;
1383
- previousWall = finite2(wallNow, previousWall);
1382
+ var wallDelta = previousWall ? Math.min(0.25, Math.max(0, (finite3(wallNow, previousWall) - previousWall) / 1e3)) : 0;
1383
+ previousWall = finite3(wallNow, previousWall);
1384
1384
  previous = now;
1385
1385
  status = "playing";
1386
1386
  publish("frame", { deltaSeconds: wallDelta });
@@ -1389,7 +1389,7 @@ function install2(__dmScope) {
1389
1389
  function start(reason) {
1390
1390
  if (frame || audio.paused || audio.ended || blocked())
1391
1391
  return publish(blocked() ? "inactive" : audio.ended ? "ended" : "paused");
1392
- previous = Math.max(0, finite2(audio.currentTime, 0));
1392
+ previous = Math.max(0, finite3(audio.currentTime, 0));
1393
1393
  previousWall = 0;
1394
1394
  status = reason || "playing";
1395
1395
  publish(status);
@@ -1453,7 +1453,7 @@ function install2(__dmScope) {
1453
1453
  start,
1454
1454
  stop,
1455
1455
  snapshot: function() {
1456
- 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) {
1456
+ 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) {
1457
1457
  return cue.id;
1458
1458
  }) };
1459
1459
  },
@@ -1476,28 +1476,28 @@ function install2(__dmScope) {
1476
1476
  if (!target)
1477
1477
  throw new Error("DanceMovesEffects spritePlayback requires a target element");
1478
1478
  var frameMap = Array.isArray(options.frameMap) && options.frameMap.length ? options.frameMap.map(function(value) {
1479
- value = Math.floor(finite2(value, -1));
1479
+ value = Math.floor(finite3(value, -1));
1480
1480
  if (value < 0)
1481
1481
  throw new TypeError("DanceMovesEffects spritePlayback frameMap values must be non-negative integers");
1482
1482
  return value;
1483
1483
  }) : null;
1484
- var frameCount = frameMap ? frameMap.length : Math.max(1, Math.round(finite2(options.frameCount, 1)));
1485
- var columns = Math.max(1, Math.round(finite2(options.columns, frameCount)));
1486
- var framesPerRow = clamp(Math.round(finite2(options.framesPerRow, columns)), 1, columns);
1487
- var columnOffset = clamp(Math.round(finite2(options.columnOffset, 0)), 0, columns - framesPerRow);
1488
- var rowOffset = Math.max(0, Math.round(finite2(options.rowOffset, 0)));
1489
- var cycleTicks = Math.max(1, Math.round(finite2(options.cycleTicks, 32)));
1490
- var phaseOffsetSeconds = finite2(options.phaseOffsetSeconds, 0);
1491
- var staticFrame = clamp(Math.round(finite2(options.staticFrame, 0)), 0, frameCount - 1);
1484
+ var frameCount = frameMap ? frameMap.length : Math.max(1, Math.round(finite3(options.frameCount, 1)));
1485
+ var columns = Math.max(1, Math.round(finite3(options.columns, frameCount)));
1486
+ var framesPerRow = clamp(Math.round(finite3(options.framesPerRow, columns)), 1, columns);
1487
+ var columnOffset = clamp(Math.round(finite3(options.columnOffset, 0)), 0, columns - framesPerRow);
1488
+ var rowOffset = Math.max(0, Math.round(finite3(options.rowOffset, 0)));
1489
+ var cycleTicks = Math.max(1, Math.round(finite3(options.cycleTicks, 32)));
1490
+ var phaseOffsetSeconds = finite3(options.phaseOffsetSeconds, 0);
1491
+ var staticFrame = clamp(Math.round(finite3(options.staticFrame, 0)), 0, frameCount - 1);
1492
1492
  var prefix = String(options.propertyPrefix || "--dance-moves-sprite").replace(/-+$/, "");
1493
1493
  if (prefix.slice(0, 2) !== "--")
1494
1494
  prefix = "--" + prefix.replace(/^-+/, "");
1495
1495
  var tiers = Array.isArray(options.qualityTiers) && options.qualityTiers.length ? options.qualityTiers.map(String) : ["full", "constrained", "minimal"];
1496
1496
  var suppliedFps = Array.isArray(options.qualityFramesPerSecond) ? options.qualityFramesPerSecond : [60, 30, 15];
1497
1497
  var qualityFps = tiers.map(function(_, index) {
1498
- return Math.max(1, finite2(suppliedFps[index], suppliedFps[suppliedFps.length - 1] || 15));
1498
+ return Math.max(1, finite3(suppliedFps[index], suppliedFps[suppliedFps.length - 1] || 15));
1499
1499
  });
1500
- var qualityIndex = clamp(Math.round(finite2(options.initialQuality, 0)), 0, tiers.length - 1);
1500
+ var qualityIndex = clamp(Math.round(finite3(options.initialQuality, 0)), 0, tiers.length - 1);
1501
1501
  var frame = 0, status = "idle", lastWall = -Infinity, renders = 0, latest = null;
1502
1502
  function blocked() {
1503
1503
  return document2.hidden || state.reduced.matches || state.forced.matches;
@@ -1507,8 +1507,8 @@ function install2(__dmScope) {
1507
1507
  return { sourceFrame, column: sourceFrame % columns, row: Math.floor(sourceFrame / columns) };
1508
1508
  }
1509
1509
  function calculate(reason, staticFallback) {
1510
- var cycleSeconds = Math.max(1e-3, finite2(motion.durationMilliseconds(cycleTicks), 1) / 1e3);
1511
- var time = Math.max(0, finite2(audio.currentTime, 0));
1510
+ var cycleSeconds = Math.max(1e-3, finite3(motion.durationMilliseconds(cycleTicks), 1) / 1e3);
1511
+ var time = Math.max(0, finite3(audio.currentTime, 0));
1512
1512
  var phase = ((time - phaseOffsetSeconds) % cycleSeconds + cycleSeconds) % cycleSeconds / cycleSeconds;
1513
1513
  var localFrame = staticFallback ? staticFrame : Math.min(frameCount - 1, Math.floor(phase * frameCount + 1e-9));
1514
1514
  var mapped = mappedFrame(localFrame);
@@ -1563,8 +1563,8 @@ function install2(__dmScope) {
1563
1563
  if (audio.paused || audio.ended || blocked())
1564
1564
  return stop(audio.ended ? "ended" : blocked() ? "inactive" : "paused", audio.ended || blocked());
1565
1565
  var minimumInterval = 1e3 / qualityFps[qualityIndex];
1566
- if (!Number.isFinite(lastWall) || finite2(wallNow, lastWall) - lastWall >= minimumInterval - 0.01) {
1567
- lastWall = finite2(wallNow, lastWall);
1566
+ if (!Number.isFinite(lastWall) || finite3(wallNow, lastWall) - lastWall >= minimumInterval - 0.01) {
1567
+ lastWall = finite3(wallNow, lastWall);
1568
1568
  status = "playing";
1569
1569
  publish("frame", false);
1570
1570
  }
@@ -1595,7 +1595,7 @@ function install2(__dmScope) {
1595
1595
  }
1596
1596
  function setQuality(value, reason) {
1597
1597
  var named = typeof value === "string" ? tiers.indexOf(value) : -1;
1598
- qualityIndex = named >= 0 ? named : clamp(Math.round(finite2(value, qualityIndex)), 0, tiers.length - 1);
1598
+ qualityIndex = named >= 0 ? named : clamp(Math.round(finite3(value, qualityIndex)), 0, tiers.length - 1);
1599
1599
  lastWall = -Infinity;
1600
1600
  publish(reason || "quality-change", audio.ended || blocked());
1601
1601
  }
@@ -1649,7 +1649,7 @@ function install2(__dmScope) {
1649
1649
  },
1650
1650
  setQuality,
1651
1651
  snapshot: function() {
1652
- 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) };
1652
+ 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) };
1653
1653
  },
1654
1654
  teardown: function() {
1655
1655
  stop("teardown", true);
@@ -1716,8 +1716,8 @@ function install2(__dmScope) {
1716
1716
  popover.appendChild(viewport);
1717
1717
  __dmScope.dataset(popover, "danceMovesLyricStage", "ready");
1718
1718
  __dmScope.setStyle(popover.style, "--dance-moves-lyric-progress", "0");
1719
- var travelTicks = Math.max(1, Math.round(finite2(options.travelTicks, 32)));
1720
- var defaultCueTicks = Math.max(1, Math.round(finite2(options.cueDurationTicks, 32)));
1719
+ var travelTicks = Math.max(1, Math.round(finite3(options.travelTicks, 32)));
1720
+ var defaultCueTicks = Math.max(1, Math.round(finite3(options.cueDurationTicks, 32)));
1721
1721
  var latest = null, phase = "idle", progress = 0, frame = 0, wakeTimer = 0, cueTimer = 0, cueCleanup = null, cueCount = 0;
1722
1722
  var lyricEffects = [];
1723
1723
  var slots = Object.freeze({ previous, current, next, viewport, track });
@@ -1766,7 +1766,7 @@ function install2(__dmScope) {
1766
1766
  publish(reason || value);
1767
1767
  }
1768
1768
  function setProgress(value) {
1769
- progress = clamp(finite2(value, 0), 0, 1);
1769
+ progress = clamp(finite3(value, 0), 0, 1);
1770
1770
  __dmScope.setStyle(popover.style, "--dance-moves-lyric-progress", progress.toFixed(5));
1771
1771
  }
1772
1772
  function copyLyrics(detail) {
@@ -1783,7 +1783,7 @@ function install2(__dmScope) {
1783
1783
  var cleanup = typeof result === "function" ? result : typeof result.cleanup === "function" ? result.cleanup : null;
1784
1784
  if (!cleanup)
1785
1785
  return;
1786
- var durationTicks = result && typeof result === "object" ? Math.max(1, Math.round(finite2(result.durationTicks, 64))) : 64;
1786
+ var durationTicks = result && typeof result === "object" ? Math.max(1, Math.round(finite3(result.durationTicks, 64))) : 64;
1787
1787
  var record = { cleanup, timer: 0 };
1788
1788
  record.timer = window2.setTimeout(function() {
1789
1789
  lyricEffects = lyricEffects.filter(function(item) {
@@ -1809,7 +1809,7 @@ function install2(__dmScope) {
1809
1809
  setProgress(0);
1810
1810
  return setPhase("waiting", "no-next-line");
1811
1811
  }
1812
- var now = Math.max(0, finite2(audio.currentTime, 0));
1812
+ var now = Math.max(0, finite3(audio.currentTime, 0));
1813
1813
  if (now < windowRange.start)
1814
1814
  return reconcile("frame-before-window");
1815
1815
  if (now >= windowRange.end) {
@@ -1839,7 +1839,7 @@ function install2(__dmScope) {
1839
1839
  setProgress(0);
1840
1840
  return setPhase("waiting", reason || "no-next-line");
1841
1841
  }
1842
- var now = Math.max(0, finite2(audio.currentTime, 0));
1842
+ var now = Math.max(0, finite3(audio.currentTime, 0));
1843
1843
  if (now >= windowRange.end) {
1844
1844
  setProgress(1);
1845
1845
  return setPhase("arrived", reason || "arrived");
@@ -1854,7 +1854,7 @@ function install2(__dmScope) {
1854
1854
  wakeTimer = window2.setTimeout(function() {
1855
1855
  wakeTimer = 0;
1856
1856
  tick();
1857
- }, Math.max(0, (windowRange.start - now) * 1e3 / Math.max(0.01, finite2(audio.playbackRate, 1))));
1857
+ }, Math.max(0, (windowRange.start - now) * 1e3 / Math.max(0.01, finite3(audio.playbackRate, 1))));
1858
1858
  }
1859
1859
  function onLyric(detail) {
1860
1860
  var previousDetail = latest;
@@ -1909,7 +1909,7 @@ function install2(__dmScope) {
1909
1909
  else if (result && typeof result === "object") {
1910
1910
  if (typeof result.cleanup === "function")
1911
1911
  cueCleanup = result.cleanup;
1912
- durationTicks = Math.max(1, Math.round(finite2(result.durationTicks, defaultCueTicks)));
1912
+ durationTicks = Math.max(1, Math.round(finite3(result.durationTicks, defaultCueTicks)));
1913
1913
  }
1914
1914
  cueTimer = window2.setTimeout(function() {
1915
1915
  clearCue("complete");
@@ -1981,16 +1981,16 @@ function install2(__dmScope) {
1981
1981
  var hazardSources = Array.isArray(options.hazardSources) ? options.hazardSources.filter(Boolean).map(String) : [];
1982
1982
  if (!shipSources.length || !hazardSources.length)
1983
1983
  throw new Error("DanceMovesEffects cooperativeArena requires ship and hazard sources");
1984
- var shipCount = clamp(Math.round(finite2(options.shipCount, 3)), 2, 12);
1985
- var hazardCount = clamp(Math.round(finite2(options.hazardCount, 8)), 3, 48);
1986
- var speed = clamp(finite2(options.speed, 1), 0.35, 2.4);
1984
+ var shipCount = clamp(Math.round(finite3(options.shipCount, 3)), 2, 12);
1985
+ var hazardCount = clamp(Math.round(finite3(options.hazardCount, 8)), 3, 48);
1986
+ var speed = clamp(finite3(options.speed, 1), 0.35, 2.4);
1987
1987
  var inlineTransform = options.inlineTransform === true;
1988
- var bpm = Math.max(1, finite2(options.bpm, motion.bpm || 120));
1989
- var shotIntervalTicks = clamp(Math.round(finite2(options.shotIntervalTicks, 32)), 4, 256);
1988
+ var bpm = Math.max(1, finite3(options.bpm, motion.bpm || 120));
1989
+ var shotIntervalTicks = clamp(Math.round(finite3(options.shotIntervalTicks, 32)), 4, 256);
1990
1990
  var shotIntervalSeconds = motion.durationMilliseconds ? motion.durationMilliseconds(shotIntervalTicks, bpm) / 1e3 : shotIntervalTicks * 60 / (bpm * 16);
1991
- var burstDurationSeconds = clamp(finite2(options.burstDurationBeats, 1.5), 0.1, 8) * 60 / bpm;
1992
- var shipHitboxScale = clamp(finite2(options.shipHitboxScale, 1), 0.4, 1);
1993
- var hazardHitboxScale = clamp(finite2(options.hazardHitboxScale, 1), 0.4, 1);
1991
+ var burstDurationSeconds = clamp(finite3(options.burstDurationBeats, 1.5), 0.1, 8) * 60 / bpm;
1992
+ var shipHitboxScale = clamp(finite3(options.shipHitboxScale, 1), 0.4, 1);
1993
+ var hazardHitboxScale = clamp(finite3(options.hazardHitboxScale, 1), 0.4, 1);
1994
1994
  var removers = state.removers, ships = [], hazards = [], shots = [], bursts = [], burstTimers = [];
1995
1995
  var activeShips = shipCount, activeHazards = hazardCount;
1996
1996
  var width = 1, height = 1, frame = 0, last = 0, elapsed = 0, nextShot = 0, hits = 0;
@@ -2322,7 +2322,7 @@ function install2(__dmScope) {
2322
2322
  reconcile(reason || "manual");
2323
2323
  },
2324
2324
  setDensity: function(nextShips, nextHazards) {
2325
- var shipsNext = clamp(Math.round(finite2(nextShips, activeShips)), 2, shipCount), hazardsNext = clamp(Math.round(finite2(nextHazards, activeHazards)), 3, hazardCount);
2325
+ var shipsNext = clamp(Math.round(finite3(nextShips, activeShips)), 2, shipCount), hazardsNext = clamp(Math.round(finite3(nextHazards, activeHazards)), 3, hazardCount);
2326
2326
  if (shipsNext === activeShips && hazardsNext === activeHazards)
2327
2327
  return;
2328
2328
  activeShips = shipsNext;
@@ -2365,12 +2365,12 @@ function install2(__dmScope) {
2365
2365
  options = options || {};
2366
2366
  var state = commonState(options, "quality");
2367
2367
  var tiers = Array.isArray(options.tiers) && options.tiers.length ? options.tiers.map(String) : ["full", "constrained", "minimal"];
2368
- var index = clamp(Math.round(finite2(options.initial, 0)), 0, tiers.length - 1);
2369
- var sampleMs = Math.max(800, finite2(options.sampleMilliseconds, 1600));
2370
- var poorWindows = Math.max(2, Math.round(finite2(options.poorWindows, 2)));
2371
- var healthyWindows = Math.max(3, Math.round(finite2(options.healthyWindows, 5)));
2372
- var downgradeRatio = clamp(finite2(options.downgradeRatio, 0.72), 0.2, 0.95);
2373
- var recoveryRatio = clamp(finite2(options.recoveryRatio, 0.9), downgradeRatio, 1);
2368
+ var index = clamp(Math.round(finite3(options.initial, 0)), 0, tiers.length - 1);
2369
+ var sampleMs = Math.max(800, finite3(options.sampleMilliseconds, 1600));
2370
+ var poorWindows = Math.max(2, Math.round(finite3(options.poorWindows, 2)));
2371
+ var healthyWindows = Math.max(3, Math.round(finite3(options.healthyWindows, 5)));
2372
+ var downgradeRatio = clamp(finite3(options.downgradeRatio, 0.72), 0.2, 0.95);
2373
+ var recoveryRatio = clamp(finite3(options.recoveryRatio, 0.9), downgradeRatio, 1);
2374
2374
  var frame = 0, frames = 0, start = 0, reference = 0, poor = 0, healthy = 0, lastShift = 0, running = true;
2375
2375
  var preferenceLimited = false, tierBeforePreference = index;
2376
2376
  function publish(reason, sample) {
@@ -2414,12 +2414,12 @@ function install2(__dmScope) {
2414
2414
  poor = 0;
2415
2415
  healthy = 0;
2416
2416
  }
2417
- if (poor >= poorWindows && index < tiers.length - 1 && now - lastShift >= finite2(options.downgradeCooldownMilliseconds, 5e3)) {
2417
+ if (poor >= poorWindows && index < tiers.length - 1 && now - lastShift >= finite3(options.downgradeCooldownMilliseconds, 5e3)) {
2418
2418
  index += 1;
2419
2419
  poor = 0;
2420
2420
  lastShift = now;
2421
2421
  publish("sustained-low-fps", { fps });
2422
- } else if (healthy >= healthyWindows && index > 0 && now - lastShift >= finite2(options.recoveryCooldownMilliseconds, 12e3)) {
2422
+ } else if (healthy >= healthyWindows && index > 0 && now - lastShift >= finite3(options.recoveryCooldownMilliseconds, 12e3)) {
2423
2423
  index -= 1;
2424
2424
  healthy = 0;
2425
2425
  lastShift = now;
@@ -2458,7 +2458,7 @@ function install2(__dmScope) {
2458
2458
  id: state.id,
2459
2459
  type: "quality",
2460
2460
  setTier: function(value, reason) {
2461
- index = clamp(Math.round(finite2(value, index)), 0, tiers.length - 1);
2461
+ index = clamp(Math.round(finite3(value, index)), 0, tiers.length - 1);
2462
2462
  publish(reason || "manual");
2463
2463
  },
2464
2464
  snapshot: function() {
@@ -6436,6 +6436,184 @@ function installPlayback(scope, api, host = scope.document.defaultView) {
6436
6436
  });
6437
6437
  }
6438
6438
 
6439
+ // .build/modules/recordings.mjs
6440
+ var WASM = "AGFzbQEAAAABDwJgAAF/YAZ/f39/f38BfAMDAgABBQUBAQKAQAYIAX8BQYCIBAsHKgMGbWVtb3J5AgAKdGFibGVfYmFzZQAAEHNhbXBsZV9yZWNvcmRpbmcAAQpvAggAQYCIhIAAC2QCAXwBfkQAAAAAAAD4fyEGAkAgAEUNACABQQFIDQAgAkEBSA0AIAVBAEgNACAFIAJPDQAgBKwgA6x9IgdCAFMNACAHIAGtWg0AIAAgB6cgAmxBA3RqIAVBA3RqKwMAIQYLIAYL";
6441
+ var MAX_VALUES = 8388608;
6442
+ var MAX_BYTES = 64 * 1024 * 1024;
6443
+ var modulePromise;
6444
+ var finite2 = (n, label) => {
6445
+ if (typeof n !== "number" || !Number.isFinite(n)) throw new TypeError(label + " must be finite");
6446
+ return n;
6447
+ };
6448
+ function validateRecording(input) {
6449
+ 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");
6450
+ if (typeof input.name !== "string" || !input.name.length || input.name.length > 128) throw new TypeError("Recording name is required");
6451
+ const dimensions = input.dimensions;
6452
+ 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");
6453
+ const c = input.clock;
6454
+ 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");
6455
+ finite2(c.beat_zero_seconds, "Beat zero");
6456
+ if (!Number.isInteger(c.start_pip) || c.start_pip < -2147483648) throw new RangeError("Invalid recording start pip");
6457
+ const rows = input.samples;
6458
+ 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");
6459
+ const values = new Float64Array(rows.length * dimensions.length);
6460
+ rows.forEach((row, i) => {
6461
+ if (!Array.isArray(row) || row.length !== dimensions.length) throw new TypeError("Recording sample dimensions differ");
6462
+ row.forEach((v, j) => {
6463
+ finite2(v, "Sample");
6464
+ if (Math.abs(v) > 1) throw new RangeError("Recording samples must be in [-1,1]");
6465
+ values[i * dimensions.length + j] = v;
6466
+ });
6467
+ });
6468
+ return Object.freeze({ name: input.name, dimensions: Object.freeze([...dimensions]), count: rows.length, clock: Object.freeze({ ...c }), values });
6469
+ }
6470
+ async function samplerFromData(data) {
6471
+ if (!modulePromise) modulePromise = WebAssembly.compile(Uint8Array.from(atob(WASM), (c) => c.charCodeAt(0))).catch((error) => {
6472
+ modulePromise = null;
6473
+ throw error;
6474
+ });
6475
+ const api = (await WebAssembly.instantiate(await modulePromise, {})).exports;
6476
+ const base = api.table_base(), pages = Math.ceil((base + data.values.byteLength - api.memory.buffer.byteLength) / 65536);
6477
+ if (pages > 0) api.memory.grow(pages);
6478
+ new Float64Array(api.memory.buffer, base, data.values.length).set(data.values);
6479
+ return Object.freeze({ ...data, sample(pip) {
6480
+ 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");
6481
+ return data.dimensions.map((_, axis) => api.sample_recording(base, data.count, data.dimensions.length, data.clock.start_pip, pip, axis));
6482
+ } });
6483
+ }
6484
+ function createRecordingService(scope, rudiments, config) {
6485
+ const handles = /* @__PURE__ */ new Map();
6486
+ async function load(url2) {
6487
+ if (scope.disposed) throw new Error("DanceMoves mount is destroyed");
6488
+ const response = await scope.window.fetch(url2);
6489
+ if (!response.ok) throw new Error("Recording request failed: " + response.status);
6490
+ if (Number(response.headers?.get("content-length")) > MAX_BYTES) throw new RangeError("Recording exceeds 64 MiB");
6491
+ let text;
6492
+ if (response.body?.getReader) {
6493
+ const reader = response.body.getReader(), chunks = [];
6494
+ let size = 0;
6495
+ for (; ; ) {
6496
+ const { done, value } = await reader.read();
6497
+ if (done) break;
6498
+ size += value.byteLength;
6499
+ if (size > MAX_BYTES) {
6500
+ await reader.cancel();
6501
+ throw new RangeError("Recording exceeds 64 MiB");
6502
+ }
6503
+ chunks.push(value);
6504
+ }
6505
+ const bytes = new Uint8Array(size);
6506
+ let offset = 0;
6507
+ for (const chunk of chunks) {
6508
+ bytes.set(chunk, offset);
6509
+ offset += chunk.length;
6510
+ }
6511
+ text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
6512
+ } else {
6513
+ text = await response.text();
6514
+ if (new TextEncoder().encode(text).length > MAX_BYTES) throw new RangeError("Recording exceeds 64 MiB");
6515
+ }
6516
+ const input = JSON.parse(text);
6517
+ validateRecording(input);
6518
+ return input;
6519
+ }
6520
+ function animate(options = {}) {
6521
+ if (scope.disposed) throw new Error("DanceMoves mount is destroyed");
6522
+ if (typeof options.id !== "string" || !options.id.trim() || handles.has(options.id)) throw new TypeError("Recording controller requires a unique id");
6523
+ if (typeof options.render !== "function") throw new TypeError("Recording render callback is required");
6524
+ if (!options.audio) throw new TypeError("Recording playback requires an audio/player source");
6525
+ if (["rate", "phasePips", "clock", "bpm"].some((k) => Object.hasOwn(options, k))) throw new TypeError("Recording clock, rate and origin come from its capture");
6526
+ const data = validateRecording(options.recording);
6527
+ if (Math.abs(data.clock.bpm - config.bpm) > 1e-9) throw new RangeError("Recording BPM does not match the authoritative page BPM");
6528
+ let inner = null, sampler = null, destroyed = false, enabled = options.enabled !== false, paused = false, status = "loading", error = null, last = null;
6529
+ const output = (frame, values, pip, reason) => {
6530
+ if (destroyed) return;
6531
+ 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 };
6532
+ options.render(last);
6533
+ };
6534
+ const handle = {
6535
+ id: options.id,
6536
+ ready: null,
6537
+ setEnabled(value) {
6538
+ if (typeof value !== "boolean") throw new TypeError("enabled must be boolean");
6539
+ enabled = value;
6540
+ inner?.setEnabled(value);
6541
+ },
6542
+ pause() {
6543
+ paused = true;
6544
+ inner?.pause();
6545
+ },
6546
+ resume() {
6547
+ paused = false;
6548
+ inner?.resume();
6549
+ },
6550
+ refresh() {
6551
+ inner?.refresh();
6552
+ },
6553
+ destroy() {
6554
+ if (destroyed) return;
6555
+ inner?.destroy();
6556
+ inner = null;
6557
+ sampler = null;
6558
+ destroyed = true;
6559
+ status = "destroyed";
6560
+ handles.delete(options.id);
6561
+ },
6562
+ snapshot() {
6563
+ const state = inner?.snapshot() ?? null;
6564
+ 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 };
6565
+ }
6566
+ };
6567
+ handle.teardown = handle.destroy;
6568
+ handles.set(options.id, handle);
6569
+ handle.ready = samplerFromData(data).then(async (native) => {
6570
+ if (destroyed || scope.disposed) return handle;
6571
+ sampler = native;
6572
+ inner = rudiments.animate({
6573
+ id: options.id,
6574
+ root: options.root,
6575
+ target: options.target,
6576
+ audio: options.audio,
6577
+ clock: "audio",
6578
+ rudiment: "circle",
6579
+ rate: 1,
6580
+ phasePips: 0,
6581
+ amplitude: 0,
6582
+ css: false,
6583
+ renderEveryFrame: true,
6584
+ offscreen: options.offscreen !== false,
6585
+ enabled,
6586
+ render(frame) {
6587
+ if (destroyed) return;
6588
+ const neutral = ["disabled", "reduced-motion", "forced-colors"].includes(frame.reason), precise = (frame.sourceBeats - data.clock.beat_zero_seconds * data.clock.bpm / 60) * 64;
6589
+ const pip = Math.floor(precise + 1e-8), outside = pip < data.clock.start_pip || pip >= data.clock.start_pip + data.count;
6590
+ status = neutral ? frame.reason : outside ? "outside-recording" : frame.reason;
6591
+ const values = neutral || outside ? data.dimensions.map(() => 0) : sampler.sample(pip);
6592
+ output(frame, values, pip, status);
6593
+ }
6594
+ });
6595
+ if (paused) inner.pause();
6596
+ await inner.ready;
6597
+ return handle;
6598
+ }).catch((failure) => {
6599
+ error = String(failure.message || failure);
6600
+ status = "error";
6601
+ inner?.destroy();
6602
+ inner = null;
6603
+ sampler = null;
6604
+ if (handles.get(options.id) === handle) handles.delete(options.id);
6605
+ throw failure;
6606
+ });
6607
+ return handle;
6608
+ }
6609
+ scope.onDispose(() => {
6610
+ for (const handle of [...handles.values()]) handle.destroy();
6611
+ });
6612
+ return Object.freeze({ load, animate, get: (id) => handles.get(id) || null, destroyAll: () => {
6613
+ for (const h of [...handles.values()]) h.destroy();
6614
+ }, snapshot: () => ({ native: "DanceRudiments 0.3.0 recording C++/WASM", instances: [...handles.values()].map((h) => h.snapshot()) }) });
6615
+ }
6616
+
6439
6617
  // .build/modules/child-playback.mjs
6440
6618
  var PROTOCOL = "dancemoves.child-playback.v1";
6441
6619
  var EVENTS = /* @__PURE__ */ new Set(["snapshot", "loadedmetadata", "durationchange", "play", "playing", "pause", "ended", "seeking", "seeked", "ratechange", "timeupdate", "waiting", "error"]);
@@ -6860,7 +7038,7 @@ function installChildPlayback(scope, api, host) {
6860
7038
  }
6861
7039
 
6862
7040
  // .build/modules/version.mjs
6863
- var VERSION = "3.1.23";
7041
+ var VERSION = "3.2.0";
6864
7042
 
6865
7043
  // .build/modules/index.mjs
6866
7044
  var mounted = /* @__PURE__ */ new WeakMap();
@@ -6913,6 +7091,7 @@ function createDanceMoves(options = {}) {
6913
7091
  config,
6914
7092
  effects: w.DanceMovesEffects,
6915
7093
  rudiments: w.DanceMovesRudiments,
7094
+ recordings: createRecordingService(scope, w.DanceMovesRudiments, config),
6916
7095
  orientationCore: w.KSEpkOrientationCore,
6917
7096
  getOrientation: () => w.__ksEpkOrientationRuntime || null,
6918
7097
  getClay: () => w.DanceMovesClayStars || null,
package/lib/index.d.ts CHANGED
@@ -92,7 +92,7 @@ export interface DanceMoves {
92
92
  resetRunningAnimations(audio?:PlaybackMedia):number;discoverAudio():void;registerPlaybackSource(source:PlaybackSource):void;unregisterPlaybackSource(source:PlaybackSource):void;
93
93
  setDiagnosticsSink(callback:((record:Record<string,unknown>)=>void)|null):boolean;diagnosticsEnabled():boolean;
94
94
  applyCatalogueTiming?():Record<string,unknown>;catalogueTimingSnapshot?():Record<string,unknown>;removeCatalogueAnimationScope?():void;
95
- effects:Effects;rudiments:import('./rudiments').RudimentApi;
95
+ effects:Effects;rudiments:import('./rudiments').RudimentApi;recordings:RecordingService;
96
96
  orientationCore:Record<string,(...args:any[])=>any>;
97
97
  getOrientation():(Controller & {reset():void})|null;getClay():(Controller & Record<string,any>)|null;getPaperPlanes():Controller|null;
98
98
  createOrientation(options:{render:(point:Offset & Record<string,unknown>)=>void;windowMilliseconds?:number;minimumSpanDegrees?:number;smoothingTimeConstantMilliseconds?:number}):OrientationController;
@@ -111,3 +111,7 @@ export function fromWordPressMeta(meta:Record<string,unknown>,resolveAttachment?
111
111
  export interface Revision {revision:number;config:PageConfig;savedAt:string}
112
112
  export interface PageStore {read(id:string):Revision|null;revisions(id:string):Revision[];save(id:string,input:PageConfig,options?:{expectedRevision?:number}):Revision;restore(id:string,revision:number,options?:{expectedRevision?:number}):Revision;delete(id:string):boolean}
113
113
  export function createMemoryPageStore(options?:{maxPages?:number;maxRevisions?:number}):PageStore;
114
+ export interface AudioRecording {format:'dancerudiments.recording';schema_version:1;pips_per_beat:64;sample_dtype:'float64';name:string;dimensions:string[];samples:number[][];clock:{bpm:number;bpm_source:string;bpm_status:'confirmed'|'estimated';beat_zero_seconds:number;phase_status:'unverified'|'estimated'|'supplied';start_pip:number};provenance?:Record<string,unknown>}
115
+ export interface RecordingFrame {recording:string;dimensions:readonly string[];values:number[];position:{x:number;y:number;z:number};pip:number;positionPips:number;sourceBeats:number;loopProgress:null;reason:string}
116
+ export interface RecordingController {id:string;ready:Promise<RecordingController>;setEnabled(value:boolean):void;pause():void;resume():void;refresh():void;destroy():void;teardown():void;snapshot():Record<string,unknown>}
117
+ export interface RecordingService {load(url:string):Promise<AudioRecording>;animate(options:{id:string;root:HTMLElement;target:HTMLElement;audio:PlaybackMedia;recording:AudioRecording;offscreen?:boolean;enabled?:boolean;render:(frame:RecordingFrame)=>void}):RecordingController;get(id:string):RecordingController|null;destroyAll():void;snapshot():Record<string,unknown>}