// 3D-наклон портрета на главной по движению курсора. // Эффект "парящей карточки": карточка поворачивается к курсору, // слои дают параллакс, поверх ложится мягкий блик. const MAX_TILT = 9; // градусов по каждой оси const GLIDE = 320; // ms возврата в покой function isTouchOrReduced() { const touch = window.matchMedia("(hover: none), (pointer: coarse)").matches; const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches; return touch || reduced; } export function initHero3DTilt() { const shell = document.querySelector(".hero__portrait-shell"); if (!shell || isTouchOrReduced()) return; let rafId = null; let targetX = 0; let targetY = 0; let currentX = 0; let currentY = 0; let active = false; function onMove(e) { const rect = shell.getBoundingClientRect(); const px = (e.clientX - rect.left) / rect.width; // 0..1 const py = (e.clientY - rect.top) / rect.height; // 0..1 targetY = (px - 0.5) * 2 * MAX_TILT; // rotateY targetX = (0.5 - py) * 2 * MAX_TILT; // rotateX shell.style.setProperty("--tilt-px", `${px * 100}%`); shell.style.setProperty("--tilt-py", `${py * 100}%`); if (!active) { active = true; shell.classList.add("hero__portrait-shell--tilting"); } if (!rafId) rafId = requestAnimationFrame(tick); } function tick() { currentX += (targetX - currentX) * 0.18; currentY += (targetY - currentY) * 0.18; shell.style.setProperty("--tilt-rx", `${currentX.toFixed(2)}deg`); shell.style.setProperty("--tilt-ry", `${currentY.toFixed(2)}deg`); const done = Math.abs(targetX - currentX) < 0.01 && Math.abs(targetY - currentX) < 0.01; if (done && !active) { rafId = null; } else { rafId = requestAnimationFrame(tick); } } function onLeave() { active = false; targetX = 0; targetY = 0; shell.classList.remove("hero__portrait-shell--tilting"); if (!rafId) rafId = requestAnimationFrame(tick); } shell.addEventListener("mousemove", onMove); shell.addEventListener("mouseleave", onLeave); }