diff --git a/theme/assets/css/main.css b/theme/assets/css/main.css index 4014122..a20b7a9 100644 --- a/theme/assets/css/main.css +++ b/theme/assets/css/main.css @@ -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)); +} diff --git a/theme/assets/js/video-editor-animator.js b/theme/assets/js/video-editor-animator.js new file mode 100644 index 0000000..21939f0 --- /dev/null +++ b/theme/assets/js/video-editor-animator.js @@ -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(); + } +})(); diff --git a/theme/blocks/index.php b/theme/blocks/index.php index 82c174d..3ac0015 100644 --- a/theme/blocks/index.php +++ b/theme/blocks/index.php @@ -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' - 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 ) { diff --git a/theme/inc/enqueue.php b/theme/inc/enqueue.php index eafd3fd..8f57974 100644 --- a/theme/inc/enqueue.php +++ b/theme/inc/enqueue.php @@ -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' ),