@braccato/core 0.1.6 → 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 -948
- 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/README.md
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
# @braccato/core
|
|
2
|
+
|
|
3
|
+
A custom element that renders synchronized lyrics and lights each syllable up as it is sung. No
|
|
4
|
+
runtime dependencies, only a types-only one on `@braccato/types`. The lines go into light DOM rather
|
|
5
|
+
than a shadow root, so the CSS already on your page reaches them.
|
|
6
|
+
|
|
7
|
+
Extracted from the [Better Lyrics](https://better-lyrics.boidu.dev) rendering engine, which is still
|
|
8
|
+
where it runs.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm i @braccato/core
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
```html
|
|
19
|
+
<audio id="player" src="song.mp3" controls></audio>
|
|
20
|
+
<braccato-lyrics source="#player"></braccato-lyrics>
|
|
21
|
+
|
|
22
|
+
<script type="module">
|
|
23
|
+
import "@braccato/core/element";
|
|
24
|
+
import "@braccato/core/styles/variables.css";
|
|
25
|
+
import "@braccato/core/styles/lyrics.css";
|
|
26
|
+
import "@braccato/core/styles/instrumental.css";
|
|
27
|
+
|
|
28
|
+
document.querySelector("braccato-lyrics").lyrics = [
|
|
29
|
+
{ startTimeMs: 0, durationMs: 4200, words: "The first line" },
|
|
30
|
+
{ startTimeMs: 4200, durationMs: 3800, words: "The second" },
|
|
31
|
+
];
|
|
32
|
+
</script>
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`source` takes a CSS selector or a media element. It is resolved when the element connects, so put
|
|
36
|
+
the `<audio>` before the tag, or write the property from script. Without a source, drive the view
|
|
37
|
+
yourself by writing `currentTime` and `playing`.
|
|
38
|
+
|
|
39
|
+
Two things catch everybody once. The element has no `display` of its own:
|
|
40
|
+
|
|
41
|
+
```css
|
|
42
|
+
braccato-lyrics {
|
|
43
|
+
display: block;
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
And autoscroll writes `scrollTop` on the nearest ancestor that scrolls, falling through to the
|
|
48
|
+
document when nothing does. If the element is not inside its own scroller, say which one it is:
|
|
49
|
+
|
|
50
|
+
```js
|
|
51
|
+
view.host = { getScrollElement: () => yourFrame };
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Lyrics
|
|
55
|
+
|
|
56
|
+
The array is the whole input, and nothing in this package produces one.
|
|
57
|
+
[`@braccato/parsers`](https://www.npmjs.com/package/@braccato/parsers) reads TTML, LRC, SRT, QRC and
|
|
58
|
+
plain text, and picks between them by looking at the file.
|
|
59
|
+
|
|
60
|
+
```js
|
|
61
|
+
import { detectParser } from "@braccato/parsers";
|
|
62
|
+
|
|
63
|
+
const text = await fetch("song.ttml").then(response => response.text());
|
|
64
|
+
view.lyrics = detectParser(text).parse(text, player.duration * 1000);
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
A `Lyric` is `{ startTimeMs, durationMs, words }`, with an optional `parts` array of the same three
|
|
68
|
+
fields for syllable or word timing, and optional `translation`, `romanization` and
|
|
69
|
+
`timedRomanization` beside them.
|
|
70
|
+
|
|
71
|
+
## Properties
|
|
72
|
+
|
|
73
|
+
Every one of these may be written before the element is in a document. The renderer is built when it
|
|
74
|
+
connects, and everything it was handed by then is applied at once.
|
|
75
|
+
|
|
76
|
+
| Property | Attribute | Type | Default | Description |
|
|
77
|
+
| --------------- | -------------- | ----------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
|
|
78
|
+
| `lyrics` | | `Lyric[] \| null` | `null` | The song. Null means it was never given one, and an empty array clears the view, so there is a way to say both. |
|
|
79
|
+
| `lyricsOptions` | | `{ loaderVisible?, noLyrics? }` | `{}` | How the lines are built. `noLyrics` marks a message as a placeholder rather than a song, which keeps passive scrolling from drifting it. |
|
|
80
|
+
| `source` | `source` | `string \| HTMLMediaElement \| null` | `null` | A selector or the media element itself. See Following a media element. |
|
|
81
|
+
| `mediaElement` | | `HTMLMediaElement \| null` (get) | `null` | What `source` resolved to. Null while disconnected, and null for a selector that missed. |
|
|
82
|
+
| `currentTime` | `current-time` | `number` | `0` | Playback position in **seconds**. Writing it renders the view again, so whoever holds the clock drives the lyrics by writing this. |
|
|
83
|
+
| `playing` | `playing` | `boolean` | `false` | A paused view animates differently from a playing one. |
|
|
84
|
+
| `tickOptions` | | `ElementTickOptions` | `{}` | The rest of a tick: four offsets taken off the clock before it is matched, whether passive scrolling is on, and when the clock was sampled. |
|
|
85
|
+
| `theme` | `theme` | `string` | `""` | A compiled stylesheet. See Theming. |
|
|
86
|
+
| `host` | | `Partial<LyricsRendererHost>` | `{}` | Overrides for what the renderer asks of its surroundings. Every member has a default. Writing it while connected rebuilds the view. |
|
|
87
|
+
| `renderer` | | `LyricsRenderer \| null` (get) | `null` | The renderer underneath, for the day the tag runs out. A different one after every reconnection. |
|
|
88
|
+
| `status` | | `ElementStatus` (get) | `"idle"` | `idle`, `rendering`, `theme-conflict` or `no-browsing-context`. |
|
|
89
|
+
|
|
90
|
+
`tickOptions` and `lyricsOptions` are stored on write and read by the next tick or the next build, so
|
|
91
|
+
writing options and the clock on the same frame renders once.
|
|
92
|
+
|
|
93
|
+
## Attributes
|
|
94
|
+
|
|
95
|
+
An attribute writes its property, and a property never writes back. Reflecting `current-time` would
|
|
96
|
+
put the playback clock into the DOM sixty times a second, and one attribute reflecting while the rest
|
|
97
|
+
do not is worse than none of them doing it.
|
|
98
|
+
|
|
99
|
+
| Attribute | Writes | Notes |
|
|
100
|
+
| -------------- | ------------- | -------------------------------------------------------------------------------------------------- |
|
|
101
|
+
| `source` | `source` | The selector form only. Another selector moves the binding, and removing it unbinds. |
|
|
102
|
+
| `theme` | `theme` | A whole stylesheet in an attribute value. It works, but nobody would ship a theme this way. |
|
|
103
|
+
| `current-time` | `currentTime` | Seconds. A value that does not parse as a number is ignored rather than read as zero. |
|
|
104
|
+
| `playing` | `playing` | An ordinary boolean attribute: its presence is what counts, so `playing="false"` is playing. |
|
|
105
|
+
|
|
106
|
+
## Events
|
|
107
|
+
|
|
108
|
+
All four bubble and are composed, so an element you put inside your own shadow root still reaches
|
|
109
|
+
your listener.
|
|
110
|
+
|
|
111
|
+
| Event | Detail | When |
|
|
112
|
+
| ------------------------ | ------------------------- | --------------------------------------------------------------------------------- |
|
|
113
|
+
| `braccato:lyrics-loaded` | `{ lineCount, syncType }` | Lyrics were applied, including an empty array. A theme change that rebuilds the lines reports itself the same way. |
|
|
114
|
+
| `braccato:line-click` | `{ timeS }` | A line was clicked. The seek has already reached the bound media element by the time you hear about it. |
|
|
115
|
+
| `braccato:scroll-state` | `{ userScrolling }` | Autoscroll stopped following the song, or started again. |
|
|
116
|
+
| `braccato:error` | `{ phase, error }` | Connecting, resolving a source, or applying lyrics or a theme went wrong. `phase` is `connect`, `conflict`, `source`, `lyrics` or `theme`. |
|
|
117
|
+
|
|
118
|
+
Errors are dispatched a microtask after they happen rather than where they happen, which is what
|
|
119
|
+
makes them receivable at all: `connectedCallback` runs before any listener a page could have added.
|
|
120
|
+
A listener added later than that still misses them, so `status` answers the same question and needs
|
|
121
|
+
no listener. Nothing thrown by a tick lands here, because sixty error events a second would bury the
|
|
122
|
+
one that mattered.
|
|
123
|
+
|
|
124
|
+
There is no `braccato:word-click`. The renderer tells its host `seek(timeS)` and nothing else, so the
|
|
125
|
+
element cannot tell a word seek from a line seek without re-deriving the click branch off the DOM.
|
|
126
|
+
The DOM is light and the class names are published, so listen for `click` on the element and read
|
|
127
|
+
`.blyrics--word` yourself.
|
|
128
|
+
|
|
129
|
+
## Theming
|
|
130
|
+
|
|
131
|
+
A theme is a stylesheet. Write CSS against the class names below and the module stays out of it. What
|
|
132
|
+
it does read is the `blyrics-*` lines inside the comments, which is how a theme changes behaviour
|
|
133
|
+
without a second configuration format.
|
|
134
|
+
|
|
135
|
+
```js
|
|
136
|
+
view.theme = `
|
|
137
|
+
/* blyrics-target-scroll-pos-ratio = 0.5; */
|
|
138
|
+
/* blyrics-long-word-threshold = 900; */
|
|
139
|
+
|
|
140
|
+
.blyrics-container {
|
|
141
|
+
--blyrics-font-size: 3.5rem;
|
|
142
|
+
--blyrics-lyric-active-color: white;
|
|
143
|
+
--blyrics-lyric-inactive-color: rgb(255 255 255 / 0.25);
|
|
144
|
+
}
|
|
145
|
+
`;
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Settings are read from comments only. Everything else is CSS the browser is going to read, and a
|
|
149
|
+
stylesheet must not be able to configure the module by accident. An empty theme puts every setting
|
|
150
|
+
back to its default. The stylesheet itself goes into the document head under the
|
|
151
|
+
`blyrics-custom-style` id.
|
|
152
|
+
|
|
153
|
+
There is no `longWordThreshold`, `lineSyncedDelay` or `disableRichsync` property. Those are theme
|
|
154
|
+
settings (`blyrics-long-word-threshold`, `blyrics-line-synced-animation-delay`,
|
|
155
|
+
`blyrics-disable-richsync`), read from the stylesheet you already hand over. A theme that set one
|
|
156
|
+
while a property said otherwise would leave the module with two answers and no rule for picking.
|
|
157
|
+
|
|
158
|
+
`parseThemeConfig` is published on `@braccato/core/themeSettings` for reading the settings out of a
|
|
159
|
+
stylesheet somewhere no renderer is running.
|
|
160
|
+
|
|
161
|
+
### Custom properties
|
|
162
|
+
|
|
163
|
+
The ones a theme reaches for first. `variables.css` declares the rest.
|
|
164
|
+
|
|
165
|
+
```css
|
|
166
|
+
.blyrics-container {
|
|
167
|
+
--blyrics-font-family: system-ui, sans-serif;
|
|
168
|
+
--blyrics-font-size: 3rem;
|
|
169
|
+
--blyrics-line-height: 1.333;
|
|
170
|
+
--blyrics-padding: 2rem;
|
|
171
|
+
--blyrics-lyric-active-color: white;
|
|
172
|
+
--blyrics-lyric-inactive-color: rgb(255 255 255 / 0.3);
|
|
173
|
+
--blyrics-glow-color: rgb(255 255 255 / 0.5);
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
`--blyrics-font-size` is what everything else is sized off, including the instrumental dots.
|
|
178
|
+
`--blyrics-padding` is the vertical room around each line, and the one to reach for before
|
|
179
|
+
`line-height`. Every word is given the glow, so a theme that wants it to mean something selects on
|
|
180
|
+
`data-long-word`, which the module sets on any part held past `blyrics-long-word-threshold`.
|
|
181
|
+
|
|
182
|
+
### Class names
|
|
183
|
+
|
|
184
|
+
These are published API rather than implementation. Renaming one costs a migration rather than a
|
|
185
|
+
refactor. Import them from `@braccato/core/constants` instead of typing them out.
|
|
186
|
+
|
|
187
|
+
| Constant | Class | What it is |
|
|
188
|
+
| ------------------------- | --------------------------- | ----------------------------------------------------------- |
|
|
189
|
+
| `LYRICS_CLASS` | `blyrics-container` | The view. One per renderer. |
|
|
190
|
+
| `LINE_CLASS` | `blyrics--line` | One line, carrying its own `dir="auto"`. |
|
|
191
|
+
| `CURRENT_LYRICS_CLASS` | `blyrics--active` | The line the song is on right now. |
|
|
192
|
+
| `WORD_CLASS` | `blyrics--word` | One word, and the unit the sweep animates. |
|
|
193
|
+
| `BACKGROUND_LYRIC_CLASS` | `blyrics-background-lyric` | A background vocal, sung over the line it answers. |
|
|
194
|
+
| `USER_SCROLLING_CLASS` | `blyrics-user-scrolling` | Set while a reader has scrolled away and autoscroll waits. |
|
|
195
|
+
| `TRANSLATED_LYRICS_CLASS` | `blyrics--translated` | A translation hung off a line that was already built. |
|
|
196
|
+
| `CUSTOM_THEME_STYLE_ID` | `blyrics-custom-style` | The id of the `<style>` the theme lands in. |
|
|
197
|
+
|
|
198
|
+
## Stylesheets
|
|
199
|
+
|
|
200
|
+
Three sheets ship with the package, and loading them is yours, the way any package's CSS is. Leave
|
|
201
|
+
them out and you get lines that are in the document and unstyled, rather than lines that are missing.
|
|
202
|
+
|
|
203
|
+
| File | What it carries |
|
|
204
|
+
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
|
|
205
|
+
| `@braccato/core/styles/variables.css` | Every `--blyrics-*` default. It goes first, because the other two read from it. |
|
|
206
|
+
| `@braccato/core/styles/lyrics.css` | The container, the lines, the words and the sweep, plus two `@property` registrations the word animation interpolates through. |
|
|
207
|
+
| `@braccato/core/styles/instrumental.css` | The waveform that fills a bar nobody sings over, and the animation that walks it. |
|
|
208
|
+
|
|
209
|
+
One thing they do not do for you. The module measures the room the first and last lines need to reach
|
|
210
|
+
the view's target scroll position and writes it on the root as `--blyrics-padding-top` and
|
|
211
|
+
`--blyrics-padding-bottom`, but `lyrics.css` only spends the bottom one. Supply the top rule:
|
|
212
|
+
|
|
213
|
+
```css
|
|
214
|
+
.blyrics-container {
|
|
215
|
+
padding-top: var(--blyrics-padding-top, 2rem);
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Light DOM, not shadow DOM
|
|
220
|
+
|
|
221
|
+
The element builds into itself. That is what lets a stylesheet at document level select the lines,
|
|
222
|
+
and what lets the package's own `@property` registrations apply to them, which they would not inside
|
|
223
|
+
a shadow root. The theme is adopted into the element's document rather than encapsulated, and the
|
|
224
|
+
package's stylesheets are yours to load for the same reason.
|
|
225
|
+
|
|
226
|
+
## Entry points
|
|
227
|
+
|
|
228
|
+
`@braccato/core` is the facade and registers nothing. `createLyricsRenderer(options)` returns one
|
|
229
|
+
`LyricsRenderer`: give it lyrics, tick it, and it owns the DOM it builds and every re-measurement
|
|
230
|
+
that DOM needs. `resetPlaybackClock`, `resumeAllAutoscroll`, `injectRomanization` and
|
|
231
|
+
`injectTranslation` are published beside it, for what one instance cannot answer for on its own.
|
|
232
|
+
|
|
233
|
+
`@braccato/core/element` registers `<braccato-lyrics>`, and `<better-lyrics>` beside it, on import.
|
|
234
|
+
Registration is a side effect, which is why it is entered separately.
|
|
235
|
+
|
|
236
|
+
Four leaves import nothing at all, so taking one does not pull the engine into your bundle with it:
|
|
237
|
+
|
|
238
|
+
- `@braccato/core/constants` for the class names and element ids above
|
|
239
|
+
- `@braccato/core/text` for script detection: `testRtl`, `containsNonLatin`, `detectNonLatinLanguage`
|
|
240
|
+
- `@braccato/core/themeSettings` for `parseThemeConfig`
|
|
241
|
+
- `@braccato/core/util` for pure helpers such as `clamp` and `toMs`
|
|
242
|
+
|
|
243
|
+
Two notes on the element entry point. A browser extension's isolated world has no custom element
|
|
244
|
+
registry, so `window.customElements` is null there and importing this file throws where it registers.
|
|
245
|
+
An extension that wants the tag has to run in the page's own world; one that stays isolated calls
|
|
246
|
+
`createLyricsRenderer` directly. And registration is silent about a name already taken, so two copies
|
|
247
|
+
of this package on one page means the first to load takes both names and `instanceof` against the
|
|
248
|
+
second copy's class is false for every element on the page. Load one copy.
|
|
249
|
+
|
|
250
|
+
## Following a media element
|
|
251
|
+
|
|
252
|
+
While a `source` is bound, the element drives itself. It reads `currentTime` and `paused` off the
|
|
253
|
+
media element on a `requestAnimationFrame` loop that runs only while the song plays, and a click on a
|
|
254
|
+
lyric line sets `currentTime` back on it. So `currentTime` and `playing` become outputs: a write to
|
|
255
|
+
either is dropped and the getter keeps reporting what the binding last read. Dropped rather than
|
|
256
|
+
reported, because a consumer who bound a source and left their own frame loop running would otherwise
|
|
257
|
+
be told about it sixty times a second. Unbind and the clock goes back to whoever asked for it.
|
|
258
|
+
|
|
259
|
+
A reading the media element has not refreshed yet is carried forward at the playback rate it was
|
|
260
|
+
taken at, capped at 100ms of frame time. That cap is what covers a stall: the view runs at most 100ms
|
|
261
|
+
past the last real reading and then waits with it. What it costs is a step backwards when the clock
|
|
262
|
+
moves again, scaled by the rate. 100ms at 1x, 400ms at 4x.
|
|
263
|
+
|
|
264
|
+
`play`, `pause`, `seeking`, `seeked` and `ratechange` are listened to. The frame loop covers the rest
|
|
265
|
+
by asking the media element whether its clock is still going rather than trusting that something said
|
|
266
|
+
so. One gap is worth knowing: `emptied` while already paused leaves no loop running to notice, so
|
|
267
|
+
swapping `audio.src` between songs without playing goes on reporting the old position until the next
|
|
268
|
+
`play`.
|
|
269
|
+
|
|
270
|
+
## One renderer per document
|
|
271
|
+
|
|
272
|
+
Two renderers in one document write over each other, so the module supports one. It is a constraint
|
|
273
|
+
rather than a setting, and it is stated rather than enforced: none of the points where two of them
|
|
274
|
+
collide is a crash.
|
|
275
|
+
|
|
276
|
+
Two things are written per document and belong to whichever renderer wrote them last: the theme's
|
|
277
|
+
`<style>` element, and the scroll padding on the root. Two more are per bundle, because a settings
|
|
278
|
+
registry and the playback clock both live at module scope: one theme means one set of values for
|
|
279
|
+
every view in that bundle, and whichever view ticked last is the one whose clock the others replay.
|
|
280
|
+
|
|
281
|
+
None of that is a limit on how many elements you may have. Two views handed the **same** theme share
|
|
282
|
+
only the settings both of them asked for, and the renderer adopts an existing theme element rather
|
|
283
|
+
than adding a rival under the same id. The line is drawn at the disagreement: when an element applies
|
|
284
|
+
a theme another element in its document was not given, both dispatch `braccato:error` with
|
|
285
|
+
`phase: "conflict"` and both read `status === "theme-conflict"`. Neither stops rendering, because a
|
|
286
|
+
blank view with a reason is worse than a themed one with a warning.
|
|
287
|
+
|
|
288
|
+
## Docs and demo
|
|
289
|
+
|
|
290
|
+
Full documentation is at [braccato.boidu.dev](https://braccato.boidu.dev).
|
|
291
|
+
|
|
292
|
+
The demo page lives at [`demo/`](https://github.com/better-lyrics/braccato/tree/master/demo) in the
|
|
293
|
+
repository and runs against the emitted package, with a control for most of what is above. Clone the
|
|
294
|
+
repository and run `pnpm -C demo dev`, then open `http://localhost:5173/`.
|
|
295
|
+
|
|
296
|
+
## Licence
|
|
297
|
+
|
|
298
|
+
MIT. See `LICENSE`.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export declare const LYRICS_WRAPPER_ID: "blyrics-wrapper";
|
|
2
|
+
export declare const LYRICS_CLASS: "blyrics-container";
|
|
3
|
+
export declare const LINE_CLASS: "blyrics--line";
|
|
4
|
+
export declare const WORD_CLASS: "blyrics--word";
|
|
5
|
+
export declare const FOOTER_CLASS: "blyrics-footer";
|
|
6
|
+
export declare const CURRENT_LYRICS_CLASS: "blyrics--active";
|
|
7
|
+
export declare const ANIMATING_CLASS: "blyrics--animating";
|
|
8
|
+
export declare const PAUSED_CLASS: "blyrics--paused";
|
|
9
|
+
export declare const ZERO_DURATION_ANIMATION_CLASS: "blyrics-zero-dur-animate";
|
|
10
|
+
export declare const USER_SCROLLING_CLASS: "blyrics-user-scrolling";
|
|
11
|
+
export declare const BACKGROUND_LYRIC_CLASS: "blyrics-background-lyric";
|
|
12
|
+
export declare const EXPLICIT_WORD_CLASS: "blyrics-explicit";
|
|
13
|
+
export declare const RTL_CLASS: "blyrics-rtl";
|
|
14
|
+
export declare const TRANSLATED_LYRICS_CLASS: "blyrics--translated";
|
|
15
|
+
export declare const ROMANIZED_LYRICS_CLASS: "blyrics--romanized";
|
|
16
|
+
export declare const CONTENT_LINE_CLASS: "blyrics-content-line";
|
|
17
|
+
export declare const LINE_MAIN_CLASS: "blyrics-line-main";
|
|
18
|
+
export declare const BACKGROUND_LINE_CLASS: "blyrics-background-line";
|
|
19
|
+
export declare const WORD_GROUP_CLASS: "blyrics-word-group";
|
|
20
|
+
export declare const LONG_WORD_GROUP_CLASS: "blyrics-word-group-long";
|
|
21
|
+
export declare const WORD_HIGHLIGHT_CLASS: "blyrics-word-highlight";
|
|
22
|
+
export declare const LINE_SYNCED_WORD_CLASS: "blyrics-line-synced-word";
|
|
23
|
+
export declare const BIDI_RUN_CLASS: "blyrics-bidi-run";
|
|
24
|
+
export declare const BIDI_SENSITIVE_CLASS: "blyrics-bidi-sensitive";
|
|
25
|
+
export declare const CUSTOM_THEME_STYLE_ID: "blyrics-custom-style";
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// These names are a public contract: marketplace themes select on them and set properties through
|
|
2
|
+
// them, so renaming one is a breaking change for every published theme, not a refactor.
|
|
3
|
+
// -- Structure --------------------------------------------
|
|
4
|
+
export const LYRICS_WRAPPER_ID = "blyrics-wrapper";
|
|
5
|
+
export const LYRICS_CLASS = "blyrics-container";
|
|
6
|
+
export const LINE_CLASS = "blyrics--line";
|
|
7
|
+
export const WORD_CLASS = "blyrics--word";
|
|
8
|
+
export const FOOTER_CLASS = "blyrics-footer";
|
|
9
|
+
// -- Playback state --------------------------------------------
|
|
10
|
+
export const CURRENT_LYRICS_CLASS = "blyrics--active";
|
|
11
|
+
export const ANIMATING_CLASS = "blyrics--animating";
|
|
12
|
+
export const PAUSED_CLASS = "blyrics--paused";
|
|
13
|
+
export const ZERO_DURATION_ANIMATION_CLASS = "blyrics-zero-dur-animate";
|
|
14
|
+
export const USER_SCROLLING_CLASS = "blyrics-user-scrolling";
|
|
15
|
+
// -- Line and word variants --------------------------------------------
|
|
16
|
+
export const BACKGROUND_LYRIC_CLASS = "blyrics-background-lyric";
|
|
17
|
+
export const EXPLICIT_WORD_CLASS = "blyrics-explicit";
|
|
18
|
+
export const RTL_CLASS = "blyrics-rtl";
|
|
19
|
+
export const TRANSLATED_LYRICS_CLASS = "blyrics--translated";
|
|
20
|
+
export const ROMANIZED_LYRICS_CLASS = "blyrics--romanized";
|
|
21
|
+
// -- Line internals --------------------------------------------
|
|
22
|
+
export const CONTENT_LINE_CLASS = "blyrics-content-line";
|
|
23
|
+
export const LINE_MAIN_CLASS = "blyrics-line-main";
|
|
24
|
+
export const BACKGROUND_LINE_CLASS = "blyrics-background-line";
|
|
25
|
+
export const WORD_GROUP_CLASS = "blyrics-word-group";
|
|
26
|
+
export const LONG_WORD_GROUP_CLASS = "blyrics-word-group-long";
|
|
27
|
+
export const WORD_HIGHLIGHT_CLASS = "blyrics-word-highlight";
|
|
28
|
+
export const LINE_SYNCED_WORD_CLASS = "blyrics-line-synced-word";
|
|
29
|
+
export const BIDI_RUN_CLASS = "blyrics-bidi-run";
|
|
30
|
+
export const BIDI_SENSITIVE_CLASS = "blyrics-bidi-sensitive";
|
|
31
|
+
// -- Theme --------------------------------------------
|
|
32
|
+
// The element a theme handed to `setTheme` is applied through, one per document a renderer builds
|
|
33
|
+
// in. Named rather than anonymous because a consumer with a second document to style has to be able
|
|
34
|
+
// to find the first one: this extension's floating window mirrors the side panel's by id.
|
|
35
|
+
//
|
|
36
|
+
// One renderer per document owns it. Two renderers in one document render against one theme
|
|
37
|
+
// whatever they are given, because the settings registry is module scope, so this is a constraint
|
|
38
|
+
// stated rather than a configuration supported. A renderer that finds the id already in its
|
|
39
|
+
// document writes into that element rather than adding a rival, so the id stays unique and a
|
|
40
|
+
// consumer reading it by id gets the sheet in force, and `destroy` takes the element away only if
|
|
41
|
+
// this renderer is what put it there.
|
|
42
|
+
export const CUSTOM_THEME_STYLE_ID = "blyrics-custom-style";
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import type { Lyric, LyricsRenderer, LyricsRendererHost, LyricSyncType, TickOptions } from "./types.js";
|
|
2
|
+
import type { SetLyricsOptions } from "./view.js";
|
|
3
|
+
/**
|
|
4
|
+
* What a tick carries beyond the clock. `isPlaying` is not among them: `playing` is the property
|
|
5
|
+
* that says so, and one question with two answers has no rule for choosing between them.
|
|
6
|
+
*
|
|
7
|
+
* @public
|
|
8
|
+
*/
|
|
9
|
+
export type ElementTickOptions = Omit<TickOptions, "isPlaying">;
|
|
10
|
+
/**
|
|
11
|
+
* How the lines are built, beyond the lines themselves. The mount is not among them: the element is
|
|
12
|
+
* the mount.
|
|
13
|
+
*
|
|
14
|
+
* @public
|
|
15
|
+
*/
|
|
16
|
+
export type ElementLyricsOptions = Partial<SetLyricsOptions>;
|
|
17
|
+
/** @public */
|
|
18
|
+
export type ElementErrorPhase = "connect" | "conflict" | "lyrics" | "source" | "theme";
|
|
19
|
+
/** @public */
|
|
20
|
+
export interface ElementErrorDetail {
|
|
21
|
+
/** What the element was doing. Nothing thrown by a tick is reported here: see the README. */
|
|
22
|
+
phase: ElementErrorPhase;
|
|
23
|
+
error: Error;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* What the element is doing, and why it is not doing what it was asked. Every `braccato:error` is
|
|
27
|
+
* dispatched a microtask after the fact so that a listener added straight after the element was
|
|
28
|
+
* inserted still hears it; this is the answer for a consumer that added one later than that, or
|
|
29
|
+
* never.
|
|
30
|
+
*
|
|
31
|
+
* @public
|
|
32
|
+
*/
|
|
33
|
+
export type ElementStatus = "idle" | "rendering" | "theme-conflict" | "no-browsing-context";
|
|
34
|
+
/** @public */
|
|
35
|
+
export interface LyricsLoadedDetail {
|
|
36
|
+
lineCount: number;
|
|
37
|
+
syncType: LyricSyncType;
|
|
38
|
+
}
|
|
39
|
+
/** @public */
|
|
40
|
+
export interface LineClickDetail {
|
|
41
|
+
/** Where the click asked the player to go, in seconds. */
|
|
42
|
+
timeS: number;
|
|
43
|
+
}
|
|
44
|
+
/** @public */
|
|
45
|
+
export interface ScrollStateDetail {
|
|
46
|
+
/** Whether the user has scrolled away, so autoscroll is waiting rather than following the song. */
|
|
47
|
+
userScrolling: boolean;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Mounts a lyrics view into itself. The renderer is built when the element is connected and
|
|
51
|
+
* destroyed when it is disconnected, so an element that is moved around the page rebuilds rather
|
|
52
|
+
* than going quiet, and every property may be written before either has happened.
|
|
53
|
+
*
|
|
54
|
+
* `dir` is not among the properties, and that is the point: `HTMLElement` already reflects it, the
|
|
55
|
+
* lines this module builds carry `dir="auto"` and resolve their own direction from their text, and a
|
|
56
|
+
* property here would be a second opinion about a question the platform has already answered.
|
|
57
|
+
*/
|
|
58
|
+
export declare class BraccatoLyricsElement extends HTMLElement {
|
|
59
|
+
#private;
|
|
60
|
+
static readonly observedAttributes: string[];
|
|
61
|
+
/**
|
|
62
|
+
* The song. An empty array clears the view, so a consumer between songs has a way to say so.
|
|
63
|
+
*/
|
|
64
|
+
get lyrics(): Lyric[] | null;
|
|
65
|
+
set lyrics(lyrics: Lyric[] | null);
|
|
66
|
+
/**
|
|
67
|
+
* How the lines are built, beyond the lines themselves: whether a loader is still covering the
|
|
68
|
+
* view, and whether these lyrics are a "not found" placeholder rather than a song. The second one
|
|
69
|
+
* is what keeps passive scrolling off a one line message it would otherwise drift for the length
|
|
70
|
+
* of the song.
|
|
71
|
+
*
|
|
72
|
+
* Read by the next build rather than causing one, so a consumer writes it beside `lyrics` rather
|
|
73
|
+
* than instead of it, and writing both renders once.
|
|
74
|
+
*/
|
|
75
|
+
get lyricsOptions(): ElementLyricsOptions;
|
|
76
|
+
set lyricsOptions(options: ElementLyricsOptions);
|
|
77
|
+
/**
|
|
78
|
+
* The media element the lyrics follow, as a CSS selector resolved in this element's own document
|
|
79
|
+
* or as the element itself. Setting it binds and null unbinds, and while it is bound the element
|
|
80
|
+
* reads the clock rather than being told it: `currentTime` and `playing` become what it reports.
|
|
81
|
+
*
|
|
82
|
+
* Bound only while connected, the way the renderer is built only while connected, and a selector
|
|
83
|
+
* is resolved again every time it is written and every time the element connects.
|
|
84
|
+
*/
|
|
85
|
+
get source(): HTMLMediaElement | string | null;
|
|
86
|
+
set source(source: HTMLMediaElement | string | null);
|
|
87
|
+
/**
|
|
88
|
+
* The media element `source` resolved to. Null whenever nothing is being followed, which is the
|
|
89
|
+
* answer for a selector that matched nothing and for an element that is not connected.
|
|
90
|
+
*/
|
|
91
|
+
get mediaElement(): HTMLMediaElement | null;
|
|
92
|
+
/**
|
|
93
|
+
* Playback position in seconds, not milliseconds: the module ticks in seconds, and converting here
|
|
94
|
+
* would leave the element and the renderer underneath it disagreeing about what a number means.
|
|
95
|
+
* Writing it renders the view again, so whoever owns the clock drives the lyrics by writing this.
|
|
96
|
+
*
|
|
97
|
+
* While a media element is bound it is the one that owns the clock, so a write is dropped and this
|
|
98
|
+
* keeps reporting what the binding last read. Dropped rather than reported: a consumer who left
|
|
99
|
+
* their own frame loop running would otherwise be told about it sixty times a second.
|
|
100
|
+
*/
|
|
101
|
+
get currentTime(): number;
|
|
102
|
+
set currentTime(currentTimeS: number);
|
|
103
|
+
/** An output rather than an input while a media element is bound, exactly as `currentTime` is. */
|
|
104
|
+
get playing(): boolean;
|
|
105
|
+
set playing(playing: boolean);
|
|
106
|
+
/**
|
|
107
|
+
* The rest of a tick: the user offsets the clock is matched against, whether passive scrolling is
|
|
108
|
+
* switched on for unsynced lyrics, and the timestamp of the player snapshot the clock came from.
|
|
109
|
+
*
|
|
110
|
+
* That last one matters beyond this element. The playback clock the module compares a tick
|
|
111
|
+
* against is module scope, so an element sharing a realm with another view has to be given the
|
|
112
|
+
* same snapshot timestamps that view is, or every tick reads as a jump away from the other one.
|
|
113
|
+
*
|
|
114
|
+
* Read by the next tick rather than causing one, so a consumer that writes these and the clock on
|
|
115
|
+
* the same frame renders the view once rather than twice.
|
|
116
|
+
*/
|
|
117
|
+
get tickOptions(): ElementTickOptions;
|
|
118
|
+
set tickOptions(options: ElementTickOptions);
|
|
119
|
+
/**
|
|
120
|
+
* A compiled stylesheet. Its `blyrics-*` comments configure the module and the sheet itself goes
|
|
121
|
+
* into this element's document. An empty one puts every setting back to its default, and is
|
|
122
|
+
* applied like any other: the settings are module scope, so an element that applied nothing would
|
|
123
|
+
* render against whatever the last theme in that bundle left behind. What that costs is that
|
|
124
|
+
* connecting an element nobody gave a theme empties the theme element already in its document.
|
|
125
|
+
*/
|
|
126
|
+
get theme(): string;
|
|
127
|
+
set theme(css: string);
|
|
128
|
+
/**
|
|
129
|
+
* Overrides for anything the renderer asks of its surroundings. Every member has a default, so a
|
|
130
|
+
* consumer with nothing to say leaves this alone. Writing it while connected rebuilds the view:
|
|
131
|
+
* the renderer is handed its host once, when it is created.
|
|
132
|
+
*/
|
|
133
|
+
get host(): Partial<LyricsRendererHost>;
|
|
134
|
+
set host(overrides: Partial<LyricsRendererHost>);
|
|
135
|
+
/**
|
|
136
|
+
* The renderer underneath, for a consumer who outgrows the element. Null while disconnected, and a
|
|
137
|
+
* different one after every reconnection.
|
|
138
|
+
*/
|
|
139
|
+
get renderer(): LyricsRenderer | null;
|
|
140
|
+
/**
|
|
141
|
+
* What the element is doing, asked rather than listened for. `theme-conflict` is the one that says
|
|
142
|
+
* the view is on the screen but not necessarily the way it was asked for: the theme settings are
|
|
143
|
+
* module scope, so a document with two elements holding different themes renders both against
|
|
144
|
+
* whichever was applied last.
|
|
145
|
+
*
|
|
146
|
+
* A `source` that named nothing to follow is deliberately not one of these. The element is still
|
|
147
|
+
* rendering, and a status saying otherwise would trade one true answer for another. What a
|
|
148
|
+
* consumer who was not listening for the error reads instead is `mediaElement`, which is null
|
|
149
|
+
* while `source` still holds the selector it could not resolve.
|
|
150
|
+
*/
|
|
151
|
+
get status(): ElementStatus;
|
|
152
|
+
connectedCallback(): void;
|
|
153
|
+
disconnectedCallback(): void;
|
|
154
|
+
attributeChangedCallback(name: string, _oldValue: string | null, newValue: string | null): void;
|
|
155
|
+
}
|