/* ============================================= 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, pointNames = null) { 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 }; } /* ── Arrival-triggered "point" reveal ────────── Each named point is the marker + leader line + label that's already baked into the road SVG (circle, icon, connector tick and title/desc text paths tagged with ids/`.road-point` in the HTML) — we animate that existing artwork in place rather than positioning a separate HTML copy. It fades in the first time the car's progress along the road reaches it. */ let points = []; if (Array.isArray(pointNames) && pointNames.length) { points = pointNames.map(name => { const markerEl = document.getElementById(`point-${name}`); const circleEl = markerEl ? markerEl.querySelector('circle') : null; const extraEls = [ document.getElementById(`text-${name}-1`), document.getElementById(`text-${name}-2`), document.getElementById(`tick-${name}`) ].filter(Boolean); if (!markerEl || !circleEl) return null; const svgX = parseFloat(circleEl.getAttribute('cx')); const svgY = parseFloat(circleEl.getAttribute('cy')); // Find the point on the road path nearest to the marker so // "arrival" lines up with where the car visually meets it. let t = 0, bestDist = Infinity; const SAMPLES = 400; for (let i = 0; i <= SAMPLES; i++) { const sampleT = i / SAMPLES; const p = pathEl.getPointAtLength(sampleT * TOTAL_LEN); const dist = (p.x - svgX) ** 2 + (p.y - svgY) ** 2; if (dist < bestDist) { bestDist = dist; t = sampleT; } } return { name, t, markerEl, extraEls, revealed: false }; }).filter(Boolean).sort((a, b) => a.t - b.t); if (typeof gsap !== 'undefined') { points.forEach(p => { gsap.set(p.markerEl, { opacity: 0, scale: 0.5, transformOrigin: '50% 50%' }); if (p.extraEls.length) gsap.set(p.extraEls, { opacity: 0 }); }); } } function revealPoint(p) { if (p.revealed) return; p.revealed = true; if (typeof gsap === 'undefined') { p.markerEl.style.opacity = '1'; p.extraEls.forEach(el => { el.style.opacity = '1'; }); return; } gsap.to(p.markerEl, { opacity: 1, scale: 1, duration: 0.4, ease: 'back.out(1.7)' }); if (p.extraEls.length) { gsap.to(p.extraEls, { opacity: 1, duration: 0.4, ease: 'power2.out', delay: 0.1 }); } } function checkPointArrivals(t) { for (const p of points) { if (!p.revealed && t >= p.t) revealPoint(p); } } 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); checkPointArrivals(progress); cancelAnimationFrame(rafId); doLoop(); return; } placeCar(progress); checkPointArrivals(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(() => { placeCar(progress); }); 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). // Screens with arrival-triggered points (pointNames) reveal each // marker individually as the car reaches it instead — see // revealPoint()/checkPointArrivals() above. if (!points.length) { 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, ['receive', 'sort', 'inspect', 'hold', 'route']); // Initialize Screen 2 setupRoadAnimation('road-svg-2', 'road-path-2', 'road-car-2', '.main-area--prob2', 'screen-prob2', 0.00016, ['scan', 'decision']); /* ═══════════════════════════════════════════ 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 } } ); } });