aura-glass
Version:
A comprehensive glassmorphism design system for React applications with 142+ production-ready components
649 lines (646 loc) • 23.4 kB
JavaScript
'use client';
import { jsx, jsxs } from 'react/jsx-runtime';
import { forwardRef, useRef, useState, useCallback, useEffect } from 'react';
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 { MotionFramer } from '../../primitives/motion/MotionFramer.js';
import { cn } from '../../lib/utilsComprehensive.js';
import { useA11yId } from '../../utils/a11y.js';
import { useMotionPreferenceContext } from '../../contexts/MotionPreferenceContext.js';
const GlassWeatherGlass = /*#__PURE__*/forwardRef(({
width = 600,
height = 400,
weather = {
type: "clear",
intensity: 0.5,
temperature: 20,
humidity: 0.6,
pressure: 1013,
windSpeed: 10,
windDirection: 45,
visibility: 10,
id: "default-weather"
},
autoUpdate = false,
updateInterval = 30000,
particleDensity = 1,
showAtmosphericEffects = true,
weatherResponsive = true,
temperatureRange = [-20, 40],
showWeatherInfo = true,
animationSpeed = 1,
windStrength = 1,
dayNightCycle = true,
timeOfDay = 12,
onWeatherChange,
onAtmosphericEvent,
showControls = true,
respectMotionPreference = true,
className,
...props
}, ref) => {
const {
prefersReducedMotion,
isMotionSafe
} = useMotionPreferenceContext();
const canvasRef = useRef(null);
const animationRef = useRef();
const weatherGlassId = useA11yId("glass-weather-glass");
const [currentWeather, setCurrentWeather] = useState(weather);
const [particles, setParticles] = useState([]);
const [atmosphericEffects, setAtmosphericEffects] = useState([]);
const [animationTime, setAnimationTime] = useState(0);
const [currentTimeOfDay, setCurrentTimeOfDay] = useState(timeOfDay);
const [glassColor, setGlassColor] = useState([255, 255, 255]);
const [backgroundGradient, setBackgroundGradient] = useState(["#87CEEB", "#E0F6FF"]);
// Weather color mappings
const weatherColors = {
clear: {
sky: ["#87CEEB", "#E0F6FF"],
glass: [255, 255, 255]
},
sunny: {
sky: ["#FFD700", "#FFF8DC"],
glass: [255, 248, 220]
},
cloudy: {
sky: ["#A9A9A9", "#D3D3D3"],
glass: [211, 211, 211]
},
rainy: {
sky: ["#4682B4", "#708090"],
glass: [70, 130, 180]
},
stormy: {
sky: ["#2F4F4F", "#483D8B"],
glass: [47, 79, 79]
},
snowy: {
sky: ["#F0F8FF", "#FFFAFA"],
glass: [240, 248, 255]
},
foggy: {
sky: ["#C0C0C0", "#F5F5F5"],
glass: [192, 192, 192]
},
windy: {
sky: ["#B0C4DE", "#E6E6FA"],
glass: [176, 196, 222]
}
};
// Temperature to color mapping
const getTemperatureColor = useCallback(temp => {
const [minTemp, maxTemp] = temperatureRange;
const normalized = Math.max(0, Math.min(1, (temp - minTemp) / (maxTemp - minTemp)));
if (normalized < 0.5) {
// Cold: blue to white
const factor = normalized * 2;
return [Math.round(135 + (255 - 135) * factor),
// Blue to white R
Math.round(206 + (255 - 206) * factor),
// Blue to white G
255 // Full blue
];
} else {
// Hot: white to red
const factor = (normalized - 0.5) * 2;
return [255,
// Full red
Math.round(255 - 100 * factor),
// White to red G
Math.round(255 - 255 * factor) // White to red B
];
}
}, [temperatureRange]);
// Update weather responsiveness
useEffect(() => {
if (!weatherResponsive) return;
const colors = weatherColors[currentWeather.type];
const nextBg = colors.sky;
if (backgroundGradient[0] !== nextBg[0] || backgroundGradient[1] !== nextBg[1]) {
setBackgroundGradient(nextBg);
}
const nextGlass = currentWeather.type === "clear" || currentWeather.type === "sunny" ? getTemperatureColor(currentWeather.temperature) : colors.glass;
if (glassColor[0] !== nextGlass[0] || glassColor[1] !== nextGlass[1] || glassColor[2] !== nextGlass[2]) {
setGlassColor(nextGlass);
}
}, [currentWeather, weatherResponsive, getTemperatureColor, backgroundGradient, glassColor]);
// Day/night cycle
useEffect(() => {
if (!dayNightCycle) return;
const isDaytime = currentTimeOfDay >= 6 && currentTimeOfDay <= 18;
// Adjust colors based on time of day
if (!isDaytime) {
setBackgroundGradient(["#191970", "#483D8B"]); // Night colors
setGlassColor([25, 25, 112]); // Midnight blue
}
}, [currentTimeOfDay, dayNightCycle]);
// Generate weather particles
const generateParticles = useCallback((weatherType, intensity) => {
const particleCount = Math.floor(intensity * 100 * particleDensity);
const newParticles = [];
for (let i = 0; i < particleCount; i++) {
let particle = {
x: Math.random() * width,
y: -10,
size: Math.random() * 3 + 1,
opacity: Math.random() * 0.8 + 0.2,
lifetime: Math.random() * 5000 + 2000,
rotation: 0,
rotationSpeed: (Math.random() - 0.5) * 2,
id: `particle-${weatherType}-${i}-${Date.now()}`
};
switch (weatherType) {
case "rainy":
particle = {
...particle,
type: "rain",
vx: (Math.random() - 0.5) * currentWeather.windSpeed * 0.1,
vy: Math.random() * 5 + 3,
color: [100, 149, 237],
size: Math.random() * 2 + 0.5
};
break;
case "snowy":
particle = {
...particle,
type: "snow",
vx: (Math.random() - 0.5) * currentWeather.windSpeed * 0.05,
vy: Math.random() * 2 + 0.5,
color: [255, 255, 255],
size: Math.random() * 4 + 2,
rotationSpeed: (Math.random() - 0.5) * 1
};
break;
case "foggy":
particle = {
...particle,
type: "fog",
x: Math.random() * width,
y: Math.random() * height,
vx: (Math.random() - 0.5) * 0.5,
vy: (Math.random() - 0.5) * 0.2,
color: [220, 220, 220],
size: Math.random() * 20 + 10,
opacity: Math.random() * 0.3 + 0.1
};
break;
case "windy":
particle = {
...particle,
type: "leaf",
vx: (Math.random() - 0.5) * currentWeather.windSpeed * 0.2,
vy: Math.random() * 3 + 1,
color: [34, 139, 34],
size: Math.random() * 3 + 1,
rotationSpeed: (Math.random() - 0.5) * 5
};
break;
default:
if (currentWeather.humidity > 0.8) {
particle = {
...particle,
type: "droplet",
vx: (Math.random() - 0.5) * 0.5,
vy: Math.random() * 1 + 0.2,
color: [173, 216, 230],
size: Math.random() * 1.5 + 0.5,
opacity: Math.random() * 0.5 + 0.1
};
}
break;
}
if (particle.type) {
newParticles.push(particle);
}
}
return newParticles;
}, [width, currentWeather.windSpeed, currentWeather.humidity, particleDensity]);
// Generate atmospheric effects
const generateAtmosphericEffects = useCallback(() => {
if (!showAtmosphericEffects) return [];
const effects = [];
// Lightning for stormy weather
if (currentWeather.type === "stormy" && Math.random() < 0.02) {
effects.push({
type: "lightning",
intensity: Math.random() * 0.8 + 0.2,
duration: 200 + Math.random() * 300,
position: {
x: Math.random() * width,
y: Math.random() * height * 0.3
},
color: [255, 255, 255],
id: `lightning-${Date.now()}`
});
}
// Rainbow after rain
if (currentWeather.type === "rainy" && currentWeather.intensity < 0.3 && Math.random() < 0.01) {
effects.push({
type: "rainbow",
intensity: Math.random() * 0.6 + 0.4,
duration: 5000 + Math.random() * 5000,
position: {
x: width * 0.8,
y: height * 0.3
},
color: [255, 255, 255],
id: `rainbow-${Date.now()}`
});
}
// Mist for high humidity
if (currentWeather.humidity > 0.8 && Math.random() < 0.1) {
effects.push({
type: "mist",
intensity: currentWeather.humidity,
duration: 3000 + Math.random() * 2000,
position: {
x: Math.random() * width,
y: height * 0.8
},
color: [230, 230, 250],
id: `mist-${Date.now()}`
});
}
return effects;
}, [showAtmosphericEffects, currentWeather, width, height]);
// Update particles
const updateParticles = useCallback(deltaTime => {
setParticles(prevParticles => {
const updated = prevParticles.map(particle => {
const windInfluence = currentWeather.windSpeed * windStrength * 0.01;
const windX = Math.cos(currentWeather.windDirection * Math.PI / 180) * windInfluence;
const windY = Math.sin(currentWeather.windDirection * Math.PI / 180) * windInfluence;
return {
...particle,
x: particle.x + (particle.vx + windX) * deltaTime * animationSpeed,
y: particle.y + (particle.vy + windY) * deltaTime * animationSpeed,
rotation: particle.rotation + particle.rotationSpeed * deltaTime * animationSpeed,
lifetime: particle.lifetime - deltaTime
};
}).filter(particle => particle.lifetime > 0 && particle.x > -50 && particle.x < width + 50 && particle.y > -50 && particle.y < height + 50);
// Add new particles
const newParticles = generateParticles(currentWeather.type, currentWeather.intensity);
return [...updated, ...newParticles.slice(0, Math.max(0, 200 - updated.length))];
});
}, [currentWeather, windStrength, animationSpeed, width, height, generateParticles]);
// Update atmospheric effects
const updateAtmosphericEffects = useCallback(deltaTime => {
setAtmosphericEffects(prevEffects => {
const updated = prevEffects.map(effect => ({
...effect,
duration: effect.duration - deltaTime
})).filter(effect => effect.duration > 0);
// Add new effects
const newEffects = generateAtmosphericEffects();
return [...updated, ...newEffects];
});
}, [generateAtmosphericEffects]);
// Render weather
const render = useCallback(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
// Clear canvas with gradient background
const gradient = ctx.createLinearGradient(0, 0, 0, height);
gradient.addColorStop(0, backgroundGradient[0]);
gradient.addColorStop(1, backgroundGradient[1]);
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, width, height);
// Draw atmospheric effects
atmosphericEffects.forEach(effect => {
ctx.save();
ctx.globalAlpha = effect.intensity * (effect.duration / 5000);
switch (effect.type) {
case "lightning":
ctx.strokeStyle = `rgb(${effect.color[0]}, ${effect.color[1]}, ${effect.color[2]})`;
ctx.lineWidth = 3;
ctx.shadowBlur = 10;
ctx.shadowColor = "white";
// Draw lightning bolt
ctx.beginPath();
ctx.moveTo(effect.position.x, 0);
ctx.lineTo(effect.position.x + Math.random() * 20 - 10, height * 0.3);
ctx.lineTo(effect.position.x + Math.random() * 30 - 15, height * 0.6);
ctx.lineTo(effect.position.x + Math.random() * 20 - 10, height);
ctx.stroke();
break;
case "rainbow":
const centerX = effect.position.x;
const centerY = effect.position.y;
const rainbowRadius = 100;
const colors = ["#FF0000", "#FF7F00", "#FFFF00", "#00FF00", "#0000FF", "#4B0082", "#9400D3"];
colors.forEach((color, i) => {
ctx.strokeStyle = color;
ctx.lineWidth = 8;
ctx.beginPath();
ctx.arc(centerX, centerY, rainbowRadius + i * 12, 0, Math.PI, false);
ctx.stroke();
});
break;
case "mist":
const mistGradient = ctx.createRadialGradient(effect.position.x, effect.position.y, 0, effect.position.x, effect.position.y, 100);
mistGradient.addColorStop(0, `rgba(${effect.color[0]}, ${effect.color[1]}, ${effect.color[2]}, ${effect.intensity})`);
mistGradient.addColorStop(1, "rgba(255, 255, 255, 0)");
ctx.fillStyle = mistGradient;
ctx.fillRect(0, 0, width, height);
break;
}
ctx.restore();
});
// Draw particles
particles.forEach(particle => {
ctx.save();
ctx.globalAlpha = particle.opacity;
ctx.translate(particle.x, particle.y);
ctx.rotate(particle.rotation);
const color = `rgb(${particle.color[0]}, ${particle.color[1]}, ${particle.color[2]})`;
switch (particle.type) {
case "rain":
ctx.strokeStyle = color;
ctx.lineWidth = particle.size;
ctx.beginPath();
ctx.moveTo(0, -particle.size);
ctx.lineTo(0, particle.size);
ctx.stroke();
break;
case "snow":
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(0, 0, particle.size, 0, Math.PI * 2);
ctx.fill();
// Add snowflake pattern
ctx.strokeStyle = color;
ctx.lineWidth = 0.5;
for (let i = 0; i < 6; i++) {
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(0, -particle.size);
ctx.stroke();
ctx.rotate(Math.PI / 3);
}
break;
case "fog":
const fogGradient = ctx.createRadialGradient(0, 0, 0, 0, 0, particle.size);
fogGradient.addColorStop(0, `rgba(${particle.color[0]}, ${particle.color[1]}, ${particle.color[2]}, ${particle.opacity})`);
fogGradient.addColorStop(1, "rgba(255, 255, 255, 0)");
ctx.fillStyle = fogGradient;
ctx.beginPath();
ctx.arc(0, 0, particle.size, 0, Math.PI * 2);
ctx.fill();
break;
case "leaf":
ctx.fillStyle = color;
ctx.beginPath();
ctx.ellipse(0, 0, particle.size, particle.size * 0.6, 0, 0, Math.PI * 2);
ctx.fill();
break;
case "droplet":
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(0, 0, particle.size, 0, Math.PI * 2);
ctx.fill();
break;
}
ctx.restore();
});
// Glass tint overlay
if (weatherResponsive) {
ctx.save();
ctx.globalAlpha = 0.1;
ctx.fillStyle = `rgb(${glassColor[0]}, ${glassColor[1]}, ${glassColor[2]})`;
ctx.fillRect(0, 0, width, height);
ctx.restore();
}
// Weather info overlay
if (showWeatherInfo) {
ctx.save();
ctx.fillStyle = "var(--glass-text-secondary-dark)";
ctx.fillRect(10, 10, 200, 120);
ctx.fillStyle = "white";
ctx.font = "14px sans-serif";
ctx.fillText(`Weather: ${currentWeather.type}`, 20, 30);
ctx.fillText(`Temperature: ${currentWeather.temperature}°C`, 20, 50);
ctx.fillText(`Humidity: ${Math.round(currentWeather.humidity * 100)}%`, 20, 70);
ctx.fillText(`Wind: ${Math.round(currentWeather.windSpeed)} km/h`, 20, 90);
ctx.fillText(`Pressure: ${currentWeather.pressure} hPa`, 20, 110);
ctx.restore();
}
}, [width, height, backgroundGradient, atmosphericEffects, particles, weatherResponsive, glassColor, showWeatherInfo, currentWeather]);
// Keep latest callbacks in refs to avoid effect restarts each render
const renderRef = useRef(render);
const updateParticlesRef = useRef(updateParticles);
const updateAtmosphericEffectsRef = useRef(updateAtmosphericEffects);
useEffect(() => {
renderRef.current = render;
}, [render]);
useEffect(() => {
updateParticlesRef.current = updateParticles;
}, [updateParticles]);
useEffect(() => {
updateAtmosphericEffectsRef.current = updateAtmosphericEffects;
}, [updateAtmosphericEffects]);
// Animation loop
useEffect(() => {
if (prefersReducedMotion && respectMotionPreference) {
renderRef.current();
return;
}
const animate = () => {
const deltaTime = 16; // 60fps
setAnimationTime(prev => prev + deltaTime);
updateParticlesRef.current(deltaTime);
updateAtmosphericEffectsRef.current(deltaTime);
renderRef.current();
animationRef.current = requestAnimationFrame(animate);
};
animationRef.current = requestAnimationFrame(animate);
return () => {
if (animationRef.current) {
cancelAnimationFrame(animationRef.current);
}
};
}, [prefersReducedMotion, respectMotionPreference]);
// Auto weather updates
useEffect(() => {
if (!autoUpdate) return;
const interval = setInterval(() => {
// Simulate weather changes
const newWeather = {
...currentWeather,
type: ["clear", "cloudy", "rainy", "sunny"][Math.floor(Math.random() * 4)],
intensity: Math.random(),
temperature: currentWeather.temperature + (Math.random() - 0.5) * 10,
humidity: Math.random(),
pressure: 1000 + Math.random() * 50,
windSpeed: Math.random() * 30,
windDirection: Math.random() * 360,
id: `weather-${Date.now()}`
};
setCurrentWeather(newWeather);
onWeatherChange?.(newWeather);
}, updateInterval);
return () => clearInterval(interval);
}, [autoUpdate, updateInterval, currentWeather, onWeatherChange]);
// Canvas setup
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
canvas.width = width;
canvas.height = height;
}, [width, height]);
// Controls
const renderControls = () => {
if (!showControls) return null;
return jsxs(OptimizedGlassCore, {
elevation: "level2",
intensity: "medium",
depth: 1,
tint: "neutral",
border: "subtle",
className: "glass-weather-controls glass-flex glass-flex-wrap glass-items-center glass-gap-4 glass-p-4 glass-radius-lg glass-glass-glass-backdrop-blur-md glass-border glass-border-glass-border/20 glass-contrast-guard",
children: [jsxs("div", {
className: "glass-flex glass-items-center glass-gap-2",
children: [jsx("label", {
className: "glass-text-sm",
children: "Weather:"
}), jsxs("select", {
value: currentWeather.type,
onChange: e => {
const newWeather = {
...currentWeather,
type: e.target.value
};
setCurrentWeather(newWeather);
onWeatherChange?.(newWeather);
},
className: "glass-px-2 glass-py-1 glass-radius-md glass-surface-overlay glass-border glass-border-glass-border/20 glass-contrast-guard glass-focus glass-touch-target",
children: [jsx("option", {
value: "clear",
children: "Clear"
}), jsx("option", {
value: "sunny",
children: "Sunny"
}), jsx("option", {
value: "cloudy",
children: "Cloudy"
}), jsx("option", {
value: "rainy",
children: "Rainy"
}), jsx("option", {
value: "stormy",
children: "Stormy"
}), jsx("option", {
value: "snowy",
children: "Snowy"
}), jsx("option", {
value: "foggy",
children: "Foggy"
}), jsx("option", {
value: "windy",
children: "Windy"
})]
})]
}), jsxs("div", {
className: "glass-flex glass-items-center glass-gap-2",
children: [jsx("label", {
className: "glass-text-sm",
children: "Intensity:"
}), jsx("input", {
type: "range",
min: "0",
max: "1",
step: "0.1",
value: currentWeather.intensity,
onChange: e => {
const newWeather = {
...currentWeather,
intensity: parseFloat(e.target.value)
};
setCurrentWeather(newWeather);
},
className: 'w-20 glass-focus glass-touch-target glass-contrast-guard'
})]
}), jsxs("div", {
className: "glass-flex glass-items-center glass-gap-2",
children: [jsx("label", {
className: "glass-text-sm",
children: "Temp:"
}), jsx("input", {
type: "range",
min: "-20",
max: "40",
value: currentWeather.temperature,
onChange: e => {
const newWeather = {
...currentWeather,
temperature: parseInt(e.target.value)
};
setCurrentWeather(newWeather);
},
className: 'w-20 glass-focus glass-touch-target glass-contrast-guard'
}), jsxs("span", {
className: 'glass-text-sm min-w-[3ch]',
children: [currentWeather.temperature, "\u00B0C"]
})]
}), jsxs("div", {
className: "glass-flex glass-items-center glass-gap-2",
children: [jsxs("label", {
className: "glass-text-sm",
children: [jsx("input", {
type: "checkbox",
checked: autoUpdate,
onChange: e => {},
className: "glass-mr-1 glass-focus glass-touch-target glass-contrast-guard"
}), "Auto Update"]
}), jsxs("label", {
className: "glass-text-sm",
children: [jsx("input", {
type: "checkbox",
checked: weatherResponsive,
onChange: e => {},
className: "glass-mr-1 glass-focus glass-touch-target glass-contrast-guard"
}), "Responsive"]
})]
})]
});
};
return jsx(OptimizedGlassCore, {
ref: ref,
id: weatherGlassId,
elevation: "level1",
intensity: "subtle",
depth: 1,
tint: "neutral",
border: "subtle",
className: cn("glass-weather-glass relative glass-radius-lg glass-glass-backdrop-blur-md border border-border/20", className),
style: {
background: weatherResponsive ? `linear-gradient(135deg, rgba(${glassColor[0]}, ${glassColor[1]}, ${glassColor[2]}, 0.1), rgba(${glassColor[0]}, ${glassColor[1]}, ${glassColor[2]}, 0.05))` : undefined
},
...props,
children: jsxs(MotionFramer, {
preset: isMotionSafe && respectMotionPreference ? "fadeIn" : "none",
className: "glass-flex glass-flex-col glass-gap-4 glass-p-4",
children: [renderControls(), jsx("div", {
className: 'relative',
children: jsx("canvas", {
ref: canvasRef,
width: width,
height: height,
className: "glass-border glass-border-glass-border/20 glass-radius-md",
style: {
width,
height
}
})
})]
})
});
});
GlassWeatherGlass.displayName = "GlassWeatherGlass";
export { GlassWeatherGlass, GlassWeatherGlass as default };
//# sourceMappingURL=GlassWeatherGlass.js.map