/** * Video Editor Timeline Animator * Animates playhead scrubbing across the timeline and video preview crossfades. * Mirrors the structure and conventions of dashboard-animator.js. */ (function () { 'use strict'; var DURATION = 10000; // ms for one full playhead sweep (left → right, then loop) var X_MIN = 104; // leftmost playhead centre (SVG units) var X_MAX = 504; // rightmost playhead centre (end of clip area) function makeState(svg) { return { svg: svg, playheadLine: svg.querySelector('#ve-playhead-line'), playheadHead: svg.querySelector('#ve-playhead-head'), scene1: svg.querySelector('#ve-scene-1'), scene2: svg.querySelector('#ve-scene-2'), scene3: svg.querySelector('#ve-scene-3'), innerScreen: svg.querySelector('#ve-inner-screen'), timecode: svg.querySelector('#ve-timecode'), scrubPct: 0, lastTime: performance.now(), paused: false }; } function lerp(a, b, t) { return a + (b - a) * t; } function updatePlayhead(st) { var xC = lerp(X_MIN, X_MAX, st.scrubPct); if (st.playheadLine) st.playheadLine.setAttribute('x', (xC - 1).toFixed(1)); if (st.playheadHead) st.playheadHead.setAttribute('transform', 'translate(' + xC.toFixed(1) + ',234)'); if (st.timecode) { var s = Math.floor(st.scrubPct * 10); st.timecode.textContent = '0:' + (s < 10 ? '0' + s : s); } } function updateScenes(st) { var p = st.scrubPct; var o1, o2, o3, fade; if (p < 0.30) { o1 = 1; o2 = 0; o3 = 0; } else if (p < 0.40) { fade = (p - 0.30) / 0.10; o1 = 1 - fade; o2 = fade; o3 = 0; } else if (p < 0.65) { o1 = 0; o2 = 1; o3 = 0; } else if (p < 0.75) { fade = (p - 0.65) / 0.10; o1 = 0; o2 = 1 - fade; o3 = fade; } else { o1 = 0; o2 = 0; o3 = 1; } if (st.scene1) st.scene1.setAttribute('opacity', o1.toFixed(3)); if (st.scene2) st.scene2.setAttribute('opacity', o2.toFixed(3)); if (st.scene3) st.scene3.setAttribute('opacity', o3.toFixed(3)); if (st.innerScreen) { st.innerScreen.setAttribute('fill', p < 0.38 ? 'url(#ve-scr-warm)' : p < 0.72 ? 'url(#ve-scr-cold)' : 'url(#ve-scr-go)' ); } } function loop(st, now) { if (!st.paused) { var delta = now - st.lastTime; st.lastTime = now; st.scrubPct += delta / DURATION; if (st.scrubPct >= 1) st.scrubPct -= 1; updatePlayhead(st); updateScenes(st); } else { st.lastTime = now; // keep fresh so there is no jump on resume } requestAnimationFrame(function (t) { loop(st, t); }); } function observe(st) { if (!('IntersectionObserver' in window)) return; new IntersectionObserver(function (entries) { for (var i = 0; i < entries.length; i++) { st.paused = !entries[i].isIntersecting; } }, { rootMargin: '200px', threshold: 0.05 }).observe(st.svg); } function boot() { var svgs = document.querySelectorAll('.ve-svg'); if (!svgs.length) return; if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return; for (var i = 0; i < svgs.length; i++) { (function (svg) { if (svg._veAnim) return; var st = makeState(svg); svg._veAnim = st; observe(st); requestAnimationFrame(function (t) { loop(st, t); }); })(svgs[i]); } } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', boot); } else { boot(); } })();