203 lines
6.5 KiB
JavaScript
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);
|
|
|
|
|
|
|
|
|
|
});
|