Fix automatic animation duration timing

This commit is contained in:
lzstealth committed 2026-10-26 22:54:17 +00:00
1 parent 554678b3c4
commit b5301f3f16
3 files changed
+80 -4

No files matched your search

+1 -1
View File
@@ -6,7 +6,7 @@ All notable changes to this project will be documented in this file.
### Fixed ### Fixed
- Animation delays now work in slide and template previews and player playback. Outro timing is configured as time before transition, preview-only region staggering is removed, and slide animation controls enforce the same advanced limits as templates. - Animation delays now work in slide and template previews and player playback. Outro timing follows each preset's computed Animate.css duration, is configured as time before transition, and preview-only region staggering is removed; slide animation controls enforce the same advanced limits as templates.
## 2.21.1 - 2026-10-26 ## 2.21.1 - 2026-10-26
+44 -1
View File
@@ -1,5 +1,7 @@
// Region animation configuration and lifecycle helpers. // Region animation configuration and lifecycle helpers.
var playerAnimationDurationMultiplierCache = Object.create(null);
function normalizePlayerAnimationConfig(value) { function normalizePlayerAnimationConfig(value) {
var raw = value; var raw = value;
if (typeof raw === 'string') { if (typeof raw === 'string') {
@@ -44,6 +46,46 @@ function normalizePlayerAnimationStep(value, fallbackPreset) {
}; };
} }
function parseAnimationDurationMs(value) {
var match = String(value || '').trim().match(/^([\d.]+)\s*(ms|s)$/i);
if (!match) {
return 0;
}
var duration = Number(match[1]);
return Number.isFinite(duration) ? duration * (match[2].toLowerCase() === 's' ? 1000 : 1) : 0;
}
function getAnimationDurationMultiplier(preset) {
if (Object.prototype.hasOwnProperty.call(playerAnimationDurationMultiplierCache, preset)) {
return playerAnimationDurationMultiplierCache[preset];
}
if (typeof document === 'undefined' || !document.body || typeof window.getComputedStyle !== 'function') {
return 1;
}
var probe = null;
try {
probe = document.createElement('div');
probe.classList.add('animate__animated', 'animate__' + preset);
probe.style.setProperty('--animate-duration', '1000ms');
probe.style.setProperty('animation-play-state', 'paused');
probe.style.setProperty('visibility', 'hidden');
document.body.appendChild(probe);
var durationMs = parseAnimationDurationMs(window.getComputedStyle(probe).animationDuration);
if (durationMs > 0) {
playerAnimationDurationMultiplierCache[preset] = durationMs / 1000;
}
} catch (_error) {
return 1;
} finally {
if (probe && probe.parentNode) {
probe.parentNode.removeChild(probe);
}
}
return playerAnimationDurationMultiplierCache[preset] || 1;
}
function getAnimationStepTimingMs(step) { function getAnimationStepTimingMs(step) {
if (!step) { if (!step) {
return 0; return 0;
@@ -69,7 +111,8 @@ function getAnimationStepTimingMs(step) {
iterations = 1; iterations = 1;
} }
return delayMs + (durationMs * iterations); var durationMultiplier = getAnimationDurationMultiplier(preset);
return delayMs + (durationMs * iterations * durationMultiplier);
} }
function getRegionAnimationPhaseTimings(root, phase) { function getRegionAnimationPhaseTimings(root, phase) {
+35 -2
View File
@@ -1190,7 +1190,7 @@ test('player applies animation delays and schedules outro triggers to finish at
const classes = new Set(); const classes = new Set();
const config = JSON.stringify({ const config = JSON.stringify({
intro: { preset: 'fadeIn', duration_ms: 600, delay_ms: 250 }, intro: { preset: 'fadeIn', duration_ms: 600, delay_ms: 250 },
outro: { preset: 'fadeOut', duration_ms: 700, delay_ms: 400 } outro: { preset: 'hinge', duration_ms: 700, delay_ms: 400 }
}); });
const element = { const element = {
dataset: {}, dataset: {},
@@ -1211,20 +1211,53 @@ test('player applies animation delays and schedules outro triggers to finish at
}; };
const root = { querySelectorAll() { return [element]; } }; const root = { querySelectorAll() { return [element]; } };
const timers = []; const timers = [];
const presetDurations = {
bounceIn: '750ms',
flipOutY: '750ms',
heartBeat: '1.3s',
hinge: '2s',
futurePreset: '1.7s'
};
const sandbox = { const sandbox = {
window: null, window: null,
app: { querySelectorAll() { return [root]; }, firstElementChild: root }, app: { querySelectorAll() { return [root]; }, firstElementChild: root },
document: {
body: {
appendChild(probe) { probe.parentNode = this; },
removeChild(probe) { probe.parentNode = null; }
},
createElement() {
const probeClasses = new Set();
return {
parentNode: null,
classList: { add(...names) { names.forEach(name => probeClasses.add(name)); } },
style: { setProperty() {} },
classes: probeClasses
};
}
},
getComputedStyle(probe) {
const presetClass = Array.from(probe.classes).find(name => name.indexOf('animate__') === 0 && name !== 'animate__animated');
return { animationDuration: presetDurations[presetClass.replace('animate__', '')] || '1s' };
},
setTimeout(callback, delay) { timers.push({ callback, delay }); return timers.length; }, setTimeout(callback, delay) { timers.push({ callback, delay }); return timers.length; },
clearTimeout() {} clearTimeout() {}
}; };
sandbox.window = sandbox; sandbox.window = sandbox;
vm.runInNewContext(fs.readFileSync(animationPath, 'utf8') + '\n' + fs.readFileSync(transitionPath, 'utf8'), sandbox); vm.runInNewContext(fs.readFileSync(animationPath, 'utf8') + '\n' + fs.readFileSync(transitionPath, 'utf8'), sandbox);
assert.equal(sandbox.getAnimationStepTimingMs({ preset: 'bounceIn', duration_ms: 1000 }), 750);
assert.equal(sandbox.getAnimationStepTimingMs({ preset: 'flipOutY', duration_ms: 1000 }), 750);
assert.equal(sandbox.getAnimationStepTimingMs({ preset: 'heartBeat', duration_ms: 1000 }), 1300);
assert.equal(sandbox.getAnimationStepTimingMs({ preset: 'hinge', duration_ms: 1000 }), 2000);
assert.equal(sandbox.getAnimationStepTimingMs({ preset: 'futurePreset', duration_ms: 1000 }), 1700);
sandbox.playRegionAnimation(element, 'intro'); sandbox.playRegionAnimation(element, 'intro');
assert.equal(values['animation-delay'], '250ms'); assert.equal(values['animation-delay'], '250ms');
sandbox.scheduleSlideOutro(5000); sandbox.scheduleSlideOutro(5000);
assert.equal(timers[0].delay, 3900); assert.equal(timers[0].delay, 3200);
timers[0].callback(); timers[0].callback();
assert.equal(values['--animate-duration'], '700ms');
assert.equal(values['animation-delay'], undefined); assert.equal(values['animation-delay'], undefined);
}); });