@braccato/core 1.16.5 → 1.16.7
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/README.md +30 -1
- package/dist/README.md +30 -1
- package/dist/engine.d.ts +1 -0
- package/dist/engine.js +84 -17
- package/dist/styles/instrumental.css +6 -0
- package/dist/styles/lyrics.css +5 -0
- package/dist/util.d.ts +5 -0
- package/dist/util.js +7 -0
- package/dist/view.js +3 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -328,6 +328,35 @@ It is written only when a word's state changes rather than every frame, so selec
|
|
|
328
328
|
theme nothing per tick. A line synced word, which has no duration of its own, is `active` from its
|
|
329
329
|
start until the next word begins.
|
|
330
330
|
|
|
331
|
+
### Browser engine
|
|
332
|
+
|
|
333
|
+
On Firefox the container carries `data-browser-engine="gecko"`. The renderer's Gecko-only CSS keys on
|
|
334
|
+
it, and a theme can do the same to target Firefox without sniffing the user agent. It is detected from
|
|
335
|
+
a feature only Gecko ships, so forks of Firefox carry it too. Script reads the same answer from
|
|
336
|
+
`isGecko()` in `@braccato/core/util`, which the renderer's Gecko-only script checks once on load.
|
|
337
|
+
|
|
338
|
+
```css
|
|
339
|
+
.blyrics-container[data-browser-engine="gecko"] .blyrics--line {
|
|
340
|
+
/* Firefox only */
|
|
341
|
+
}
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
The letter wave uses both. WebRender rounds text moved by a 2D translation to whole device
|
|
345
|
+
pixels, and it only redraws a cached tile once something on it has moved a quarter of a pixel, so the
|
|
346
|
+
wave stepped on Firefox. Lettered words on animating lines get a distant `perspective` there, and each
|
|
347
|
+
base letter in the wave runs an invisible additive opacity ripple that changes every frame and forces the
|
|
348
|
+
redraw. Chromium gets neither.
|
|
349
|
+
|
|
350
|
+
The playback clock checks `isGecko()`. Firefox advances a media element's `currentTime` about every 40ms, so
|
|
351
|
+
each read lags the true time by a different amount and never leads it. The renderer took each new lag
|
|
352
|
+
for drift and reseeked the line, so the highlight swipe jumped back mid-word. On Firefox the tick keeps
|
|
353
|
+
its own clock instead: a read ahead of it is taken, a read behind it pulls it back by a tenth per
|
|
354
|
+
second, and a read more than 60ms off, about one Firefox step plus margin, counts as a seek.
|
|
355
|
+
|
|
356
|
+
The instrumental fill uses the attribute. Gecko resolves a `fill-box` transform origin wrongly for a shape
|
|
357
|
+
inside a `clipPath`, so the flattening wave crest came loose from the rising fill. On Firefox the wave
|
|
358
|
+
scales from its floor in user units instead, which Chromium already renders identically.
|
|
359
|
+
|
|
331
360
|
### Class names
|
|
332
361
|
|
|
333
362
|
These are published API rather than implementation. Renaming one costs a migration rather than a
|
|
@@ -428,7 +457,7 @@ Four leaves import nothing at all, so taking one does not pull the engine into y
|
|
|
428
457
|
- `@braccato/core/constants` for the class names and element ids above
|
|
429
458
|
- `@braccato/core/text` for script detection: `testRtl`, `containsNonLatin`, `detectNonLatinLanguage`
|
|
430
459
|
- `@braccato/core/themeSettings` for `parseThemeConfig`
|
|
431
|
-
- `@braccato/core/util` for pure helpers such as `clamp` and `toMs`
|
|
460
|
+
- `@braccato/core/util` for pure helpers such as `clamp` and `toMs`, and `isGecko` for engine-specific fixes
|
|
432
461
|
|
|
433
462
|
Two notes on the element entry point. A browser extension's isolated world has no custom element
|
|
434
463
|
registry, so `window.customElements` is null there and importing this file throws where it registers.
|
package/dist/README.md
CHANGED
|
@@ -328,6 +328,35 @@ It is written only when a word's state changes rather than every frame, so selec
|
|
|
328
328
|
theme nothing per tick. A line synced word, which has no duration of its own, is `active` from its
|
|
329
329
|
start until the next word begins.
|
|
330
330
|
|
|
331
|
+
### Browser engine
|
|
332
|
+
|
|
333
|
+
On Firefox the container carries `data-browser-engine="gecko"`. The renderer's Gecko-only CSS keys on
|
|
334
|
+
it, and a theme can do the same to target Firefox without sniffing the user agent. It is detected from
|
|
335
|
+
a feature only Gecko ships, so forks of Firefox carry it too. Script reads the same answer from
|
|
336
|
+
`isGecko()` in `@braccato/core/util`, which the renderer's Gecko-only script checks once on load.
|
|
337
|
+
|
|
338
|
+
```css
|
|
339
|
+
.blyrics-container[data-browser-engine="gecko"] .blyrics--line {
|
|
340
|
+
/* Firefox only */
|
|
341
|
+
}
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
The letter wave uses both. WebRender rounds text moved by a 2D translation to whole device
|
|
345
|
+
pixels, and it only redraws a cached tile once something on it has moved a quarter of a pixel, so the
|
|
346
|
+
wave stepped on Firefox. Lettered words on animating lines get a distant `perspective` there, and each
|
|
347
|
+
base letter in the wave runs an invisible additive opacity ripple that changes every frame and forces the
|
|
348
|
+
redraw. Chromium gets neither.
|
|
349
|
+
|
|
350
|
+
The playback clock checks `isGecko()`. Firefox advances a media element's `currentTime` about every 40ms, so
|
|
351
|
+
each read lags the true time by a different amount and never leads it. The renderer took each new lag
|
|
352
|
+
for drift and reseeked the line, so the highlight swipe jumped back mid-word. On Firefox the tick keeps
|
|
353
|
+
its own clock instead: a read ahead of it is taken, a read behind it pulls it back by a tenth per
|
|
354
|
+
second, and a read more than 60ms off, about one Firefox step plus margin, counts as a seek.
|
|
355
|
+
|
|
356
|
+
The instrumental fill uses the attribute. Gecko resolves a `fill-box` transform origin wrongly for a shape
|
|
357
|
+
inside a `clipPath`, so the flattening wave crest came loose from the rising fill. On Firefox the wave
|
|
358
|
+
scales from its floor in user units instead, which Chromium already renders identically.
|
|
359
|
+
|
|
331
360
|
### Class names
|
|
332
361
|
|
|
333
362
|
These are published API rather than implementation. Renaming one costs a migration rather than a
|
|
@@ -428,7 +457,7 @@ Four leaves import nothing at all, so taking one does not pull the engine into y
|
|
|
428
457
|
- `@braccato/core/constants` for the class names and element ids above
|
|
429
458
|
- `@braccato/core/text` for script detection: `testRtl`, `containsNonLatin`, `detectNonLatinLanguage`
|
|
430
459
|
- `@braccato/core/themeSettings` for `parseThemeConfig`
|
|
431
|
-
- `@braccato/core/util` for pure helpers such as `clamp` and `toMs`
|
|
460
|
+
- `@braccato/core/util` for pure helpers such as `clamp` and `toMs`, and `isGecko` for engine-specific fixes
|
|
432
461
|
|
|
433
462
|
Two notes on the element entry point. A browser extension's isolated world has no custom element
|
|
434
463
|
registry, so `window.customElements` is null there and importing this file throws where it registers.
|
package/dist/engine.d.ts
CHANGED
|
@@ -98,6 +98,7 @@ export declare function createAnimationEngineInstance(engineDocument: Document,
|
|
|
98
98
|
* rather than as a jump away from the previous one.
|
|
99
99
|
*/
|
|
100
100
|
export declare function resetPlaybackClock(): void;
|
|
101
|
+
export declare function smoothedPlaybackTime(previousTime: number, previousAt: number, rawTime: number, now: number, playbackRate: number): number;
|
|
101
102
|
/**
|
|
102
103
|
* The user asked for autoscroll back, now.
|
|
103
104
|
*/
|
package/dist/engine.js
CHANGED
|
@@ -20,7 +20,7 @@ import { ANIMATING_CLASS, CREDITS_CLASS, CURRENT_LYRICS_CLASS, FOOTER_CLASS, LIN
|
|
|
20
20
|
import { INSTRUMENTAL_WAVE_PATH_HIGH, INSTRUMENTAL_WAVE_PATH_LOW } from "./instrumental.js";
|
|
21
21
|
import { layoutStage, planStage, queuedStageY, stageTextSpan, } from "./stage.js";
|
|
22
22
|
import { registerThemeSetting } from "./themeSettings.js";
|
|
23
|
-
import { clamp, getRelativeLayoutBounds, positiveModulo, roundedMs, toMs } from "./util.js";
|
|
23
|
+
import { clamp, getRelativeLayoutBounds, isGecko, positiveModulo, roundedMs, toMs } from "./util.js";
|
|
24
24
|
const NO_LYRICS_ELEMENT_LOG = "No lyrics element found on the page, skipping lyrics injection";
|
|
25
25
|
const LYRICS_CHECK_INTERVAL_ERROR = "Error in lyrics check interval:";
|
|
26
26
|
const PAUSING_LYRICS_SCROLL_LOG = "Pausing Lyrics Autoscroll Due to User Scroll";
|
|
@@ -29,6 +29,11 @@ const PASSIVE_USER_SCROLL_RESUME_DELAY_MS = 5000;
|
|
|
29
29
|
const LYRIC_ENDING_THRESHOLD_S = registerThemeSetting("blyrics-lyric-ending-threshold-s", 0.5);
|
|
30
30
|
const EARLY_SCROLL_CONSIDER = registerThemeSetting("blyrics-early-scroll-consider-s", 0.54);
|
|
31
31
|
const TIME_JUMP_THRESHOLD = 0.5;
|
|
32
|
+
// Gecko steps media time every ~40ms, so reads lag by a random amount (never lead) that reseeks lines as drift.
|
|
33
|
+
const SMOOTH_PLAYBACK_CLOCK = isGecko();
|
|
34
|
+
// One Gecko step (~45ms) plus margin: a read further off is a seek or a timer spike, not media lag.
|
|
35
|
+
const CLOCK_RESYNC_THRESHOLD_S = 0.06;
|
|
36
|
+
const CLOCK_SLEW_PER_S = 0.1;
|
|
32
37
|
const REDUCED_MOTION_QUERY = "(prefers-reduced-motion: reduce)";
|
|
33
38
|
const SCROLL_PREPARE_LEAD_MS = 120;
|
|
34
39
|
const LINE_SCROLL_FALLBACK_DURATION_MS = 750;
|
|
@@ -185,6 +190,8 @@ const playbackClock = {
|
|
|
185
190
|
lastTime: 0,
|
|
186
191
|
lastPlayState: false,
|
|
187
192
|
lastEventCreationTime: NO_PLAYER_SNAPSHOT,
|
|
193
|
+
smoothedTime: 0,
|
|
194
|
+
smoothedAt: NO_PLAYER_SNAPSHOT,
|
|
188
195
|
};
|
|
189
196
|
/**
|
|
190
197
|
* Forgets the last player snapshot, so the next tick is treated as the first one of a new song
|
|
@@ -194,6 +201,17 @@ export function resetPlaybackClock() {
|
|
|
194
201
|
playbackClock.lastTime = 0;
|
|
195
202
|
playbackClock.lastPlayState = false;
|
|
196
203
|
playbackClock.lastEventCreationTime = NO_PLAYER_SNAPSHOT;
|
|
204
|
+
playbackClock.smoothedAt = NO_PLAYER_SNAPSHOT;
|
|
205
|
+
}
|
|
206
|
+
export function smoothedPlaybackTime(previousTime, previousAt, rawTime, now, playbackRate) {
|
|
207
|
+
if (previousAt === NO_PLAYER_SNAPSHOT)
|
|
208
|
+
return rawTime;
|
|
209
|
+
const elapsedS = Math.max(0, now - previousAt) / 1000;
|
|
210
|
+
const predictedTime = previousTime + elapsedS * playbackRate;
|
|
211
|
+
const errorS = rawTime - predictedTime;
|
|
212
|
+
if (errorS >= 0 || Math.abs(errorS) >= CLOCK_RESYNC_THRESHOLD_S)
|
|
213
|
+
return rawTime;
|
|
214
|
+
return predictedTime + errorS * Math.min(1, elapsedS * CLOCK_SLEW_PER_S);
|
|
197
215
|
}
|
|
198
216
|
// -- View operations --------------------------
|
|
199
217
|
/**
|
|
@@ -436,7 +454,7 @@ function mirrorImageHighlightAnimations(part, animations) {
|
|
|
436
454
|
mirror.currentTime = animation.currentTime;
|
|
437
455
|
const timing = animationTimingTracks.get(animation);
|
|
438
456
|
if (timing)
|
|
439
|
-
animationTimingTracks.set(mirror, timing);
|
|
457
|
+
animationTimingTracks.set(mirror, { ...timing, skipDriftSample: true });
|
|
440
458
|
mirrors.push(mirror);
|
|
441
459
|
}
|
|
442
460
|
return mirrors;
|
|
@@ -529,8 +547,11 @@ function timingValueToMs(value) {
|
|
|
529
547
|
function animationCurrentTimeMs(animation) {
|
|
530
548
|
return timingValueToMs(animation.currentTime);
|
|
531
549
|
}
|
|
532
|
-
|
|
533
|
-
|
|
550
|
+
// A finished animation's currentTime stops at its end time, which counts the delay, so that is where
|
|
551
|
+
// the expected time has to stop too. Clamping to the active duration alone saturates a delayed
|
|
552
|
+
// animation as soon as it is created and hides any drift in it.
|
|
553
|
+
function animationEndTimeMs(animation) {
|
|
554
|
+
return timingValueToMs(animation.effect?.getComputedTiming().endTime);
|
|
534
555
|
}
|
|
535
556
|
function wrappedTimingOffsetMs(actualTimeMs, expectedTimeMs, wrapDurationMs) {
|
|
536
557
|
return positiveModulo(actualTimeMs - expectedTimeMs + wrapDurationMs / 2, wrapDurationMs) - wrapDurationMs / 2;
|
|
@@ -542,18 +563,18 @@ function normalizeAnimationTimeMs(animation, timeMs, timing) {
|
|
|
542
563
|
if (timing.wrapDurationMs && timing.wrapDurationMs > 0) {
|
|
543
564
|
return positiveModulo(timeMs, timing.wrapDurationMs);
|
|
544
565
|
}
|
|
545
|
-
const
|
|
546
|
-
if (
|
|
547
|
-
return Math.min(timeMs,
|
|
566
|
+
const endTimeMs = animationEndTimeMs(animation);
|
|
567
|
+
if (endTimeMs !== null && Number.isFinite(endTimeMs)) {
|
|
568
|
+
return Math.min(timeMs, endTimeMs);
|
|
548
569
|
}
|
|
549
570
|
return timeMs;
|
|
550
571
|
}
|
|
551
572
|
function animationTimingSample(part, animation, currentTime) {
|
|
552
|
-
|
|
573
|
+
const timing = animationTimingTracks.get(animation);
|
|
574
|
+
if (!timing || timing.skipDriftSample) {
|
|
553
575
|
return null;
|
|
554
576
|
}
|
|
555
|
-
|
|
556
|
-
if (!timing) {
|
|
577
|
+
if (animation.playState === "idle") {
|
|
557
578
|
return null;
|
|
558
579
|
}
|
|
559
580
|
const actualTimeMs = animationCurrentTimeMs(animation);
|
|
@@ -844,6 +865,11 @@ function swipeRamp(config) {
|
|
|
844
865
|
}
|
|
845
866
|
// How far through its float a letter crests, as a keyframe offset.
|
|
846
867
|
const LETTER_WAVE_CREST_OFFSET = 0.4;
|
|
868
|
+
// WebRender redraws a cached tile only after a quarter device pixel of movement, so Gecko letters step.
|
|
869
|
+
// An invisible additive opacity ripple changes every frame and forces the redraw.
|
|
870
|
+
const GECKO_REDRAW = isGecko();
|
|
871
|
+
const GECKO_REDRAW_KEYFRAMES = [{ opacity: 0 }, { opacity: -0.002 }, { opacity: 0 }];
|
|
872
|
+
const GECKO_REDRAW_PERIOD_MS = 200;
|
|
847
873
|
// When each letter's wave starts, counted from the start of the word, and how long it lasts. A letter
|
|
848
874
|
// crests just as the swipe finishes lighting it. If the swipe lights it faster than the float rises,
|
|
849
875
|
// it starts early enough to make that, which on a short word means before the word itself does. If the
|
|
@@ -887,7 +913,11 @@ function startRichSyncedHighlightAnimations(engine, part, config, swipeTimeMs, w
|
|
|
887
913
|
offset: frame.offset,
|
|
888
914
|
maskPosition: frame.maskPosition,
|
|
889
915
|
WebkitMaskPosition: frame.maskPosition,
|
|
890
|
-
})), { duration: sweep.durationMs, delay: sweep.delayMs, easing: sweep.easing, fill: "both" }),
|
|
916
|
+
})), { duration: sweep.durationMs, delay: sweep.delayMs, easing: sweep.easing, fill: "both" }),
|
|
917
|
+
// The last letter's sweep ends last, so it alone covers the whole word.
|
|
918
|
+
index === sweeps.length - 1 && target === highlightLetters[index]
|
|
919
|
+
? swipeTiming
|
|
920
|
+
: { ...swipeTiming, skipDriftSample: true });
|
|
891
921
|
animation.currentTime = swipeCurrentTimeMs;
|
|
892
922
|
if (index === 0)
|
|
893
923
|
swipeAnimation = animation;
|
|
@@ -1042,20 +1072,50 @@ function startWordAnimations(engine, part, config, currentTime, appliedTimingOff
|
|
|
1042
1072
|
// as the wave running behind once it finishes. Run the wave's clock ahead by the head start
|
|
1043
1073
|
// instead, the way the swipe carries its lead, so every delay stays at zero or above.
|
|
1044
1074
|
const waveLeadMs = Math.max(0, -Math.min(...waves.map(wave => wave.delayMs)));
|
|
1045
|
-
const
|
|
1075
|
+
const waveEndsMs = waves.map(wave => wave.delayMs + wave.durationMs);
|
|
1076
|
+
const lastWaveEndMs = Math.max(...waveEndsMs);
|
|
1077
|
+
const lastEndingWaveIndex = waveEndsMs.lastIndexOf(lastWaveEndMs);
|
|
1078
|
+
const cascadeDurationMs = waveLeadMs + lastWaveEndMs;
|
|
1046
1079
|
const floatStartMs = correctedAnimationTimeMs(wordTimeMs + waveLeadMs, appliedTimingOffsetMs, cascadeDurationMs);
|
|
1047
1080
|
const floatKeyframeSignature = JSON.stringify(floatKeyframes);
|
|
1048
|
-
|
|
1049
|
-
|
|
1081
|
+
// Glow letters only show while the glow does: never when it is off or renders nothing for this
|
|
1082
|
+
// word, and not once it has faded out. Their waves stop there instead of running to the end.
|
|
1083
|
+
const glowLetters = config.enabled.highlightGlow && !part.glowSuppressed ? part.imageLayers?.glowLetters : undefined;
|
|
1084
|
+
const glowEndMs = Number.parseFloat(config.highlight.imageGlowOpacityTo) === 0 ? waveLeadMs + glowDurationMs : Infinity;
|
|
1085
|
+
const animateLetters = (letters, endMs, sampled) => {
|
|
1086
|
+
letters?.forEach((letterElement, index) => {
|
|
1087
|
+
const delayMs = waves[index].delayMs + waveLeadMs;
|
|
1088
|
+
if (delayMs >= endMs)
|
|
1089
|
+
return;
|
|
1090
|
+
const endDelayMs = Math.min(0, endMs - delayMs - waves[index].durationMs);
|
|
1050
1091
|
const animation = trackLyricAnimationTiming(engine, acquireWaveAnimation(engine, letterElement, floatKeyframes, floatKeyframeSignature, {
|
|
1051
1092
|
duration: waves[index].durationMs,
|
|
1052
|
-
delay:
|
|
1093
|
+
delay: delayMs,
|
|
1094
|
+
endDelay: endDelayMs,
|
|
1053
1095
|
fill: "forwards",
|
|
1054
|
-
}), {
|
|
1096
|
+
}), {
|
|
1097
|
+
appliedTimingOffsetMs,
|
|
1098
|
+
offsetMs: waveLeadMs,
|
|
1099
|
+
// The wave that ends last covers the whole word. A theme's swipe ramp can make that an early letter.
|
|
1100
|
+
skipDriftSample: !sampled || index !== lastEndingWaveIndex,
|
|
1101
|
+
});
|
|
1055
1102
|
animation.currentTime = floatStartMs;
|
|
1056
1103
|
wobbleAnimations.push(animation);
|
|
1104
|
+
if (!GECKO_REDRAW || !sampled)
|
|
1105
|
+
return;
|
|
1106
|
+
const redraw = trackLyricAnimationTiming(engine, letterElement.animate(GECKO_REDRAW_KEYFRAMES, {
|
|
1107
|
+
duration: GECKO_REDRAW_PERIOD_MS,
|
|
1108
|
+
iterations: waves[index].durationMs / GECKO_REDRAW_PERIOD_MS,
|
|
1109
|
+
delay: delayMs,
|
|
1110
|
+
composite: "add",
|
|
1111
|
+
}), { appliedTimingOffsetMs, offsetMs: waveLeadMs, skipDriftSample: true });
|
|
1112
|
+
redraw.currentTime = floatStartMs;
|
|
1113
|
+
wobbleAnimations.push(redraw);
|
|
1057
1114
|
});
|
|
1058
|
-
}
|
|
1115
|
+
};
|
|
1116
|
+
animateLetters(part.letterElements, Infinity, true);
|
|
1117
|
+
animateLetters(part.highlightLetterElements, Infinity, false);
|
|
1118
|
+
animateLetters(glowLetters, glowEndMs, false);
|
|
1059
1119
|
}
|
|
1060
1120
|
}
|
|
1061
1121
|
part.animations = [
|
|
@@ -2259,6 +2319,13 @@ export function tickView(engine, currentTime, options) {
|
|
|
2259
2319
|
timeOffset = 0;
|
|
2260
2320
|
}
|
|
2261
2321
|
currentTime += timeOffset / 1000;
|
|
2322
|
+
if (SMOOTH_PLAYBACK_CLOCK) {
|
|
2323
|
+
if (isPlaying && !timeJumped) {
|
|
2324
|
+
currentTime = smoothedPlaybackTime(playbackClock.smoothedTime, playbackClock.smoothedAt, currentTime, now, engine.playbackRate);
|
|
2325
|
+
}
|
|
2326
|
+
playbackClock.smoothedTime = currentTime;
|
|
2327
|
+
playbackClock.smoothedAt = isPlaying ? now : NO_PLAYER_SNAPSHOT;
|
|
2328
|
+
}
|
|
2262
2329
|
if (!engine.host.isViewVisible()) {
|
|
2263
2330
|
clearVisibleLyricWillChange(engine);
|
|
2264
2331
|
return "ok";
|
|
@@ -68,6 +68,12 @@
|
|
|
68
68
|
d: var(--blyrics-instrumental-wave-path-high);
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
/* Gecko misplaces a fill-box origin inside a clipPath, so pin the wave's floor (y=4) in user units. */
|
|
72
|
+
.blyrics-container[data-browser-engine="gecko"] .blyrics--wave-path {
|
|
73
|
+
transform-box: view-box;
|
|
74
|
+
transform-origin: 0 4px;
|
|
75
|
+
}
|
|
76
|
+
|
|
71
77
|
/* Trigger the Flattening */
|
|
72
78
|
.blyrics--instrumental.blyrics--animating:not(.blyrics--paused) .blyrics--wave-path {
|
|
73
79
|
/* Flatten the wave to 0 height */
|
package/dist/styles/lyrics.css
CHANGED
|
@@ -177,6 +177,11 @@
|
|
|
177
177
|
unicode-bidi: isolate;
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
+
/* WebRender snaps 2D-translated glyphs to whole pixels; perspective rasterizes them in local space. */
|
|
181
|
+
.blyrics-container[data-browser-engine="gecko"] .blyrics--animating .blyrics-word--lettered {
|
|
182
|
+
perspective: 100000px;
|
|
183
|
+
}
|
|
184
|
+
|
|
180
185
|
.blyrics-word-highlight.blyrics-word-highlight--lettered {
|
|
181
186
|
background-image: none;
|
|
182
187
|
}
|
package/dist/util.d.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
export declare function clamp(value: number, min: number, max: number): number;
|
|
2
2
|
export declare function positiveModulo(value: number, divisor: number): number;
|
|
3
|
+
/**
|
|
4
|
+
* Whether the page renders with Gecko (Firefox). Detects the engine by a feature only it ships, so
|
|
5
|
+
* spoofed user agents and Firefox forks resolve the same way.
|
|
6
|
+
*/
|
|
7
|
+
export declare function isGecko(): boolean;
|
|
3
8
|
export declare function roundedMs(value: number): number;
|
|
4
9
|
/**
|
|
5
10
|
* Converts CSS duration value to milliseconds.
|
package/dist/util.js
CHANGED
|
@@ -4,6 +4,13 @@ export function clamp(value, min, max) {
|
|
|
4
4
|
export function positiveModulo(value, divisor) {
|
|
5
5
|
return ((value % divisor) + divisor) % divisor;
|
|
6
6
|
}
|
|
7
|
+
/**
|
|
8
|
+
* Whether the page renders with Gecko (Firefox). Detects the engine by a feature only it ships, so
|
|
9
|
+
* spoofed user agents and Firefox forks resolve the same way.
|
|
10
|
+
*/
|
|
11
|
+
export function isGecko() {
|
|
12
|
+
return typeof CSS !== "undefined" && CSS.supports("-moz-appearance", "none");
|
|
13
|
+
}
|
|
7
14
|
export function roundedMs(value) {
|
|
8
15
|
return Math.round(value * 10) / 10;
|
|
9
16
|
}
|
package/dist/view.js
CHANGED
|
@@ -15,6 +15,7 @@ import { addSeekHandler, applyDirection, buildLineSyncedParts, createLyricsLine,
|
|
|
15
15
|
import { refreshInstrumentalImages } from "./imageHighlights.js";
|
|
16
16
|
import { createInstrumentalElement } from "./instrumental.js";
|
|
17
17
|
import { applyLyricLanguage, resolveLyricLanguages } from "./language.js";
|
|
18
|
+
import { isGecko } from "./util.js";
|
|
18
19
|
const INITIAL_SKIP_SCROLLS = 2;
|
|
19
20
|
const SKIP_SCROLL_DECAY_MS = 2000;
|
|
20
21
|
function buildInstrumentalLine(doc, lyricElement, lyrics, lineIndex) {
|
|
@@ -73,6 +74,8 @@ export function setLyrics(engine, mount, lyrics, options) {
|
|
|
73
74
|
const doc = engine.document;
|
|
74
75
|
const container = doc.createElement("div");
|
|
75
76
|
container.className = LYRICS_CLASS;
|
|
77
|
+
if (isGecko())
|
|
78
|
+
container.dataset.browserEngine = "gecko";
|
|
76
79
|
mount.replaceChildren(container);
|
|
77
80
|
const allZero = lyrics.every(item => item.startTimeMs === 0);
|
|
78
81
|
const seek = (timeS) => engine.host.seek(timeS);
|