@braccato/core 1.15.1 → 1.15.2

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
@@ -72,6 +72,8 @@ export interface AnimationEngineInstance extends AnimEngineViewState {
72
72
  animationTimingVisibilityLogUntil: number;
73
73
  layout: LyricsLayout;
74
74
  stageKey: string;
75
+ stageRemeasured: boolean;
76
+ stageHeight: number;
75
77
  stageBox: StageBox | null;
76
78
  stageMetrics: Map<HTMLElement, StageMetrics>;
77
79
  stageY: Map<HTMLElement, number>;
package/dist/engine.js CHANGED
@@ -18,7 +18,7 @@
18
18
  // that never meet. `themeSettings.ts` holds the third thing under that rule.
19
19
  import { ANIMATING_CLASS, CREDITS_CLASS, CURRENT_LYRICS_CLASS, FOOTER_CLASS, LINE_CLASS, PAUSED_CLASS, ROMANIZED_LYRICS_CLASS, RTL_CLASS, TRANSLATED_LYRICS_CLASS, USER_SCROLLING_CLASS, WORD_STATE_ACTIVE, WORD_STATE_ATTR, WORD_STATE_PAST, WORD_STATE_UPCOMING, } from "./constants.js";
20
20
  import { INSTRUMENTAL_WAVE_PATH_HIGH, INSTRUMENTAL_WAVE_PATH_LOW } from "./instrumental.js";
21
- import { layoutStage, planStage } from "./stage.js";
21
+ import { layoutStage, planStage, queuedStageY, } from "./stage.js";
22
22
  import { registerThemeSetting } from "./themeSettings.js";
23
23
  import { clamp, getRelativeLayoutBounds, positiveModulo, roundedMs, toMs } from "./util.js";
24
24
  const NO_LYRICS_ELEMENT_LOG = "No lyrics element found on the page, skipping lyrics injection";
@@ -156,6 +156,8 @@ export function createAnimationEngineInstance(engineDocument, engineWindow, host
156
156
  animationTimingVisibilityLogUntil: 0,
157
157
  layout,
158
158
  stageKey: "",
159
+ stageRemeasured: false,
160
+ stageHeight: 0,
159
161
  stageBox: null,
160
162
  stageMetrics: new Map(),
161
163
  stageY: new Map(),
@@ -316,6 +318,7 @@ function resetStage(engine) {
316
318
  engine.stageMetrics.clear();
317
319
  engine.stageY.clear();
318
320
  engine.stageKey = "";
321
+ engine.stageHeight = 0;
319
322
  if (engine.stageBox !== null) {
320
323
  engine.stageBox = null;
321
324
  engine.host.onStageLayout?.(null);
@@ -1992,12 +1995,18 @@ export function measureStage(engine) {
1992
1995
  });
1993
1996
  }
1994
1997
  engine.stageKey = "";
1998
+ engine.stageRemeasured = true;
1995
1999
  }
1996
2000
  function currentTranslateY(engine, element, fallback) {
1997
2001
  const parts = engine.window.getComputedStyle(element).translate.split(" ");
1998
2002
  const y = Number.parseFloat(parts[1] ?? "");
1999
2003
  return Number.isFinite(y) ? y : fallback;
2000
2004
  }
2005
+ function snapStageElement(engine, element, y) {
2006
+ engine.stageMoves.get(element)?.cancel();
2007
+ engine.stageMoves.set(element, element.animate([{ translate: `0 ${y}px` }], { duration: 0, fill: "forwards" }));
2008
+ engine.stageY.set(element, y);
2009
+ }
2001
2010
  function placeStageElement(engine, { element, placement, wasVisible, fromY, fromOpacity }, instant, reduced) {
2002
2011
  engine.stageMoves.get(element)?.cancel();
2003
2012
  engine.stageFades.get(element)?.cancel();
@@ -2075,12 +2084,21 @@ function applyStage(engine, timeS, instant) {
2075
2084
  if (key === engine.stageKey && !instant)
2076
2085
  return;
2077
2086
  engine.stageKey = key;
2087
+ const remeasured = engine.stageRemeasured;
2088
+ engine.stageRemeasured = false;
2078
2089
  const metrics = elements.map(element => engine.stageMetrics.get(element) ?? { height: 0, left: 0, width: 0, originX: 0 });
2079
- const { placements, box } = layoutStage(roles, items, metrics, elements.map(element => engine.stageY.get(element) ?? null), {
2090
+ const geometry = {
2080
2091
  stageHeight: container.clientHeight,
2081
2092
  gap: engine.stageFontSize * STAGE_GAP_EM,
2082
2093
  activeScale: getCSSNumber(engine, container, "--blyrics-active-scale", 1),
2083
- });
2094
+ };
2095
+ // Lines sit on the floor, so a resize moves every place by how far the floor moved.
2096
+ const floorShift = remeasured && engine.stageHeight > 0 ? geometry.stageHeight - engine.stageHeight : 0;
2097
+ engine.stageHeight = geometry.stageHeight;
2098
+ const { placements, box } = layoutStage(roles, items, metrics, elements.map(element => {
2099
+ const y = engine.stageY.get(element);
2100
+ return y === undefined ? null : y + floorShift;
2101
+ }), geometry);
2084
2102
  // Only the elements whose place changed move. A line that stays where it is keeps its running
2085
2103
  // animations, and one already hidden stays hidden without being animated again.
2086
2104
  const moves = [];
@@ -2089,13 +2107,24 @@ function applyStage(engine, timeS, instant) {
2089
2107
  const previousY = engine.stageY.get(element);
2090
2108
  const wasVisible = element.dataset.stageRole !== undefined && isStageRoleVisible(element.dataset.stageRole);
2091
2109
  const unchanged = previousY !== undefined && wasVisible === placement.visible && (!placement.visible || previousY === placement.y);
2110
+ // After a resize a line still on screen, fading out included, jumps to its new place and keeps
2111
+ // the fade and blur it was in the middle of.
2112
+ const onScreen = placement.visible || element.dataset.stageVisible !== undefined;
2113
+ if (remeasured && !instant && onScreen && element.dataset.stageRole === roles[index]) {
2114
+ if (previousY !== placement.y)
2115
+ snapStageElement(engine, element, placement.y);
2116
+ return;
2117
+ }
2092
2118
  if (unchanged && !instant)
2093
2119
  return;
2094
2120
  moves.push({
2095
2121
  element,
2096
2122
  placement,
2097
2123
  wasVisible,
2098
- fromY: currentTranslateY(engine, element, previousY ?? placement.y),
2124
+ // A queued line rises from below the floor as it is now, never from where an older size put it.
2125
+ fromY: element.dataset.stageRole === "queued" && placement.visible
2126
+ ? queuedStageY(geometry.stageHeight, metrics[index].height, geometry.gap)
2127
+ : currentTranslateY(engine, element, previousY ?? placement.y),
2099
2128
  fromOpacity: Number(engine.window.getComputedStyle(element).opacity) || 0,
2100
2129
  });
2101
2130
  });
package/dist/stage.d.ts CHANGED
@@ -35,6 +35,8 @@ export interface StageBox {
35
35
  width: number;
36
36
  height: number;
37
37
  }
38
+ /** Where a line waits below the floor, and where it rises from as it enters. */
39
+ export declare function queuedStageY(stageHeight: number, height: number, gap: number): number;
38
40
  export declare function layoutStage(roles: readonly StageRole[], items: readonly StageItem[], metrics: readonly StageMetrics[], previousY: readonly (number | null)[], geometry: StageGeometry): {
39
41
  placements: StagePlacement[];
40
42
  box: StageBox | null;
package/dist/stage.js CHANGED
@@ -45,6 +45,10 @@ function unionBox(box, next) {
45
45
  height: Math.max(box.y + box.height, next.y + next.height) - y,
46
46
  };
47
47
  }
48
+ /** Where a line waits below the floor, and where it rises from as it enters. */
49
+ export function queuedStageY(stageHeight, height, gap) {
50
+ return stageHeight - height + gap;
51
+ }
48
52
  export function layoutStage(roles, items, metrics, previousY, geometry) {
49
53
  const { stageHeight, gap } = geometry;
50
54
  const current = roles.indexOf("current");
@@ -57,7 +61,7 @@ export function layoutStage(roles, items, metrics, previousY, geometry) {
57
61
  case "previous":
58
62
  return { y: currentY - gap - height, visible: true };
59
63
  case "queued":
60
- return { y: stageHeight - height + gap, visible: false };
64
+ return { y: queuedStageY(stageHeight, height, gap), visible: false };
61
65
  case "gone":
62
66
  return { y: previousY[index] ?? currentY, visible: false };
63
67
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@braccato/core",
3
- "version": "1.15.1",
3
+ "version": "1.15.2",
4
4
  "description": "Synchronized lyrics renderer with word-by-word animations",
5
5
  "type": "module",
6
6
  "license": "MIT",