import { toast } from "./dom.js"; import { t } from "./i18n.js"; // Background parallax ------------------------------------------------ export function setupBackgroundPan(config = {}) { const root = document.documentElement; const maxOffset = config.maxOffsetPx ?? 30; const ease = config.ease ?? 0.08; const scaleFactor = config.scaleFactor ?? 1.12; if (scaleFactor) { root.style.setProperty("--bg-scale", scaleFactor); } let targetX = 0; let targetY = 0; let currX = 0; let currY = 0; const setTarget = (x, y) => { targetX = x; targetY = y; }; const step = () => { currX += (targetX - currX) * ease; currY += (targetY - currY) * ease; root.style.setProperty("--bg-x", `${currX}px`); root.style.setProperty("--bg-y", `${currY}px`); requestAnimationFrame(step); }; window.addEventListener("mousemove", (e) => { const nx = (e.clientX / window.innerWidth - 0.5) * 2; const ny = (e.clientY / window.innerHeight - 0.5) * 2; setTarget(-nx * maxOffset, -ny * maxOffset); }); window.addEventListener("mouseleave", () => setTarget(0, 0)); window.addEventListener("blur", () => setTarget(0, 0)); step(); } // Screenshot hover --------------------------------------------------- export function setupCardVisualHover(el, url) { if (!el || !url) return; const img = new Image(); let naturalW = 0; let naturalH = 0; let loaded = false; img.src = url; img.onload = () => { naturalW = img.naturalWidth; naturalH = img.naturalHeight; loaded = true; }; const reset = () => { el.classList.remove("hovering"); el.style.backgroundSize = ""; el.style.backgroundPosition = ""; el.style.backgroundRepeat = ""; }; el.addEventListener("mouseenter", () => { el.classList.add("hovering"); el.style.backgroundSize = "auto"; el.style.backgroundRepeat = "no-repeat"; el.style.backgroundPosition = "center"; }); el.addEventListener("mousemove", (e) => { if (!loaded) return; const rect = el.getBoundingClientRect(); const overW = naturalW - rect.width; const overH = naturalH - rect.height; if (overW <= 0 && overH <= 0) { el.style.backgroundPosition = "center"; return; } const xRatio = (e.clientX - rect.left) / rect.width; const yRatio = (e.clientY - rect.top) / rect.height; const xPercent = overW > 0 ? xRatio * 100 : 50; const yPercent = overH > 0 ? yRatio * 100 : 50; el.style.backgroundPosition = `${xPercent}% ${yPercent}%`; }); ["mouseleave", "blur"].forEach((evt) => el.addEventListener(evt, reset)); } // Celebration FX ----------------------------------------------------- let fxCanvas; let fxCtx; let fxParticles = []; let fxAnimating = false; function ensureFxCanvas() { if (fxCanvas) return; fxCanvas = document.createElement("canvas"); fxCanvas.className = "fx-canvas"; fxCanvas.style.position = "fixed"; fxCanvas.style.inset = "0"; fxCanvas.style.pointerEvents = "none"; fxCanvas.style.zIndex = "120"; fxCanvas.width = window.innerWidth; fxCanvas.height = window.innerHeight; fxCtx = fxCanvas.getContext("2d"); document.body.appendChild(fxCanvas); window.addEventListener("resize", () => { fxCanvas.width = window.innerWidth; fxCanvas.height = window.innerHeight; }); } export function triggerCelebration(button) { ensureFxCanvas(); const rect = (button || document.body).getBoundingClientRect(); const x = rect.left + rect.width / 2; const y = rect.top + rect.height / 2; spawnConfetti(x, y, 80); spawnFirework(x, y, 40); if (!fxAnimating) { fxAnimating = true; requestAnimationFrame(fxStep); } } function spawnConfetti(x, y, count) { for (let i = 0; i < count; i++) { fxParticles.push({ x: x + (Math.random() - 0.5) * 20, y: y + (Math.random() - 0.5) * 200, vx: (Math.random() - 0.5) * 6, vy: Math.random() * -6 - 2, size: 6 + Math.random() * 4, life: 600 + Math.random() * 200, color: randomColor(), type: "confetti", wobble: Math.random() * Math.PI * 2, }); } } function spawnFirework(x, y, count) { for (let i = 0; i < count; i++) { const angle = (Math.PI * 2 * i) / count + Math.random() * 0.3; const speed = (3 + Math.random() * 3) * 0.1; fxParticles.push({ x, y, vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed - 3, size: 4 + Math.random() * 3, life: 500 + Math.random() * 200, color: randomColor(), type: "spark", }); } } function fxStep() { if (!fxCtx || !fxCanvas) return; fxCtx.clearRect(0, 0, fxCanvas.width, fxCanvas.height); fxParticles = fxParticles.filter((p) => p.life > 0); for (const p of fxParticles) { if (p.type === "confetti") { p.vy += 0.05; p.vx *= 0.999; p.wobble += 0.2; p.x += p.vx + Math.cos(p.wobble) * 0.8; p.y += p.vy; } else { p.vy += 0.02; p.vx *= 0.9995; p.x += p.vx; p.y += p.vy; } p.life -= 1; fxCtx.fillStyle = p.color; fxCtx.beginPath(); if (p.type === "confetti") { fxCtx.fillRect(p.x, p.y, p.size, p.size * 0.6); } else { fxCtx.arc(p.x, p.y, p.size * 0.5, 0, Math.PI * 2); fxCtx.fill(); } } if (fxParticles.length > 0) { requestAnimationFrame(fxStep); } else { fxCtx.clearRect(0, 0, fxCanvas.width, fxCanvas.height); fxAnimating = false; } } function randomColor() { const palette = ["#ff595e", "#ffca3a", "#8ac926", "#1982c4", "#6a4c93"]; return palette[Math.floor(Math.random() * palette.length)]; }