Modularize frontend app script
This commit is contained in:
197
wwwroot/js/effects.js
vendored
Normal file
197
wwwroot/js/effects.js
vendored
Normal file
@@ -0,0 +1,197 @@
|
||||
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,
|
||||
y,
|
||||
vx: (Math.random() - 0.5) * 6,
|
||||
vy: Math.random() * -6 - 2,
|
||||
size: 6 + Math.random() * 4,
|
||||
life: 60 + Math.random() * 20,
|
||||
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;
|
||||
fxParticles.push({
|
||||
x,
|
||||
y,
|
||||
vx: Math.cos(angle) * speed,
|
||||
vy: Math.sin(angle) * speed,
|
||||
size: 4 + Math.random() * 3,
|
||||
life: 50 + Math.random() * 20,
|
||||
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.18;
|
||||
p.vx *= 0.99;
|
||||
p.wobble += 0.2;
|
||||
p.x += p.vx + Math.cos(p.wobble) * 0.8;
|
||||
p.y += p.vy;
|
||||
} else {
|
||||
p.vy += 0.08;
|
||||
p.vx *= 0.995;
|
||||
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)];
|
||||
}
|
||||
Reference in New Issue
Block a user