Add video editor timeline animator with animated playhead and scene transitions

This commit is contained in:
Matt Batchelder
2026-02-21 19:55:47 -05:00
parent 2c82b2e432
commit 2a0b949ea9
4 changed files with 344 additions and 44 deletions

View File

@@ -5812,3 +5812,32 @@ p:last-child { margin-bottom: 0; }
transition: none !important;
}
}
/* ══════════════════════════════════════════════════════════════════════════════
VIDEO EDITOR ANIMATOR (.platform-visual.has-video-editor)
Laptop frame with dark editor UI, animated timeline playhead and preview.
══════════════════════════════════════════════════════════════════════════════ */
.platform-visual.has-video-editor {
background: none !important;
border: none !important;
border-radius: 0;
aspect-ratio: unset;
padding: 0;
overflow: visible;
display: flex;
align-items: center;
justify-content: center;
min-height: 200px;
}
.ve-stage {
width: 100%;
max-width: 560px;
}
.ve-svg {
width: 100%;
height: auto;
display: block;
filter: drop-shadow(0 8px 28px rgba(0, 0, 0, 0.18));
}

View File

@@ -0,0 +1,110 @@
/**
* 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();
}
})();

View File

@@ -1359,51 +1359,203 @@ function oribi_render_platform_section( $a, $content ) {
/* ── Platform Row (child - renders one service row) ────────────────────────── */
function oribi_render_camera_animation() {
return <<<'HTML'
<div class="cam-stage" aria-hidden="true">
<div class="pc-wrap">
<div class="pc-body">
<div class="pc-flash-unit"></div>
<div class="pc-top"><div class="pc-shutter-btn"></div><div class="pc-viewfinder"></div></div>
<div class="pc-front">
<div class="pc-lens-ring"><div class="pc-lens-glass"><div class="pc-lens-reflex"></div></div></div>
<div class="pc-grip"></div>
</div>
</div>
<div class="pc-prints">
<div class="pc-print pc-print--1"><div class="pc-print__img"></div></div>
<div class="pc-print pc-print--2"><div class="pc-print__img"></div></div>
<div class="pc-print pc-print--3"><div class="pc-print__img"></div></div>
</div>
</div>
<div class="ve-stage" aria-hidden="true"><svg viewBox="0 0 540 360" xmlns="http://www.w3.org/2000/svg" class="ve-svg" role="img" aria-label="Animated video editor timeline">
<defs>
<clipPath id="ve-preview-clip">
<rect x="54" y="36" width="256" height="184"/>
</clipPath>
<linearGradient id="ve-sg-warm" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#3d0800"/>
<stop offset="50%" stop-color="#a02500"/>
<stop offset="100%" stop-color="#1d0400"/>
</linearGradient>
<linearGradient id="ve-sg-cold" x1="100%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#0a1f3d"/>
<stop offset="50%" stop-color="#0e3d6e"/>
<stop offset="100%" stop-color="#06111e"/>
</linearGradient>
<linearGradient id="ve-sg-go" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#07180d"/>
<stop offset="50%" stop-color="#1c6038"/>
<stop offset="100%" stop-color="#040e07"/>
</linearGradient>
<linearGradient id="ve-scr-warm" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#ff9500"/>
<stop offset="100%" stop-color="#ff4500"/>
</linearGradient>
<linearGradient id="ve-scr-cold" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#1a8aff"/>
<stop offset="100%" stop-color="#0055cc"/>
</linearGradient>
<linearGradient id="ve-scr-go" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#00cc66"/>
<stop offset="100%" stop-color="#008844"/>
</linearGradient>
<pattern id="ve-scanlines" x="0" y="0" width="1" height="3" patternUnits="userSpaceOnUse">
<rect width="1" height="1" fill="rgba(0,0,0,0.18)"/>
</pattern>
</defs>
<div class="cam-scene">
<div class="cam-subject cam-subject--shoe"></div>
<div class="cam-subject cam-subject--food"></div>
<div class="cam-subject cam-subject--laptop"></div>
<div class="cam-flash-overlay"></div>
<div class="cam-vid-overlay"></div>
</div>
<!-- Laptop lid outer shell -->
<rect x="20" y="5" width="500" height="300" rx="12" fill="#d2d2d0" stroke="#b0aca8" stroke-width="1.5"/>
<rect x="28" y="12" width="484" height="280" rx="8" fill="#1c1c1c"/>
<rect x="32" y="16" width="476" height="272" rx="5" fill="#0e1117"/>
<!-- Laptop base -->
<rect x="10" y="305" width="520" height="26" rx="5" fill="#c8c8c8" stroke="#b0ada8" stroke-width="0.8"/>
<rect x="80" y="301" width="380" height="5" rx="2" fill="#a8a6a2"/>
<rect x="205" y="311" width="130" height="13" rx="5" fill="#b8b5b0" stroke="#a0a09a" stroke-width="0.7"/>
<rect x="38" y="306" width="155" height="18" rx="2" fill="#bcbab6" opacity="0.45"/>
<rect x="347" y="306" width="155" height="18" rx="2" fill="#bcbab6" opacity="0.45"/>
<div class="vc-wrap">
<div class="vc-camera">
<div class="vc-handle"></div>
<div class="vc-body">
<div class="vc-lens-barrel"><div class="vc-lens-tip"><div class="vc-lens-glass"><div class="vc-lens-reflex"></div></div></div></div>
<div class="vc-top-rail"></div>
<div class="vc-rec-light"></div>
<div class="vc-eyepiece"></div>
</div>
</div>
<div class="vc-tripod">
<div class="vc-stem"></div>
<div class="vc-legs">
<div class="vc-leg vc-leg--l"></div>
<div class="vc-leg vc-leg--c"></div>
<div class="vc-leg vc-leg--r"></div>
</div>
</div>
</div>
</div>
<!-- Editor title bar -->
<rect x="32" y="16" width="476" height="20" rx="4" fill="#1e2229"/>
<circle cx="50" cy="26" r="5" fill="#FF5F56"/>
<circle cx="66" cy="26" r="5" fill="#FFBD2E"/>
<circle cx="82" cy="26" r="5" fill="#27C93F"/>
<rect x="180" y="21" width="180" height="10" rx="3" fill="#3a3f48"/>
<rect x="100" y="22" width="28" height="8" rx="2" fill="#2e333c"/>
<rect x="132" y="22" width="24" height="8" rx="2" fill="#2e333c"/>
<rect x="160" y="22" width="16" height="8" rx="2" fill="#2e333c"/>
<!-- Left toolbar -->
<rect x="32" y="36" width="22" height="184" fill="#181c22"/>
<line x1="54" y1="36" x2="54" y2="220" stroke="#2a2e38" stroke-width="1"/>
<rect x="36" y="46" width="14" height="14" rx="2" fill="#3a4152"/>
<rect x="36" y="66" width="14" height="14" rx="2" fill="#3a4152"/>
<rect x="36" y="86" width="14" height="14" rx="2" fill="#3a4152"/>
<rect x="36" y="106" width="14" height="14" rx="2" fill="#3a4152"/>
<rect x="36" y="126" width="14" height="14" rx="2" fill="#3a4152"/>
<rect x="33" y="46" width="3" height="14" rx="1" fill="#D83302"/>
<!-- Preview pane -->
<rect x="54" y="36" width="256" height="184" fill="#0a0b0e"/>
<g clip-path="url(#ve-preview-clip)">
<rect id="ve-scene-1" x="54" y="36" width="256" height="184" fill="url(#ve-sg-warm)" opacity="1"/>
<rect id="ve-scene-2" x="54" y="36" width="256" height="184" fill="url(#ve-sg-cold)" opacity="0"/>
<rect id="ve-scene-3" x="54" y="36" width="256" height="184" fill="url(#ve-sg-go)" opacity="0"/>
<!-- Laptop in the preview video -->
<rect x="102" y="52" width="156" height="106" rx="5" fill="#1c1c1c" stroke="#2e2e2e" stroke-width="1"/>
<rect x="106" y="56" width="148" height="98" rx="3" fill="#111111"/>
<rect id="ve-inner-screen" x="108" y="58" width="144" height="94" rx="1" fill="url(#ve-scr-warm)"/>
<polygon points="108,58 160,58 108,88" fill="white" opacity="0.04"/>
<rect x="108" y="58" width="144" height="94" fill="url(#ve-scanlines)" opacity="0.45"/>
<circle cx="180" cy="55" r="2.5" fill="#232323" stroke="#2e2e2e" stroke-width="0.5"/>
<rect x="92" y="158" width="176" height="17" rx="2" fill="#222222" stroke="#2d2d2d" stroke-width="0.5"/>
<rect x="150" y="162" width="60" height="9" rx="3" fill="#2a2a2a" stroke="#353535" stroke-width="0.5"/>
<rect x="97" y="159" width="166" height="1" fill="#2c2c2c"/>
<line x1="54" y1="175" x2="310" y2="175" stroke="#1c1c1c" stroke-width="2"/>
<rect x="54" y="175" width="256" height="45" fill="#040507" opacity="0.75"/>
<rect x="54" y="36" width="256" height="184" fill="url(#ve-scanlines)" opacity="0.25"/>
</g>
<rect x="54" y="36" width="256" height="184" fill="none" stroke="#2a2e38" stroke-width="0.5"/>
<line x1="310" y1="36" x2="310" y2="220" stroke="#2a2e38" stroke-width="1"/>
<!-- Inspector / Properties panel -->
<rect x="310" y="36" width="198" height="184" fill="#141619"/>
<rect x="310" y="36" width="198" height="20" fill="#1a1e25"/>
<rect x="318" y="42" width="80" height="8" rx="2" fill="#3a3f48"/>
<rect x="318" y="64" width="50" height="7" rx="2" fill="#2a2e38"/>
<rect x="378" y="64" width="82" height="7" rx="2" fill="#333a45"/>
<rect x="318" y="78" width="45" height="7" rx="2" fill="#2a2e38"/>
<rect x="378" y="78" width="96" height="7" rx="2" fill="#333a45"/>
<rect x="318" y="92" width="35" height="7" rx="2" fill="#2a2e38"/>
<rect x="378" y="92" width="62" height="7" rx="2" fill="#333a45"/>
<rect x="318" y="106" width="55" height="7" rx="2" fill="#2a2e38"/>
<rect x="378" y="106" width="72" height="7" rx="2" fill="#333a45"/>
<rect x="318" y="120" width="40" height="7" rx="2" fill="#2a2e38"/>
<rect x="378" y="120" width="54" height="7" rx="2" fill="#333a45"/>
<line x1="318" y1="136" x2="498" y2="136" stroke="#2a2e38" stroke-width="0.5"/>
<rect x="310" y="136" width="198" height="12" fill="#1a1e25"/>
<rect x="318" y="140" width="60" height="6" rx="2" fill="#2a2e38"/>
<rect x="318" y="150" width="32" height="24" rx="1" fill="#C0390A" opacity="0.8"/>
<rect x="354" y="150" width="32" height="24" rx="1" fill="#1a52c8" opacity="0.8"/>
<rect x="390" y="150" width="32" height="24" rx="1" fill="#1a7a3d" opacity="0.8"/>
<rect x="426" y="150" width="32" height="24" rx="1" fill="#c07800" opacity="0.8"/>
<rect x="462" y="150" width="32" height="24" rx="1" fill="#7030d0" opacity="0.8"/>
<!-- Timeline section -->
<rect x="32" y="220" width="476" height="68" fill="#161616"/>
<line x1="32" y1="220" x2="508" y2="220" stroke="#2a2e38" stroke-width="1"/>
<rect x="32" y="220" width="476" height="14" fill="#1a1e24"/>
<rect x="32" y="220" width="72" height="68" fill="#1a1e24"/>
<line x1="104" y1="220" x2="104" y2="288" stroke="#2a2e38" stroke-width="1"/>
<!-- Ruler ticks and labels -->
<line x1="104" y1="220" x2="104" y2="232" stroke="#4a4e58" stroke-width="1"/>
<text x="105" y="231" fill="#565a64" font-size="8" font-family="monospace">0:00</text>
<line x1="184" y1="220" x2="184" y2="232" stroke="#4a4e58" stroke-width="1"/>
<text x="185" y="231" fill="#565a64" font-size="8" font-family="monospace">0:02</text>
<line x1="264" y1="220" x2="264" y2="232" stroke="#4a4e58" stroke-width="1"/>
<text x="265" y="231" fill="#565a64" font-size="8" font-family="monospace">0:04</text>
<line x1="344" y1="220" x2="344" y2="232" stroke="#4a4e58" stroke-width="1"/>
<text x="345" y="231" fill="#565a64" font-size="8" font-family="monospace">0:06</text>
<line x1="424" y1="220" x2="424" y2="232" stroke="#4a4e58" stroke-width="1"/>
<text x="425" y="231" fill="#565a64" font-size="8" font-family="monospace">0:08</text>
<line x1="144" y1="220" x2="144" y2="226" stroke="#2e3240" stroke-width="0.5"/>
<line x1="224" y1="220" x2="224" y2="226" stroke="#2e3240" stroke-width="0.5"/>
<line x1="304" y1="220" x2="304" y2="226" stroke="#2e3240" stroke-width="0.5"/>
<line x1="384" y1="220" x2="384" y2="226" stroke="#2e3240" stroke-width="0.5"/>
<line x1="464" y1="220" x2="464" y2="226" stroke="#2e3240" stroke-width="0.5"/>
<!-- Track label colour strips -->
<rect x="36" y="237" width="56" height="14" rx="2" fill="#283040"/>
<rect x="36" y="255" width="56" height="14" rx="2" fill="#203020"/>
<rect x="36" y="273" width="56" height="14" rx="2" fill="#302818"/>
<!-- Track 1: VIDEO clips -->
<rect x="104" y="234" width="404" height="18" fill="#1a1a22"/>
<rect x="104" y="235" width="88" height="16" rx="2" fill="#2563EB"/>
<rect x="104" y="235" width="88" height="5" rx="2" fill="rgba(255,255,255,0.07)"/>
<rect x="196" y="235" width="66" height="16" rx="2" fill="#1D4ED8"/>
<rect x="196" y="235" width="66" height="5" rx="2" fill="rgba(255,255,255,0.07)"/>
<rect x="266" y="235" width="106" height="16" rx="2" fill="#2563EB"/>
<rect x="266" y="235" width="106" height="5" rx="2" fill="rgba(255,255,255,0.07)"/>
<rect x="376" y="235" width="88" height="16" rx="2" fill="#1E40AF"/>
<rect x="376" y="235" width="88" height="5" rx="2" fill="rgba(255,255,255,0.07)"/>
<!-- Track 2: AUDIO/MUSIC clips -->
<rect x="104" y="252" width="404" height="18" fill="#141c12"/>
<rect x="104" y="253" width="178" height="16" rx="2" fill="#15803D"/>
<rect x="104" y="253" width="178" height="5" rx="2" fill="rgba(255,255,255,0.06)"/>
<!-- Inline waveform bars -->
<rect x="108" y="259" width="2" height="5" fill="rgba(255,255,255,0.22)"/>
<rect x="112" y="257" width="2" height="8" fill="rgba(255,255,255,0.22)"/>
<rect x="116" y="260" width="2" height="4" fill="rgba(255,255,255,0.22)"/>
<rect x="120" y="257" width="2" height="7" fill="rgba(255,255,255,0.22)"/>
<rect x="124" y="260" width="2" height="5" fill="rgba(255,255,255,0.22)"/>
<rect x="128" y="257" width="2" height="7" fill="rgba(255,255,255,0.22)"/>
<rect x="132" y="259" width="2" height="5" fill="rgba(255,255,255,0.22)"/>
<rect x="136" y="257" width="2" height="8" fill="rgba(255,255,255,0.22)"/>
<rect x="140" y="260" width="2" height="4" fill="rgba(255,255,255,0.22)"/>
<rect x="144" y="258" width="2" height="7" fill="rgba(255,255,255,0.22)"/>
<rect x="148" y="261" width="2" height="4" fill="rgba(255,255,255,0.22)"/>
<rect x="152" y="258" width="2" height="6" fill="rgba(255,255,255,0.22)"/>
<rect x="156" y="259" width="2" height="5" fill="rgba(255,255,255,0.22)"/>
<rect x="160" y="257" width="2" height="7" fill="rgba(255,255,255,0.22)"/>
<rect x="164" y="259" width="2" height="5" fill="rgba(255,255,255,0.22)"/>
<rect x="168" y="257" width="2" height="8" fill="rgba(255,255,255,0.22)"/>
<rect x="172" y="261" width="2" height="4" fill="rgba(255,255,255,0.22)"/>
<rect x="176" y="259" width="2" height="5" fill="rgba(255,255,255,0.22)"/>
<rect x="286" y="253" width="148" height="16" rx="2" fill="#166534"/>
<rect x="286" y="253" width="148" height="5" rx="2" fill="rgba(255,255,255,0.06)"/>
<rect x="438" y="253" width="66" height="16" rx="2" fill="#15803D"/>
<rect x="438" y="253" width="66" height="5" rx="2" fill="rgba(255,255,255,0.06)"/>
<!-- Track 3: GFX / TEXT clips -->
<rect x="104" y="270" width="404" height="18" fill="#181710"/>
<rect x="120" y="271" width="60" height="16" rx="2" fill="#D97706"/>
<rect x="120" y="271" width="60" height="5" rx="2" fill="rgba(255,255,255,0.07)"/>
<rect x="226" y="271" width="52" height="16" rx="2" fill="#B45309"/>
<rect x="226" y="271" width="52" height="5" rx="2" fill="rgba(255,255,255,0.07)"/>
<rect x="334" y="271" width="78" height="16" rx="2" fill="#D97706"/>
<rect x="334" y="271" width="78" height="5" rx="2" fill="rgba(255,255,255,0.07)"/>
<rect x="452" y="271" width="50" height="16" rx="2" fill="#B45309"/>
<rect x="452" y="271" width="50" height="5" rx="2" fill="rgba(255,255,255,0.07)"/>
<!-- Animated playhead -->
<polygon id="ve-playhead-head" points="0,-8 -6,-2 6,-2" transform="translate(104,234)" fill="#FF4500"/>
<rect id="ve-playhead-line" x="103" y="220" width="2" height="68" fill="#FF4500" opacity="0.92"/>
<!-- Timecode display -->
<text id="ve-timecode" x="36" y="230" fill="#8a8e98" font-size="8" font-family="monospace">0:00</text>
</svg></div>
HTML;
}
@@ -1794,7 +1946,7 @@ function oribi_render_platform_row( $a ) {
$visual_cls = 'platform-visual has-branded';
} elseif ( ! empty( $a['cameraAnim'] ) ) {
$visual_html = oribi_render_camera_animation();
$visual_cls = 'platform-visual has-camera';
$visual_cls = 'platform-visual has-video-editor';
/* ── Gallery TV Slideshow ───────────────────────────────── */
} elseif ( ! empty( $a['galleryIds'] ) && is_array( $a['galleryIds'] ) && count( $a['galleryIds'] ) > 0 ) {

View File

@@ -47,6 +47,15 @@ add_action( 'wp_enqueue_scripts', function () {
true
);
// Video editor timeline animator - animated playhead and preview crossfades
wp_enqueue_script(
'oribi-video-editor-animator',
ORIBI_URI . '/assets/js/video-editor-animator.js',
[],
ORIBI_VERSION . '.' . filemtime( ORIBI_DIR . '/assets/js/video-editor-animator.js' ),
true
);
// Localize AJAX endpoint for the contact form
wp_localize_script( 'oribi-main', 'oribiAjax', [
'url' => admin_url( 'admin-ajax.php' ),