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>
192 lines
8.1 KiB
JavaScript
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);
|
|
}
|