hecq-et-lesort/assets/js/characters/jump.js
isUnknown 967afa28c3 characters : refacto clean code, découpage en modules ES
Noms de variables et fonctions explicites (sans abréviation), fonctions
unitaires, paths SVG construits via template literals avec constantes nommées.
Découpage en assets/js/characters/jump.js et pupilTracking.js, importés dans
script.js. Clamp de la rotation des pieds pour éviter le bounce à l'atterrissage.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 08:11:47 +02:00

192 lines
8.1 KiB
JavaScript

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);
}