@braccato/core 1.0.0 → 1.1.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/README.md +7 -1
- package/dist/README.md +7 -1
- package/dist/element.js +8 -1
- package/dist/engine.d.ts +1 -0
- package/dist/engine.js +32 -0
- package/dist/types.d.ts +7 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -81,7 +81,7 @@ connects, and everything it was handed by then is applied at once.
|
|
|
81
81
|
| `mediaElement` | | `HTMLMediaElement \| null` (get) | `null` | What `source` resolved to. Null while disconnected, and null for a selector that missed. |
|
|
82
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
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,
|
|
84
|
+
| `tickOptions` | | `ElementTickOptions` | `{}` | The rest of a tick: four offsets taken off the clock before it is matched, whether passive scrolling is on, when the clock was sampled, and the rate the song is playing at. |
|
|
85
85
|
| `theme` | `theme` | `string` | `""` | A compiled stylesheet. See Theming. |
|
|
86
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
87
|
| `renderer` | | `LyricsRenderer \| null` (get) | `null` | The renderer underneath, for the day the tag runs out. A different one after every reconnection. |
|
|
@@ -256,6 +256,12 @@ either is dropped and the getter keeps reporting what the binding last read. Dro
|
|
|
256
256
|
reported, because a consumer who bound a source and left their own frame loop running would otherwise
|
|
257
257
|
be told about it sixty times a second. Unbind and the clock goes back to whoever asked for it.
|
|
258
258
|
|
|
259
|
+
The rate is read off the media element too, and passed on as `tickOptions.playbackRate`, so a song at
|
|
260
|
+
half or double speed animates at half or double speed rather than sweeping at 1x and being corrected
|
|
261
|
+
on the next tick. A consumer driving the clock itself sets that option instead. Only the animations
|
|
262
|
+
that follow the song are scaled: a line's exit, a word's fade and the scroll between lines keep the
|
|
263
|
+
timing the theme asked for at every rate.
|
|
264
|
+
|
|
259
265
|
A reading the media element has not refreshed yet is carried forward at the playback rate it was
|
|
260
266
|
taken at, capped at 100ms of frame time. That cap is what covers a stall: the view runs at most 100ms
|
|
261
267
|
past the last real reading and then waits with it. What it costs is a step backwards when the clock
|
package/dist/README.md
CHANGED
|
@@ -81,7 +81,7 @@ connects, and everything it was handed by then is applied at once.
|
|
|
81
81
|
| `mediaElement` | | `HTMLMediaElement \| null` (get) | `null` | What `source` resolved to. Null while disconnected, and null for a selector that missed. |
|
|
82
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
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,
|
|
84
|
+
| `tickOptions` | | `ElementTickOptions` | `{}` | The rest of a tick: four offsets taken off the clock before it is matched, whether passive scrolling is on, when the clock was sampled, and the rate the song is playing at. |
|
|
85
85
|
| `theme` | `theme` | `string` | `""` | A compiled stylesheet. See Theming. |
|
|
86
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
87
|
| `renderer` | | `LyricsRenderer \| null` (get) | `null` | The renderer underneath, for the day the tag runs out. A different one after every reconnection. |
|
|
@@ -256,6 +256,12 @@ either is dropped and the getter keeps reporting what the binding last read. Dro
|
|
|
256
256
|
reported, because a consumer who bound a source and left their own frame loop running would otherwise
|
|
257
257
|
be told about it sixty times a second. Unbind and the clock goes back to whoever asked for it.
|
|
258
258
|
|
|
259
|
+
The rate is read off the media element too, and passed on as `tickOptions.playbackRate`, so a song at
|
|
260
|
+
half or double speed animates at half or double speed rather than sweeping at 1x and being corrected
|
|
261
|
+
on the next tick. A consumer driving the clock itself sets that option instead. Only the animations
|
|
262
|
+
that follow the song are scaled: a line's exit, a word's fade and the scroll between lines keep the
|
|
263
|
+
timing the theme asked for at every rate.
|
|
264
|
+
|
|
259
265
|
A reading the media element has not refreshed yet is carried forward at the playback rate it was
|
|
260
266
|
taken at, capped at 100ms of frame time. That cap is what covers a stall: the view runs at most 100ms
|
|
261
267
|
past the last real reading and then waits with it. What it costs is a step backwards when the clock
|
package/dist/element.js
CHANGED
|
@@ -422,8 +422,15 @@ export class BraccatoLyricsElement extends HTMLElement {
|
|
|
422
422
|
// consumer whose clock runs before the lyrics arrive is the ordinary case rather than a fault.
|
|
423
423
|
if (renderer === null || renderer.container === null)
|
|
424
424
|
return;
|
|
425
|
+
// The rate a bound media element is playing at is the element's to report, the same way the
|
|
426
|
+
// clock is, so it overrides what the consumer wrote for exactly as long as the binding lasts.
|
|
427
|
+
const boundRate = this.#media === null ? undefined : this.#media.playbackRate;
|
|
425
428
|
// The play state last, so a consumer writing plain JavaScript cannot answer that question twice.
|
|
426
|
-
renderer.tick(this.#currentTimeS, {
|
|
429
|
+
renderer.tick(this.#currentTimeS, {
|
|
430
|
+
...this.#tickOptions,
|
|
431
|
+
...(boundRate === undefined ? {} : { playbackRate: boundRate }),
|
|
432
|
+
isPlaying: this.#playing,
|
|
433
|
+
});
|
|
427
434
|
}
|
|
428
435
|
#upgradeProperty(name) {
|
|
429
436
|
// Written through the class rather than `this`: TypeScript refuses an indexed write to a
|
package/dist/engine.d.ts
CHANGED
|
@@ -56,6 +56,7 @@ export interface AnimationEngineInstance extends AnimEngineViewState {
|
|
|
56
56
|
* from a tick, so it has no options object to read.
|
|
57
57
|
*/
|
|
58
58
|
passiveScrollEnabled: boolean;
|
|
59
|
+
playbackRate: number;
|
|
59
60
|
passiveRAFId: number | null;
|
|
60
61
|
pendingLyricsUpdateFrame: number | null;
|
|
61
62
|
learnedAnimationTimingOffsetMs: number;
|
package/dist/engine.js
CHANGED
|
@@ -145,6 +145,7 @@ export function createAnimationEngineInstance(engineDocument, engineWindow, host
|
|
|
145
145
|
cachedCSSValues: new Map(),
|
|
146
146
|
cachedAnimationSettings: null,
|
|
147
147
|
passiveScrollEnabled: false,
|
|
148
|
+
playbackRate: 1,
|
|
148
149
|
passiveRAFId: null,
|
|
149
150
|
pendingLyricsUpdateFrame: null,
|
|
150
151
|
learnedAnimationTimingOffsetMs: 0,
|
|
@@ -351,8 +352,29 @@ function trackLyricAnimationTiming(engine, animation, timing) {
|
|
|
351
352
|
...timing,
|
|
352
353
|
appliedTimingOffsetMs: timing.appliedTimingOffsetMs ?? engine.learnedAnimationTimingOffsetMs,
|
|
353
354
|
});
|
|
355
|
+
// Being tracked is what makes an animation the song's rather than the interface's, so it is also
|
|
356
|
+
// what decides which ones follow the song's rate.
|
|
357
|
+
animation.playbackRate = engine.playbackRate;
|
|
354
358
|
return animation;
|
|
355
359
|
}
|
|
360
|
+
/**
|
|
361
|
+
* Puts the animations already running onto a new rate. Setting `playbackRate` keeps `currentTime`,
|
|
362
|
+
* so each one carries on from where the song left it rather than restarting.
|
|
363
|
+
*/
|
|
364
|
+
function applyPlaybackRateToRunningAnimations(engine) {
|
|
365
|
+
for (const line of engine.lines) {
|
|
366
|
+
for (const animation of line.animations) {
|
|
367
|
+
if (animationTimingTracks.has(animation))
|
|
368
|
+
animation.playbackRate = engine.playbackRate;
|
|
369
|
+
}
|
|
370
|
+
for (const part of line.parts) {
|
|
371
|
+
for (const animation of part.animations) {
|
|
372
|
+
if (animationTimingTracks.has(animation))
|
|
373
|
+
animation.playbackRate = engine.playbackRate;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
356
378
|
function correctedAnimationTimeMs(targetTimeMs, appliedTimingOffsetMs, maxTimeMs) {
|
|
357
379
|
const scheduledTimeMs = targetTimeMs - appliedTimingOffsetMs;
|
|
358
380
|
return maxTimeMs === undefined ? scheduledTimeMs : Math.min(scheduledTimeMs, maxTimeMs);
|
|
@@ -1452,14 +1474,24 @@ export function resolveTickOptions(options) {
|
|
|
1452
1474
|
richsyncOffsetTrim: options.richsyncOffsetTrim ?? 0,
|
|
1453
1475
|
lineOffsetTrim: options.lineOffsetTrim ?? 0,
|
|
1454
1476
|
passiveScrollEnabled: options.passiveScrollEnabled ?? false,
|
|
1477
|
+
playbackRate: resolvePlaybackRate(options.playbackRate),
|
|
1455
1478
|
};
|
|
1456
1479
|
}
|
|
1480
|
+
// A rate of zero or less would freeze every animation that follows the song, which is a second
|
|
1481
|
+
// answer to the question `isPlaying` already answers.
|
|
1482
|
+
function resolvePlaybackRate(rate) {
|
|
1483
|
+
return rate !== undefined && Number.isFinite(rate) && rate > 0 ? rate : 1;
|
|
1484
|
+
}
|
|
1457
1485
|
/**
|
|
1458
1486
|
* Renders one view against a tick with nothing left out.
|
|
1459
1487
|
*/
|
|
1460
1488
|
export function tickView(engine, currentTime, options) {
|
|
1461
1489
|
const { eventCreationTime, isPlaying, smoothScroll } = options;
|
|
1462
1490
|
engine.passiveScrollEnabled = options.passiveScrollEnabled;
|
|
1491
|
+
if (engine.playbackRate !== options.playbackRate) {
|
|
1492
|
+
engine.playbackRate = options.playbackRate;
|
|
1493
|
+
applyPlaybackRateToRunningAnimations(engine);
|
|
1494
|
+
}
|
|
1463
1495
|
const now = Date.now();
|
|
1464
1496
|
if (currentTime === 0 && !isPlaying) {
|
|
1465
1497
|
return "ok";
|
package/dist/types.d.ts
CHANGED
|
@@ -28,6 +28,13 @@ export interface TickOptions {
|
|
|
28
28
|
* false.
|
|
29
29
|
*/
|
|
30
30
|
passiveScrollEnabled?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* How fast the song moves through its own timeline, as a multiple of real time. Defaults to 1, and
|
|
33
|
+
* zero or less reads as 1 because `isPlaying` is what says a song is stopped. Scales the animations
|
|
34
|
+
* that follow the song; a line's exit, a word's fade and the scroll between lines are the
|
|
35
|
+
* interface's own gestures and keep the timing the theme asked for at every rate.
|
|
36
|
+
*/
|
|
37
|
+
playbackRate?: number;
|
|
31
38
|
}
|
|
32
39
|
/**
|
|
33
40
|
* A tick with nothing left out. The engine reads every field unconditionally, so the defaults are
|