111 lines
3.5 KiB
JavaScript
111 lines
3.5 KiB
JavaScript
/**
|
|
* 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();
|
|
}
|
|
})();
|