@braccato/core 1.16.4 → 1.16.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/engine.d.ts +5 -0
- package/dist/engine.js +89 -27
- package/package.json +1 -1
package/dist/engine.d.ts
CHANGED
|
@@ -272,6 +272,11 @@ export interface LetterMaskSweep {
|
|
|
272
272
|
easing: string;
|
|
273
273
|
}
|
|
274
274
|
export declare function planLetterMaskSweep(swipe: SwipeRamp, letterCount: number, swipeDurationMs: number, rtl: boolean): LetterMaskSweep[];
|
|
275
|
+
export interface LetterWaveTiming {
|
|
276
|
+
delayMs: number;
|
|
277
|
+
durationMs: number;
|
|
278
|
+
}
|
|
279
|
+
export declare function planLetterWave(swipe: SwipeRamp, letterCount: number, wordDurationMs: number, swipeDurationMs: number, swipeLeadMs: number, floatDurationMs: number): LetterWaveTiming[];
|
|
275
280
|
export declare function clearStyleCaches(engine: AnimationEngineInstance): void;
|
|
276
281
|
export declare function parseColorAlpha(value: string): number | null;
|
|
277
282
|
interface PreparedLineScroll {
|
package/dist/engine.js
CHANGED
|
@@ -436,7 +436,7 @@ function mirrorImageHighlightAnimations(part, animations) {
|
|
|
436
436
|
mirror.currentTime = animation.currentTime;
|
|
437
437
|
const timing = animationTimingTracks.get(animation);
|
|
438
438
|
if (timing)
|
|
439
|
-
animationTimingTracks.set(mirror, timing);
|
|
439
|
+
animationTimingTracks.set(mirror, { ...timing, skipDriftSample: true });
|
|
440
440
|
mirrors.push(mirror);
|
|
441
441
|
}
|
|
442
442
|
return mirrors;
|
|
@@ -529,8 +529,11 @@ function timingValueToMs(value) {
|
|
|
529
529
|
function animationCurrentTimeMs(animation) {
|
|
530
530
|
return timingValueToMs(animation.currentTime);
|
|
531
531
|
}
|
|
532
|
-
|
|
533
|
-
|
|
532
|
+
// A finished animation's currentTime stops at its end time, which counts the delay, so that is where
|
|
533
|
+
// the expected time has to stop too. Clamping to the active duration alone saturates a delayed
|
|
534
|
+
// animation as soon as it is created and hides any drift in it.
|
|
535
|
+
function animationEndTimeMs(animation) {
|
|
536
|
+
return timingValueToMs(animation.effect?.getComputedTiming().endTime);
|
|
534
537
|
}
|
|
535
538
|
function wrappedTimingOffsetMs(actualTimeMs, expectedTimeMs, wrapDurationMs) {
|
|
536
539
|
return positiveModulo(actualTimeMs - expectedTimeMs + wrapDurationMs / 2, wrapDurationMs) - wrapDurationMs / 2;
|
|
@@ -542,18 +545,18 @@ function normalizeAnimationTimeMs(animation, timeMs, timing) {
|
|
|
542
545
|
if (timing.wrapDurationMs && timing.wrapDurationMs > 0) {
|
|
543
546
|
return positiveModulo(timeMs, timing.wrapDurationMs);
|
|
544
547
|
}
|
|
545
|
-
const
|
|
546
|
-
if (
|
|
547
|
-
return Math.min(timeMs,
|
|
548
|
+
const endTimeMs = animationEndTimeMs(animation);
|
|
549
|
+
if (endTimeMs !== null && Number.isFinite(endTimeMs)) {
|
|
550
|
+
return Math.min(timeMs, endTimeMs);
|
|
548
551
|
}
|
|
549
552
|
return timeMs;
|
|
550
553
|
}
|
|
551
554
|
function animationTimingSample(part, animation, currentTime) {
|
|
552
|
-
|
|
555
|
+
const timing = animationTimingTracks.get(animation);
|
|
556
|
+
if (!timing || timing.skipDriftSample) {
|
|
553
557
|
return null;
|
|
554
558
|
}
|
|
555
|
-
|
|
556
|
-
if (!timing) {
|
|
559
|
+
if (animation.playState === "idle") {
|
|
557
560
|
return null;
|
|
558
561
|
}
|
|
559
562
|
const actualTimeMs = animationCurrentTimeMs(animation);
|
|
@@ -833,6 +836,43 @@ export function planLetterMaskSweep(swipe, letterCount, swipeDurationMs, rtl) {
|
|
|
833
836
|
return { keyframes, delayMs: 0, durationMs, easing: swipe.easing };
|
|
834
837
|
});
|
|
835
838
|
}
|
|
839
|
+
function swipeRamp(config) {
|
|
840
|
+
return {
|
|
841
|
+
easing: config.highlight.swipeEasing,
|
|
842
|
+
startFrom: config.highlight.swipeStartFrom,
|
|
843
|
+
startTo: config.highlight.swipeStartTo,
|
|
844
|
+
endFrom: config.highlight.swipeEndFrom,
|
|
845
|
+
endTo: config.highlight.swipeEndTo,
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
// How far through its float a letter crests, as a keyframe offset.
|
|
849
|
+
const LETTER_WAVE_CREST_OFFSET = 0.4;
|
|
850
|
+
// When each letter's wave starts, counted from the start of the word, and how long it lasts. A letter
|
|
851
|
+
// crests just as the swipe finishes lighting it. If the swipe lights it faster than the float rises,
|
|
852
|
+
// it starts early enough to make that, which on a short word means before the word itself does. If the
|
|
853
|
+
// swipe lights it slower, it starts as the swipe touches it and its float stretches to crest on time.
|
|
854
|
+
export function planLetterWave(swipe, letterCount, wordDurationMs, swipeDurationMs, swipeLeadMs, floatDurationMs) {
|
|
855
|
+
const riseMs = floatDurationMs * LETTER_WAVE_CREST_OFFSET;
|
|
856
|
+
const windows = computeLetterSwipeWindows({ ...swipe, easing: "linear" }, letterCount, swipeDurationMs);
|
|
857
|
+
const timings = [];
|
|
858
|
+
for (let index = 0; index < letterCount; index++) {
|
|
859
|
+
if (!windows) {
|
|
860
|
+
// No windows for this ramp, so spread the letters evenly over the word.
|
|
861
|
+
timings.push({ delayMs: (wordDurationMs * index) / letterCount, durationMs: floatDurationMs });
|
|
862
|
+
continue;
|
|
863
|
+
}
|
|
864
|
+
// The swipe runs swipeLeadMs ahead of the word, so take the lead back off.
|
|
865
|
+
const touchedMs = windows[index].delayMs - swipeLeadMs;
|
|
866
|
+
const litMs = touchedMs + windows[index].durationMs;
|
|
867
|
+
if (litMs - riseMs > touchedMs) {
|
|
868
|
+
timings.push({ delayMs: touchedMs, durationMs: (litMs - touchedMs) / LETTER_WAVE_CREST_OFFSET });
|
|
869
|
+
}
|
|
870
|
+
else {
|
|
871
|
+
timings.push({ delayMs: litMs - riseMs, durationMs: floatDurationMs });
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
return timings;
|
|
875
|
+
}
|
|
836
876
|
function startRichSyncedHighlightAnimations(engine, part, config, swipeTimeMs, wordTimeMs, swipeDurationMs, glowDurationMs, appliedTimingOffsetMs) {
|
|
837
877
|
const animations = [];
|
|
838
878
|
const highlight = part.highlightElement;
|
|
@@ -842,13 +882,7 @@ function startRichSyncedHighlightAnimations(engine, part, config, swipeTimeMs, w
|
|
|
842
882
|
const swipeCurrentTimeMs = correctedAnimationTimeMs(swipeTimeMs, appliedTimingOffsetMs, swipeDurationMs);
|
|
843
883
|
const highlightLetters = part.highlightLetterElements;
|
|
844
884
|
if (highlightLetters && highlightLetters.length > 0) {
|
|
845
|
-
const sweeps = planLetterMaskSweep(
|
|
846
|
-
easing: config.highlight.swipeEasing,
|
|
847
|
-
startFrom: config.highlight.swipeStartFrom,
|
|
848
|
-
startTo: config.highlight.swipeStartTo,
|
|
849
|
-
endFrom: config.highlight.swipeEndFrom,
|
|
850
|
-
endTo: config.highlight.swipeEndTo,
|
|
851
|
-
}, highlightLetters.length, swipeDurationMs, part.highlightElement.classList.contains(RTL_CLASS));
|
|
885
|
+
const sweeps = planLetterMaskSweep(swipeRamp(config), highlightLetters.length, swipeDurationMs, part.highlightElement.classList.contains(RTL_CLASS));
|
|
852
886
|
sweeps.forEach((sweep, index) => {
|
|
853
887
|
const targets = [highlightLetters[index], part.imageLayers?.glowLetters[index]].filter((target) => target !== undefined);
|
|
854
888
|
for (const target of targets) {
|
|
@@ -856,7 +890,11 @@ function startRichSyncedHighlightAnimations(engine, part, config, swipeTimeMs, w
|
|
|
856
890
|
offset: frame.offset,
|
|
857
891
|
maskPosition: frame.maskPosition,
|
|
858
892
|
WebkitMaskPosition: frame.maskPosition,
|
|
859
|
-
})), { duration: sweep.durationMs, delay: sweep.delayMs, easing: sweep.easing, fill: "both" }),
|
|
893
|
+
})), { duration: sweep.durationMs, delay: sweep.delayMs, easing: sweep.easing, fill: "both" }),
|
|
894
|
+
// The last letter's sweep ends last, so it alone covers the whole word.
|
|
895
|
+
index === sweeps.length - 1 && target === highlightLetters[index]
|
|
896
|
+
? swipeTiming
|
|
897
|
+
: { ...swipeTiming, skipDriftSample: true });
|
|
860
898
|
animation.currentTime = swipeCurrentTimeMs;
|
|
861
899
|
if (index === 0)
|
|
862
900
|
swipeAnimation = animation;
|
|
@@ -1000,27 +1038,51 @@ function startWordAnimations(engine, part, config, currentTime, appliedTimingOff
|
|
|
1000
1038
|
{ transform: `translateY(0)${emphasisRest}`, easing: config.letterWave.riseEasing },
|
|
1001
1039
|
{
|
|
1002
1040
|
transform: `${config.letterWave.transform}${emphasisPeak}`,
|
|
1003
|
-
offset:
|
|
1041
|
+
offset: LETTER_WAVE_CREST_OFFSET,
|
|
1004
1042
|
easing: config.letterWave.fallEasing,
|
|
1005
1043
|
},
|
|
1006
1044
|
{ transform: `${config.letterWave.settle}${emphasisRest}` },
|
|
1007
1045
|
];
|
|
1008
1046
|
const letterCount = letters.length;
|
|
1009
|
-
const
|
|
1010
|
-
|
|
1011
|
-
|
|
1047
|
+
const waves = planLetterWave(swipeRamp(config), letterCount, timedDurationMs, swipeDurationMs, swipeLeadMs, config.letterWave.durationMs);
|
|
1048
|
+
// A letter that starts before the word would have a negative delay, which the drift check reads
|
|
1049
|
+
// as the wave running behind once it finishes. Run the wave's clock ahead by the head start
|
|
1050
|
+
// instead, the way the swipe carries its lead, so every delay stays at zero or above.
|
|
1051
|
+
const waveLeadMs = Math.max(0, -Math.min(...waves.map(wave => wave.delayMs)));
|
|
1052
|
+
const waveEndsMs = waves.map(wave => wave.delayMs + wave.durationMs);
|
|
1053
|
+
const lastWaveEndMs = Math.max(...waveEndsMs);
|
|
1054
|
+
const lastEndingWaveIndex = waveEndsMs.lastIndexOf(lastWaveEndMs);
|
|
1055
|
+
const cascadeDurationMs = waveLeadMs + lastWaveEndMs;
|
|
1056
|
+
const floatStartMs = correctedAnimationTimeMs(wordTimeMs + waveLeadMs, appliedTimingOffsetMs, cascadeDurationMs);
|
|
1012
1057
|
const floatKeyframeSignature = JSON.stringify(floatKeyframes);
|
|
1013
|
-
|
|
1014
|
-
|
|
1058
|
+
// Glow letters only show while the glow does: never when it is off or renders nothing for this
|
|
1059
|
+
// word, and not once it has faded out. Their waves stop there instead of running to the end.
|
|
1060
|
+
const glowLetters = config.enabled.highlightGlow && !part.glowSuppressed ? part.imageLayers?.glowLetters : undefined;
|
|
1061
|
+
const glowEndMs = Number.parseFloat(config.highlight.imageGlowOpacityTo) === 0 ? waveLeadMs + glowDurationMs : Infinity;
|
|
1062
|
+
const animateLetters = (letters, endMs, sampled) => {
|
|
1063
|
+
letters?.forEach((letterElement, index) => {
|
|
1064
|
+
const delayMs = waves[index].delayMs + waveLeadMs;
|
|
1065
|
+
if (delayMs >= endMs)
|
|
1066
|
+
return;
|
|
1067
|
+
const endDelayMs = Math.min(0, endMs - delayMs - waves[index].durationMs);
|
|
1015
1068
|
const animation = trackLyricAnimationTiming(engine, acquireWaveAnimation(engine, letterElement, floatKeyframes, floatKeyframeSignature, {
|
|
1016
|
-
duration:
|
|
1017
|
-
delay:
|
|
1069
|
+
duration: waves[index].durationMs,
|
|
1070
|
+
delay: delayMs,
|
|
1071
|
+
endDelay: endDelayMs,
|
|
1018
1072
|
fill: "forwards",
|
|
1019
|
-
}), {
|
|
1073
|
+
}), {
|
|
1074
|
+
appliedTimingOffsetMs,
|
|
1075
|
+
offsetMs: waveLeadMs,
|
|
1076
|
+
// The wave that ends last covers the whole word. A theme's swipe ramp can make that an early letter.
|
|
1077
|
+
skipDriftSample: !sampled || index !== lastEndingWaveIndex,
|
|
1078
|
+
});
|
|
1020
1079
|
animation.currentTime = floatStartMs;
|
|
1021
1080
|
wobbleAnimations.push(animation);
|
|
1022
1081
|
});
|
|
1023
|
-
}
|
|
1082
|
+
};
|
|
1083
|
+
animateLetters(part.letterElements, Infinity, true);
|
|
1084
|
+
animateLetters(part.highlightLetterElements, Infinity, false);
|
|
1085
|
+
animateLetters(glowLetters, glowEndMs, false);
|
|
1024
1086
|
}
|
|
1025
1087
|
}
|
|
1026
1088
|
part.animations = [
|