aura-glass
Version:
A comprehensive glassmorphism design system for React applications with 142+ production-ready components
462 lines (459 loc) • 13.1 kB
JavaScript
import { useState, useRef, useCallback, useEffect } from 'react';
function useAnimationSequence(steps, config = {}) {
const {
autoPlay = false,
loop = false,
reverse = false,
speed = 1,
paused = false,
parallel = false
} = config;
const [state, setState] = useState({
isPlaying: false,
isPaused: false,
currentStep: 0,
totalSteps: steps.length,
progress: 0,
elapsed: 0,
totalDuration: parallel ? Math.max(...steps.map(s => s.duration + (s.delay || 0))) : steps.reduce((total, step) => total + step.duration + (step.delay || 0), 0)
});
const elementRef = useRef(null);
const animationRef = useRef();
const startTimeRef = useRef(0);
const stepStartTimesRef = useRef([]);
const completedStepsRef = useRef(new Set());
// Calculate total duration
const totalDuration = parallel ? Math.max(...steps.map(s => s.duration + (s.delay || 0))) : steps.reduce((total, step) => total + step.duration + (step.delay || 0), 0);
const applyStepTransform = useCallback((step, progress = 1) => {
if (!elementRef.current) return;
const element = elementRef.current;
const transform = step.transform;
if (transform) {
const transforms = [];
if (transform.x !== undefined || transform.y !== undefined) {
transforms.push(`translate(${transform.x || 0}px, ${transform.y || 0}px)`);
}
if (transform.scale !== undefined) {
transforms.push(`scale(${transform.scale})`);
}
if (transform.rotation !== undefined) {
transforms.push(`rotate(${transform.rotation}deg)`);
}
if (transforms.length > 0) {
element.style.transform = transforms.join(' ');
}
if (transform.opacity !== undefined) {
element.style.opacity = transform.opacity.toString();
}
}
// Apply additional styles
if (step.style) {
Object.assign(element.style, step.style);
}
}, []);
const animateStep = useCallback((stepIndex, stepProgress) => {
const step = steps[stepIndex];
if (!step) return;
// Apply transform based on progress
const easedProgress = step.easing ? easeValue(stepProgress, step.easing) : stepProgress;
// Interpolate between current and target values
if (step.transform) {
const interpolatedTransform = {
x: step.transform.x !== undefined ? step.transform.x * easedProgress : undefined,
y: step.transform.y !== undefined ? step.transform.y * easedProgress : undefined,
scale: step.transform.scale !== undefined ? 1 + (step.transform.scale - 1) * easedProgress : undefined,
rotation: step.transform.rotation !== undefined ? step.transform.rotation * easedProgress : undefined,
opacity: step.transform.opacity !== undefined ? step.transform.opacity * easedProgress : undefined
};
if (elementRef.current) {
applyStepTransform({
...step,
transform: interpolatedTransform
}, 1);
}
}
// Call update callback
step.onUpdate?.(easedProgress);
}, [steps, applyStepTransform]);
const runAnimation = useCallback(timestamp => {
if (!startTimeRef.current) {
startTimeRef.current = timestamp;
stepStartTimesRef.current = new Array(steps.length).fill(0);
completedStepsRef.current.clear();
}
const elapsed = (timestamp - startTimeRef.current) * speed;
if (parallel) {
// Parallel execution
let allCompleted = true;
let maxProgress = 0;
steps.forEach((step, index) => {
const stepDelay = step.delay || 0;
const stepStartTime = stepStartTimesRef.current[index] || startTimeRef.current + stepDelay;
if (elapsed >= stepDelay) {
if (stepStartTimesRef.current[index] === 0) {
stepStartTimesRef.current[index] = timestamp;
step.onStart?.();
}
const stepElapsed = (timestamp - stepStartTime) * speed;
const stepProgress = Math.min(stepElapsed / step.duration, 1);
animateStep(index, stepProgress);
maxProgress = Math.max(maxProgress, stepProgress);
if (stepProgress >= 1 && !completedStepsRef.current.has(index)) {
completedStepsRef.current.add(index);
step.onComplete?.();
}
if (stepProgress < 1) {
allCompleted = false;
}
} else {
allCompleted = false;
}
});
setState(prev => ({
...prev,
progress: maxProgress,
elapsed
}));
if (allCompleted) {
setState(prev => ({
...prev,
isPlaying: false,
progress: 1
}));
if (loop) {
// Restart animation
startTimeRef.current = 0;
stepStartTimesRef.current = new Array(steps.length).fill(0);
completedStepsRef.current.clear();
setState(prev => ({
...prev,
isPlaying: true,
currentStep: 0,
progress: 0,
elapsed: 0
}));
animationRef.current = requestAnimationFrame(runAnimation);
}
return;
}
} else {
// Sequential execution
let accumulatedTime = 0;
let currentStepIndex = 0;
let stepElapsed = 0;
for (let i = 0; i < steps.length; i++) {
const step = steps[i];
const stepDuration = step.duration + (step.delay || 0);
if (elapsed >= accumulatedTime && elapsed < accumulatedTime + stepDuration) {
currentStepIndex = i;
stepElapsed = elapsed - accumulatedTime;
break;
}
accumulatedTime += stepDuration;
}
// Handle step transitions
if (currentStepIndex !== state.currentStep) {
// Complete previous step
const prevStep = steps[state.currentStep];
prevStep?.onComplete?.();
// Start new step
const newStep = steps[currentStepIndex];
newStep?.onStart?.();
}
// Calculate step progress
const currentStep = steps[currentStepIndex];
const stepDelay = currentStep.delay || 0;
const stepDuration = currentStep.duration;
const stepProgress = Math.max(0, Math.min(1, (stepElapsed - stepDelay) / stepDuration));
// Apply animation
animateStep(currentStepIndex, stepProgress);
// Update state
const overallProgress = elapsed / totalDuration;
setState(prev => ({
...prev,
currentStep: currentStepIndex,
progress: overallProgress,
elapsed
}));
if (elapsed >= totalDuration) {
// Animation complete
setState(prev => ({
...prev,
isPlaying: false,
progress: 1
}));
// Complete final step
const finalStep = steps[steps.length - 1];
finalStep.onComplete?.();
if (loop) {
// Restart animation
startTimeRef.current = 0;
stepStartTimesRef.current = new Array(steps.length).fill(0);
completedStepsRef.current.clear();
setState(prev => ({
...prev,
isPlaying: true,
currentStep: 0,
progress: 0,
elapsed: 0
}));
animationRef.current = requestAnimationFrame(runAnimation);
}
return;
}
}
animationRef.current = requestAnimationFrame(runAnimation);
}, [steps, speed, totalDuration, reverse, loop, parallel, applyStepTransform, animateStep, state.currentStep]);
const play = useCallback(() => {
if (state.isPlaying) return;
setState(prev => ({
...prev,
isPlaying: true,
isPaused: false
}));
startTimeRef.current = 0;
stepStartTimesRef.current = new Array(steps.length).fill(0);
completedStepsRef.current = new Set();
animationRef.current = requestAnimationFrame(runAnimation);
}, [state.isPlaying, runAnimation]);
const pause = useCallback(() => {
if (!state.isPlaying) return;
setState(prev => ({
...prev,
isPaused: true
}));
if (animationRef.current) {
cancelAnimationFrame(animationRef.current);
animationRef.current = undefined;
}
}, [state.isPlaying]);
const stop = useCallback(() => {
if (animationRef.current) {
cancelAnimationFrame(animationRef.current);
animationRef.current = undefined;
}
startTimeRef.current = 0;
stepStartTimesRef.current = new Array(steps.length).fill(0);
completedStepsRef.current = new Set();
setState(prev => ({
...prev,
isPlaying: false,
isPaused: false,
currentStep: 0,
progress: 0,
elapsed: 0
}));
// Reset to initial state
if (elementRef.current && steps.length > 0) {
applyStepTransform(steps[0], 0);
}
}, [applyStepTransform, steps]);
const seek = useCallback(progress => {
const seekTime = progress * totalDuration;
startTimeRef.current = Date.now() - seekTime / speed;
setState(prev => ({
...prev,
progress,
elapsed: seekTime
}));
// This is a simplified seek implementation
// In a full implementation, you'd need to calculate which step and at what progress
}, [totalDuration, speed]);
// Auto-play on mount
useEffect(() => {
if (autoPlay && !paused) {
play();
}
return () => {
if (animationRef.current) {
cancelAnimationFrame(animationRef.current);
}
};
}, [autoPlay, paused, play]);
// Handle pause state
useEffect(() => {
if (paused && state.isPlaying) {
pause();
}
}, [paused, state.isPlaying, pause]);
return {
ref: elementRef,
state,
play,
pause,
stop,
seek,
totalDuration
};
}
// Easing function utilities
function easeValue(progress, easing) {
// Simple easing function parser
const [type, ...params] = easing.split('(');
const values = params.join('(').replace(')', '').split(',').map(v => parseFloat(v.trim()));
switch (type) {
case 'ease-in':
return progress * progress;
case 'ease-out':
return progress * (2 - progress);
case 'ease-in-out':
return progress < 0.5 ? 2 * progress * progress : -1 + (4 - 2 * progress) * progress;
case 'cubic-bezier':
return cubicBezier(progress, values[0], values[1], values[2], values[3]);
default:
return progress;
}
}
function cubicBezier(t, p0, p1, p2, p3) {
const u = 1 - t;
const tt = t * t;
const uu = u * u;
const ttt = tt * t;
return 3 * uu * t * p1 + 3 * u * tt * p2 + ttt;
}
// Predefined orchestration sequences
const orchestrationPresets = {
staggeredFadeIn: [{
id: 'step1',
duration: 300,
delay: 0,
transform: {
opacity: 1
}
}, {
id: 'step2',
duration: 300,
delay: 100,
transform: {
opacity: 1
}
}, {
id: 'step3',
duration: 300,
delay: 200,
transform: {
opacity: 1
}
}],
cascadingSlideIn: [{
id: 'slide1',
duration: 400,
delay: 0,
transform: {
x: 0
},
easing: 'ease-out'
}, {
id: 'slide2',
duration: 400,
delay: 150,
transform: {
x: 0
},
easing: 'ease-out'
}, {
id: 'slide3',
duration: 400,
delay: 300,
transform: {
x: 0
},
easing: 'ease-out'
}],
parallelScaleIn: [{
id: 'scale1',
duration: 500,
delay: 0,
transform: {
scale: 1
},
easing: 'ease-out'
}, {
id: 'scale2',
duration: 500,
delay: 0,
transform: {
scale: 1
},
easing: 'ease-out'
}, {
id: 'scale3',
duration: 500,
delay: 0,
transform: {
scale: 1
},
easing: 'ease-out'
}],
complexOrchestration: [{
id: 'initial',
duration: 200,
transform: {
opacity: 1,
scale: 0.8
}
}, {
id: 'grow',
duration: 300,
delay: 100,
transform: {
scale: 1.1
},
easing: 'ease-out'
}, {
id: 'settle',
duration: 200,
delay: 50,
transform: {
scale: 1
},
easing: 'ease-in-out'
}, {
id: 'slide',
duration: 400,
delay: 0,
transform: {
x: 0
},
easing: 'ease-out'
}]
};
// Utility functions for creating complex orchestrations
const createOrchestration = {
fromSteps: (steps, config) => ({
steps,
config: {
...config
}
}),
staggered: (baseStep, count, staggerDelay) => {
return Array.from({
length: count
}, (_, i) => ({
...baseStep,
id: `step-${i}`,
delay: i * staggerDelay
}));
},
parallel: steps => {
return steps.map((step, i) => ({
...step,
id: `parallel-${i}`,
delay: step.delay || 0
}));
},
sequence: steps => {
let accumulatedDelay = 0;
return steps.map((step, i) => {
const result = {
...step,
id: `sequence-${i}`,
delay: accumulatedDelay
};
accumulatedDelay += step.duration + (step.delay || 0);
return result;
});
}
};
export { createOrchestration, orchestrationPresets, useAnimationSequence };
//# sourceMappingURL=useAnimationSequenceOrchestrator.js.map