Skiply.ai/skiply/path-animation.js
2026-08-19 13:41:35 +05:30

203 lines
6.5 KiB
JavaScript

/* =============================================
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);
});