document.addEventListener("DOMContentLoaded", () => { const svg = document.querySelector(".characters svg"); initCharacterJump(); initPupilTracking(svg); }); function initCharacterJump() { // Gravité constante — contrairement à un ressort, la force est uniforme : // la chute accélère jusqu'au sol au lieu de ralentir en apesanteur const GRAVITY = 900; // accélération vers le bas (unités SVG/s²) const LAUNCH_V = 225; // impulsion vers le haut (unités SVG/s) const RESTITUTION = 0.15; // restitution du mini-rebond à l'atterrissage const CYCLE = 1200; // ms entre deux lancers const SQUAT_MS = 220; // durée du squat avant lancer const EXTEND_MS = 100; // extension des jambes juste après le lancer const FOOT_ROT = 35; // rotation max des pieds (degrés) // Distance dont le top des jambes descend à compression max (173.32 → 197) const BODY_SQUAT = 197 - 173.32; const rightChar = document.getElementById("right-character"); const rightBody = document.getElementById("right-character-body"); const rightLeg = document.getElementById("right-leg1"); const leftLeg = document.getElementById("left-leg1"); const rightFoot = document.getElementById("right-foot1"); const leftFoot = document.getElementById("left-foot1"); const LEG_R_REST = "M209.01,216.53C221.33,194.34 209.01,173.32 209.01,173.32"; const LEG_L_REST = "M189.01,216.53C176.69,194.34 189.01,173.32 189.01,173.32"; const LEG_R_COMP = "M209.01,216.53C231.01,207 209.01,197 209.01,197"; const LEG_L_COMP = "M189.01,216.53C167.01,207 189.01,197 189.01,197"; function lerpPath(p1, p2, t) { const re = /-?\d+\.?\d*/g; const n1 = p1.match(re).map(Number); const n2 = p2.match(re).map(Number); let i = 0; return p1.replace(re, () => { const v = n1[i] + (n2[i] - n1[i]) * t; i++; return v.toFixed(2); }); } const easeInOut = t => t < 0.5 ? 2*t*t : 1 - (-2*t+2)**2/2; const easeIn = t => t*t; // Crée un ressort secondaire indépendant (fermeture sur pos/vel) function makeSpring(k, c) { let pos = 0, vel = 0; return (target, dt) => { const acc = -k * (pos - target) - c * vel; vel += acc * dt; pos += vel * dt; return pos; }; } // Cinématique verticale — pos > 0 = en l'air let yPos = 0, yVel = 0, landingCompress = 0; // Ressorts secondaires : chaque partie suit sa cible avec son propre délai // Corps/tête : k élevé → réactif, léger overshoot sur le relâché // Jambes : mêmes paramètres que le corps pour rester visuellement connectés // Pieds : k bas → floppy, ils traînent naturellement const bodySpring = makeSpring(300, 18); const legSpring = makeSpring(300, 18); const rFootSpring = makeSpring(150, 10); const lFootSpring = makeSpring(150, 10); let hovered = false; let inCycle = false; let cycleStart = null; let launched = false; let lastTime = null; const wrapper = document.querySelector(".characters"); wrapper.addEventListener("mouseenter", () => { hovered = true; }); wrapper.addEventListener("mouseleave", () => { hovered = false; }); function tick(time) { if (!lastTime) lastTime = time; const dt = Math.min((time - lastTime) / 1000, 0.05); lastTime = time; if (hovered && !inCycle) { cycleStart = time; inCycle = true; launched = false; } let elapsed = inCycle ? time - cycleStart : 0; if (inCycle && elapsed >= CYCLE) { if (hovered) { cycleStart = time; elapsed = 0; launched = false; } else { inCycle = false; elapsed = 0; } } // Squat + lancer seulement pendant un cycle actif let squatCompress = 0; if (inCycle) { if (elapsed < SQUAT_MS) { squatCompress = easeInOut(elapsed / SQUAT_MS); } else if (elapsed < SQUAT_MS + EXTEND_MS) { squatCompress = 1 - easeIn((elapsed - SQUAT_MS) / EXTEND_MS); } if (!launched && elapsed >= SQUAT_MS) { yVel += LAUNCH_V; launched = true; } } // Cinématique verticale : gravité constante, contrainte sol if (yPos > 0 || yVel > 0) { yVel -= GRAVITY * dt; } yPos += yVel * dt; if (yPos < 0) { if (yVel < 0) { const impactV = -yVel; if (impactV > 50) { landingCompress = Math.min(0.6, impactV / LAUNCH_V * 0.8); } yVel = impactV > 15 ? impactV * RESTITUTION : 0; } yPos = 0; } landingCompress = Math.max(0, landingCompress - 4 * dt); const ty = -yPos; const compressTarget = Math.max(squatCompress, landingCompress); // Rotation cible des pieds proportionnelle à la hauteur const footRotTarget = yPos > 3 ? Math.min(1, (yPos - 3) / 12) : 0; // Ressorts secondaires : calcul des valeurs avec inertie propre const bodyDisp = bodySpring(compressTarget * BODY_SQUAT, dt); const legMorph = Math.max(0, Math.min(1, legSpring(compressTarget, dt))); const rFootAngle = rFootSpring(footRotTarget * FOOT_ROT, dt); const lFootAngle = lFootSpring(-footRotTarget * FOOT_ROT, dt); rightChar.setAttribute("transform", `translate(0,${ty.toFixed(2)})`); rightBody.setAttribute("transform", `translate(0,${bodyDisp.toFixed(2)})`); rightLeg.setAttribute("d", lerpPath(LEG_R_REST, LEG_R_COMP, legMorph)); leftLeg.setAttribute("d", lerpPath(LEG_L_REST, LEG_L_COMP, legMorph)); rightFoot.setAttribute("transform", `rotate(${rFootAngle.toFixed(1)},209.01,217.6)`); leftFoot.setAttribute("transform", `rotate(${lFootAngle.toFixed(1)},189.01,217.6)`); requestAnimationFrame(tick); } requestAnimationFrame(tick); } function initPupilTracking(svg) { const VB_W = 304; const VB_H = 259; const S = 1.267471; const BASE_TX = S * 52.5; const BASE_TY = S * 87.5; const MAX_R = 3.5; const LERP = 0.12; // Centre de chaque œil — personnage gauche uniquement const eyes = [ { id: "left-pupil", cx: 54.03, cy: 86.79 }, { id: "right-pupil", cx: 103.55, cy: 86.79 }, ]; const cur = eyes.map(e => ({ x: e.cx, y: e.cy })); const tgt = eyes.map(e => ({ x: e.cx, y: e.cy })); document.addEventListener("mousemove", (e) => { const rect = svg.getBoundingClientRect(); const mx = (e.clientX - rect.left) * (VB_W / rect.width); const my = (e.clientY - rect.top) * (VB_H / rect.height); eyes.forEach((eye, i) => { const dx = mx - eye.cx; const dy = my - eye.cy; const dist = Math.hypot(dx, dy); const ratio = dist > 0 ? Math.min(MAX_R, dist) / dist : 0; tgt[i].x = eye.cx + dx * ratio; tgt[i].y = eye.cy + dy * ratio; }); }); document.addEventListener("mouseleave", () => { eyes.forEach((eye, i) => { tgt[i].x = eye.cx; tgt[i].y = eye.cy; }); }); function tick() { eyes.forEach((eye, i) => { cur[i].x += (tgt[i].x - cur[i].x) * LERP; cur[i].y += (tgt[i].y - cur[i].y) * LERP; document.getElementById(eye.id)?.setAttribute( "transform", `matrix(${S},0,0,${S},${cur[i].x - BASE_TX},${cur[i].y - BASE_TY})` ); }); requestAnimationFrame(tick); } requestAnimationFrame(tick); }