diff --git a/CHANGELOG.md b/CHANGELOG.md index 383ebe0..9bfafa8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. +## 2.21.2 - 2026-10-26 + +### 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. + ## 2.21.1 - 2026-10-26 ### Fixed diff --git a/build/package.player.json b/build/package.player.json index 0250328..0b033e3 100644 --- a/build/package.player.json +++ b/build/package.player.json @@ -1,6 +1,6 @@ { "name": "pulse-signage-player", - "version": "2.21.1", + "version": "2.21.2", "private": false, "description": "Pulse Signage player application bundle", "engines": { diff --git a/build/package.web.json b/build/package.web.json index 4c1de97..437227e 100644 --- a/build/package.web.json +++ b/build/package.web.json @@ -1,6 +1,6 @@ { "name": "pulse-signage-web", - "version": "2.21.1", + "version": "2.21.2", "private": false, "description": "Pulse Signage web and bridge application bundle", "engines": { diff --git a/package-lock.json b/package-lock.json index 1ae161a..7a8badc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pulse-signage", - "version": "2.21.1", + "version": "2.21.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pulse-signage", - "version": "2.21.1", + "version": "2.21.2", "dependencies": { "@better-auth/passkey": "^1.7.13", "animate.css": "^4.1.1", diff --git a/package.json b/package.json index 79ca5f8..aae49fa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pulse-signage", - "version": "2.21.1", + "version": "2.21.2", "private": false, "description": "Pulse Signage application with MySQL and media storage", "engines": { diff --git a/src/player/public/js/player-page-animation.js b/src/player/public/js/player-page-animation.js index b762609..085e93b 100644 --- a/src/player/public/js/player-page-animation.js +++ b/src/player/public/js/player-page-animation.js @@ -144,6 +144,7 @@ function clearRegionAnimationClasses(root) { element.style.removeProperty('--animate-duration'); element.style.removeProperty('--animate-delay'); element.style.removeProperty('--animate-repeat'); + element.style.removeProperty('animation-delay'); }); } @@ -161,13 +162,16 @@ function applyAnimationStep(element, step, phase) { return; } - element.classList.add('animate__animated', 'animate__' + preset); element.style.setProperty('--animate-duration', String(Math.max(1, Number(step.duration_ms || 0) || 1000)) + 'ms'); - if (Number(step.delay_ms || 0) > 0) { - element.style.setProperty('--animate-delay', String(Math.max(0, Number(step.delay_ms || 0))) + 'ms'); + var delayMs = phase === 'outro' ? 0 : Math.max(0, Number(step.delay_ms || 0)); + if (delayMs > 0) { + element.style.setProperty('--animate-delay', String(delayMs) + 'ms'); + element.style.setProperty('animation-delay', String(delayMs) + 'ms'); } else { element.style.removeProperty('--animate-delay'); + element.style.removeProperty('animation-delay'); } + element.classList.add('animate__animated', 'animate__' + preset); if (phase === 'loop') { var repeatCount = Number(step.iterations); diff --git a/src/web/public/js/shared/region-animation-controls.js b/src/web/public/js/shared/region-animation-controls.js index 6fff2db..2e75703 100644 --- a/src/web/public/js/shared/region-animation-controls.js +++ b/src/web/public/js/shared/region-animation-controls.js @@ -76,8 +76,22 @@ }); return normalize(config); } + function clampAdvancedField(input) { + if (!input || !input.dataset) return; + var field = input.dataset.animationModalField; + var value = String(input.value || '').trim(); + if (!value) return; + var number = Number(value); + if (!Number.isFinite(number)) return; + var normalized; + if (field === 'duration_ms') normalized = Math.min(200, Math.max(1, Math.round(number))); + else if (field === 'iterations' && input.dataset.animationModalStep === 'loop') normalized = Math.min(999, Math.max(1, Math.round(number))); + else return; + if (String(normalized) !== String(input.value)) input.value = String(normalized); + } window.pulseRegionAnimationControls = { read: read, write: write, setPreset: setPreset, bind: bind, - populateAdvanced: populateAdvanced, readAdvanced: readAdvanced + populateAdvanced: populateAdvanced, readAdvanced: readAdvanced, + clampAdvancedField: clampAdvancedField }; })(); \ No newline at end of file diff --git a/src/web/public/js/slides/slide-animation-editor.js b/src/web/public/js/slides/slide-animation-editor.js index 0e87cf1..f1dba06 100644 --- a/src/web/public/js/slides/slide-animation-editor.js +++ b/src/web/public/js/slides/slide-animation-editor.js @@ -141,7 +141,7 @@ export function createSlideAnimationEditor(options) { element.className.split(/\s+/).forEach(function (className) { if (className.indexOf('animate__') === 0) element.classList.remove(className); }); - ['--animate-duration', '--animate-delay', '--animate-repeat'].forEach(function (property) { element.style.removeProperty(property); }); + ['--animate-duration', '--animate-delay', '--animate-repeat', 'animation-delay'].forEach(function (property) { element.style.removeProperty(property); }); }); updatePreviewButtons(); } @@ -168,16 +168,18 @@ export function createSlideAnimationEditor(options) { previewMode = mode; var token = previewToken; var longest = 0; - getPreviewElements().forEach(function (element, index) { + getPreviewElements().forEach(function (element) { var config = window.templateDesignerUtils.normalizeAnimationConfig(getConfig(element.dataset.regionKey)); var step = config[mode] || {}; var preset = window.templateAnimationPresets.normalize(step.preset); if (preset === 'none') return; - var delay = Math.max(0, Number(step.delay_ms) || 0) + index * 90; + var configuredDelay = mode === 'outro' ? 0 : Math.max(0, Number(step.delay_ms) || 0); + var delay = configuredDelay; var duration = Math.max(1, Number(step.duration_ms) || 1000); var iterations = mode === 'loop' ? Math.max(1, Number(step.iterations) || 1) : 1; element.style.setProperty('--animate-duration', duration + 'ms'); element.style.setProperty('--animate-delay', delay + 'ms'); + element.style.setProperty('animation-delay', delay + 'ms'); element.style.setProperty('--animate-repeat', String(iterations)); element.classList.remove('animate__animated', 'animate__' + preset, 'animate__repeat-1'); void element.offsetWidth; @@ -266,6 +268,9 @@ export function createSlideAnimationEditor(options) { advanced.addEventListener('shown.bs.modal', function () { advanced.querySelector('[data-animation-modal-field]').focus(); }); + advanced.addEventListener('input', function (event) { + controls.clampAdvancedField(event.target); + }); advanced.addEventListener('hidden.bs.modal', function () { window.pulseModal.getOrCreate(advanced)._scrollBar = advancedScrollBar; advancedScrollBar = null; diff --git a/src/web/public/js/templates/template-designer.js b/src/web/public/js/templates/template-designer.js index d4b9982..e635615 100644 --- a/src/web/public/js/templates/template-designer.js +++ b/src/web/public/js/templates/template-designer.js @@ -363,6 +363,7 @@ rect.style.removeProperty('--animate-duration'); rect.style.removeProperty('--animate-delay'); rect.style.removeProperty('--animate-repeat'); + rect.style.removeProperty('animation-delay'); rect.style.opacity = ''; rect.style.transform = ''; rect.style.filter = ''; @@ -442,15 +443,20 @@ var durationMs = Number(step && step.duration_ms); var delayMs = Number(step && step.delay_ms); var repeatCount = Number(step && step.iterations); + if (mode === 'out') delayMs = 0; var isAttentionSeeker = preset === 'bounce' || preset === 'flash' || preset === 'pulse' || preset === 'rubberBand' || preset === 'shakeX' || preset === 'shakeY' || preset === 'headShake' || preset === 'swing' || preset === 'tada' || preset === 'wobble' || preset === 'jello' || preset === 'heartBeat'; rect.classList.remove('animate__animated', 'animate__infinite'); rect.style.removeProperty('--animate-duration'); rect.style.removeProperty('--animate-delay'); rect.style.removeProperty('--animate-repeat'); + rect.style.removeProperty('animation-delay'); rect.style.setProperty('--animate-duration', Math.max(0, Number.isFinite(durationMs) && durationMs > 0 ? durationMs : 1000) + 'ms'); if (Number.isFinite(delayMs) && delayMs > 0) { rect.style.setProperty('--animate-delay', Math.max(0, delayMs) + 'ms'); + rect.style.setProperty('animation-delay', Math.max(0, delayMs) + 'ms'); + } else { + rect.style.removeProperty('animation-delay'); } rect.classList.add('animate__animated', animateClass); if (isAttentionSeeker) { @@ -475,6 +481,9 @@ if (!Number.isFinite(delayMs) || delayMs < 0) { delayMs = 0; } + if (normalizedMode === 'out') { + delayMs = 0; + } if (!Number.isFinite(iterations) || iterations < 1) { iterations = 1; } @@ -595,39 +604,6 @@ window.pulseRegionAnimationControls.populateAdvanced(animationAdvancedModal, config); } - function readNumberField(value) { - var raw = String(value || '').trim(); - if (!raw) { - return null; - } - var next = Number(raw); - return Number.isFinite(next) ? Math.round(next) : null; - } - - function clampAnimationModalField(field) { - if (!field || !field.matches) { - return; - } - - var isLoopIterationsField = field.matches('[data-animation-modal-step="loop"][data-animation-modal-field="iterations"]'); - var isDurationField = field.matches('[data-animation-modal-field="duration_ms"]'); - if (!isLoopIterationsField && !isDurationField) { - return; - } - - var fieldValue = readNumberField(field.value); - if (fieldValue === null) { - return; - } - - var normalizedValue = isLoopIterationsField - ? Math.min(MAX_ANIMATION_REPEAT_COUNT, Math.max(1, fieldValue)) - : Math.min(200, Math.max(1, fieldValue)); - if (String(normalizedValue) !== String(field.value)) { - field.value = String(normalizedValue); - } - } - function readAnimationModalConfig() { if (!animationAdvancedModal) { return null; @@ -732,7 +708,6 @@ function animatePreviewRects(mode, regions) { var rects = getPreviewRects(); var token = previewState.token; - var stagger = 90; var duration = 900; var overlayRect = getOverlayRect(); var canvasMovement = Math.max(12, Math.round(overlayRect.height * 0.02)); @@ -740,11 +715,11 @@ previewState.animations = []; - rects.forEach(function (rect, index) { + rects.forEach(function (rect) { var region = regions && regions[index] ? regions[index] : null; var step = getPreviewRegionAnimationStep(region, mode); var preset = normalizePreviewAnimation(step && step.preset); - var delay = index * stagger; + var delay = mode === 'out' ? 0 : Math.max(0, Number(step && step.delay_ms) || 0); var animation = null; var animateCssMode = isAnimateCssPreset(preset); var timingMs = delay + getPreviewAnimationStepTimingMs(step, mode, animateCssMode); @@ -1785,7 +1760,7 @@ activeAnimationCard = null; }); animationAdvancedModal.addEventListener('input', function (event) { - clampAnimationModalField(event.target); + window.pulseRegionAnimationControls.clampAdvancedField(event.target); markTemplateFormDirty(); }); animationAdvancedModal.addEventListener('change', function () { diff --git a/src/web/views/signage/templates/animation-advanced-fields.hbs b/src/web/views/signage/templates/animation-advanced-fields.hbs index 51a9cf4..ec103aa 100644 --- a/src/web/views/signage/templates/animation-advanced-fields.hbs +++ b/src/web/views/signage/templates/animation-advanced-fields.hbs @@ -15,7 +15,7 @@