@braccato/core 1.16.5 → 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.
Files changed (2) hide show
  1. package/dist/engine.js +43 -16
  2. package/package.json +1 -1
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
- function animationActiveDurationMs(animation) {
533
- return timingValueToMs(animation.effect?.getComputedTiming().activeDuration);
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 activeDurationMs = animationActiveDurationMs(animation);
546
- if (activeDurationMs !== null && Number.isFinite(activeDurationMs)) {
547
- return Math.min(timeMs, activeDurationMs);
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
- if (animation.playState === "idle") {
555
+ const timing = animationTimingTracks.get(animation);
556
+ if (!timing || timing.skipDriftSample) {
553
557
  return null;
554
558
  }
555
- const timing = animationTimingTracks.get(animation);
556
- if (!timing) {
559
+ if (animation.playState === "idle") {
557
560
  return null;
558
561
  }
559
562
  const actualTimeMs = animationCurrentTimeMs(animation);
@@ -887,7 +890,11 @@ function startRichSyncedHighlightAnimations(engine, part, config, swipeTimeMs, w
887
890
  offset: frame.offset,
888
891
  maskPosition: frame.maskPosition,
889
892
  WebkitMaskPosition: frame.maskPosition,
890
- })), { duration: sweep.durationMs, delay: sweep.delayMs, easing: sweep.easing, fill: "both" }), swipeTiming);
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 });
891
898
  animation.currentTime = swipeCurrentTimeMs;
892
899
  if (index === 0)
893
900
  swipeAnimation = animation;
@@ -1042,20 +1049,40 @@ function startWordAnimations(engine, part, config, currentTime, appliedTimingOff
1042
1049
  // as the wave running behind once it finishes. Run the wave's clock ahead by the head start
1043
1050
  // instead, the way the swipe carries its lead, so every delay stays at zero or above.
1044
1051
  const waveLeadMs = Math.max(0, -Math.min(...waves.map(wave => wave.delayMs)));
1045
- const cascadeDurationMs = waveLeadMs + Math.max(...waves.map(wave => wave.delayMs + wave.durationMs));
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;
1046
1056
  const floatStartMs = correctedAnimationTimeMs(wordTimeMs + waveLeadMs, appliedTimingOffsetMs, cascadeDurationMs);
1047
1057
  const floatKeyframeSignature = JSON.stringify(floatKeyframes);
1048
- for (const set of [part.letterElements, part.highlightLetterElements, part.imageLayers?.glowLetters]) {
1049
- set?.forEach((letterElement, index) => {
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);
1050
1068
  const animation = trackLyricAnimationTiming(engine, acquireWaveAnimation(engine, letterElement, floatKeyframes, floatKeyframeSignature, {
1051
1069
  duration: waves[index].durationMs,
1052
- delay: waves[index].delayMs + waveLeadMs,
1070
+ delay: delayMs,
1071
+ endDelay: endDelayMs,
1053
1072
  fill: "forwards",
1054
- }), { appliedTimingOffsetMs, offsetMs: waveLeadMs });
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
+ });
1055
1079
  animation.currentTime = floatStartMs;
1056
1080
  wobbleAnimations.push(animation);
1057
1081
  });
1058
- }
1082
+ };
1083
+ animateLetters(part.letterElements, Infinity, true);
1084
+ animateLetters(part.highlightLetterElements, Infinity, false);
1085
+ animateLetters(glowLetters, glowEndMs, false);
1059
1086
  }
1060
1087
  }
1061
1088
  part.animations = [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@braccato/core",
3
- "version": "1.16.5",
3
+ "version": "1.16.6",
4
4
  "description": "Synchronized lyrics renderer with word-by-word animations",
5
5
  "type": "module",
6
6
  "license": "MIT",