feat(fx): add dynamic 60fps rising gold & magenta shimmer sparkle particles
This commit is contained in:
+3
-1
@@ -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 />
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user