@braccato/core 1.6.1 → 1.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/engine.d.ts CHANGED
@@ -41,8 +41,6 @@ export interface AnimationEngineInstance extends AnimEngineViewState {
41
41
  window: EngineWindow;
42
42
  host: LyricsRendererHost;
43
43
  cachedTabRendererHeight: number | null;
44
- cachedMaxScrollTop: number | null;
45
- cachedScrollTop: number | null;
46
44
  cachedFooterItem: LineScrollItem | null;
47
45
  cachedLineScrollTiming: Map<string, LineScrollTiming>;
48
46
  tabRendererResizeObserver: ResizeObserver | null;
package/dist/engine.js CHANGED
@@ -137,8 +137,6 @@ export function createAnimationEngineInstance(engineDocument, engineWindow, host
137
137
  passiveScrollAccumulatedTime: 0,
138
138
  passiveLastWallTime: 0,
139
139
  cachedTabRendererHeight: null,
140
- cachedMaxScrollTop: null,
141
- cachedScrollTop: null,
142
140
  cachedFooterItem: null,
143
141
  cachedLineScrollTiming: new Map(),
144
142
  tabRendererResizeObserver: null,
@@ -1444,6 +1442,8 @@ export function setupLineCullObserver(engine) {
1444
1442
  engine.lineCullObserver = null;
1445
1443
  return;
1446
1444
  }
1445
+ // Scroll anchoring would compensate residual cull height-shifts with a scroll the engine misreads as the user's (Firefox); the engine owns scrollTop, so disable it.
1446
+ engine.host.getScrollElement()?.style.setProperty("overflow-anchor", "none");
1447
1447
  // Pin each line's skipped placeholder to its last rendered size, so skipping a line never shifts the
1448
1448
  // container's scroll height, which the engine would misread as a user scroll.
1449
1449
  const restingHeights = engine.lines.map(line => line.lyricElement.offsetHeight);
@@ -1749,7 +1749,6 @@ function passiveScrollEngine(engine, isPlaying) {
1749
1749
  const prevScrollTop = tabRenderer.scrollTop;
1750
1750
  tabRenderer.scrollTop = targetScroll;
1751
1751
  const appliedScrollTop = tabRenderer.scrollTop;
1752
- engine.cachedScrollTop = appliedScrollTop;
1753
1752
  // Only skip the next scroll event if scrollTop actually changed.
1754
1753
  // When it doesn't change (pause phases, sub-pixel rounding), no programmatic
1755
1754
  // scroll event fires, so setting skipScrolls would eat user scroll events instead.
@@ -1769,19 +1768,11 @@ function setupTabRendererObserver(engine, element) {
1769
1768
  dropPendingLineScroll(engine);
1770
1769
  if (element && element.isConnected) {
1771
1770
  engine.cachedTabRendererHeight = element.getBoundingClientRect().height;
1772
- refreshScrollMetrics(engine, element);
1773
1771
  }
1774
1772
  });
1775
1773
  engine.tabRendererResizeObserver.observe(element);
1776
1774
  engine.observedTabRenderer = element;
1777
1775
  engine.cachedTabRendererHeight = element.getBoundingClientRect().height;
1778
- refreshScrollMetrics(engine, element);
1779
- }
1780
- // Scroll position and bounds are cached here (and refreshed where layout is measured on purpose) so
1781
- // the tick never reads them off the DOM, which would force a synchronous layout flush mid-frame.
1782
- function refreshScrollMetrics(engine, tabRenderer) {
1783
- engine.cachedMaxScrollTop = Math.max(0, tabRenderer.scrollHeight - tabRenderer.clientHeight);
1784
- engine.cachedScrollTop = tabRenderer.scrollTop;
1785
1776
  }
1786
1777
  /**
1787
1778
  * Fills in everything a caller left out of a tick. The tick reads each of these arithmetically, so
@@ -1883,17 +1874,9 @@ export function tickView(engine, currentTime, options) {
1883
1874
  setupTabRendererObserver(engine, tabRenderer);
1884
1875
  }
1885
1876
  const tabRendererHeight = engine.cachedTabRendererHeight ?? tabRenderer.getBoundingClientRect().height;
1886
- // Read the DOM position only while the user scrolls (they own it then); otherwise reuse the cached
1887
- // value the engine last wrote, so the tick never forces a mid-frame layout flush.
1888
- let scrollTop;
1889
- if (engine.scrollResumeTime >= now || engine.cachedScrollTop === null) {
1890
- scrollTop = tabRenderer.scrollTop;
1891
- engine.cachedScrollTop = scrollTop;
1892
- }
1893
- else {
1894
- scrollTop = engine.cachedScrollTop;
1895
- }
1896
- const maxScrollTop = engine.cachedMaxScrollTop ?? Math.max(0, tabRenderer.scrollHeight - tabRenderer.clientHeight);
1877
+ // Read before the loop's class writes so this layout read is batched and forces no mid-frame reflow.
1878
+ let scrollTop = tabRenderer.scrollTop;
1879
+ const maxScrollTop = Math.max(0, tabRenderer.scrollHeight - tabRenderer.clientHeight);
1897
1880
  if (animationConfig.enabled.scroll) {
1898
1881
  updateVisibleLyricWillChange(engine, lines, scrollTop, engine.pendingLineScroll?.toScrollTop ?? scrollTop, tabRendererHeight);
1899
1882
  }
@@ -2152,7 +2135,6 @@ export function tickView(engine, currentTime, options) {
2152
2135
  scrollTop = scrollPos;
2153
2136
  engine.scrollPos = scrollTop;
2154
2137
  tabRenderer.scrollTop = scrollTop;
2155
- engine.cachedScrollTop = scrollTop;
2156
2138
  engine.skipScrolls += 1;
2157
2139
  engine.skipScrollsDecayTimes.push(Date.now() + 2000);
2158
2140
  }
@@ -2269,9 +2251,6 @@ export function relayout(engine, measureLines) {
2269
2251
  measureFooterItem(engine, lyricsElement);
2270
2252
  // Re-arm from the fresh measurements, so a line skipped after this holds its new placeholder height.
2271
2253
  setupLineCullObserver(engine);
2272
- const tabRenderer = engine.host.getScrollElement();
2273
- if (tabRenderer)
2274
- refreshScrollMetrics(engine, tabRenderer);
2275
2254
  engine.wasUserScrolling = true; // trigger rescrolls
2276
2255
  engine.host.debug?.resize();
2277
2256
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@braccato/core",
3
- "version": "1.6.1",
3
+ "version": "1.6.3",
4
4
  "description": "Synchronized lyrics renderer with word-by-word animations",
5
5
  "type": "module",
6
6
  "license": "MIT",