aura-glass
Version:
A comprehensive glassmorphism design system for React applications with 142+ production-ready components
497 lines (479 loc) • 16.9 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 { AnimatePresence, motion } from 'framer-motion';
import { RotateCcw, Zap, Triangle } 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';
// Shatter geometry factory
const ShatterGeometryFactory = {
createGlassShard: (size = 1, complexity = 3, random = new SeededRandom()) => {
const geometry = new THREE.PlaneGeometry(size, size, complexity, complexity);
// Add random jagged edges
const positions = geometry.attributes.position.array;
for (let i = 0; i < positions.length; i += 3) {
const x = positions[i];
const y = positions[i + 1];
// Add some randomness to edges
const edgeFactor = Math.abs(x) + Math.abs(y);
if (edgeFactor > 0.8) {
positions[i] += random.nextSigned() * 0.1;
positions[i + 1] += random.nextSigned() * 0.1;
positions[i + 2] += random.nextSigned() * 0.05;
}
}
geometry.computeVertexNormals();
return geometry;
},
createShatterField: (count = 20, spread = 5, random = new SeededRandom()) => {
const geometries = [];
for (let i = 0; i < count; i++) {
const size = 0.5 + random.next() * 1.5;
const complexity = 2 + random.nextInt(0, 2);
const geometry = ShatterGeometryFactory.createGlassShard(size, complexity, random);
geometries.push(geometry);
}
return geometries;
}
};
// Shatter material factory
const ShatterMaterialFactory = {
createGlassShardMaterial: (options = {}) => {
const {
color = new THREE.Color(0.7, 0.9, 1.0),
opacity = 0.8,
refractionRatio = 1.5,
reflectivity = 0.8
} = options;
return new THREE.ShaderMaterial({
uniforms: {
time: {
value: 0
},
color: {
value: color
},
opacity: {
value: opacity
},
refractionRatio: {
value: refractionRatio
},
reflectivity: {
value: reflectivity
},
shatterProgress: {
value: 0
}
},
vertexShader: `
varying vec3 vPosition;
varying vec3 vNormal;
varying vec2 vUv;
void main() {
vPosition = position;
vNormal = normal;
vUv = uv;
// Add shatter displacement
vec3 displacedPosition = position;
displacedPosition += normal * sin(time * 10.0 + position.x * 5.0) * 0.02;
gl_Position = projectionMatrix * modelViewMatrix * vec4(displacedPosition, 1.0);
}
`,
fragmentShader: `
uniform float time;
uniform vec3 color;
uniform float opacity;
uniform float refractionRatio;
uniform float reflectivity;
uniform float shatterProgress;
varying vec3 vPosition;
varying vec3 vNormal;
varying vec2 vUv;
void main() {
// Fresnel effect for glass-like appearance
float fresnel = pow(1.0 - dot(vNormal, vec3(0.0, 0.0, 1.0)), 2.0);
// Add some color variation based on position
vec3 finalColor = color;
finalColor += vec3(sin(vPosition.x * 10.0) * 0.1, cos(vPosition.y * 10.0) * 0.1, sin(vPosition.z * 10.0) * 0.1);
// Shatter effect
float shatterEffect = sin(time * 20.0 + vPosition.x * 5.0 + vPosition.y * 5.0) * 0.5 + 0.5;
finalColor *= (1.0 - shatterProgress * 0.3);
float finalOpacity = opacity * (0.7 + 0.3 * fresnel) * (1.0 - shatterProgress * 0.2);
gl_FragColor = vec4(finalColor, finalOpacity);
}
`,
transparent: true,
side: THREE.DoubleSide
});
},
createRefractionMaterial: (options = {}) => {
const {
color = new THREE.Color(0.5, 0.8, 1.0),
intensity = 1.0
} = options;
return new THREE.ShaderMaterial({
uniforms: {
time: {
value: 0
},
color: {
value: color
},
intensity: {
value: intensity
},
refractionStrength: {
value: 0.1
}
},
vertexShader: `
varying vec3 vPosition;
varying vec3 vNormal;
void main() {
vPosition = position;
vNormal = normalize(normalMatrix * normal);
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
uniform float time;
uniform vec3 color;
uniform float intensity;
uniform float refractionStrength;
varying vec3 vPosition;
varying vec3 vNormal;
void main() {
// Simulate refraction through glass
vec3 refractedColor = color;
refractedColor.r += sin(vPosition.x * 20.0 + time * 5.0) * refractionStrength;
refractedColor.g += cos(vPosition.y * 20.0 + time * 5.0) * refractionStrength;
refractedColor.b += sin(vPosition.z * 20.0 + time * 5.0) * refractionStrength;
// Add some sparkle effect
float sparkle = sin(vPosition.x * 50.0 + time * 10.0) *
cos(vPosition.y * 50.0 + time * 10.0) * 0.5 + 0.5;
refractedColor += sparkle * intensity * 0.3;
gl_FragColor = vec4(refractedColor, 0.8);
}
`,
transparent: true
});
}
};
// Shatter animation system
const ShatterAnimations = {
createShardAnimation: (shard, targetPosition, targetRotation, duration = 2) => {
const startPosition = shard.position.clone();
const startRotation = shard.rotation.clone();
const startTime = Date.now();
return () => {
const elapsed = (Date.now() - startTime) / 1000;
const progress = Math.min(elapsed / duration, 1);
// Easing function for smooth animation
const easeOut = t => 1 - Math.pow(1 - t, 3);
const easedProgress = easeOut(progress);
// Interpolate position
shard.position.lerpVectors(startPosition, targetPosition, easedProgress);
// Interpolate rotation
shard.rotation.x = THREE.MathUtils.lerp(startRotation.x, targetRotation.x, easedProgress);
shard.rotation.y = THREE.MathUtils.lerp(startRotation.y, targetRotation.y, easedProgress);
shard.rotation.z = THREE.MathUtils.lerp(startRotation.z, targetRotation.z, easedProgress);
return progress >= 1;
};
},
createExplosionAnimation: (shards, center, force = 10, duration = 1) => {
useReducedMotion();
const animations = shards.map((shard, index) => {
const direction = new THREE.Vector3((Math.random() - 0.5) * 2, (Math.random() - 0.5) * 2, (Math.random() - 0.5) * 2).normalize();
const distance = 2 + Math.random() * 3;
const targetPosition = center.clone().add(direction.multiplyScalar(distance));
const targetRotation = new THREE.Euler(Math.random() * Math.PI * 2, Math.random() * Math.PI * 2, Math.random() * Math.PI * 2);
return ShatterAnimations.createShardAnimation(shard, targetPosition, targetRotation, duration);
});
return () => {
const allComplete = animations.every(animation => animation());
return allComplete;
};
},
createReformAnimation: (shards, originalPositions, duration = 2) => {
const animations = shards.map((shard, index) => {
const targetPosition = originalPositions[index];
const targetRotation = new THREE.Euler(0, 0, 0);
return ShatterAnimations.createShardAnimation(shard, targetPosition, targetRotation, duration);
});
return () => {
const allComplete = animations.every(animation => animation());
return allComplete;
};
}
};
function GlassShatterEffects({
children,
className = "",
trigger = "click",
duration = 2,
intensity = 1,
shardCount = 12,
autoReform = true,
reformDelay = 3000,
onShatter,
onReform,
disabled = false,
showControls = false,
seed
}) {
const prefersReducedMotion = useReducedMotion();
const containerRef = useRef(null);
const canvasRef = useRef(null);
const [isShattered, setIsShattered] = useState(false);
const [isAnimating, setIsAnimating] = useState(false);
const [shards, setShards] = useState([]);
const [originalPositions, setOriginalPositions] = useState([]);
const [currentAnimation, setCurrentAnimation] = useState(null);
// Initialize shards
useEffect(() => {
if (!canvasRef.current) return;
const seededRandom = new SeededRandom(seed ?? `${shardCount}`);
const geometries = ShatterGeometryFactory.createShatterField(shardCount, 5, seededRandom);
const newShards = [];
const positions = [];
geometries.forEach((geometry, index) => {
const material = ShatterMaterialFactory.createGlassShardMaterial({
opacity: 0.8 - seededRandom.next() * 0.3,
refractionRatio: 1.3 + seededRandom.next() * 0.4
});
const shard = new THREE.Mesh(geometry, material);
shard.position.set(seededRandom.nextSigned() * 2, seededRandom.nextSigned() * 2, 0);
shard.rotation.set(seededRandom.next() * Math.PI * 2, seededRandom.next() * Math.PI * 2, seededRandom.next() * Math.PI * 2);
newShards.push(shard);
positions.push(shard.position.clone());
});
setShards(newShards);
setOriginalPositions(positions);
}, [shardCount, seed]);
// Trigger shatter effect
const triggerShatter = useCallback(() => {
if (disabled || isAnimating) return;
setIsShattered(true);
setIsAnimating(true);
onShatter?.();
// Create explosion animation
const center = new THREE.Vector3(0, 0, 0);
const animation = ShatterAnimations.createExplosionAnimation(shards, center, intensity * 10, duration);
setCurrentAnimation(() => animation);
// Auto reform after delay
if (autoReform) {
setTimeout(() => {
triggerReform();
}, reformDelay);
}
}, [disabled, isAnimating, shards, intensity, duration, autoReform, reformDelay, onShatter]);
// Trigger reform effect
const triggerReform = useCallback(() => {
if (!isShattered || isAnimating) return;
setIsAnimating(true);
onReform?.();
const animation = ShatterAnimations.createReformAnimation(shards, originalPositions, duration);
setCurrentAnimation(() => animation);
}, [isShattered, isAnimating, shards, originalPositions, duration, onReform]);
// Manual trigger
const handleManualTrigger = useCallback(() => {
if (isShattered) {
triggerReform();
} else {
triggerShatter();
}
}, [isShattered, triggerReform, triggerShatter]);
// Handle different trigger types
useEffect(() => {
if (disabled) return;
const element = containerRef.current;
if (!element) return;
const handleClick = () => {
if (trigger === "click") {
handleManualTrigger();
}
};
const handleMouseEnter = () => {
if (trigger === "hover") {
triggerShatter();
}
};
const handleMouseLeave = () => {
if (trigger === "hover" && autoReform) {
setTimeout(() => {
triggerReform();
}, reformDelay);
}
};
if (trigger === "click") {
element.addEventListener("click", handleClick);
} else if (trigger === "hover") {
element.addEventListener("mouseenter", handleMouseEnter);
element.addEventListener("mouseleave", handleMouseLeave);
}
return () => {
element.removeEventListener("click", handleClick);
element.removeEventListener("mouseenter", handleMouseEnter);
element.removeEventListener("mouseleave", handleMouseLeave);
};
}, [trigger, disabled, handleManualTrigger, triggerShatter, triggerReform, autoReform, reformDelay]);
// Auto trigger effect
useEffect(() => {
if (trigger === "auto" && !disabled) {
const interval = setInterval(() => {
useReducedMotion();
if (!isAnimating) {
triggerShatter();
}
}, 5000);
return () => clearInterval(interval);
}
}, [trigger, disabled, isAnimating, triggerShatter]);
return jsxs("div", {
ref: containerRef,
className: cn("glass-shatter-effects glass-relative glass-overflow-hidden", className),
style: {
position: "relative",
cursor: trigger === "click" ? "pointer" : "default"
},
children: [jsx("div", {
className: cn("content glass-transition-opacity glass-duration-300", isShattered ? "glass-opacity-0" : "glass-opacity-100"),
children: children
}), jsx(AnimatePresence, {
children: isShattered && jsx(motion.div, {
initial: {
opacity: 0
},
animate: prefersReducedMotion ? {} : {
opacity: 1
},
exit: {
opacity: 0
},
className: cn("glass-absolute glass-inset-0 glass-pointer-events-none"),
children: jsx(Canvas, {
ref: canvasRef,
className: cn("glass-w-full glass-h-full"),
camera: {
position: [0, 0, 5],
fov: 75
},
gl: {
alpha: true,
antialias: true
},
children: jsx(ShatterScene, {
shards: shards,
currentAnimation: currentAnimation,
setCurrentAnimation: setCurrentAnimation,
setIsAnimating: setIsAnimating,
setIsShattered: setIsShattered,
isShattered: isShattered
})
})
})
}), showControls && jsxs(motion.div, {
initial: {
opacity: 0,
y: 20
},
animate: prefersReducedMotion ? {} : {
opacity: 1,
y: 0
},
className: cn("glass-absolute glass-bottom-4 glass-right-4 glass-flex glass-gap-2"),
children: [jsx("button", {
onClick: handleManualTrigger,
disabled: isAnimating,
className: cn("glass-p-2 glass-surface-subtle glass-foundation-complete glass-radius-lg glass-border glass-border-primary hover:glass-surface-hover glass-transition-colors disabled:glass-opacity-50 glass-focus glass-touch-target glass-contrast-guard"),
title: isShattered ? "Reform" : "Shatter",
children: isShattered ? jsx(RotateCcw, {
className: cn("glass-w-4 glass-h-4")
}) : jsx(Zap, {
className: cn("glass-w-4 glass-h-4")
})
}), isAnimating && jsxs("div", {
className: cn("glass-flex glass-items-center glass-gap-2 glass-px-3 glass-py-2 glass-surface-info glass-foundation-complete glass-radius-lg glass-text-info glass-text-sm"),
children: [jsx(motion.div, {
animate: prefersReducedMotion ? {} : {
rotate: 360
},
transition: prefersReducedMotion ? {
duration: 0
} : {
duration: 1,
repeat: Infinity,
ease: "linear"
},
children: jsx(Triangle, {
className: cn("glass-w-3 glass-h-3")
})
}), "Animating..."]
})]
})]
});
}
// 3D Shatter Scene Component
function ShatterScene({
shards,
currentAnimation,
setCurrentAnimation,
setIsAnimating,
setIsShattered,
isShattered
}) {
const {
scene
} = useThree();
// Add shards to scene
useEffect(() => {
shards.forEach(shard => {
scene.add(shard);
});
return () => {
shards.forEach(shard => {
scene.remove(shard);
});
};
}, [shards, scene]);
// Animation loop
useFrame(state => {
const time = state.clock.elapsedTime;
// Update shard materials
shards.forEach((shard, index) => {
if (shard.material && "uniforms" in shard.material && shard.material.uniforms) {
if (typeof shard.material.uniforms === "object" && "time" in shard.material.uniforms) shard.material.uniforms.time.value = time;
if (typeof shard.material.uniforms === "object" && "shatterProgress" in shard.material.uniforms) shard.material.uniforms.shatterProgress.value = isShattered ? 1 : 0;
}
});
// Run current animation
if (currentAnimation) {
const complete = currentAnimation();
if (complete) {
setCurrentAnimation(null);
setIsAnimating(false);
if (!isShattered) {
setIsShattered(false);
}
}
}
});
return jsxs(Fragment, {
children: [jsx("ambientLight", {
intensity: 0.4
}), jsx("pointLight", {
position: [10, 10, 10],
intensity: 0.8
}), jsx("pointLight", {
position: [-10, -10, -10],
intensity: 0.3,
color: 0x4488ff
})]
});
}
export { GlassShatterEffects, GlassShatterEffects as default };
//# sourceMappingURL=GlassShatterEffects.js.map