@braccato/core 1.15.1 → 1.15.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
@@ -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);
@@ -1957,7 +1960,11 @@ function stageElements(engine) {
1957
1960
  lastEnd = Math.max(lastEnd, end);
1958
1961
  elements.push(line.lyricElement);
1959
1962
  items.push({
1960
- kind: line.lyricElement.dataset.instrumental === "true" ? "instrumental" : "line",
1963
+ kind: line.lyricElement.dataset.instrumental === "true"
1964
+ ? "instrumental"
1965
+ : line.lyricElement.dataset.blank === "true"
1966
+ ? "blank"
1967
+ : "line",
1961
1968
  start: line.time,
1962
1969
  end,
1963
1970
  });
@@ -1992,12 +1999,18 @@ export function measureStage(engine) {
1992
1999
  });
1993
2000
  }
1994
2001
  engine.stageKey = "";
2002
+ engine.stageRemeasured = true;
1995
2003
  }
1996
2004
  function currentTranslateY(engine, element, fallback) {
1997
2005
  const parts = engine.window.getComputedStyle(element).translate.split(" ");
1998
2006
  const y = Number.parseFloat(parts[1] ?? "");
1999
2007
  return Number.isFinite(y) ? y : fallback;
2000
2008
  }
2009
+ function snapStageElement(engine, element, y) {
2010
+ engine.stageMoves.get(element)?.cancel();
2011
+ engine.stageMoves.set(element, element.animate([{ translate: `0 ${y}px` }], { duration: 0, fill: "forwards" }));
2012
+ engine.stageY.set(element, y);
2013
+ }
2001
2014
  function placeStageElement(engine, { element, placement, wasVisible, fromY, fromOpacity }, instant, reduced) {
2002
2015
  engine.stageMoves.get(element)?.cancel();
2003
2016
  engine.stageFades.get(element)?.cancel();
@@ -2075,12 +2088,21 @@ function applyStage(engine, timeS, instant) {
2075
2088
  if (key === engine.stageKey && !instant)
2076
2089
  return;
2077
2090
  engine.stageKey = key;
2091
+ const remeasured = engine.stageRemeasured;
2092
+ engine.stageRemeasured = false;
2078
2093
  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), {
2094
+ const geometry = {
2080
2095
  stageHeight: container.clientHeight,
2081
2096
  gap: engine.stageFontSize * STAGE_GAP_EM,
2082
2097
  activeScale: getCSSNumber(engine, container, "--blyrics-active-scale", 1),
2083
- });
2098
+ };
2099
+ // Lines sit on the floor, so a resize moves every place by how far the floor moved.
2100
+ const floorShift = remeasured && engine.stageHeight > 0 ? geometry.stageHeight - engine.stageHeight : 0;
2101
+ engine.stageHeight = geometry.stageHeight;
2102
+ const { placements, box } = layoutStage(roles, items, metrics, elements.map(element => {
2103
+ const y = engine.stageY.get(element);
2104
+ return y === undefined ? null : y + floorShift;
2105
+ }), geometry);
2084
2106
  // Only the elements whose place changed move. A line that stays where it is keeps its running
2085
2107
  // animations, and one already hidden stays hidden without being animated again.
2086
2108
  const moves = [];
@@ -2089,13 +2111,24 @@ function applyStage(engine, timeS, instant) {
2089
2111
  const previousY = engine.stageY.get(element);
2090
2112
  const wasVisible = element.dataset.stageRole !== undefined && isStageRoleVisible(element.dataset.stageRole);
2091
2113
  const unchanged = previousY !== undefined && wasVisible === placement.visible && (!placement.visible || previousY === placement.y);
2114
+ // After a resize a line still on screen, fading out included, jumps to its new place and keeps
2115
+ // the fade and blur it was in the middle of.
2116
+ const onScreen = placement.visible || element.dataset.stageVisible !== undefined;
2117
+ if (remeasured && !instant && onScreen && element.dataset.stageRole === roles[index]) {
2118
+ if (previousY !== placement.y)
2119
+ snapStageElement(engine, element, placement.y);
2120
+ return;
2121
+ }
2092
2122
  if (unchanged && !instant)
2093
2123
  return;
2094
2124
  moves.push({
2095
2125
  element,
2096
2126
  placement,
2097
2127
  wasVisible,
2098
- fromY: currentTranslateY(engine, element, previousY ?? placement.y),
2128
+ // A queued line rises from below the floor as it is now, never from where an older size put it.
2129
+ fromY: element.dataset.stageRole === "queued" && placement.visible
2130
+ ? queuedStageY(geometry.stageHeight, metrics[index].height, geometry.gap)
2131
+ : currentTranslateY(engine, element, previousY ?? placement.y),
2099
2132
  fromOpacity: Number(engine.window.getComputedStyle(element).opacity) || 0,
2100
2133
  });
2101
2134
  });
package/dist/stage.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export type StageItemKind = "line" | "instrumental" | "credits";
1
+ export type StageItemKind = "line" | "blank" | "instrumental" | "credits";
2
2
  export type StageRole = "current" | "previous" | "queued" | "gone";
3
3
  export interface StageItem {
4
4
  kind: StageItemKind;
@@ -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
@@ -3,7 +3,10 @@ export const STAGE_HANDOFF_S = 0.12;
3
3
  export const STAGE_OVERLAP_S = 0.15;
4
4
  export function overlapsPrevious(items, index) {
5
5
  const previous = items[index - 1];
6
- return previous !== undefined && previous.kind === "line" && items[index].start < previous.end - STAGE_OVERLAP_S;
6
+ return (previous !== undefined &&
7
+ previous.kind === "line" &&
8
+ items[index].kind !== "blank" &&
9
+ items[index].start < previous.end - STAGE_OVERLAP_S);
7
10
  }
8
11
  export function stageEnterTimes(items) {
9
12
  return items.map((item, index) => {
@@ -45,6 +48,10 @@ function unionBox(box, next) {
45
48
  height: Math.max(box.y + box.height, next.y + next.height) - y,
46
49
  };
47
50
  }
51
+ /** Where a line waits below the floor, and where it rises from as it enters. */
52
+ export function queuedStageY(stageHeight, height, gap) {
53
+ return stageHeight - height + gap;
54
+ }
48
55
  export function layoutStage(roles, items, metrics, previousY, geometry) {
49
56
  const { stageHeight, gap } = geometry;
50
57
  const current = roles.indexOf("current");
@@ -57,14 +64,15 @@ export function layoutStage(roles, items, metrics, previousY, geometry) {
57
64
  case "previous":
58
65
  return { y: currentY - gap - height, visible: true };
59
66
  case "queued":
60
- return { y: stageHeight - height + gap, visible: false };
67
+ return { y: queuedStageY(stageHeight, height, gap), visible: false };
61
68
  case "gone":
62
69
  return { y: previousY[index] ?? currentY, visible: false };
63
70
  }
64
71
  });
65
72
  let box = null;
66
73
  roles.forEach((role, index) => {
67
- if ((role !== "current" && role !== "previous") || items[index].kind === "instrumental")
74
+ const { kind } = items[index];
75
+ if ((role !== "current" && role !== "previous") || kind === "instrumental" || kind === "blank")
68
76
  return;
69
77
  const { height, left, width, originX } = metrics[index];
70
78
  const scale = geometry.activeScale;
package/dist/view.js CHANGED
@@ -95,6 +95,8 @@ export function setLyrics(engine, mount, lyrics, options) {
95
95
  }
96
96
  else {
97
97
  buildSungLine(doc, lyricElement, lyricItem, line);
98
+ if (!lyricItem.words?.trim())
99
+ lyricElement.dataset.blank = "true";
98
100
  }
99
101
  container.appendChild(lyricElement);
100
102
  }
@@ -108,7 +110,10 @@ export function setLyrics(engine, mount, lyrics, options) {
108
110
  container.dataset.sync = syncType;
109
111
  if (engine.layout === "stage") {
110
112
  container.dataset.layout = "stage";
111
- if (lyrics.some(item => item.agent === "v2" || item.agent === "v3")) {
113
+ // Sides only when both are sung, so a song with one singer stays centred whichever voice it is.
114
+ const sung = lyrics.filter(item => !item.isInstrumental && item.words?.trim() && item.agent !== "v1000");
115
+ const onRight = (item) => item.agent === "v2" || item.agent === "v3";
116
+ if (sung.some(onRight) && sung.some(item => !onRight(item))) {
112
117
  container.dataset.stageDuet = "";
113
118
  }
114
119
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@braccato/core",
3
- "version": "1.15.1",
3
+ "version": "1.15.3",
4
4
  "description": "Synchronized lyrics renderer with word-by-word animations",
5
5
  "type": "module",
6
6
  "license": "MIT",