feat(fx): add dynamic 60fps rising gold & magenta shimmer sparkle particles

This commit is contained in:
2026-08-29 17:40:27 -07:00
parent dad517916b
commit 054631ebe3
60 changed files with 231 additions and 63 deletions
+3 -1
View File
@@ -7,10 +7,12 @@ import ServicesSection from '@/components/ServicesSection';
import AboutSection from '@/components/AboutSection';
import BookingForm from '@/components/BookingForm';
import Footer from '@/components/Footer';
import GoldSparkles from '@/components/GoldSparkles';
export default function Home() {
return (
<main>
<main style={{ position: 'relative' }}>
<GoldSparkles />
<Header />
<Hero />
<PillarsSection />
+164
View File
@@ -0,0 +1,164 @@
'use client';
import React, { useEffect, useRef } from 'react';
interface Particle {
x: number;
y: number;
size: number;
speedY: number;
speedX: number;
opacity: number;
fadeSpeed: number;
color: string;
isStar: boolean;
rotation: number;
rotationSpeed: number;
swayOffset: number;
swaySpeed: number;
}
export default function GoldSparkles() {
const canvasRef = useRef<HTMLCanvasElement | null>(null);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
let animationFrameId: number;
let width = (canvas.width = window.innerWidth);
let height = (canvas.height = window.innerHeight);
const handleResize = () => {
if (!canvas) return;
width = canvas.width = window.innerWidth;
height = canvas.height = window.innerHeight;
};
window.addEventListener('resize', handleResize);
const goldColors = [
'#FDF0CD', // pale champagne sparkle
'#F7E7B4', // light gold
'#D4AF37', // metallic gold
'#AA771C', // deep amber gold
'#FF69B4', // rare magenta pink shimmer
];
const particleCount = Math.min(65, Math.floor(width / 22));
const particles: Particle[] = [];
for (let i = 0; i < particleCount; i++) {
particles.push({
x: Math.random() * width,
y: Math.random() * height,
size: Math.random() * 2.5 + 0.8,
speedY: Math.random() * 0.45 + 0.15,
speedX: (Math.random() - 0.5) * 0.2,
opacity: Math.random() * 0.8 + 0.2,
fadeSpeed: (Math.random() * 0.01 + 0.005) * (Math.random() > 0.5 ? 1 : -1),
color: goldColors[Math.floor(Math.random() * goldColors.length)],
isStar: Math.random() > 0.65,
rotation: Math.random() * Math.PI * 2,
rotationSpeed: (Math.random() - 0.5) * 0.02,
swayOffset: Math.random() * Math.PI * 2,
swaySpeed: Math.random() * 0.015 + 0.008,
});
}
const drawStar = (cx: number, cy: number, spikes: number, outerRadius: number, innerRadius: number, rot: number) => {
let rotStep = Math.PI / spikes;
ctx.beginPath();
ctx.moveTo(cx + Math.cos(rot) * outerRadius, cy + Math.sin(rot) * outerRadius);
for (let i = 0; i < spikes; i++) {
let x = cx + Math.cos(rot) * outerRadius;
let y = cy + Math.sin(rot) * outerRadius;
ctx.lineTo(x, y);
rot += rotStep;
x = cx + Math.cos(rot) * innerRadius;
y = cy + Math.sin(rot) * innerRadius;
ctx.lineTo(x, y);
rot += rotStep;
}
ctx.lineTo(cx + Math.cos(rot) * outerRadius, cy + Math.sin(rot) * outerRadius);
ctx.closePath();
};
let tick = 0;
const render = () => {
tick++;
ctx.clearRect(0, 0, width, height);
for (let i = 0; i < particles.length; i++) {
const p = particles[i];
// Move upward with subtle horizontal swaying
p.y -= p.speedY;
p.x += Math.sin(tick * p.swaySpeed + p.swayOffset) * 0.4 + p.speedX;
p.rotation += p.rotationSpeed;
// Twinkle / pulse opacity
p.opacity += p.fadeSpeed;
if (p.opacity >= 0.95 || p.opacity <= 0.15) {
p.fadeSpeed = -p.fadeSpeed;
}
// Reset when moving off top or sides
if (p.y < -20) {
p.y = height + 10;
p.x = Math.random() * width;
}
if (p.x < -20) p.x = width + 10;
if (p.x > width + 20) p.x = -10;
ctx.save();
ctx.globalAlpha = Math.max(0.1, Math.min(1, p.opacity));
ctx.fillStyle = p.color;
ctx.shadowColor = p.color;
ctx.shadowBlur = p.isStar ? 8 : 4;
if (p.isStar) {
drawStar(p.x, p.y, 4, p.size * 2.2, p.size * 0.7, p.rotation);
ctx.fill();
} else {
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
}
animationFrameId = requestAnimationFrame(render);
};
render();
return () => {
window.removeEventListener('resize', handleResize);
cancelAnimationFrame(animationFrameId);
};
}, []);
return (
<canvas
ref={canvasRef}
style={{
position: 'fixed',
top: 0,
left: 0,
width: '100vw',
height: '100vh',
pointerEvents: 'none',
zIndex: 1,
opacity: 0.85,
}}
aria-hidden="true"
/>
);
}