const RIGHT_LEG_AXIS_X = 209.01; const LEFT_LEG_AXIS_X = 189.01; const ANKLE_Y = 216.53; const HIP_Y_STANDING = 173.32; const HIP_Y_SQUATTING = 197; // Point de contrôle de Bézier qui détermine la courbure latérale de la jambe const rightLegBowControlStanding = { x: 221.33, y: 194.34 }; const rightLegBowControlSquatting = { x: 231.01, y: 207 }; const leftLegBowControlStanding = { x: 176.69, y: 194.34 }; const leftLegBowControlSquatting = { x: 167.01, y: 207 }; function buildLegSvgPath(legAxisX, bowControl, hipY) { return `M${legAxisX},${ANKLE_Y}C${bowControl.x},${bowControl.y} ${legAxisX},${hipY} ${legAxisX},${hipY}`; } const RIGHT_LEG_REST_PATH = buildLegSvgPath(RIGHT_LEG_AXIS_X, rightLegBowControlStanding, HIP_Y_STANDING); const LEFT_LEG_REST_PATH = buildLegSvgPath(LEFT_LEG_AXIS_X, leftLegBowControlStanding, HIP_Y_STANDING); const RIGHT_LEG_COMPRESSED_PATH = buildLegSvgPath(RIGHT_LEG_AXIS_X, rightLegBowControlSquatting, HIP_Y_SQUATTING); const LEFT_LEG_COMPRESSED_PATH = buildLegSvgPath(LEFT_LEG_AXIS_X, leftLegBowControlSquatting, HIP_Y_SQUATTING); export 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_VELOCITY = 225; // impulsion vers le haut (unités SVG/s) const RESTITUTION = 0.15; // restitution du mini-rebond à l'atterrissage const JUMP_CYCLE_DURATION_MS = 1200; // ms entre deux lancers const SQUAT_DURATION_MS = 220; // durée du squat avant lancer const LEG_EXTEND_DURATION_MS = 100; // extension des jambes juste après le lancer const FOOT_MAX_ROTATION_DEG = 45; // rotation max des pieds (degrés) const BODY_SQUAT_AMPLITUDE = HIP_Y_SQUATTING - HIP_Y_STANDING; const rightCharacter = document.getElementById("right-character"); const rightCharacterBody = 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"); function interpolateSvgPath(startPath, endPath, interpolationFactor) { const numberPattern = /-?\d+\.?\d*/g; const startNumbers = startPath.match(numberPattern).map(Number); const endNumbers = endPath.match(numberPattern).map(Number); let numberIndex = 0; return startPath.replace(numberPattern, () => { const interpolatedValue = startNumbers[numberIndex] + (endNumbers[numberIndex] - startNumbers[numberIndex]) * interpolationFactor; numberIndex++; return interpolatedValue.toFixed(2); }); } const easeInOutQuadratic = (t) => (t < 0.5 ? 2 * t * t : 1 - (-2 * t + 2) ** 2 / 2); const easeInQuadratic = (t) => t * t; // Crée un ressort secondaire indépendant (fermeture sur position/vitesse) function createSpringSimulator(stiffness, damping) { let position = 0; let velocity = 0; return (target, deltaTime) => { const acceleration = -stiffness * (position - target) - damping * velocity; velocity += acceleration * deltaTime; position += velocity * deltaTime; return position; }; } // Cinématique verticale — position > 0 = en l'air let verticalPosition = 0; let verticalVelocity = 0; let landingCompressionRatio = 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 = createSpringSimulator(300, 18); const legSpring = createSpringSimulator(300, 18); const rightFootSpring = createSpringSimulator(150, 10); const leftFootSpring = createSpringSimulator(150, 10); let isHovered = false; let isInJumpCycle = false; let jumpCycleStartTime = null; let hasLaunched = false; let previousFrameTime = null; const wrapper = document.querySelector(".characters"); wrapper.addEventListener("mouseenter", () => { isHovered = true; }); wrapper.addEventListener("mouseleave", () => { isHovered = false; }); function computeSquatCompressionRatio(elapsedCycleTime) { if (elapsedCycleTime < SQUAT_DURATION_MS) { return easeInOutQuadratic(elapsedCycleTime / SQUAT_DURATION_MS); } if (elapsedCycleTime < SQUAT_DURATION_MS + LEG_EXTEND_DURATION_MS) { return 1 - easeInQuadratic((elapsedCycleTime - SQUAT_DURATION_MS) / LEG_EXTEND_DURATION_MS); } return 0; } function applyLandingBounce(impactVelocity) { if (impactVelocity > 50) { landingCompressionRatio = Math.min(0.6, (impactVelocity / LAUNCH_VELOCITY) * 0.8); } verticalVelocity = impactVelocity > 15 ? impactVelocity * RESTITUTION : 0; } function updateVerticalPhysics(deltaTime) { if (verticalPosition > 0 || verticalVelocity > 0) { verticalVelocity -= GRAVITY * deltaTime; } verticalPosition += verticalVelocity * deltaTime; if (verticalPosition < 0) { if (verticalVelocity < 0) { applyLandingBounce(-verticalVelocity); } verticalPosition = 0; } landingCompressionRatio = Math.max(0, landingCompressionRatio - 4 * deltaTime); } function updateJumpCycle(currentTime) { if (isHovered && !isInJumpCycle) { jumpCycleStartTime = currentTime; isInJumpCycle = true; hasLaunched = false; } let elapsedCycleTime = isInJumpCycle ? currentTime - jumpCycleStartTime : 0; if (isInJumpCycle && elapsedCycleTime >= JUMP_CYCLE_DURATION_MS) { if (isHovered) { jumpCycleStartTime = currentTime; elapsedCycleTime = 0; hasLaunched = false; } else { isInJumpCycle = false; elapsedCycleTime = 0; } } let squatCompressionRatio = 0; if (isInJumpCycle) { squatCompressionRatio = computeSquatCompressionRatio(elapsedCycleTime); if (!hasLaunched && elapsedCycleTime >= SQUAT_DURATION_MS) { verticalVelocity += LAUNCH_VELOCITY; hasLaunched = true; } } return squatCompressionRatio; } function applyTransforms(compressionTarget, deltaTime) { const verticalTranslation = -verticalPosition; const footRotationTarget = verticalPosition > 3 ? Math.min(1, (verticalPosition - 3) / 12) : 0; const bodyVerticalDisplacement = bodySpring(compressionTarget * BODY_SQUAT_AMPLITUDE, deltaTime); const legCompressionRatio = Math.max(0, Math.min(1, legSpring(compressionTarget, deltaTime))); const rightFootRotationAngle = Math.max(0, rightFootSpring(footRotationTarget * FOOT_MAX_ROTATION_DEG, deltaTime)); const leftFootRotationAngle = Math.min(0, leftFootSpring(-footRotationTarget * FOOT_MAX_ROTATION_DEG, deltaTime)); rightCharacter.setAttribute("transform", `translate(0,${verticalTranslation.toFixed(2)})`); rightCharacterBody.setAttribute("transform", `translate(0,${bodyVerticalDisplacement.toFixed(2)})`); rightLeg.setAttribute("d", interpolateSvgPath(RIGHT_LEG_REST_PATH, RIGHT_LEG_COMPRESSED_PATH, legCompressionRatio)); leftLeg.setAttribute("d", interpolateSvgPath(LEFT_LEG_REST_PATH, LEFT_LEG_COMPRESSED_PATH, legCompressionRatio)); rightFoot.setAttribute("transform", `rotate(${rightFootRotationAngle.toFixed(1)},${RIGHT_LEG_AXIS_X},217.6)`); leftFoot.setAttribute("transform", `rotate(${leftFootRotationAngle.toFixed(1)},${LEFT_LEG_AXIS_X},217.6)`); } function animateJump(currentTime) { if (!previousFrameTime) previousFrameTime = currentTime; const deltaTime = Math.min((currentTime - previousFrameTime) / 1000, 0.05); previousFrameTime = currentTime; const squatCompressionRatio = updateJumpCycle(currentTime); updateVerticalPhysics(deltaTime); const compressionTarget = Math.max(squatCompressionRatio, landingCompressionRatio); applyTransforms(compressionTarget, deltaTime); requestAnimationFrame(animateJump); } requestAnimationFrame(animateJump); }