@braccato/core 0.1.7 → 1.0.0
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/LICENSE +1 -1
- package/README.md +267 -32
- package/dist/LICENSE +21 -0
- package/dist/README.md +298 -0
- package/dist/constants.d.ts +25 -0
- package/dist/constants.js +42 -0
- package/dist/element.d.ts +155 -0
- package/dist/element.js +655 -0
- package/dist/engine.d.ts +309 -0
- package/dist/engine.js +1940 -0
- package/dist/index.d.ts +12 -128
- package/dist/index.js +11 -949
- package/dist/inject.d.ts +37 -0
- package/dist/inject.js +376 -0
- package/dist/instrumental.d.ts +10 -0
- package/dist/instrumental.js +78 -0
- package/dist/renderer.d.ts +29 -0
- package/dist/renderer.js +349 -0
- package/dist/seek.d.ts +1 -0
- package/dist/seek.js +27 -0
- package/dist/styles/instrumental.css +103 -0
- package/dist/styles/lyrics.css +274 -0
- package/dist/styles/variables.css +181 -0
- package/dist/text.d.ts +11 -0
- package/dist/text.js +54 -0
- package/dist/themeSettings.d.ts +23 -0
- package/dist/themeSettings.js +126 -0
- package/dist/types.d.ts +199 -0
- package/dist/types.js +1 -0
- package/dist/util.d.ts +21 -0
- package/dist/util.js +64 -0
- package/dist/view.d.ts +22 -0
- package/dist/view.js +93 -0
- package/package.json +34 -21
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import type { Lyric, LyricPart, LyricSyncType } from "@braccato/types";
|
|
2
|
+
import type { AnimationTickStatus } from "./engine.js";
|
|
3
|
+
import type { LineData } from "./inject.js";
|
|
4
|
+
import type { SetLyricsOptions } from "./view.js";
|
|
5
|
+
export type { Lyric, LyricPart, LyricSyncType };
|
|
6
|
+
/**
|
|
7
|
+
* The play state is the only thing a tick cannot be given a sensible default for. Everything else
|
|
8
|
+
* describes a setting the consumer may not have, so it may be left out.
|
|
9
|
+
*/
|
|
10
|
+
export interface TickOptions {
|
|
11
|
+
/**
|
|
12
|
+
* Wall clock timestamp of the player snapshot the time came from, in milliseconds.
|
|
13
|
+
* Defaults to -1, which reads as a time that was not sampled from a live player.
|
|
14
|
+
*/
|
|
15
|
+
eventCreationTime?: number;
|
|
16
|
+
isPlaying: boolean;
|
|
17
|
+
/** Defaults to true. */
|
|
18
|
+
smoothScroll?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* User offsets, in seconds, subtracted from the playback time before matching. Each defaults to 0.
|
|
21
|
+
*/
|
|
22
|
+
globalLyricOffset?: number;
|
|
23
|
+
lyricOffset?: number;
|
|
24
|
+
richsyncOffsetTrim?: number;
|
|
25
|
+
lineOffsetTrim?: number;
|
|
26
|
+
/**
|
|
27
|
+
* Whether the user has passive scrolling switched on. Only unsynced lyrics use it. Defaults to
|
|
28
|
+
* false.
|
|
29
|
+
*/
|
|
30
|
+
passiveScrollEnabled?: boolean;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* A tick with nothing left out. The engine reads every field unconditionally, so the defaults are
|
|
34
|
+
* filled in at the edge rather than guessed at each read.
|
|
35
|
+
*/
|
|
36
|
+
export type ResolvedTickOptions = Required<TickOptions>;
|
|
37
|
+
export interface LyricsRendererDebugSink {
|
|
38
|
+
/**
|
|
39
|
+
* Clears the debug surface and returns a context translated to the given scroll offset.
|
|
40
|
+
*/
|
|
41
|
+
beginFrame(scrollOffset: number): CanvasRenderingContext2D | null;
|
|
42
|
+
resize(): void;
|
|
43
|
+
}
|
|
44
|
+
export interface LyricsRendererHost {
|
|
45
|
+
isViewVisible(): boolean;
|
|
46
|
+
isLoaderActive(): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Reports whether an ad is playing, and lets the host move whatever it shows in place of lyrics
|
|
49
|
+
* into the matching state. One call rather than a query and a command, because the renderer never
|
|
50
|
+
* needs one without the other and the host already knows the answer when it is asked.
|
|
51
|
+
*/
|
|
52
|
+
syncAdState(): boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Resolved per tick rather than handed over once: YouTube Music swaps its scroll container out,
|
|
55
|
+
* and the renderer is constructed before that container exists.
|
|
56
|
+
*/
|
|
57
|
+
getScrollElement(): HTMLElement | null;
|
|
58
|
+
/**
|
|
59
|
+
* Shows or puts away whatever the host offers for resuming autoscroll. The host decides what it
|
|
60
|
+
* looks like and where it lives; the renderer only says whether it is wanted.
|
|
61
|
+
*/
|
|
62
|
+
setResumeAffordanceVisible(visible: boolean): void;
|
|
63
|
+
/**
|
|
64
|
+
* Called when a lyric line is clicked. How the seek reaches the player is the host's business:
|
|
65
|
+
* this extension dispatches an event at the page world, another consumer might set
|
|
66
|
+
* currentTime on a media element directly.
|
|
67
|
+
*/
|
|
68
|
+
seek(timeS: number): void;
|
|
69
|
+
/**
|
|
70
|
+
* Where the view's diagnostics go. The host owns the prefix and whether logging is on at all.
|
|
71
|
+
*/
|
|
72
|
+
log(...args: unknown[]): void;
|
|
73
|
+
debug?: LyricsRendererDebugSink;
|
|
74
|
+
}
|
|
75
|
+
export interface LyricsRendererOptions {
|
|
76
|
+
document: Document;
|
|
77
|
+
window: Window;
|
|
78
|
+
/**
|
|
79
|
+
* Where the lyrics are built. `setLyrics` takes one too, for a consumer whose mount only exists
|
|
80
|
+
* once there is something to put in it; the last one given is the one in use.
|
|
81
|
+
*/
|
|
82
|
+
mount?: HTMLElement;
|
|
83
|
+
/**
|
|
84
|
+
* Every member has a default, so a consumer with nothing to say about its surroundings says
|
|
85
|
+
* nothing at all.
|
|
86
|
+
*/
|
|
87
|
+
host?: Partial<LyricsRendererHost>;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* One rendered lyrics view, and everything it takes to keep it measured. The consumer owns the
|
|
91
|
+
* clock and the lyrics; the renderer owns the DOM it builds from them and every re-measurement that
|
|
92
|
+
* DOM needs.
|
|
93
|
+
*/
|
|
94
|
+
export interface LyricsRenderer {
|
|
95
|
+
/**
|
|
96
|
+
* Replaces whatever this renderer last built. Every option has a default, so a consumer with an
|
|
97
|
+
* opinion about only one of them says only that one. The mount rides along with the CSS flags
|
|
98
|
+
* rather than taking a parameter of its own: it is an option of this call like the others, and a
|
|
99
|
+
* third parameter would have every caller that only wants to move the mount pass over the two in
|
|
100
|
+
* front of it.
|
|
101
|
+
*/
|
|
102
|
+
setLyrics(lyrics: Lyric[], options?: Partial<SetLyricsOptions> & {
|
|
103
|
+
mount?: HTMLElement;
|
|
104
|
+
}): void;
|
|
105
|
+
/**
|
|
106
|
+
* Applies a compiled theme. The `blyrics-*` configuration in its comments becomes the module's
|
|
107
|
+
* theme settings, and the stylesheet itself is applied to the document this renderer builds in.
|
|
108
|
+
*
|
|
109
|
+
* The settings are the module's rather than this instance's, so a second renderer in the same
|
|
110
|
+
* realm renders against the last theme any of them was given. The stylesheet is per document.
|
|
111
|
+
*
|
|
112
|
+
* @returns Whether a setting the lines are built out of changed, so the caller can hand the
|
|
113
|
+
* lyrics back. The renderer does not hold them, so it cannot rebuild them itself.
|
|
114
|
+
*/
|
|
115
|
+
setTheme(css: string): boolean;
|
|
116
|
+
/**
|
|
117
|
+
* Renders the view at a playback time, in seconds. The module owns no clock, so this is the whole
|
|
118
|
+
* of how the song reaches it, and the answer says whether there was a view here to render.
|
|
119
|
+
*/
|
|
120
|
+
tick(currentTimeS: number, options: TickOptions): AnimationTickStatus;
|
|
121
|
+
/**
|
|
122
|
+
* Re-reads the layout and renders nothing, leaving the lines it just re-read to the next tick.
|
|
123
|
+
*
|
|
124
|
+
* @param measureLines - Defaults to true, and asking is not the same as getting: lines the page is
|
|
125
|
+
* not rendering measure as zero height at zero offset, so the renderer holds the line
|
|
126
|
+
* measurement back itself while they are off the screen. Pass false to rewrite only the padding,
|
|
127
|
+
* which is sized against the viewport and so is knowable either way.
|
|
128
|
+
*/
|
|
129
|
+
relayout(measureLines?: boolean): void;
|
|
130
|
+
/**
|
|
131
|
+
* Drops the song and the DOM built for it. The renderer built that DOM, so it takes it away again.
|
|
132
|
+
*/
|
|
133
|
+
clear(): void;
|
|
134
|
+
/**
|
|
135
|
+
* Clears, and gives up the window and document it was holding. Final: everything here does
|
|
136
|
+
* nothing afterwards.
|
|
137
|
+
*/
|
|
138
|
+
destroy(): void;
|
|
139
|
+
/**
|
|
140
|
+
* The user scrolled the view. Whether that resumes sooner because the lyrics are unsynced is the
|
|
141
|
+
* renderer's own reading of what it has on screen, not something the caller has to supply.
|
|
142
|
+
*/
|
|
143
|
+
noteUserScroll(): void;
|
|
144
|
+
/**
|
|
145
|
+
* The document this view is in was shown or hidden. Nothing is reset either way, and a view told
|
|
146
|
+
* nothing goes on rendering: this is where the view records what it decided about the animations
|
|
147
|
+
* it is running, and only when a theme has asked for the timing diagnostics.
|
|
148
|
+
*/
|
|
149
|
+
noteVisibilityChange(): void;
|
|
150
|
+
resumeAutoscroll(): void;
|
|
151
|
+
/**
|
|
152
|
+
* Takes this view's lines off the screen, keeping the container they were in, and reports whether
|
|
153
|
+
* there were any there to take. For a consumer that is about to build the next song into the same
|
|
154
|
+
* place and wants the last one gone in the meantime: `clear` is the one that drops the song, and
|
|
155
|
+
* this one drops only what is on the screen.
|
|
156
|
+
*/
|
|
157
|
+
clearOnScreenLyrics(): boolean;
|
|
158
|
+
/**
|
|
159
|
+
* Re-measures the lines on the next frame and renders the view again against them. The predicate
|
|
160
|
+
* is the caller's half of whether that frame does anything, and only that half: whether the lines
|
|
161
|
+
* are on the screen to be measured at all is the renderer's own question, and it asks it itself.
|
|
162
|
+
*
|
|
163
|
+
* @param isTicking - Whether whoever drives this view is still driving it, asked on the frame
|
|
164
|
+
* rather than now. A driver that has stopped is one whose lines may no longer be rendered, and
|
|
165
|
+
* an unrendered line measures as zero height at zero offset, which is what every scroll target
|
|
166
|
+
* for the rest of the song would then be read off. A false answer declines both the
|
|
167
|
+
* re-measurement and the render.
|
|
168
|
+
* @param retick - Runs after the re-measurement, on the frame.
|
|
169
|
+
*/
|
|
170
|
+
scheduleLyricPositionUpdate(isTicking: () => boolean, retick: () => void): void;
|
|
171
|
+
/**
|
|
172
|
+
* Renders this view again against the last player snapshot, measuring nothing, which is what an
|
|
173
|
+
* offset nudge needs.
|
|
174
|
+
*
|
|
175
|
+
* The snapshot is the module's rather than this renderer's: whichever view in this bundle ticked
|
|
176
|
+
* last is the one that wrote it, so two renderers in one realm replay each other's clock here.
|
|
177
|
+
* That is the same constraint the theme settings are under, and `resetPlaybackClock` is what
|
|
178
|
+
* forgets the snapshot.
|
|
179
|
+
*
|
|
180
|
+
* @param buildOptions - Given the snapshot the tick will run against, returns what to render it
|
|
181
|
+
* with. Built here rather than handed in, so a caller that reads its settings at tick time still
|
|
182
|
+
* reads them at tick time.
|
|
183
|
+
*/
|
|
184
|
+
retickFromPlaybackClock(buildOptions: (eventCreationTime: number, isPlaying: boolean) => TickOptions): AnimationTickStatus;
|
|
185
|
+
/** The element this view built its lines into, and null before it has built any. */
|
|
186
|
+
readonly container: HTMLElement | null;
|
|
187
|
+
/**
|
|
188
|
+
* The render records this view built, which are the view's own live state rather than a copy of
|
|
189
|
+
* it: each one holds the line's element and the `Animation` objects running against it.
|
|
190
|
+
*
|
|
191
|
+
* `readonly` freezes the array and nothing else. The records are writable, deliberately, because
|
|
192
|
+
* rewriting a line's time or hanging a translation off its element is how a consumer decorates a
|
|
193
|
+
* song that is already built. What it does not do is reach a second view: these elements belong
|
|
194
|
+
* to this renderer, so a consumer with two of them decorates each one.
|
|
195
|
+
*/
|
|
196
|
+
readonly lines: readonly LineData[];
|
|
197
|
+
/** How the loaded lyrics are timed, and `"none"` once there are none loaded. */
|
|
198
|
+
readonly syncType: LyricSyncType;
|
|
199
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/util.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export declare function clamp(value: number, min: number, max: number): number;
|
|
2
|
+
export declare function positiveModulo(value: number, divisor: number): number;
|
|
3
|
+
export declare function roundedMs(value: number): number;
|
|
4
|
+
/**
|
|
5
|
+
* Converts CSS duration value to milliseconds.
|
|
6
|
+
*
|
|
7
|
+
* @returns Duration in milliseconds
|
|
8
|
+
*/
|
|
9
|
+
export declare function toMs(cssDuration: string): number;
|
|
10
|
+
/**
|
|
11
|
+
* Forces a reflow/repaint of the element by accessing its offsetHeight.
|
|
12
|
+
*
|
|
13
|
+
* @param elt - Element to reflow
|
|
14
|
+
*/
|
|
15
|
+
export declare function reflow(elt: HTMLElement): void;
|
|
16
|
+
/**
|
|
17
|
+
* Returns layout position/dimensions without including transient CSS transforms.
|
|
18
|
+
* This is important for lyric scroll math because line-scale and per-line scroll
|
|
19
|
+
* animations are transform based.
|
|
20
|
+
*/
|
|
21
|
+
export declare function getRelativeLayoutBounds(parent: HTMLElement, child: HTMLElement): DOMRect;
|
package/dist/util.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export function clamp(value, min, max) {
|
|
2
|
+
return Math.max(min, Math.min(max, value));
|
|
3
|
+
}
|
|
4
|
+
export function positiveModulo(value, divisor) {
|
|
5
|
+
return ((value % divisor) + divisor) % divisor;
|
|
6
|
+
}
|
|
7
|
+
export function roundedMs(value) {
|
|
8
|
+
return Math.round(value * 10) / 10;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Converts CSS duration value to milliseconds.
|
|
12
|
+
*
|
|
13
|
+
* @returns Duration in milliseconds
|
|
14
|
+
*/
|
|
15
|
+
export function toMs(cssDuration) {
|
|
16
|
+
if (!cssDuration)
|
|
17
|
+
return 0;
|
|
18
|
+
if (cssDuration.endsWith("ms")) {
|
|
19
|
+
return parseFloat(cssDuration.slice(0, -2));
|
|
20
|
+
}
|
|
21
|
+
else if (cssDuration.endsWith("s")) {
|
|
22
|
+
return parseFloat(cssDuration.slice(0, -1)) * 1000;
|
|
23
|
+
}
|
|
24
|
+
return 0;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Forces a reflow/repaint of the element by accessing its offsetHeight.
|
|
28
|
+
*
|
|
29
|
+
* @param elt - Element to reflow
|
|
30
|
+
*/
|
|
31
|
+
export function reflow(elt) {
|
|
32
|
+
void elt.offsetHeight;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Returns the position and dimensions of a child element relative to its parent.
|
|
36
|
+
*
|
|
37
|
+
* @param parent - The parent element
|
|
38
|
+
* @param child - The child element
|
|
39
|
+
* @returns Rectangle with relative position and dimensions
|
|
40
|
+
*/
|
|
41
|
+
function getRelativeBounds(parent, child) {
|
|
42
|
+
const parentBound = parent.getBoundingClientRect();
|
|
43
|
+
const childBound = child.getBoundingClientRect();
|
|
44
|
+
return new DOMRect(childBound.x - parentBound.x, childBound.y - parentBound.y, childBound.width, childBound.height);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Returns layout position/dimensions without including transient CSS transforms.
|
|
48
|
+
* This is important for lyric scroll math because line-scale and per-line scroll
|
|
49
|
+
* animations are transform based.
|
|
50
|
+
*/
|
|
51
|
+
export function getRelativeLayoutBounds(parent, child) {
|
|
52
|
+
let x = 0;
|
|
53
|
+
let y = 0;
|
|
54
|
+
let element = child;
|
|
55
|
+
while (element && element !== parent) {
|
|
56
|
+
x += element.offsetLeft;
|
|
57
|
+
y += element.offsetTop;
|
|
58
|
+
element = element.offsetParent;
|
|
59
|
+
}
|
|
60
|
+
if (element !== parent) {
|
|
61
|
+
return getRelativeBounds(parent, child);
|
|
62
|
+
}
|
|
63
|
+
return new DOMRect(x, y, Math.max(child.offsetWidth, child.scrollWidth), Math.max(child.offsetHeight, child.scrollHeight));
|
|
64
|
+
}
|
package/dist/view.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { AnimationEngineInstance } from "./engine.js";
|
|
2
|
+
import type { Lyric } from "./types.js";
|
|
3
|
+
export interface SetLyricsOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Whether the loader is still covering the view. Recorded on the container for CSS to key on.
|
|
6
|
+
*/
|
|
7
|
+
loaderVisible: boolean;
|
|
8
|
+
/**
|
|
9
|
+
* These lyrics are a "not found" placeholder rather than a real result.
|
|
10
|
+
*/
|
|
11
|
+
noLyrics: boolean;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Replaces whatever the mount holds with a container built from these lyrics, and hands the render
|
|
15
|
+
* records, the container and its measured size to the engine that animates them.
|
|
16
|
+
*
|
|
17
|
+
* @param engine - Instance that owns the document to build in and the host to seek through
|
|
18
|
+
* @param mount - Element whose children the built container replaces
|
|
19
|
+
* @param lyrics - Lines to render, left untouched
|
|
20
|
+
* @param options - What the container records for CSS to key on
|
|
21
|
+
*/
|
|
22
|
+
export declare function setLyrics(engine: AnimationEngineInstance, mount: HTMLElement, lyrics: Lyric[], options: SetLyricsOptions): void;
|
package/dist/view.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// Builds the view: one container under the mount, one element per lyric line, and the render
|
|
2
|
+
// records the engine animates them through. `inject.ts` builds what goes inside a line; this builds
|
|
3
|
+
// the line and everything around it.
|
|
4
|
+
//
|
|
5
|
+
// A song replaces the whole view rather than being diffed into the last one. Every record the
|
|
6
|
+
// engine holds points at an element built here and carries the `Animation` objects running against
|
|
7
|
+
// it, so a partial update would leave the engine holding pieces of the song before.
|
|
8
|
+
//
|
|
9
|
+
// This is also the one place outside the engine that writes engine view state, because building the
|
|
10
|
+
// lines is where those values are first knowable: the container, the records, the sync type, the
|
|
11
|
+
// size they were measured at, and the scrolls to swallow before the view has settled.
|
|
12
|
+
import { LINE_CLASS, LYRICS_CLASS, RTL_CLASS } from "./constants.js";
|
|
13
|
+
import { addSeekHandler, applyDirection, buildLineSyncedParts, createLyricsLine, deriveSyncType, disableRichsync, findNearestAgent, isNearestLyricRtl, newLineData, } from "./inject.js";
|
|
14
|
+
import { createInstrumentalElement } from "./instrumental.js";
|
|
15
|
+
const INITIAL_SKIP_SCROLLS = 2;
|
|
16
|
+
const SKIP_SCROLL_DECAY_MS = 2000;
|
|
17
|
+
function buildInstrumentalLine(doc, lyricElement, lyrics, lineIndex) {
|
|
18
|
+
createInstrumentalElement(doc, lyricElement, lyrics[lineIndex].durationMs, lineIndex);
|
|
19
|
+
lyricElement.dataset.instrumental = "true";
|
|
20
|
+
const agent = findNearestAgent(lyrics, lineIndex);
|
|
21
|
+
if (agent) {
|
|
22
|
+
lyricElement.dataset.agent = agent;
|
|
23
|
+
}
|
|
24
|
+
if (isNearestLyricRtl(lyrics, lineIndex)) {
|
|
25
|
+
lyricElement.classList.add(RTL_CLASS);
|
|
26
|
+
lyricElement.dataset.direction = "rtl";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function buildSungLine(doc, lyricElement, lyricItem, line) {
|
|
30
|
+
// Rebuilt parts stay local so the provider's lyrics survive injection intact and a second
|
|
31
|
+
// build over the same array produces the same result.
|
|
32
|
+
const parts = lyricItem.parts && lyricItem.parts.length > 0 && !disableRichsync.getBooleanValue()
|
|
33
|
+
? lyricItem.parts
|
|
34
|
+
: buildLineSyncedParts(lyricItem);
|
|
35
|
+
applyDirection(lyricElement, lyricItem.words);
|
|
36
|
+
createLyricsLine(doc, parts, line, lyricElement);
|
|
37
|
+
lyricElement.style.setProperty("--blyrics-duration", lyricItem.durationMs + "ms");
|
|
38
|
+
if (lyricItem.agent) {
|
|
39
|
+
lyricElement.dataset.agent = lyricItem.agent;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Replaces whatever the mount holds with a container built from these lyrics, and hands the render
|
|
44
|
+
* records, the container and its measured size to the engine that animates them.
|
|
45
|
+
*
|
|
46
|
+
* @param engine - Instance that owns the document to build in and the host to seek through
|
|
47
|
+
* @param mount - Element whose children the built container replaces
|
|
48
|
+
* @param lyrics - Lines to render, left untouched
|
|
49
|
+
* @param options - What the container records for CSS to key on
|
|
50
|
+
*/
|
|
51
|
+
export function setLyrics(engine, mount, lyrics, options) {
|
|
52
|
+
const doc = engine.document;
|
|
53
|
+
const container = doc.createElement("div");
|
|
54
|
+
container.className = LYRICS_CLASS;
|
|
55
|
+
mount.replaceChildren(container);
|
|
56
|
+
const allZero = lyrics.every(item => item.startTimeMs === 0);
|
|
57
|
+
const seek = (timeS) => engine.host.seek(timeS);
|
|
58
|
+
const lines = [];
|
|
59
|
+
const syncType = deriveSyncType(lyrics);
|
|
60
|
+
for (const [lineIndex, lyricItem] of lyrics.entries()) {
|
|
61
|
+
const lyricElement = doc.createElement("div");
|
|
62
|
+
const line = newLineData(lyricElement, lyricItem.startTimeMs, lyricItem.durationMs);
|
|
63
|
+
lyricElement.dataset.time = String(line.time);
|
|
64
|
+
lyricElement.dataset.duration = String(line.duration);
|
|
65
|
+
lyricElement.dataset.lineNumber = String(lineIndex);
|
|
66
|
+
lyricElement.classList.add(LINE_CLASS);
|
|
67
|
+
lyricElement.dir = "auto";
|
|
68
|
+
addSeekHandler(seek, lyricElement, allZero);
|
|
69
|
+
lines.push(line);
|
|
70
|
+
if (lyricItem.isInstrumental) {
|
|
71
|
+
buildInstrumentalLine(doc, lyricElement, lyrics, lineIndex);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
buildSungLine(doc, lyricElement, lyricItem, line);
|
|
75
|
+
}
|
|
76
|
+
container.appendChild(lyricElement);
|
|
77
|
+
}
|
|
78
|
+
engine.skipScrolls = INITIAL_SKIP_SCROLLS;
|
|
79
|
+
engine.skipScrollsDecayTimes = Array.from({ length: INITIAL_SKIP_SCROLLS }, () => Date.now() + SKIP_SCROLL_DECAY_MS);
|
|
80
|
+
engine.scrollResumeTime = 0;
|
|
81
|
+
container.dataset.sync = syncType;
|
|
82
|
+
container.dataset.loaderVisible = String(options.loaderVisible);
|
|
83
|
+
if (options.noLyrics) {
|
|
84
|
+
container.dataset.noLyrics = "true";
|
|
85
|
+
}
|
|
86
|
+
engine.lines = lines;
|
|
87
|
+
engine.lyricsContainer = container;
|
|
88
|
+
engine.syncType = syncType;
|
|
89
|
+
// Measured last: the container is in the document, filled, and carrying the attributes CSS
|
|
90
|
+
// sizes it by.
|
|
91
|
+
engine.lyricWidth = container.clientWidth;
|
|
92
|
+
engine.lyricHeight = container.clientHeight;
|
|
93
|
+
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@braccato/core",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Synchronized lyrics
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Synchronized lyrics renderer with word-by-word animations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
|
-
"url": "https://github.com/better-lyrics/braccato",
|
|
9
|
+
"url": "git+https://github.com/better-lyrics/braccato.git",
|
|
10
10
|
"directory": "packages/core"
|
|
11
11
|
},
|
|
12
12
|
"homepage": "https://braccato.boidu.dev",
|
|
@@ -14,19 +14,41 @@
|
|
|
14
14
|
"lyrics",
|
|
15
15
|
"sync",
|
|
16
16
|
"web-component",
|
|
17
|
-
"lit",
|
|
18
17
|
"music",
|
|
19
18
|
"karaoke",
|
|
20
19
|
"ttml"
|
|
21
20
|
],
|
|
22
|
-
"sideEffects":
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
"sideEffects": [
|
|
22
|
+
"./dist/element.js",
|
|
23
|
+
"./dist/styles/*.css"
|
|
24
|
+
],
|
|
25
25
|
"exports": {
|
|
26
26
|
".": {
|
|
27
27
|
"types": "./dist/index.d.ts",
|
|
28
28
|
"import": "./dist/index.js"
|
|
29
|
-
}
|
|
29
|
+
},
|
|
30
|
+
"./element": {
|
|
31
|
+
"types": "./dist/element.d.ts",
|
|
32
|
+
"import": "./dist/element.js"
|
|
33
|
+
},
|
|
34
|
+
"./constants": {
|
|
35
|
+
"types": "./dist/constants.d.ts",
|
|
36
|
+
"import": "./dist/constants.js"
|
|
37
|
+
},
|
|
38
|
+
"./text": {
|
|
39
|
+
"types": "./dist/text.d.ts",
|
|
40
|
+
"import": "./dist/text.js"
|
|
41
|
+
},
|
|
42
|
+
"./themeSettings": {
|
|
43
|
+
"types": "./dist/themeSettings.d.ts",
|
|
44
|
+
"import": "./dist/themeSettings.js"
|
|
45
|
+
},
|
|
46
|
+
"./util": {
|
|
47
|
+
"types": "./dist/util.d.ts",
|
|
48
|
+
"import": "./dist/util.js"
|
|
49
|
+
},
|
|
50
|
+
"./styles/*.css": "./dist/styles/*.css",
|
|
51
|
+
"./package.json": "./package.json"
|
|
30
52
|
},
|
|
31
53
|
"files": [
|
|
32
54
|
"dist"
|
|
@@ -35,21 +57,12 @@
|
|
|
35
57
|
"access": "public"
|
|
36
58
|
},
|
|
37
59
|
"dependencies": {
|
|
38
|
-
"
|
|
39
|
-
"@braccato/parsers": "^0.1.2"
|
|
40
|
-
},
|
|
41
|
-
"devDependencies": {
|
|
42
|
-
"jsdom": "^28.1.0",
|
|
43
|
-
"typescript": "^5.7.3",
|
|
44
|
-
"vite": "^6.0.7",
|
|
45
|
-
"vite-plugin-dts": "^4.3.0",
|
|
46
|
-
"vitest": "^3.0.4"
|
|
60
|
+
"@braccato/types": "^1.0.0"
|
|
47
61
|
},
|
|
48
62
|
"scripts": {
|
|
49
|
-
"build": "
|
|
50
|
-
"dev": "vite build --watch",
|
|
63
|
+
"build": "tsx ../../tooling/build-package.ts",
|
|
51
64
|
"typecheck": "tsc --noEmit",
|
|
52
|
-
"
|
|
53
|
-
"
|
|
65
|
+
"selfcheck": "tsx ../../tooling/run-selfchecks.ts",
|
|
66
|
+
"clean": "rm -rf dist"
|
|
54
67
|
}
|
|
55
68
|
}
|