aura-glass
Version:
A comprehensive glassmorphism design system for React applications with 142+ production-ready components
363 lines (360 loc) • 12.8 kB
JavaScript
'use client';
import { jsxs, jsx } from 'react/jsx-runtime';
import { forwardRef, useRef, useState, useEffect, useCallback } from 'react';
import { useAnimationFrame } from 'framer-motion';
import { cn } from '../../lib/utilsComprehensive.js';
import '../../primitives/GlassCore.js';
import '../../primitives/glass/GlassAdvanced.js';
import { OptimizedGlassCore } from '../../primitives/OptimizedGlassCore.js';
import '../../primitives/glass/OptimizedGlassAdvanced.js';
import '../../primitives/MotionNative.js';
import '../../primitives/motion/MotionFramer.js';
import { useA11yId } from '../../utils/a11y.js';
const GlassParticles = /*#__PURE__*/forwardRef(({
className,
count = 50,
maxSize = 8,
minSize = 2,
speed = 1,
connectionDistance = 100,
mouseInteraction = true,
mouseRadius = 150,
colorScheme = "gradient",
colors = ["var(--glass-color-primary)", "#8b5cf6", "#ec4899"],
blur = true,
glow = true,
shape = "circle",
behavior = "float",
emitRate = 0,
lifetime = 0,
"aria-label": ariaLabel,
respectMotionPreference = true
}, ref) => {
const canvasRef = useRef(null);
const particles = useRef([]);
const mousePos = useRef({
x: -1000,
y: -1000
});
const frame = useRef(0);
const lastEmit = useRef(0);
const [dimensions, setDimensions] = useState({
width: 0,
height: 0
});
const componentId = useA11yId("glass-particles");
const [prefersReducedMotion, setPrefersReducedMotion] = useState(false);
// Check for reduced motion preference
useEffect(() => {
if (!respectMotionPreference) return;
const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
setPrefersReducedMotion(mediaQuery.matches);
const handleChange = e => setPrefersReducedMotion(e.matches);
mediaQuery.addEventListener("change", handleChange);
return () => mediaQuery.removeEventListener("change", handleChange);
}, [respectMotionPreference]);
// Initialize particles
const initParticles = useCallback(() => {
const canvas = canvasRef.current;
if (!canvas) return;
particles.current = Array.from({
length: count
}, (_, i) => ({
id: i,
x: Math.random() * canvas.width,
y: Math.random() * canvas.height,
vx: (Math.random() - 0.5) * speed,
vy: (Math.random() - 0.5) * speed,
size: minSize + Math.random() * (maxSize - minSize),
opacity: 0.3 + Math.random() * 0.7,
hue: Math.random() * 360,
life: lifetime > 0 ? Math.random() * lifetime : Infinity,
maxLife: lifetime,
connections: []
}));
}, [count, maxSize, minSize, speed, lifetime]);
// Handle resize
useEffect(() => {
const handleResize = () => {
const canvas = canvasRef.current;
if (!canvas) return;
const parent = canvas.parentElement;
if (!parent) return;
canvas.width = parent.clientWidth;
canvas.height = parent.clientHeight;
setDimensions({
width: canvas.width,
height: canvas.height
});
// Reinitialize particles on resize
initParticles();
};
handleResize();
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, [initParticles]);
// Mouse interaction
useEffect(() => {
if (!mouseInteraction) return;
const handleMouseMove = e => {
const canvas = canvasRef.current;
if (!canvas) return;
const rect = canvas.getBoundingClientRect();
mousePos.current = {
x: e.clientX - rect.left,
y: e.clientY - rect.top
};
};
const handleMouseLeave = () => {
mousePos.current = {
x: -1000,
y: -1000
};
};
window.addEventListener("mousemove", handleMouseMove);
window.addEventListener("mouseleave", handleMouseLeave);
return () => {
window.removeEventListener("mousemove", handleMouseMove);
window.removeEventListener("mouseleave", handleMouseLeave);
};
}, [mouseInteraction]);
// Draw particle shape
const drawParticle = useCallback((ctx, particle, color) => {
ctx.save();
ctx.translate(particle.x, particle.y);
// Apply glow effect
if (glow) {
ctx.shadowBlur = particle.size * 2;
ctx.shadowColor = color;
}
ctx.fillStyle = color;
ctx.globalAlpha = particle.opacity * (particle.life / particle.maxLife);
switch (shape) {
case "circle":
ctx.beginPath();
ctx.arc(0, 0, particle.size, 0, Math.PI * 2);
ctx.fill();
break;
case "square":
ctx.fillRect(-particle.size, -particle.size, particle.size * 2, particle.size * 2);
break;
case "triangle":
ctx.beginPath();
ctx.moveTo(0, -particle.size);
ctx.lineTo(-particle.size, particle.size);
ctx.lineTo(particle.size, particle.size);
ctx.closePath();
ctx.fill();
break;
case "star":
ctx.beginPath();
for (let i = 0; i < 5; i++) {
const angle = (i * 72 - 90) * Math.PI / 180;
const x = Math.cos(angle) * particle.size;
const y = Math.sin(angle) * particle.size;
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
const innerAngle = (i * 72 + 36 - 90) * Math.PI / 180;
const innerX = Math.cos(innerAngle) * (particle.size / 2);
const innerY = Math.sin(innerAngle) * (particle.size / 2);
ctx.lineTo(innerX, innerY);
}
ctx.closePath();
ctx.fill();
break;
}
ctx.restore();
}, [shape, glow]);
// Get particle color
const getParticleColor = useCallback((particle, index) => {
switch (colorScheme) {
case "monochrome":
return `hsla(220, 70%, 60%, ${particle.opacity})`;
case "rainbow":
return `hsla(${particle.hue}, 70%, 60%, ${particle.opacity})`;
case "gradient":
const colorIndex = Math.floor(index / particles.current.length * colors.length);
return colors[Math.min(colorIndex, colors.length - 1)];
case "custom":
return colors[index % colors.length];
default:
return `hsla(220, 70%, 60%, ${particle.opacity})`;
}
}, [colorScheme, colors]);
// Update particle behavior
const updateParticle = useCallback((particle, canvas) => {
switch (behavior) {
case "float":
particle.x += particle.vx;
particle.y += particle.vy;
// Add gentle floating motion
particle.y += Math.sin(frame.current * 0.01 + particle.id) * 0.2;
particle.x += Math.cos(frame.current * 0.01 + particle.id) * 0.1;
break;
case "swarm":
// Particles attract to center of mass
const centerX = particles.current.reduce((sum, p) => sum + p.x, 0) / particles.current.length;
const centerY = particles.current.reduce((sum, p) => sum + p.y, 0) / particles.current.length;
const toCenterX = (centerX - particle.x) * 0.0005;
const toCenterY = (centerY - particle.y) * 0.0005;
particle.vx += toCenterX;
particle.vy += toCenterY;
particle.x += particle.vx;
particle.y += particle.vy;
// Limit velocity
const maxVel = 2;
const vel = Math.sqrt(particle.vx * particle.vx + particle.vy * particle.vy);
if (vel > maxVel) {
particle.vx = particle.vx / vel * maxVel;
particle.vy = particle.vy / vel * maxVel;
}
break;
case "orbit":
const centerOX = canvas.width / 2;
const centerOY = canvas.height / 2;
const angle = Math.atan2(particle.y - centerOY, particle.x - centerOX);
const radius = Math.sqrt(Math.pow(particle.x - centerOX, 2) + Math.pow(particle.y - centerOY, 2));
const newAngle = angle + 0.01 * speed;
particle.x = centerOX + Math.cos(newAngle) * radius;
particle.y = centerOY + Math.sin(newAngle) * radius;
break;
case "explode":
const explodeX = canvas.width / 2;
const explodeY = canvas.height / 2;
const dx = particle.x - explodeX;
const dy = particle.y - explodeY;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist > 1) {
particle.vx = dx / dist * speed * 2;
particle.vy = dy / dist * speed * 2;
}
particle.x += particle.vx;
particle.y += particle.vy;
particle.vx *= 0.98; // Decay
particle.vy *= 0.98;
break;
case "gravity":
particle.vy += 0.1; // Gravity
particle.x += particle.vx;
particle.y += particle.vy;
// Bounce off bottom
if (particle.y > canvas.height - particle.size) {
particle.y = canvas.height - particle.size;
particle.vy *= -0.8; // Energy loss
}
break;
}
// Mouse interaction
if (mouseInteraction) {
const dx = mousePos.current.x - particle.x;
const dy = mousePos.current.y - particle.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < mouseRadius) {
const force = (mouseRadius - distance) / mouseRadius;
particle.vx -= dx / distance * force * 2;
particle.vy -= dy / distance * force * 2;
}
}
// Boundary conditions
if (behavior !== "gravity") {
if (particle.x < 0 || particle.x > canvas.width) particle.vx *= -1;
if (particle.y < 0 || particle.y > canvas.height) particle.vy *= -1;
particle.x = Math.max(0, Math.min(canvas.width, particle.x));
particle.y = Math.max(0, Math.min(canvas.height, particle.y));
}
// Update life
if (lifetime > 0) {
particle.life -= 1;
if (particle.life <= 0) {
// Respawn particle
particle.x = Math.random() * canvas.width;
particle.y = behavior === "gravity" ? 0 : Math.random() * canvas.height;
particle.vx = (Math.random() - 0.5) * speed;
particle.vy = (Math.random() - 0.5) * speed;
particle.life = lifetime;
}
}
}, [behavior, speed, mouseInteraction, mouseRadius, lifetime]);
// Animation loop
useAnimationFrame(time => {
if (prefersReducedMotion) return; // Skip animation if user prefers reduced motion
const canvas = canvasRef.current;
const ctx = canvas?.getContext("2d");
if (!canvas || !ctx) return;
frame.current += 1;
// Clear canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Apply blur filter if enabled
if (blur) {
ctx.filter = "blur(0.5px)";
}
// Update and draw particles
particles.current.forEach((particle, i) => {
updateParticle(particle, canvas);
// Find connections
particle.connections = [];
particles.current.forEach((other, j) => {
if (i === j) return;
const dx = particle.x - other.x;
const dy = particle.y - other.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < connectionDistance) {
particle.connections.push(j);
// Draw connection
const opacity = (1 - distance / connectionDistance) * 0.2;
ctx.strokeStyle = `rgba(255, 255, 255, ${opacity})`;
ctx.lineWidth = 0.5;
ctx.beginPath();
ctx.moveTo(particle.x, particle.y);
ctx.lineTo(other.x, other.y);
ctx.stroke();
}
});
// Draw particle
const color = getParticleColor(particle, i);
drawParticle(ctx, particle, color);
});
// Emit new particles
if (emitRate > 0 && time - lastEmit.current > 1000 / emitRate) {
const newParticle = {
id: particles.current.length,
x: canvas.width / 2,
y: canvas.height / 2,
vx: (Math.random() - 0.5) * speed * 2,
vy: (Math.random() - 0.5) * speed * 2,
size: minSize + Math.random() * (maxSize - minSize),
opacity: 1,
hue: Math.random() * 360,
life: lifetime || Infinity,
maxLife: lifetime || Infinity,
connections: []
};
particles.current.push(newParticle);
// Remove oldest if too many
if (particles.current.length > count * 2) {
particles.current.shift();
}
lastEmit.current = time;
}
});
return jsxs("div", {
ref: ref,
id: componentId,
className: cn("relative overflow-hidden", className),
role: "presentation",
"aria-label": ariaLabel || "Interactive particle effects",
"aria-hidden": !ariaLabel,
children: [jsx("canvas", {
ref: canvasRef,
className: 'absolute inset-0 glass-w-full glass-h-full',
"aria-hidden": "true"
}), jsx(OptimizedGlassCore, {
className: 'absolute inset-0 pointer-events-none opacity-30',
intent: "neutral",
elevation: "level1",
blur: "subtle",
intensity: "medium"
})]
});
});
export { GlassParticles };
//# sourceMappingURL=GlassParticles.js.map