/* ============================================= Skiply.ai — JavaScript - Car travels along the exact road-path-1.svg path - Back-and-forth loop using requestAnimationFrame - Step icons pinned to real path coordinates - Entry reveal animations via IntersectionObserver ============================================= */ 'use strict'; document.addEventListener('DOMContentLoaded', () => { /* ═══════════════════════════════════════════ SCREEN 1 — Car motion along road path ═══════════════════════════════════════════ */ /* ═══════════════════════════════════════════ Reusable Car Animation Setup ═══════════════════════════════════════════ */ function setupRoadAnimation(svgId, pathId, carId, mainAreaSelector, screenId, speed = 0.00016) { const svgEl = document.getElementById(svgId); const pathEl = document.getElementById(pathId); const car = document.getElementById(carId); const mainArea = document.querySelector(mainAreaSelector); const screen = document.getElementById(screenId); if (!pathEl || !car || !svgEl || !mainArea || !screen) { console.warn(`Missing elements for ${screenId}:`, {svgEl, pathEl, car, mainArea, screen}); return; } const TOTAL_LEN = pathEl.getTotalLength(); function getSVGScale() { const rect = svgEl.getBoundingClientRect(); const vbW = svgEl.viewBox.baseVal.width || 1005; const vbH = svgEl.viewBox.baseVal.height || 650; const scaleX = rect.width / vbW; const scaleY = rect.height / vbH; const scale = Math.min(scaleX, scaleY); const offsetX = (rect.width - vbW * scale) / 2; const offsetY = (rect.height - vbH * scale) / 2; return { scale, offsetX, offsetY, rect }; } let progress = 0; const SPEED = speed; let lastTime = null; let rafId = null; let running = false; let looping = false; function getAngle(t, delta = 0.002) { const t1 = Math.max(0, t - delta); const t2 = Math.min(1, t + delta); const p1 = pathEl.getPointAtLength(t1 * TOTAL_LEN); const p2 = pathEl.getPointAtLength(t2 * TOTAL_LEN); return Math.atan2(p2.y - p1.y, p2.x - p1.x) * (180 / Math.PI); } function placeCar(t) { const { scale, offsetX, offsetY, rect } = getSVGScale(); const containerRect = mainArea.getBoundingClientRect(); const pt = pathEl.getPointAtLength(t * TOTAL_LEN); const angle = getAngle(t); const relX = (rect.left + offsetX + pt.x * scale) - containerRect.left; const relY = (rect.top + offsetY + pt.y * scale) - containerRect.top; car.style.left = `${relX}px`; car.style.top = `${relY}px`; car.style.transform = `translate(-50%, -50%) rotate(${angle}deg)`; } function doLoop() { looping = true; car.style.transition = 'opacity 0.25s ease'; car.style.opacity = '0'; setTimeout(() => { progress = 0; placeCar(0); setTimeout(() => { car.style.transition = 'opacity 0.3s ease'; car.style.opacity = '1'; setTimeout(() => { car.style.transition = ''; looping = false; lastTime = null; rafId = requestAnimationFrame(animateCar); }, 320); }, 60); }, 280); } function animateCar(timestamp) { if (looping) return; if (!lastTime) lastTime = timestamp; const dt = Math.min(timestamp - lastTime, 50); lastTime = timestamp; progress += SPEED * dt; if (progress >= 1) { progress = 1; placeCar(1); cancelAnimationFrame(rafId); doLoop(); return; } placeCar(progress); rafId = requestAnimationFrame(animateCar); } function startCar() { if (running) return; running = true; lastTime = null; progress = 0; looping = false; placeCar(0); car.style.transition = 'opacity 0.4s ease'; car.style.opacity = '1'; setTimeout(() => { car.style.transition = ''; rafId = requestAnimationFrame(animateCar); }, 420); } const ro = new ResizeObserver(() => {}); ro.observe(mainArea); // Use GSAP ScrollTrigger for Fade In / Fade Out effect if (typeof gsap !== 'undefined' && typeof ScrollTrigger !== 'undefined') { gsap.registerPlugin(ScrollTrigger); // 1. Fade in the left side panel text gsap.fromTo(document.querySelectorAll('#' + screenId + ' .side-panel > *'), { opacity: 0, y: 20 }, { opacity: 1, y: 0, duration: 0.6, stagger: 0.15, ease: "power2.out", scrollTrigger: { trigger: screen, start: "top 60%", end: "bottom 40%", toggleActions: "play reverse play reverse" } } ); // 2. Stagger fade-in for SVG pointers (navy circles and white icons) const pointers = Array.from(document.querySelectorAll('#' + screenId + ' svg circle, #' + screenId + ' svg path')).filter(el => { const fill = el.getAttribute('fill'); if (!fill) return false; const upper = fill.toUpperCase(); return upper === '#0A1A37' || upper === 'WHITE' || upper === '#FFFFFF'; }); gsap.fromTo(pointers, { opacity: 0, scale: 0, transformOrigin: "50% 50%" }, { opacity: 1, scale: 1, duration: 0.4, stagger: 0.15, ease: "back.out(1.5)", scrollTrigger: { trigger: screen, start: "top 50%", end: "bottom 20%", toggleActions: "play reverse play reverse" } } ); // 3. Start car animation when screen is in view ScrollTrigger.create({ trigger: screen, start: "top 60%", onEnter: startCar, once: true }); } } // Initialize Screen 1 setupRoadAnimation('road-svg', 'road-path', 'road-car', '.main-area--prob1', 'screen-prob1', 0.00008); // Initialize Screen 2 setupRoadAnimation('road-svg-2', 'road-path-2', 'road-car-2', '.main-area--prob2', 'screen-prob2', 0.00016); /* ═══════════════════════════════════════════ Screen 2 Fade In Effect (Original Smooth Version) ═══════════════════════════════════════════ */ if (typeof gsap !== 'undefined' && typeof ScrollTrigger !== 'undefined') { gsap.fromTo('.screen--prob2', { opacity: 0 }, { opacity: 1, ease: "none", scrollTrigger: { trigger: '.screen--prob2', start: "top 75%", end: "top 0%", scrub: true } } ); } });