@kieransimkin/dancemoves 3.0.2 → 3.0.4

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
@@ -536,6 +536,17 @@ function install(__dmScope) {
536
536
  };
537
537
  }
538
538
  function dispatchLyric(entry, audio, index) {
539
+ var previousVisibleIndex = -1;
540
+ var previousVisibleEntry = null;
541
+ if (Number.isFinite(index)) {
542
+ for (var previousCursor = index - 1; previousCursor >= 0; previousCursor -= 1) {
543
+ if (String(lyricList[previousCursor].text || "").trim()) {
544
+ previousVisibleIndex = previousCursor;
545
+ previousVisibleEntry = lyricList[previousCursor];
546
+ break;
547
+ }
548
+ }
549
+ }
539
550
  var nextIndex = Number.isFinite(index) && index + 1 < lyricList.length ? index + 1 : -1;
540
551
  var nextEntry = nextIndex >= 0 ? lyricList[nextIndex] : null;
541
552
  var nextVisibleIndex = -1;
@@ -555,6 +566,10 @@ function install(__dmScope) {
555
566
  text: entry ? entry.text : "",
556
567
  normalisedText: entry ? entry.normalisedText : "",
557
568
  index: Number.isFinite(index) ? index : -1,
569
+ previousVisibleTime: previousVisibleEntry ? previousVisibleEntry.time : null,
570
+ previousVisibleText: previousVisibleEntry ? previousVisibleEntry.text : "",
571
+ previousVisibleNormalisedText: previousVisibleEntry ? previousVisibleEntry.normalisedText : "",
572
+ previousVisibleIndex,
558
573
  nextTime: nextEntry ? nextEntry.time : null,
559
574
  nextText: nextEntry ? nextEntry.text : "",
560
575
  nextNormalisedText: nextEntry ? nextEntry.normalisedText : "",
@@ -628,6 +643,7 @@ function install(__dmScope) {
628
643
  __dmScope.dataset(this.element, "danceMovesLyricState", "idle");
629
644
  }
630
645
  };
646
+ document2.dispatchEvent(new CustomEvent("dance-moves-lyric-ready", { detail: { element: container } }));
631
647
  return lyricRenderer;
632
648
  }
633
649
  function callHandlers(key, detail) {
@@ -1376,6 +1392,317 @@ function install2(__dmScope) {
1376
1392
  }
1377
1393
  }));
1378
1394
  }
1395
+ function lyricStage(options) {
1396
+ options = options || {};
1397
+ var state = commonState(options, "lyric-stage");
1398
+ var audio = element(options.audio, state.root.querySelector("audio"));
1399
+ var popover = element(options.popover, document2.querySelector(".dance-moves-lyric-popover"));
1400
+ if (!audio)
1401
+ throw new Error("DanceMovesEffects lyricStage requires an audio element");
1402
+ if (!popover) {
1403
+ let mountWhenReady = function() {
1404
+ if (!pending || !document2.querySelector(".dance-moves-lyric-popover"))
1405
+ return;
1406
+ pending = false;
1407
+ state.removers.splice(0).forEach(function(remove) {
1408
+ remove();
1409
+ });
1410
+ lyricStage(options);
1411
+ };
1412
+ var pending = true;
1413
+ listen(document2, "dance-moves-lyric-ready", mountWhenReady, void 0, state.removers);
1414
+ return register(state.id, Object.freeze({
1415
+ id: state.id,
1416
+ type: "lyric-stage-pending",
1417
+ snapshot: function() {
1418
+ return { id: state.id, type: "lyric-stage-pending", phase: "waiting-for-popover" };
1419
+ },
1420
+ teardown: function() {
1421
+ pending = false;
1422
+ state.removers.splice(0).forEach(function(remove) {
1423
+ remove();
1424
+ });
1425
+ instances.delete(state.id);
1426
+ }
1427
+ }));
1428
+ }
1429
+ var current = popover.querySelector(".dance-moves-lyric-popover__text");
1430
+ if (!current)
1431
+ throw new Error("DanceMovesEffects lyricStage requires the DanceMoves current lyric element");
1432
+ var viewport = document2.createElement("div");
1433
+ var track = document2.createElement("div");
1434
+ var previous = document2.createElement("span");
1435
+ var next = document2.createElement("span");
1436
+ viewport.className = "dance-moves-lyric-stage__viewport";
1437
+ track.className = "dance-moves-lyric-stage__track";
1438
+ previous.className = "dance-moves-lyric-stage__line dance-moves-lyric-stage__line--previous";
1439
+ current.className += " dance-moves-lyric-stage__line dance-moves-lyric-stage__line--current";
1440
+ next.className = "dance-moves-lyric-stage__line dance-moves-lyric-stage__line--next";
1441
+ __dmScope.dataset(previous, "danceMovesLyricSlot", "previous");
1442
+ __dmScope.dataset(current, "danceMovesLyricSlot", "current");
1443
+ __dmScope.dataset(next, "danceMovesLyricSlot", "next");
1444
+ track.appendChild(previous);
1445
+ track.appendChild(current);
1446
+ track.appendChild(next);
1447
+ viewport.appendChild(track);
1448
+ popover.appendChild(viewport);
1449
+ __dmScope.dataset(popover, "danceMovesLyricStage", "ready");
1450
+ __dmScope.setStyle(popover.style, "--dance-moves-lyric-progress", "0");
1451
+ var travelTicks = Math.max(1, Math.round(finite2(options.travelTicks, 32)));
1452
+ var defaultCueTicks = Math.max(1, Math.round(finite2(options.cueDurationTicks, 32)));
1453
+ var latest = null, phase = "idle", progress = 0, frame = 0, wakeTimer = 0, cueTimer = 0, cueCleanup = null, cueCount = 0;
1454
+ var lyricEffects = [];
1455
+ var slots = Object.freeze({ previous, current, next, viewport, track });
1456
+ function safely(callback, payload, label) {
1457
+ if (typeof callback !== "function")
1458
+ return null;
1459
+ try {
1460
+ return callback(payload);
1461
+ } catch (error) {
1462
+ if (window2.console && typeof window2.console.error === "function")
1463
+ window2.console.error("DanceMoves " + label + " callback failed", error);
1464
+ }
1465
+ return null;
1466
+ }
1467
+ function blocked() {
1468
+ return document2.hidden || state.reduced.matches || state.forced.matches;
1469
+ }
1470
+ function stopClock() {
1471
+ if (frame)
1472
+ window2.cancelAnimationFrame(frame);
1473
+ if (wakeTimer)
1474
+ window2.clearTimeout(wakeTimer);
1475
+ frame = 0;
1476
+ wakeTimer = 0;
1477
+ }
1478
+ function publish(reason) {
1479
+ __dmScope.dataset(popover, "danceMovesLyricPhase", phase);
1480
+ safely(options.render, {
1481
+ root: state.root,
1482
+ popover,
1483
+ audio,
1484
+ slots,
1485
+ detail: latest,
1486
+ phase,
1487
+ progress,
1488
+ reason,
1489
+ playing: !audio.paused && !audio.ended && !blocked(),
1490
+ reducedMotion: state.reduced.matches,
1491
+ forcedColours: state.forced.matches
1492
+ }, "lyricStage render");
1493
+ }
1494
+ function setPhase(value, reason) {
1495
+ if (phase === value)
1496
+ return;
1497
+ phase = value;
1498
+ publish(reason || value);
1499
+ }
1500
+ function setProgress(value) {
1501
+ progress = clamp(finite2(value, 0), 0, 1);
1502
+ __dmScope.setStyle(popover.style, "--dance-moves-lyric-progress", progress.toFixed(5));
1503
+ }
1504
+ function copyLyrics(detail) {
1505
+ previous.textContent = detail ? String(detail.previousVisibleText || "") : "";
1506
+ current.textContent = detail ? String(detail.text || "") : "";
1507
+ next.textContent = detail ? String(detail.nextVisibleText || "") : "";
1508
+ __dmScope.dataset(previous, "danceMovesLyricIndex", detail ? String(detail.previousVisibleIndex) : "-1");
1509
+ __dmScope.dataset(current, "danceMovesLyricIndex", detail ? String(detail.index) : "-1");
1510
+ __dmScope.dataset(next, "danceMovesLyricIndex", detail ? String(detail.nextVisibleIndex) : "-1");
1511
+ }
1512
+ function scheduleLyricEffect(result) {
1513
+ if (!result)
1514
+ return;
1515
+ var cleanup = typeof result === "function" ? result : typeof result.cleanup === "function" ? result.cleanup : null;
1516
+ if (!cleanup)
1517
+ return;
1518
+ var durationTicks = result && typeof result === "object" ? Math.max(1, Math.round(finite2(result.durationTicks, 64))) : 64;
1519
+ var record = { cleanup, timer: 0 };
1520
+ record.timer = window2.setTimeout(function() {
1521
+ lyricEffects = lyricEffects.filter(function(item) {
1522
+ return item !== record;
1523
+ });
1524
+ safely(cleanup, { reason: "complete" }, "lyricStage lyric cleanup");
1525
+ }, motion.durationMilliseconds(durationTicks));
1526
+ lyricEffects.push(record);
1527
+ }
1528
+ function travelWindow() {
1529
+ if (!latest || !Number.isFinite(Number(latest.nextVisibleTime)))
1530
+ return null;
1531
+ var end = Number(latest.nextVisibleTime);
1532
+ var duration = motion.durationMilliseconds(travelTicks) / 1e3;
1533
+ return { start: Math.max(Number(latest.time) || 0, end - duration), end };
1534
+ }
1535
+ function tick() {
1536
+ frame = 0;
1537
+ if (audio.paused || audio.ended || blocked())
1538
+ return reconcile("frame-inactive");
1539
+ var windowRange = travelWindow();
1540
+ if (!windowRange) {
1541
+ setProgress(0);
1542
+ return setPhase("waiting", "no-next-line");
1543
+ }
1544
+ var now = Math.max(0, finite2(audio.currentTime, 0));
1545
+ if (now < windowRange.start)
1546
+ return reconcile("frame-before-window");
1547
+ if (now >= windowRange.end) {
1548
+ setProgress(1);
1549
+ return setPhase("arrived", "line-arrival");
1550
+ }
1551
+ setPhase("travelling", "travel-start");
1552
+ setProgress((now - windowRange.start) / Math.max(1e-3, windowRange.end - windowRange.start));
1553
+ frame = window2.requestAnimationFrame(tick);
1554
+ }
1555
+ function reconcile(reason) {
1556
+ stopClock();
1557
+ if (blocked()) {
1558
+ setProgress(0);
1559
+ return setPhase("inactive", reason || "inactive");
1560
+ }
1561
+ if (audio.ended) {
1562
+ setProgress(0);
1563
+ return setPhase("ended", reason || "ended");
1564
+ }
1565
+ if (audio.paused) {
1566
+ setProgress(0);
1567
+ return setPhase("paused", reason || "paused");
1568
+ }
1569
+ var windowRange = travelWindow();
1570
+ if (!windowRange) {
1571
+ setProgress(0);
1572
+ return setPhase("waiting", reason || "no-next-line");
1573
+ }
1574
+ var now = Math.max(0, finite2(audio.currentTime, 0));
1575
+ if (now >= windowRange.end) {
1576
+ setProgress(1);
1577
+ return setPhase("arrived", reason || "arrived");
1578
+ }
1579
+ if (now >= windowRange.start) {
1580
+ setPhase("travelling", reason || "travelling");
1581
+ frame = window2.requestAnimationFrame(tick);
1582
+ return;
1583
+ }
1584
+ setProgress(0);
1585
+ setPhase("waiting", reason || "waiting");
1586
+ wakeTimer = window2.setTimeout(function() {
1587
+ wakeTimer = 0;
1588
+ tick();
1589
+ }, Math.max(0, (windowRange.start - now) * 1e3 / Math.max(0.01, finite2(audio.playbackRate, 1))));
1590
+ }
1591
+ function onLyric(detail) {
1592
+ var previousDetail = latest;
1593
+ var previousText = String(current.textContent || "");
1594
+ latest = detail || null;
1595
+ copyLyrics(latest);
1596
+ if (!blocked() && typeof options.renderLyric === "function") {
1597
+ scheduleLyricEffect(safely(options.renderLyric, {
1598
+ root: state.root,
1599
+ popover,
1600
+ audio,
1601
+ slots,
1602
+ detail: latest,
1603
+ previousDetail,
1604
+ previousText,
1605
+ reducedMotion: false,
1606
+ forcedColours: false
1607
+ }, "lyricStage lyric"));
1608
+ }
1609
+ setProgress(0);
1610
+ publish("lyric-change");
1611
+ reconcile("lyric-change");
1612
+ }
1613
+ function clearCue(reason) {
1614
+ if (cueTimer)
1615
+ window2.clearTimeout(cueTimer);
1616
+ cueTimer = 0;
1617
+ if (cueCleanup)
1618
+ safely(cueCleanup, { reason: reason || "complete" }, "lyricStage cue cleanup");
1619
+ cueCleanup = null;
1620
+ __dmScope.dataset(state.root, "danceMovesLyricCue", reason || "idle");
1621
+ }
1622
+ function onCue(detail) {
1623
+ clearCue("restart");
1624
+ if (blocked() || typeof options.renderCue !== "function")
1625
+ return;
1626
+ cueCount += 1;
1627
+ __dmScope.dataset(state.root, "danceMovesLyricCue", String(detail.normalisedName || detail.name || "cue"));
1628
+ var result = safely(options.renderCue, {
1629
+ root: state.root,
1630
+ popover,
1631
+ audio,
1632
+ slots,
1633
+ detail,
1634
+ count: cueCount,
1635
+ reducedMotion: false,
1636
+ forcedColours: false
1637
+ }, "lyricStage cue");
1638
+ var durationTicks = defaultCueTicks;
1639
+ if (typeof result === "function")
1640
+ cueCleanup = result;
1641
+ else if (result && typeof result === "object") {
1642
+ if (typeof result.cleanup === "function")
1643
+ cueCleanup = result.cleanup;
1644
+ durationTicks = Math.max(1, Math.round(finite2(result.durationTicks, defaultCueTicks)));
1645
+ }
1646
+ cueTimer = window2.setTimeout(function() {
1647
+ clearCue("complete");
1648
+ }, motion.durationMilliseconds(durationTicks));
1649
+ }
1650
+ var unsubscribeLyric = motion.onLyric(onLyric, { id: state.id });
1651
+ var unsubscribeCue = motion.onCue("*", onCue, { id: state.id + ":cue" });
1652
+ ["play", "playing", "pause", "ended", "seeking", "seeked", "ratechange", "timeupdate", "loadedmetadata"].forEach(function(name) {
1653
+ listen(audio, name, function() {
1654
+ reconcile(name);
1655
+ }, void 0, state.removers);
1656
+ });
1657
+ listen(document2, "visibilitychange", function() {
1658
+ reconcile(document2.hidden ? "hidden" : "visible");
1659
+ }, { passive: true }, state.removers);
1660
+ listen(window2, "resize", function() {
1661
+ publish("resize");
1662
+ }, { passive: true }, state.removers);
1663
+ listen(state.reduced, "change", function() {
1664
+ reconcile(state.reduced.matches ? "reduced-motion" : "motion-restored");
1665
+ }, void 0, state.removers);
1666
+ listen(state.forced, "change", function() {
1667
+ reconcile(state.forced.matches ? "forced-colours" : "colours-restored");
1668
+ }, void 0, state.removers);
1669
+ copyLyrics(null);
1670
+ publish("initial");
1671
+ reconcile("initial");
1672
+ return register(state.id, Object.freeze({
1673
+ id: state.id,
1674
+ type: "lyric-stage",
1675
+ restore: function(reason) {
1676
+ reconcile(reason || "manual-restore");
1677
+ },
1678
+ snapshot: function() {
1679
+ return { id: state.id, type: "lyric-stage", phase, progress, playing: !audio.paused && !audio.ended && !blocked(), lyricIndex: latest ? latest.index : -1, nextVisibleIndex: latest ? latest.nextVisibleIndex : -1, cueCount };
1680
+ },
1681
+ teardown: function() {
1682
+ stopClock();
1683
+ clearCue("teardown");
1684
+ unsubscribeLyric();
1685
+ unsubscribeCue();
1686
+ lyricEffects.splice(0).forEach(function(record) {
1687
+ if (record.timer)
1688
+ window2.clearTimeout(record.timer);
1689
+ safely(record.cleanup, { reason: "teardown" }, "lyricStage lyric cleanup");
1690
+ });
1691
+ state.removers.splice(0).forEach(function(remove) {
1692
+ remove();
1693
+ });
1694
+ current.className = current.className.replace(/\s*dance-moves-lyric-stage__line(?:--current)?/g, "").trim();
1695
+ __dmScope.dataset(current, "danceMovesLyricSlot", null);
1696
+ popover.appendChild(current);
1697
+ if (viewport.parentNode === popover)
1698
+ popover.removeChild(viewport);
1699
+ __dmScope.dataset(popover, "danceMovesLyricStage", null);
1700
+ __dmScope.dataset(popover, "danceMovesLyricPhase", null);
1701
+ __dmScope.removeStyle(popover.style, "--dance-moves-lyric-progress");
1702
+ instances.delete(state.id);
1703
+ }
1704
+ }));
1705
+ }
1379
1706
  function quality(options) {
1380
1707
  options = options || {};
1381
1708
  var state = commonState(options, "quality");
@@ -1490,6 +1817,7 @@ function install2(__dmScope) {
1490
1817
  playbackPulse,
1491
1818
  cueClass,
1492
1819
  cueTimeline,
1820
+ lyricStage,
1493
1821
  quality,
1494
1822
  get: function(id) {
1495
1823
  return instances.get(String(id)) || null;
@@ -4720,7 +5048,7 @@ function createMotionRecorder(scope, { durationMilliseconds = 3e4, maxSamples =
4720
5048
  }
4721
5049
 
4722
5050
  // .build/modules/version.mjs
4723
- var VERSION = "3.0.2";
5051
+ var VERSION = "3.0.4";
4724
5052
 
4725
5053
  // .build/modules/index.mjs
4726
5054
  var mounted = /* @__PURE__ */ new WeakMap();
package/lib/index.d.ts CHANGED
@@ -14,19 +14,24 @@ export interface Options extends PageConfig {
14
14
  }
15
15
  export type OrientationAdapter = 'light-will-win'|'dying-for-a-diagnosis'|'presents-and-chocolate'|'fully-nocturnal'|'amnesty-honestly'|'walk-with-me'|'dmitri-my-talisman'|'clay-stars'|'california-screamin';
16
16
  export interface Cue {time:number;type:string;name:string;label?:string;normalisedType?:string;normalisedName?:string;normalisedLabel?:string;audio?:HTMLAudioElement|null}
17
- export interface Lyric {time:number;text:string;normalisedText?:string;audio?:HTMLAudioElement|null;nextTime?:number|null;nextText?:string;nextVisibleTime?:number|null;nextVisibleText?:string;[field:string]:unknown}
17
+ export interface Lyric {time:number;text:string;normalisedText?:string;audio?:HTMLAudioElement|null;previousVisibleTime?:number|null;previousVisibleText?:string;previousVisibleNormalisedText?:string;previousVisibleIndex?:number;nextTime?:number|null;nextText?:string;nextVisibleTime?:number|null;nextVisibleText?:string;nextVisibleNormalisedText?:string;nextVisibleIndex?:number;[field:string]:unknown}
18
18
  export interface Clock {clock?:'page'|'audio';audio?:HTMLAudioElement;bpm?:number;strictlyFuture?:boolean;handlerId?:string}
19
19
  export interface IntervalDetail {intervalTicks:number;boundaryTick:number;audio:HTMLAudioElement|null;clock:'page'|'audio'}
20
20
  export interface Controller {id?:string;teardown():void;snapshot():Record<string,unknown>}
21
21
  export interface PointerController extends Controller {set(x:number,y:number,reason?:string):void;reset(reason?:string):void}
22
22
  export interface QualityController extends Controller {setTier(index:number,reason?:string):void}
23
23
  export interface CueTimelineController extends Controller {restore(reason?:string):void;start(reason?:string):void;stop(reason?:string):void}
24
+ export interface LyricStageController extends Controller {restore(reason?:string):void}
25
+ export interface LyricStageSlots {previous:HTMLElement;current:HTMLElement;next:HTMLElement;viewport:HTMLElement;track:HTMLElement}
26
+ export interface LyricStageState {root:Element;popover:HTMLElement;audio:HTMLAudioElement;slots:LyricStageSlots;detail?:Lyric|null;phase?:string;progress?:number;reason?:string;playing?:boolean;reducedMotion:boolean;forcedColours:boolean;[field:string]:unknown}
27
+ export type LyricStageCleanup = void | (()=>void) | {durationTicks?:number;cleanup?:()=>void};
24
28
  export interface Effects {
25
29
  version:string;
26
30
  pointer(options:EffectOptions & {target?:Element|string;bounds?:Element|string;render?:(point:{x:number;y:number;reason?:string})=>void}):PointerController;
27
31
  playbackPulse(options:EffectOptions & {audio?:HTMLAudioElement|string;ticks?:number;className?:string;propertyPrefix?:string;render?:(state:Record<string,unknown> & {audio:HTMLAudioElement})=>void}):Controller & {sync():void};
28
32
  cueClass(options:EffectOptions & {cue?:string;className?:string;durationTicks?:number;render?:(state:Record<string,unknown>)=>void}):Controller & {fire(cue?:Cue):void;clear(reason?:string):void};
29
33
  cueTimeline(options:EffectOptions & {audio?:HTMLAudioElement|string;cues:Array<{id?:string;time:number;end?:number;[key:string]:unknown}>;onCue?:(event:Record<string,unknown>)=>void;render?:(state:Record<string,unknown>)=>void}):CueTimelineController;
34
+ lyricStage(options:EffectOptions & {audio?:HTMLAudioElement|string;popover?:HTMLElement|string;travelTicks?:number;cueDurationTicks?:number;render?:(state:LyricStageState)=>void;renderLyric?:(state:LyricStageState & {previousDetail?:Lyric|null;previousText?:string})=>LyricStageCleanup;renderCue?:(state:LyricStageState & {detail:Cue;count:number})=>LyricStageCleanup}):LyricStageController|Controller;
30
35
  quality(options:EffectOptions & {tiers?:string[];render?:(state:{tier:string;index:number;reason:string;sample?:{fps:number}})=>void;[key:string]:unknown}):QualityController;
31
36
  get(id:string):Controller|null;snapshot():Record<string,unknown>[];teardown(id:string):void;teardownAll():void;
32
37
  }