159 lines
4.8 KiB
JavaScript
159 lines
4.8 KiB
JavaScript
// Effects & helpers --------------------------------------------------
|
|
|
|
// 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)];
|
|
}
|