characters : physique gravité, déclenchement au survol, refactor JS
- Remplace le ressort principal par une cinématique gravité constante (900 SVG/s²) pour un décollage vif et une chute naturelle - L'animation de saut se déclenche en boucle au mouseenter et s'arrête au mouseleave (atterrissage propre en cours de vol) - Extrait la logique d'animation dans characters.js, script.js garde uniquement le scroll - SVG : groupe #right-character-body pour dissocier corps/tête des jambes lors de la compression - CSS : overflow visible sur le SVG pour le débordement pendant le saut Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
904726951e
commit
da3fd9db2f
4 changed files with 228 additions and 9 deletions
|
|
@ -30,6 +30,7 @@
|
|||
<path id="right-exterior-hair" d="M141.53,118.34C138.12,139.12 158.27,148.88 146.26,171.34" style="fill:none;fill-rule:nonzero;stroke:black;stroke-width:4.4px;"/>
|
||||
</g>
|
||||
<g id="right-character">
|
||||
<g id="right-character-body">
|
||||
<g id="right-character-background" transform="matrix(1,0,0,1,-0.12,0)">
|
||||
<path d="M249.93,112.67C257.02,113.3 256.91,107.59 256.61,102.85C256.12,95.14 260.71,91.79 261.31,88.25C262.61,80.54 257.62,77.55 253.38,80.39L248.44,86.77C252.75,53.53 252.3,39.3 242.17,26.37C229.79,10.57 209.7,2.2 198.36,2.2C187.01,2.2 166.92,10.57 154.55,26.37C144.931,38.648 144.04,52.099 147.665,81.887C147.858,83.468 148.063,85.095 148.28,86.77L143.58,80.39C139.34,77.55 134.35,80.54 135.65,88.25C136.25,91.79 140.84,95.14 140.35,102.85C140.05,107.59 139.94,113.3 147.03,112.67L150.12,113.56C149.66,117.67 150.16,119.74 150.65,122.32C151.92,129.09 155.79,139.79 167.27,149.93C174.38,156.21 176.05,170.49 198.93,170.49C221.81,170.49 223.48,156.22 230.59,149.93C239.3,142.23 243.62,134.22 245.76,127.84C246.22,126.48 246.96,123.91 247.17,122.97L249.93,112.67Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
|
|
@ -37,10 +38,6 @@
|
|||
<path id="left-chick" d="M169.84,132.38C163.4,118.53 181.92,121.34 188.5,108.15" style="fill:none;fill-rule:nonzero;stroke:black;stroke-width:4.4px;"/>
|
||||
<path id="right-chick" d="M231.14,132.79C236.78,120.67 224.08,120.72 216.98,113.74" style="fill:none;fill-rule:nonzero;stroke:black;stroke-width:4.4px;"/>
|
||||
<path id="mouth1" serif:id="mouth" d="M198.92,132.62C199.76,132.6 200.74,132.42 201.93,132C209.46,129.4 218.69,132 218.69,132C212.56,138.24 204.63,140.14 198.97,139.83L198.85,139.83C193.19,140.14 185.26,138.23 179.13,132C179.13,132 188.36,129.4 195.89,132C197.09,132.41 198.06,132.59 198.9,132.62L198.92,132.62Z" style="fill-rule:nonzero;"/>
|
||||
<path id="right-foot1" serif:id="right-foot" d="M210.93,217.6C210.93,217.6 225.79,220.44 231.13,224.24C236.47,228.04 208.82,226.26 208.82,226.26L210.94,217.6L210.93,217.6Z" style="fill-rule:nonzero;"/>
|
||||
<path id="right-leg1" serif:id="right-leg" d="M209.01,216.53C221.33,194.34 209.01,173.32 209.01,173.32" style="fill:none;fill-rule:nonzero;stroke:black;stroke-width:4.4px;"/>
|
||||
<path id="left-foot1" serif:id="left-foot" d="M187.09,217.6C187.09,217.6 172.23,220.44 166.89,224.24C161.55,228.04 189.2,226.26 189.2,226.26L187.08,217.6L187.09,217.6Z" style="fill-rule:nonzero;"/>
|
||||
<path id="left-leg1" serif:id="left-leg" d="M189.01,216.53C176.69,194.34 189.01,173.32 189.01,173.32" style="fill:none;fill-rule:nonzero;stroke:black;stroke-width:4.4px;"/>
|
||||
<path id="right-ear" d="M249.93,112.67C257.02,113.3 256.91,107.59 256.61,102.85C256.12,95.14 260.71,91.79 261.31,88.25C262.61,80.54 257.62,77.55 253.38,80.39" style="fill:none;fill-rule:nonzero;stroke:black;stroke-width:4.4px;"/>
|
||||
<path id="right-ear-ring" d="M255.94,107.41C259.48,109.52 260.64,114 258.53,117.42C256.42,120.84 251.84,121.91 248.31,119.8C244.77,117.69 243.61,113.21 245.72,109.79" style="fill:none;fill-rule:nonzero;stroke:black;stroke-width:4.4px;"/>
|
||||
<path id="left-ear" d="M147.03,112.67C139.94,113.3 140.05,107.59 140.35,102.85C140.84,95.14 136.25,91.79 135.65,88.25C134.35,80.54 139.34,77.55 143.58,80.39" style="fill:none;fill-rule:nonzero;stroke:black;stroke-width:4.4px;"/>
|
||||
|
|
@ -60,4 +57,9 @@
|
|||
<path id="right-eye-top1" serif:id="right-eye-top" d="M209.33,82.47C215.12,77.33 224.14,69.78 236.68,83.31" style="fill:none;fill-rule:nonzero;stroke:black;stroke-width:4.4px;"/>
|
||||
<path id="skull" d="M248.44,86.77C252.75,53.53 252.3,39.3 242.17,26.37C229.79,10.57 209.7,2.2 198.36,2.2C187.01,2.2 166.92,10.57 154.55,26.37C144.42,39.3 143.97,53.53 148.28,86.77" style="fill:none;fill-rule:nonzero;stroke:black;stroke-width:4.4px;"/>
|
||||
</g>
|
||||
<path id="right-foot1" serif:id="right-foot" d="M210.93,217.6C210.93,217.6 225.79,220.44 231.13,224.24C236.47,228.04 208.82,226.26 208.82,226.26L210.94,217.6L210.93,217.6Z" style="fill-rule:nonzero;"/>
|
||||
<path id="right-leg1" serif:id="right-leg" d="M209.01,216.53C221.33,194.34 209.01,173.32 209.01,173.32" style="fill:none;fill-rule:nonzero;stroke:black;stroke-width:4.4px;"/>
|
||||
<path id="left-foot1" serif:id="left-foot" d="M187.09,217.6C187.09,217.6 172.23,220.44 166.89,224.24C161.55,228.04 189.2,226.26 189.2,226.26L187.08,217.6L187.09,217.6Z" style="fill-rule:nonzero;"/>
|
||||
<path id="left-leg1" serif:id="left-leg" d="M189.01,216.53C176.69,194.34 189.01,173.32 189.01,173.32" style="fill:none;fill-rule:nonzero;stroke:black;stroke-width:4.4px;"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
217
assets/js/characters.js
Normal file
217
assets/js/characters.js
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
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);
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const charactersWrapper = document.querySelector(".characters");
|
||||
const logo = document.querySelector(".logo");
|
||||
const svg = charactersWrapper.querySelector("svg");
|
||||
|
||||
const CHAR_SPEED = 0.6;
|
||||
const LOGO_SPEED = 1.2;
|
||||
|
|
@ -15,8 +16,6 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||
|
||||
function onScroll() {
|
||||
const scrollY = window.scrollY;
|
||||
|
||||
// Characters : descend de charMax à 0 au fil du scroll
|
||||
moveCharacters(scrollY);
|
||||
scaleCharacters(scrollY);
|
||||
moveLogo(scrollY);
|
||||
|
|
@ -29,8 +28,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||
|
||||
function scaleCharacters(scrollY) {
|
||||
const charOffset = Math.max(0, charMax - (scrollY / 16) * CHAR_SPEED);
|
||||
const charWidth = CHAR_WIDTH_MIN + (charOffset / charMax) * (CHAR_WIDTH_MAX - CHAR_WIDTH_MIN);
|
||||
const svg = charactersWrapper.querySelector("svg");
|
||||
const charWidth =
|
||||
CHAR_WIDTH_MIN + (charOffset / charMax) * (CHAR_WIDTH_MAX - CHAR_WIDTH_MIN);
|
||||
svg.setAttribute("width", charWidth + "px");
|
||||
svg.setAttribute("height", charWidth + "px");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,5 +7,6 @@
|
|||
<meta name="robots" content="noindex,nofollow">
|
||||
|
||||
<link rel="stylesheet" href="<?= url('assets/css/style.css') ?>">
|
||||
<script src="<?= url('assets/js/script.js') ?>"></script>
|
||||
<script src="<?= url('assets/js/script.js') ?>" defer></script>
|
||||
<script src="<?= url('assets/js/characters.js') ?>" defer></script>
|
||||
</head>
|
||||
Loading…
Add table
Add a link
Reference in a new issue