@braccato/core 1.16.12 → 1.16.14

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.
Files changed (2) hide show
  1. package/dist/engine.js +33 -10
  2. package/package.json +1 -1
package/dist/engine.js CHANGED
@@ -122,7 +122,7 @@ export function createAnimationEngineInstance(engineDocument, engineWindow, host
122
122
  skipScrolls: 0,
123
123
  skipScrollsDecayTimes: [],
124
124
  scrollResumeTime: 0,
125
- scrollPos: 0,
125
+ scrollPos: -1,
126
126
  selectedElementIndex: 0,
127
127
  wasUserScrolling: false,
128
128
  lastScrollElements: [],
@@ -1679,7 +1679,18 @@ function measureTrailingItem(lyricsElement, className) {
1679
1679
  const bounds = getRelativeLayoutBounds(lyricsElement, element);
1680
1680
  return { lyricElement: element, position: bounds.y, height: bounds.height };
1681
1681
  }
1682
- function prepareLineScrollOffsets(engine, lines, activeLineIndex, scrollDeltaPx, fromScrollTop, toScrollTop, viewportHeight, config) {
1682
+ // A leading line slower than the one behind closes their gap by distance times progress difference.
1683
+ function lineScrollStaggerScale(lines, activeLineIndex, scrollDistancePx, naturalAdvance) {
1684
+ if (naturalAdvance)
1685
+ return 1;
1686
+ const active = lines[activeLineIndex];
1687
+ const steps = [lines[activeLineIndex - 1], lines[activeLineIndex + 1]]
1688
+ .filter(neighbour => neighbour !== undefined)
1689
+ .map(neighbour => Math.abs(active.position - neighbour.position));
1690
+ const stepPx = steps.length > 0 ? Math.min(...steps) : active.height;
1691
+ return Math.min(1, stepPx / scrollDistancePx);
1692
+ }
1693
+ function prepareLineScrollOffsets(engine, lines, activeLineIndex, scrollDeltaPx, fromScrollTop, toScrollTop, viewportHeight, config, naturalAdvance) {
1683
1694
  if (!config.enabled.scroll || activeLineIndex < 0) {
1684
1695
  return null;
1685
1696
  }
@@ -1742,12 +1753,16 @@ function prepareLineScrollOffsets(engine, lines, activeLineIndex, scrollDeltaPx,
1742
1753
  startTranslates = batchResolveLineScrollProperty(engine, items, "translate", item => lineScrollTranslate(item.side, "start", config.lineScroll.differentialEffects), style => normalizedTranslate(style.translate));
1743
1754
  endTranslates = batchResolveLineScrollProperty(engine, items, "translate", item => lineScrollTranslate(item.side, "end", config.lineScroll.differentialEffects), style => normalizedTranslate(style.translate));
1744
1755
  }
1756
+ const staggerScale = lineScrollStaggerScale(lines, activeLineIndex, scrollDistancePx, naturalAdvance);
1757
+ const activeDurationMs = requests.find(request => request.relativeIndex === 0)?.timing?.durationMs;
1745
1758
  return {
1746
1759
  items: requests.map((request, requestIndex) => {
1747
1760
  const timing = request.timing;
1748
1761
  return {
1749
1762
  ...request.item,
1750
- durationMs: timing.durationMs,
1763
+ durationMs: activeDurationMs === undefined
1764
+ ? timing.durationMs
1765
+ : activeDurationMs + (timing.durationMs - activeDurationMs) * staggerScale,
1751
1766
  startEasing: timing.startEasing,
1752
1767
  endEasing: timing.endEasing,
1753
1768
  startTranslate: startTranslates ? startTranslates[requestIndex] : defaultStartTranslate,
@@ -1789,7 +1804,7 @@ function pendingLineScrollMatches(engine, activeLine, fromScrollTop, toScrollTop
1789
1804
  Math.abs(engine.pendingLineScroll.fromScrollTop - fromScrollTop) <= 2 &&
1790
1805
  Math.abs(engine.pendingLineScroll.toScrollTop - toScrollTop) <= 2);
1791
1806
  }
1792
- function commitOrPrepareLineScroll(engine, lines, activeLine, scrollDeltaPx, fromScrollTop, toScrollTop, viewportHeight, config) {
1807
+ function commitOrPrepareLineScroll(engine, lines, activeLine, scrollDeltaPx, fromScrollTop, toScrollTop, viewportHeight, config, naturalAdvance) {
1793
1808
  if (pendingLineScrollMatches(engine, activeLine, fromScrollTop, toScrollTop)) {
1794
1809
  const pending = engine.pendingLineScroll;
1795
1810
  engine.pendingLineScroll = null;
@@ -1797,16 +1812,16 @@ function commitOrPrepareLineScroll(engine, lines, activeLine, scrollDeltaPx, fro
1797
1812
  return;
1798
1813
  }
1799
1814
  dropPendingLineScroll(engine);
1800
- const plan = prepareLineScrollOffsets(engine, lines, lines.findIndex(line => line.lyricElement === activeLine.lyricElement), scrollDeltaPx, fromScrollTop, toScrollTop, viewportHeight, config);
1815
+ const plan = prepareLineScrollOffsets(engine, lines, lines.findIndex(line => line.lyricElement === activeLine.lyricElement), scrollDeltaPx, fromScrollTop, toScrollTop, viewportHeight, config, naturalAdvance);
1801
1816
  if (plan)
1802
1817
  startPreparedLineScroll(engine, plan);
1803
1818
  }
1804
- function prepareUpcomingLineScroll(engine, lines, activeLine, scrollDeltaPx, fromScrollTop, toScrollTop, viewportHeight, config) {
1819
+ function prepareUpcomingLineScroll(engine, lines, activeLine, scrollDeltaPx, fromScrollTop, toScrollTop, viewportHeight, config, naturalAdvance) {
1805
1820
  if (pendingLineScrollMatches(engine, activeLine, fromScrollTop, toScrollTop))
1806
1821
  return;
1807
1822
  dropPendingLineScroll(engine);
1808
1823
  const activeLineIndex = lines.findIndex(line => line.lyricElement === activeLine.lyricElement);
1809
- const plan = prepareLineScrollOffsets(engine, lines, activeLineIndex, scrollDeltaPx, fromScrollTop, toScrollTop, viewportHeight, config);
1824
+ const plan = prepareLineScrollOffsets(engine, lines, activeLineIndex, scrollDeltaPx, fromScrollTop, toScrollTop, viewportHeight, config, naturalAdvance);
1810
1825
  if (plan) {
1811
1826
  engine.pendingLineScroll = {
1812
1827
  plan,
@@ -2332,6 +2347,12 @@ export function tickView(engine, currentTime, options) {
2332
2347
  }
2333
2348
  const timeJumped = Math.abs(currentTime - playbackClock.lastTime - (eventCreationTime - playbackClock.lastEventCreationTime) / 1000) >
2334
2349
  TIME_JUMP_THRESHOLD;
2350
+ // Without a player snapshot the reading is the media clock, which runs backwards only on a seek, so a
2351
+ // loop shorter than the jump threshold still re-arms lines. A snapshot host's extrapolated clock is
2352
+ // pulled back by every correction, so it is left to the threshold.
2353
+ const mediaSteppedBack = eventCreationTime === NO_PLAYER_SNAPSHOT &&
2354
+ isPlaying &&
2355
+ currentTime < playbackClock.lastTime - CLOCK_RESYNC_THRESHOLD_S;
2335
2356
  if (timeJumped)
2336
2357
  dropPendingLineScroll(engine);
2337
2358
  playbackClock.lastTime = currentTime;
@@ -2444,7 +2465,7 @@ export function tickView(engine, currentTime, options) {
2444
2465
  lineData.isAnimating = false; // reset the animation against current media time
2445
2466
  }
2446
2467
  // Lead-in animations have no timing sample for the drift check, so a jump while playing must reset them.
2447
- if (timeJumped && isPlaying && lineData.isAnimating)
2468
+ if (((timeJumped && isPlaying) || mediaSteppedBack) && lineData.isAnimating)
2448
2469
  resetLineAnimationState(lineData);
2449
2470
  updateWordStates(lineData, currentTime);
2450
2471
  const nativeTimingSample = lineNativeTimingSample(lineData, currentTime);
@@ -2634,6 +2655,8 @@ export function tickView(engine, currentTime, options) {
2634
2655
  }
2635
2656
  }
2636
2657
  const timeUntilUpcomingScrollMs = (lastActiveLyric.time - lyricScrollTime) * 1000;
2658
+ // Only a scroll from where the engine itself left the view keeps the theme's full stagger.
2659
+ const naturalAdvance = !timeJumped && !engine.wasUserScrolling && engine.scrollPos >= 0 && Math.abs(scrollTop - engine.scrollPos) <= 2;
2637
2660
  if (smoothScroll &&
2638
2661
  animationConfig.enabled.scroll &&
2639
2662
  !newLyricSelected &&
@@ -2643,7 +2666,7 @@ export function tickView(engine, currentTime, options) {
2643
2666
  !engine.lastScrollElements.includes(lastActiveLyric) &&
2644
2667
  Math.abs(scrollTop - scrollPos) > 2) {
2645
2668
  updateVisibleLyricWillChange(engine, lines, scrollTop, scrollPos, tabRendererHeight);
2646
- prepareUpcomingLineScroll(engine, getLineScrollItems(engine, lines), lastActiveLyric, scrollPos - scrollTop, scrollTop, scrollPos, tabRendererHeight, animationConfig);
2669
+ prepareUpcomingLineScroll(engine, getLineScrollItems(engine, lines), lastActiveLyric, scrollPos - scrollTop, scrollTop, scrollPos, tabRendererHeight, animationConfig, naturalAdvance);
2647
2670
  }
2648
2671
  if (engine.wasUserScrolling || newLyricSelected) {
2649
2672
  // Remember future lines only when their group is committed, so they cannot scroll
@@ -2658,7 +2681,7 @@ export function tickView(engine, currentTime, options) {
2658
2681
  if (animationConfig.enabled.scroll) {
2659
2682
  updateVisibleLyricWillChange(engine, lines, scrollTop, scrollPos, tabRendererHeight);
2660
2683
  const lineScrollItems = getLineScrollItems(engine, lines);
2661
- commitOrPrepareLineScroll(engine, lineScrollItems, lastActiveLyric, scrollDeltaPx, scrollTop, scrollPos, tabRendererHeight, animationConfig);
2684
+ commitOrPrepareLineScroll(engine, lineScrollItems, lastActiveLyric, scrollDeltaPx, scrollTop, scrollPos, tabRendererHeight, animationConfig, naturalAdvance);
2662
2685
  }
2663
2686
  }
2664
2687
  else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@braccato/core",
3
- "version": "1.16.12",
3
+ "version": "1.16.14",
4
4
  "description": "Synchronized lyrics renderer with word-by-word animations",
5
5
  "type": "module",
6
6
  "license": "MIT",