@braccato/core 1.16.0 → 1.16.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.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, queuedStageY, } from "./stage.js";
21
+ import { layoutStage, planStage, queuedStageY, stageTextSpan, } 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";
@@ -1976,26 +1976,63 @@ function stageElements(engine) {
1976
1976
  }
1977
1977
  return { elements, items };
1978
1978
  }
1979
- function originXOf(engine, element) {
1980
- const align = engine.window.getComputedStyle(element).textAlign;
1979
+ function originXOf(align) {
1981
1980
  if (align === "center")
1982
1981
  return 0.5;
1983
1982
  if (align === "right" || align === "end")
1984
1983
  return 1;
1985
1984
  return 0;
1986
1985
  }
1986
+ const ELEMENT_NODE = 1;
1987
+ const TEXT_NODE = 3;
1988
+ function contentWidthOf(element, style) {
1989
+ return (element.clientWidth - (Number.parseFloat(style.paddingLeft) || 0) - (Number.parseFloat(style.paddingRight) || 0));
1990
+ }
1991
+ // A block that fills its parent is looked inside; inline boxes and shrunk blocks (the romanization pill) count whole.
1992
+ function collectContentRects(engine, range, node, contentWidth, rects) {
1993
+ for (const child of node.childNodes) {
1994
+ if (child.nodeType === TEXT_NODE) {
1995
+ range.selectNodeContents(child);
1996
+ rects.push(...range.getClientRects());
1997
+ continue;
1998
+ }
1999
+ if (child.nodeType !== ELEMENT_NODE)
2000
+ continue;
2001
+ const element = child;
2002
+ const style = engine.window.getComputedStyle(element);
2003
+ if (style.display === "none")
2004
+ continue;
2005
+ if (style.display === "contents") {
2006
+ collectContentRects(engine, range, element, contentWidth, rects);
2007
+ }
2008
+ else if (style.display.startsWith("inline")) {
2009
+ rects.push(...element.getClientRects());
2010
+ }
2011
+ else if (element.offsetWidth < contentWidth - 1) {
2012
+ rects.push(element.getBoundingClientRect());
2013
+ }
2014
+ else {
2015
+ collectContentRects(engine, range, element, contentWidthOf(element, style), rects);
2016
+ }
2017
+ }
2018
+ return rects;
2019
+ }
1987
2020
  export function measureStage(engine) {
1988
2021
  engine.stageMetrics.clear();
1989
2022
  const container = engine.lyricsContainer;
1990
2023
  if (!container)
1991
2024
  return;
1992
2025
  engine.stageFontSize = Number.parseFloat(engine.window.getComputedStyle(container).fontSize) || 16;
2026
+ const range = engine.document.createRange();
1993
2027
  for (const element of stageElements(engine).elements) {
2028
+ const style = engine.window.getComputedStyle(element);
2029
+ const originX = originXOf(style.textAlign);
2030
+ const span = stageTextSpan(element.offsetLeft, element.offsetWidth, element.getBoundingClientRect(), collectContentRects(engine, range, element, contentWidthOf(element, style), []), { left: Number.parseFloat(style.paddingLeft) || 0, right: Number.parseFloat(style.paddingRight) || 0 }, originX);
1994
2031
  engine.stageMetrics.set(element, {
1995
2032
  height: element.offsetHeight,
1996
- left: element.offsetLeft,
1997
- width: element.offsetWidth,
1998
- originX: originXOf(engine, element),
2033
+ left: span.left,
2034
+ width: span.width,
2035
+ originX,
1999
2036
  });
2000
2037
  }
2001
2038
  engine.stageKey = "";
package/dist/stage.d.ts CHANGED
@@ -35,6 +35,21 @@ export interface StageBox {
35
35
  width: number;
36
36
  height: number;
37
37
  }
38
+ export interface StageSpanRect {
39
+ x: number;
40
+ width: number;
41
+ }
42
+ /**
43
+ * A wrapped `fit-content` box fills the width offered, so the plate follows the text instead. The rects
44
+ * miss printed `::before` labels, so the span is widened to what the alignment guarantees.
45
+ */
46
+ export declare function stageTextSpan(offsetLeft: number, offsetWidth: number, box: StageSpanRect, textRects: Iterable<StageSpanRect>, padding: {
47
+ left: number;
48
+ right: number;
49
+ }, originX: number): {
50
+ left: number;
51
+ width: number;
52
+ };
38
53
  /** Where a line waits below the floor, and where it rises from as it enters. */
39
54
  export declare function queuedStageY(stageHeight: number, height: number, gap: number): number;
40
55
  export declare function layoutStage(roles: readonly StageRole[], items: readonly StageItem[], metrics: readonly StageMetrics[], previousY: readonly (number | null)[], geometry: StageGeometry): {
package/dist/stage.js CHANGED
@@ -36,6 +36,40 @@ export function planStage(items, timeS) {
36
36
  }
37
37
  return roles;
38
38
  }
39
+ /**
40
+ * A wrapped `fit-content` box fills the width offered, so the plate follows the text instead. The rects
41
+ * miss printed `::before` labels, so the span is widened to what the alignment guarantees.
42
+ */
43
+ export function stageTextSpan(offsetLeft, offsetWidth, box, textRects, padding, originX) {
44
+ const whole = { left: offsetLeft, width: offsetWidth };
45
+ if (box.width <= 0 || offsetWidth <= 0 || originX === 1)
46
+ return whole;
47
+ const scale = box.width / offsetWidth;
48
+ let start = Number.POSITIVE_INFINITY;
49
+ let end = Number.NEGATIVE_INFINITY;
50
+ for (const rect of textRects) {
51
+ if (rect.width <= 0)
52
+ continue;
53
+ start = Math.min(start, (rect.x - box.x) / scale);
54
+ end = Math.max(end, (rect.x + rect.width - box.x) / scale);
55
+ }
56
+ if (start > end)
57
+ return whole;
58
+ const contentLeft = padding.left;
59
+ const contentRight = offsetWidth - padding.right;
60
+ if (originX === 0) {
61
+ start = contentLeft;
62
+ }
63
+ else {
64
+ const centre = (contentLeft + contentRight) / 2;
65
+ const half = Math.max(end - centre, centre - start);
66
+ start = centre - half;
67
+ end = centre + half;
68
+ }
69
+ const left = Math.max(0, start - padding.left);
70
+ const right = Math.min(offsetWidth, end + padding.right);
71
+ return { left: offsetLeft + left, width: right - left };
72
+ }
39
73
  function unionBox(box, next) {
40
74
  if (box === null)
41
75
  return next;
@@ -40,6 +40,12 @@
40
40
  transform-origin: center center !important;
41
41
  }
42
42
 
43
+ /* The romanization pill is a fit-content block, so text-align cannot place it. */
44
+ .blyrics-container[data-layout="stage"] > .blyrics--line > .blyrics--romanized {
45
+ margin-right: auto;
46
+ margin-left: auto;
47
+ }
48
+
43
49
  /* A duet puts each singer on their own side, mirrored for right-to-left lines; everyone together
44
50
  stays centred. */
45
51
  .blyrics-container[data-layout="stage"][data-stage-duet] > .blyrics--line:not([data-agent="v2"], [data-agent="v3"], [data-agent="v1000"]):not([data-direction="rtl"]),
@@ -49,9 +55,19 @@
49
55
  transform-origin: left center !important;
50
56
  }
51
57
 
58
+ .blyrics-container[data-layout="stage"][data-stage-duet] > .blyrics--line:not([data-agent="v2"], [data-agent="v3"], [data-agent="v1000"]):not([data-direction="rtl"]) > .blyrics--romanized,
59
+ .blyrics-container[data-layout="stage"][data-stage-duet] > .blyrics--line:is([data-agent="v2"], [data-agent="v3"])[data-direction="rtl"] > .blyrics--romanized {
60
+ margin-left: 0;
61
+ }
62
+
52
63
  .blyrics-container[data-layout="stage"][data-stage-duet] > .blyrics--line:is([data-agent="v2"], [data-agent="v3"]):not([data-direction="rtl"]),
53
64
  .blyrics-container[data-layout="stage"][data-stage-duet] > .blyrics--line:not([data-agent="v2"], [data-agent="v3"], [data-agent="v1000"])[data-direction="rtl"] {
54
65
  margin: 0 0 0 auto !important;
55
66
  text-align: right;
56
67
  transform-origin: right center !important;
57
68
  }
69
+
70
+ .blyrics-container[data-layout="stage"][data-stage-duet] > .blyrics--line:is([data-agent="v2"], [data-agent="v3"]):not([data-direction="rtl"]) > .blyrics--romanized,
71
+ .blyrics-container[data-layout="stage"][data-stage-duet] > .blyrics--line:not([data-agent="v2"], [data-agent="v3"], [data-agent="v1000"])[data-direction="rtl"] > .blyrics--romanized {
72
+ margin-right: 0;
73
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@braccato/core",
3
- "version": "1.16.0",
3
+ "version": "1.16.2",
4
4
  "description": "Synchronized lyrics renderer with word-by-word animations",
5
5
  "type": "module",
6
6
  "license": "MIT",