aura-glass
Version:
A comprehensive glassmorphism design system for React applications with 142+ production-ready components
306 lines (303 loc) • 8.68 kB
JavaScript
import { useState, useRef, useEffect, useCallback } from 'react';
const DEFAULT_CONFIG = {
stiffness: 100,
damping: 10,
mass: 1,
precision: 0.01,
velocity: 0
};
function useMultiSpring(initialValues = {}, options = {}) {
const {
config: userConfig = {},
onUpdate,
onComplete
} = options;
const config = {
...DEFAULT_CONFIG,
...userConfig
};
const [values, setValues] = useState(initialValues);
const [isAnimating, setIsAnimating] = useState(false);
const animationRef = useRef();
const stateRef = useRef({});
const targetsRef = useRef({});
const lastTimeRef = useRef(0);
// Initialize state for each key
useEffect(() => {
const newState = {};
Object.keys(initialValues).forEach(key => {
newState[key] = {
position: initialValues[key],
velocity: config.velocity,
isAnimating: false
};
});
stateRef.current = newState;
targetsRef.current = {
...initialValues
};
}, [initialValues, config.velocity]);
const animate = useCallback(timestamp => {
if (!lastTimeRef.current) {
lastTimeRef.current = timestamp;
}
const deltaTime = (timestamp - lastTimeRef.current) / 1000; // Convert to seconds
lastTimeRef.current = timestamp;
let hasActiveAnimations = false;
const newValues = {};
// Update each spring
Object.keys(stateRef.current).forEach(key => {
const spring = stateRef.current[key];
const target = targetsRef.current[key];
if (target === undefined) return;
// Calculate spring forces
const displacement = target - spring.position;
const springForce = displacement * config.stiffness;
const dampingForce = -spring.velocity * config.damping;
// Update acceleration (F = ma)
const acceleration = (springForce + dampingForce) / config.mass;
// Update velocity (v = v + a * dt)
spring.velocity += acceleration * deltaTime;
// Update position (p = p + v * dt)
spring.position += spring.velocity * deltaTime;
// Check if animation is complete
const isAtRest = Math.abs(spring.velocity) < config.precision && Math.abs(displacement) < config.precision;
if (isAtRest) {
spring.position = target;
spring.velocity = 0;
spring.isAnimating = false;
} else {
spring.isAnimating = true;
hasActiveAnimations = true;
}
newValues[key] = spring.position;
});
// Update values
setValues(newValues);
onUpdate?.(newValues);
// Check if all animations are complete
if (!hasActiveAnimations) {
setIsAnimating(false);
onComplete?.(newValues);
animationRef.current = undefined;
return;
}
animationRef.current = requestAnimationFrame(animate);
}, [config, onUpdate, onComplete]);
const start = useCallback(targets => {
// Update targets
targetsRef.current = {
...targetsRef.current,
...targets
};
// Start animations for changed targets
Object.keys(targets).forEach(key => {
if (stateRef.current[key]) {
stateRef.current[key].isAnimating = true;
}
});
setIsAnimating(true);
if (!animationRef.current) {
lastTimeRef.current = 0;
animationRef.current = requestAnimationFrame(animate);
}
}, [animate]);
const set = useCallback(values => {
// Immediately set values without animation
const newValues = {
...values
};
const newState = {};
Object.keys(values).forEach(key => {
newState[key] = {
position: values[key],
velocity: 0,
isAnimating: false
};
});
stateRef.current = {
...stateRef.current,
...newState
};
targetsRef.current = {
...targetsRef.current,
...values
};
setValues(prev => ({
...prev,
...newValues
}));
}, []);
const stop = useCallback(() => {
if (animationRef.current) {
cancelAnimationFrame(animationRef.current);
animationRef.current = undefined;
}
// Stop all animations
Object.keys(stateRef.current).forEach(key => {
stateRef.current[key].isAnimating = false;
});
setIsAnimating(false);
}, []);
const reset = useCallback(() => {
stop();
setValues(initialValues);
targetsRef.current = {
...initialValues
};
const resetState = {};
Object.keys(initialValues).forEach(key => {
resetState[key] = {
position: initialValues[key],
velocity: 0,
isAnimating: false
};
});
stateRef.current = resetState;
}, [stop, initialValues]);
// Cleanup on unmount
useEffect(() => {
return () => {
if (animationRef.current) {
cancelAnimationFrame(animationRef.current);
}
};
}, []);
return {
values,
isAnimating,
start,
set,
stop,
reset,
config
};
}
// Hook for spring-based transforms
function useSpringTransform(initialTransform = {}, options = {}) {
const springValues = {
x: initialTransform.x || 0,
y: initialTransform.y || 0,
scale: initialTransform.scale || 1,
rotation: initialTransform.rotation || 0,
opacity: initialTransform.opacity || 1
};
const spring = useMultiSpring(springValues, {
...options,
onUpdate: values => {
options.onUpdate?.(values);
}
});
const transform = `translate(${spring.values.x}px, ${spring.values.y}px) scale(${spring.values.scale}) rotate(${spring.values.rotation}deg)`;
const style = {
transform,
opacity: spring.values.opacity
};
return {
...spring,
transform,
style
};
}
// Hook for spring-based color transitions
function useSpringColor(initialColor = 'var(--glass-black)', options = {}) {
// Parse initial color
const parseColor = color => {
const div = document.createElement('div');
div.style.color = color;
document.body.appendChild(div);
const computed = getComputedStyle(div).color;
document.body.removeChild(div);
const match = computed.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/);
if (match) {
return {
r: parseInt(match[1]),
g: parseInt(match[2]),
b: parseInt(match[3])
};
}
return {
r: 0,
g: 0,
b: 0
};
};
const initialColorValues = parseColor(initialColor);
const spring = useMultiSpring(initialColorValues, options);
const color = `rgb(${Math.round(spring.values.r)}, ${Math.round(spring.values.g)}, ${Math.round(spring.values.b)})`;
return {
...spring,
color
};
}
// Hook for spring-based array animations
function useSpringArray(initialArray = [], options = {}) {
const springValues = {};
initialArray.forEach((value, index) => {
springValues[`item-${index}`] = value;
});
const spring = useMultiSpring(springValues, options);
const array = Object.keys(spring.values).sort().map(key => spring.values[key]);
return {
...spring,
array
};
}
// Utility functions for common spring patterns
const createSpringPreset = preset => {
const presets = {
gentle: {
stiffness: 120,
damping: 14,
mass: 1
},
wobbly: {
stiffness: 180,
damping: 12,
mass: 1
},
stiff: {
stiffness: 210,
damping: 20,
mass: 1
},
slow: {
stiffness: 280,
damping: 60,
mass: 1
},
bouncy: {
stiffness: 170,
damping: 8,
mass: 1
}
};
return presets[preset];
};
const createSpringSequence = (targets, config = {}) => {
// Create a sequence of spring targets
return targets;
};
const interpolateSprings = (springs, progress) => {
if (springs.length === 0) return DEFAULT_CONFIG;
if (springs.length === 1) return springs[0].config || DEFAULT_CONFIG;
const index = Math.floor(progress * (springs.length - 1));
const localProgress = progress * (springs.length - 1) % 1;
const current = {
...DEFAULT_CONFIG,
...(springs[index]?.config || {})
};
const next = {
...DEFAULT_CONFIG,
...(springs[index + 1]?.config || {})
};
return {
stiffness: current.stiffness + (next.stiffness - current.stiffness) * localProgress,
damping: current.damping + (next.damping - current.damping) * localProgress,
mass: current.mass + (next.mass - current.mass) * localProgress,
precision: current.precision + (next.precision - current.precision) * localProgress,
velocity: current.velocity + (next.velocity - current.velocity) * localProgress
};
};
export { createSpringPreset, createSpringSequence, interpolateSprings, useMultiSpring, useSpringArray, useSpringColor, useSpringTransform };
//# sourceMappingURL=useMultiSpringBasic.js.map