aura-glass
Version:
A comprehensive glassmorphism design system for React applications with 142+ production-ready components
452 lines (449 loc) • 16.1 kB
JavaScript
'use client';
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
import { useReducedMotion } from '../../hooks/useReducedMotion.js';
import { Canvas, useThree, useFrame } from '@react-three/fiber';
import { motion } from 'framer-motion';
import { Pause, Play, Wind, Cloud, Leaf, Sun, Flower, Snowflake } from 'lucide-react';
import { useRef, useState, useEffect, useCallback } from 'react';
import { cn } from '../../lib/utilsComprehensive.js';
import * as THREE from 'three';
import { SeededRandom } from '../../utils/random.js';
// Seasonal particle systems
const SeasonalParticleFactory = {
// Snow particles
createSnowParticle: (size = 1, complexity = 6) => {
const geometry = new THREE.ConeGeometry(size * 0.1, size * 0.3, complexity);
return geometry;
},
// Leaf particles
createLeafParticle: (size = 1) => {
const geometry = new THREE.PlaneGeometry(size, size * 1.5);
// Add some curvature to make it look like a leaf
const positions = geometry.attributes.position.array;
for (let i = 0; i < positions.length; i += 3) {
const x = positions[i];
const y = positions[i + 1];
positions[i + 2] = Math.sin(x * 5) * 0.1 * Math.abs(y);
}
geometry.computeVertexNormals();
return geometry;
},
// Flower petals
createPetalParticle: (size = 1) => {
const geometry = new THREE.PlaneGeometry(size, size * 2);
// Curve the petal
const positions = geometry.attributes.position.array;
for (let i = 0; i < positions.length; i += 3) {
const x = positions[i];
const y = positions[i + 1];
positions[i + 2] = Math.sin(x * 3) * 0.2 * (1 - Math.abs(y));
}
geometry.computeVertexNormals();
return geometry;
},
// Sun rays
createSunRayParticle: (length = 1, width = 0.1) => {
const geometry = new THREE.CylinderGeometry(width, width * 0.5, length, 8);
return geometry;
},
// Rain drops
createRainDropParticle: (length = 1) => {
const geometry = new THREE.CylinderGeometry(0.02, 0.02, length, 6);
return geometry;
},
// Create particle field for season
createSeasonalField: (season, count = 50, random = new SeededRandom()) => {
const particles = [];
for (let i = 0; i < count; i++) {
let geometry;
let material;
switch (season) {
case "winter":
geometry = SeasonalParticleFactory.createSnowParticle(0.5 + random.next() * 0.5);
material = SeasonalParticleFactory.createSnowMaterial();
break;
case "autumn":
geometry = SeasonalParticleFactory.createLeafParticle(0.3 + random.next() * 0.4);
material = SeasonalParticleFactory.createLeafMaterial();
break;
case "spring":
geometry = SeasonalParticleFactory.createPetalParticle(0.2 + random.next() * 0.3);
material = SeasonalParticleFactory.createPetalMaterial(random);
break;
case "summer":
geometry = SeasonalParticleFactory.createSunRayParticle(1 + random.next() * 2);
material = SeasonalParticleFactory.createSunRayMaterial();
break;
default:
geometry = new THREE.SphereGeometry(0.1);
material = new THREE.MeshBasicMaterial({
color: 0xffffff
});
}
const particle = new THREE.Mesh(geometry, material);
particle.position.set(random.nextSigned() * 10, random.next() * 15, random.nextSigned() * 5);
particle.rotation.set(random.next() * Math.PI * 2, random.next() * Math.PI * 2, random.next() * Math.PI * 2);
// Add custom properties for animation
particle.userData = {
originalY: particle.position.y,
fallSpeed: 0.01 + random.next() * 0.05,
rotationSpeed: random.nextSigned() * 0.02,
windOffset: random.next() * Math.PI * 2,
season
};
particles.push(particle);
}
return particles;
},
// Material factories
createSnowMaterial: () => new THREE.MeshPhongMaterial({
color: 0xffffff,
transparent: true,
opacity: 0.8,
shininess: 100
}),
createLeafMaterial: () => new THREE.MeshPhongMaterial({
color: new THREE.Color().setHSL(0.1, 0.8, 0.4),
transparent: true,
opacity: 0.9,
side: THREE.DoubleSide
}),
createPetalMaterial: random => new THREE.MeshPhongMaterial({
color: new THREE.Color().setHSL(random.nextInRange(0.8, 1.0), 0.8, 0.6),
transparent: true,
opacity: 0.8,
side: THREE.DoubleSide
}),
createSunRayMaterial: () => new THREE.MeshBasicMaterial({
color: new THREE.Color(1, 0.9, 0.3),
transparent: true,
opacity: 0.6
}),
createRainMaterial: () => new THREE.MeshBasicMaterial({
color: new THREE.Color(0.5, 0.7, 1),
transparent: true,
opacity: 0.7
})
};
// Seasonal animation system
const SeasonalAnimations = {
// Snow falling animation
snowFall: (particle, time, windStrength = 1) => {
particle.position.y -= particle.userData.fallSpeed;
particle.position.x += Math.sin(time * 2 + particle.userData.windOffset) * 0.01 * windStrength;
particle.rotation.x += particle.userData.rotationSpeed;
particle.rotation.y += particle.userData.rotationSpeed * 0.5;
// Reset particle when it falls below screen
if (particle.position.y < -10) {
particle.position.y = 15 + Math.random() * 5;
particle.position.x = (Math.random() - 0.5) * 20;
}
},
// Leaf falling animation
leafFall: (particle, time, windStrength = 1) => {
particle.position.y -= particle.userData.fallSpeed;
particle.position.x += Math.sin(time * 1.5 + particle.userData.windOffset) * 0.02 * windStrength;
particle.rotation.x += particle.userData.rotationSpeed;
particle.rotation.z += particle.userData.rotationSpeed * 2;
if (particle.position.y < -10) {
particle.position.y = 15 + Math.random() * 5;
particle.position.x = (Math.random() - 0.5) * 20;
}
},
// Petal floating animation
petalFloat: (particle, time, windStrength = 1) => {
particle.position.y -= particle.userData.fallSpeed * 0.5;
particle.position.x += Math.sin(time * 1 + particle.userData.windOffset) * 0.015 * windStrength;
particle.rotation.y += particle.userData.rotationSpeed;
particle.rotation.x += Math.sin(time * 3 + particle.userData.windOffset) * 0.01;
if (particle.position.y < -10) {
particle.position.y = 15 + Math.random() * 5;
particle.position.x = (Math.random() - 0.5) * 20;
}
},
// Sun ray pulsing animation
sunRayPulse: (particle, time) => {
const pulse = Math.sin(time * 4) * 0.3 + 0.7;
particle.scale.y = pulse;
if (particle.material && "opacity" in particle.material) {
particle.material.opacity = 0.3 + pulse * 0.4;
}
},
// Rain falling animation
rainFall: (particle, time, windStrength = 1) => {
particle.position.y -= particle.userData.fallSpeed * 3;
particle.position.x += windStrength * 0.05;
if (particle.position.y < -10) {
particle.position.y = 15 + Math.random() * 5;
particle.position.x = (Math.random() - 0.5) * 20;
}
}
};
function SeasonalParticles({
season = "auto",
particleCount = 30,
windStrength = 1,
animationSpeed = 1,
className = "",
showControls = false,
autoSeason = true,
seasonDuration = 10000,
// 10 seconds per season
onSeasonChange,
children,
seed
}) {
const prefersReducedMotion = useReducedMotion();
const canvasRef = useRef(null);
const [currentSeason, setCurrentSeason] = useState(season === "auto" ? "winter" : season);
const [particles, setParticles] = useState([]);
const [isPlaying, setIsPlaying] = useState(true);
const [seasonIndex, setSeasonIndex] = useState(0);
const seasons = ["winter", "spring", "summer", "autumn"];
// Initialize particles for current season
useEffect(() => {
const seededRandom = new SeededRandom(seed ?? `${currentSeason}-${particleCount}`);
const newParticles = SeasonalParticleFactory.createSeasonalField(currentSeason, particleCount, seededRandom);
setParticles(newParticles);
onSeasonChange?.(currentSeason);
}, [currentSeason, particleCount, onSeasonChange, seed]);
// Auto season rotation
useEffect(() => {
if (!autoSeason || season !== "auto") return;
const interval = setInterval(() => {
setSeasonIndex(prev => {
const next = (prev + 1) % seasons.length;
setCurrentSeason(seasons[next]);
return next;
});
}, seasonDuration);
return () => clearInterval(interval);
}, [autoSeason, season, seasonDuration, seasons]);
// Manual season control
const changeSeason = useCallback(newSeason => {
setCurrentSeason(newSeason);
setSeasonIndex(seasons.indexOf(newSeason));
}, [seasons]);
// Play/pause control
const togglePlay = useCallback(() => {
setIsPlaying(prev => !prev);
}, []);
// Get season icon
const getSeasonIcon = seasonName => {
switch (seasonName) {
case "winter":
return jsx(Snowflake, {
className: cn("glass-w-4 glass-h-4")
});
case "spring":
return jsx(Flower, {
className: cn("glass-w-4 glass-h-4")
});
case "summer":
return jsx(Sun, {
className: cn("glass-w-4 glass-h-4")
});
case "autumn":
return jsx(Leaf, {
className: cn("glass-w-4 glass-h-4")
});
default:
return jsx(Cloud, {
className: cn("glass-w-4 glass-h-4")
});
}
};
// Get season colors for UI
const getSeasonColors = seasonName => {
switch (seasonName) {
case "winter":
return "text-blue-400 border-blue-400/30 bg-blue-400/10";
case "spring":
return "text-pink-400 border-pink-400/30 bg-pink-400/10";
case "summer":
return "text-yellow-400 border-yellow-400/30 bg-yellow-400/10";
case "autumn":
return "text-orange-400 border-orange-400/30 bg-orange-400/10";
default:
return "text-gray-400 border-gray-400/30 bg-gray-400/10";
}
};
return jsxs("div", {
className: cn("seasonal-particles glass-relative glass-overflow-hidden", className),
children: [jsx(Canvas, {
ref: canvasRef,
className: cn("glass-absolute glass-inset-0 glass-pointer-events-none"),
camera: {
position: [0, 5, 10],
fov: 75
},
gl: {
alpha: true,
antialias: true
},
children: jsx(SeasonalScene, {
particles: particles,
currentSeason: currentSeason,
windStrength: windStrength,
animationSpeed: animationSpeed,
isPlaying: isPlaying
})
}), jsx("div", {
className: cn("glass-relative glass-z-10"),
children: children
}), jsx(motion.div, {
initial: {
opacity: 0,
y: 20
},
animate: prefersReducedMotion ? {} : {
opacity: 1,
y: 0
},
className: cn("glass-absolute glass-top-4 glass-left-4 glass-px-3 glass-py-2 glass-radius-lg glass-foundation-complete glass-border", getSeasonColors(currentSeason)),
children: jsxs("div", {
className: cn("glass-flex glass-items-center glass-gap-2"),
children: [getSeasonIcon(currentSeason), jsx("span", {
className: cn("glass-text-sm glass-font-medium glass-capitalize"),
children: currentSeason
})]
})
}), showControls && jsxs(motion.div, {
initial: {
opacity: 0,
y: 20
},
animate: prefersReducedMotion ? {} : {
opacity: 1,
y: 0
},
className: 'absolute bottom-4 right-4 glass-flex glass-flex-col glass-gap-2',
children: [jsx("div", {
className: "glass-flex glass-gap-2 glass-p-2 glass-surface-dark/20 glass-glass-glass-backdrop-blur-lg glass-radius-lg glass-border glass-border-white/10 glass-contrast-guard",
children: seasons.map(seasonName => jsx("button", {
onClick: () => changeSeason(seasonName),
className: `p-2 rounded-lg transition-colors glass-focus glass-touch-target glass-contrast-guard ${currentSeason === seasonName ? "bg-white/20 text-white" : "text-white/60 hover:text-white hover:bg-white/10"}`,
title: `Switch to ${seasonName}`,
children: getSeasonIcon(seasonName)
}, seasonName))
}), jsxs("div", {
className: "glass-flex glass-gap-2 glass-p-2 glass-surface-dark/20 glass-glass-glass-backdrop-blur-lg glass-radius-lg glass-border glass-border-white/10 glass-contrast-guard",
children: [jsx("button", {
onClick: togglePlay,
className: 'glass-p-2 glass-radius-lg text-primary hover:glass-surface-subtle/10 transition-colors glass-focus glass-touch-target glass-contrast-guard',
title: isPlaying ? "Pause" : "Play",
children: isPlaying ? jsx(Pause, {
className: 'w-4 h-4'
}) : jsx(Play, {
className: 'w-4 h-4'
})
}), jsxs("div", {
className: 'glass-flex glass-items-center glass-gap-2 text-primary/60 glass-text-sm',
children: [jsx(Wind, {
className: 'w-3 h-3'
}), jsx("span", {
children: windStrength.toFixed(1)
})]
})]
})]
}), windStrength > 0 && jsx(motion.div, {
initial: {
opacity: 0,
x: -20
},
animate: prefersReducedMotion ? {} : {
opacity: 1,
x: 0
},
className: 'absolute top-4 right-4 glass-px-3 glass-py-2 glass-surface-dark/20 glass-glass-backdrop-blur-lg glass-radius-lg glass-border glass-border-white/10 glass-contrast-guard',
children: jsxs("div", {
className: 'glass-flex glass-items-center glass-gap-2 text-primary/60 glass-text-sm',
children: [jsx(motion.div, {
animate: prefersReducedMotion ? {} : {
x: windStrength > 0 ? [0, 5, 0] : 0
},
transition: prefersReducedMotion ? {
duration: 0
} : {
duration: 2,
repeat: Infinity,
ease: "easeInOut"
},
children: jsx(Wind, {
className: 'w-3 h-3'
})
}), jsxs("span", {
children: ["Wind: ", windStrength.toFixed(1)]
})]
})
})]
});
}
// 3D Seasonal Scene Component
function SeasonalScene({
particles,
currentSeason,
windStrength,
animationSpeed,
isPlaying
}) {
const {
scene
} = useThree();
// Add particles to scene
useEffect(() => {
particles.forEach(particle => {
scene.add(particle);
});
return () => {
particles.forEach(particle => {
scene.remove(particle);
});
};
}, [particles, scene]);
// Animation loop
useFrame(state => {
if (!isPlaying) return;
const time = state.clock.elapsedTime * animationSpeed;
particles.forEach(particle => {
if (!particle.userData) return;
switch (particle.userData.season || currentSeason) {
case "winter":
SeasonalAnimations.snowFall(particle, time, windStrength);
break;
case "autumn":
SeasonalAnimations.leafFall(particle, time, windStrength);
break;
case "spring":
SeasonalAnimations.petalFloat(particle, time, windStrength);
break;
case "summer":
SeasonalAnimations.sunRayPulse(particle, time);
break;
}
});
});
return jsxs(Fragment, {
children: [jsx("ambientLight", {
intensity: 0.3
}), currentSeason === "summer" && jsx("directionalLight", {
position: [10, 10, 5],
intensity: 1.2,
color: 0xffeb3b
}), currentSeason === "winter" && jsx("directionalLight", {
position: [5, 5, 10],
intensity: 0.8,
color: 0xe3f2fd
}), currentSeason === "autumn" && jsx("directionalLight", {
position: [5, 10, 5],
intensity: 0.9,
color: 0xff9800
}), currentSeason === "spring" && jsx("directionalLight", {
position: [10, 5, 5],
intensity: 1.0,
color: 0xe91e63
})]
});
}
export { SeasonalParticles, SeasonalParticles as default };
//# sourceMappingURL=SeasonalParticles.js.map