@erosnicolau/animated-text
Version:
Advanced React animated text component with comprehensive animation effects
345 lines (344 loc) • 12.5 kB
JavaScript
// Transform values for string-based animation effects
const getStringTransformValues = (effect) => {
const transforms = {
fadeIn: { from: '', to: '' },
'slide-up': { from: 'translateY(20px)', to: 'translateY(0)' },
'slide-down': { from: 'translateY(-20px)', to: 'translateY(0)' },
'slide-left': { from: 'translateX(20px)', to: 'translateX(0)' },
'slide-right': { from: 'translateX(-20px)', to: 'translateX(0)' },
'slide-up-left': { from: 'translate(20px, 20px)', to: 'translate(0, 0)' },
'slide-up-right': { from: 'translate(-20px, 20px)', to: 'translate(0, 0)' },
'slide-down-left': { from: 'translate(20px, -20px)', to: 'translate(0, 0)' },
'slide-down-right': { from: 'translate(-20px, -20px)', to: 'translate(0, 0)' }
};
return transforms[effect] || { from: '', to: '' };
};
// Transform values for object-based animation effects
const getObjectTransformValues = (effect) => {
switch (effect.type) {
case 'scale':
return {
from: `scale(${effect.from})`,
to: `scale(${effect.to})`
};
case 'rotate':
return {
from: `rotate(${effect.from}deg)`,
to: `rotate(${effect.to}deg)`
};
case 'slide': {
const fromX = effect.x || 0;
const fromY = effect.y || 0;
return {
from: `translate(${fromX}px, ${fromY}px)`,
to: `translate(0, 0)`
};
}
case 'color':
// Color effects don't use transform
return { from: '', to: '' };
case 'backgroundColor':
// Background color effects don't use transform
return { from: '', to: '' };
case 'textShadow':
// Text shadow effects don't use transform
return { from: '', to: '' };
case 'fontSize':
// Font size effects don't use transform
return { from: '', to: '' };
case 'letterSpacing':
// Letter spacing effects don't use transform
return { from: '', to: '' };
case 'padding':
case 'paddingTop':
case 'paddingRight':
case 'paddingBottom':
case 'paddingLeft':
case 'margin':
case 'marginTop':
case 'marginRight':
case 'marginBottom':
case 'marginLeft':
case 'borderRadius':
// Layout effects don't use transform
return { from: '', to: '' };
case 'backdropFilter':
case 'boxShadow':
case 'filter':
// Visual effects don't use transform
return { from: '', to: '' };
case 'lineHeight':
case 'fontWeight':
case 'border':
case 'borderWidth':
case 'borderColor':
case 'borderStyle':
case 'width':
case 'minWidth':
case 'maxWidth':
case 'height':
case 'minHeight':
case 'maxHeight':
// Advanced property effects don't use transform
return { from: '', to: '' };
default:
return { from: '', to: '' };
}
};
// Unified transform values processing
const getTransformValues = (effect) => {
if (typeof effect === 'string') {
return getStringTransformValues(effect);
}
else {
return getObjectTransformValues(effect);
}
};
// Compose multiple effects into combined transform, opacity, color, text shadow, font size, and letter spacing values
const getComposedEffectValues = (effects) => {
const effectsArray = Array.isArray(effects) ? effects : [effects];
let hasFadeIn = false;
let combinedTransformFrom = '';
let combinedTransformTo = '';
const colorEffects = [];
const backgroundColorEffects = [];
const textShadowEffects = [];
const fontSizeEffects = [];
const letterSpacingEffects = [];
const layoutEffects = [];
const visualEffects = [];
const advancedEffects = [];
effectsArray.forEach(effect => {
if (typeof effect === 'string') {
if (effect === 'fadeIn') {
hasFadeIn = true;
}
else {
const transforms = getTransformValues(effect);
if (transforms.from) {
combinedTransformFrom += transforms.from + ' ';
}
if (transforms.to) {
combinedTransformTo += transforms.to + ' ';
}
}
}
else {
// Object effect
if (effect.type === 'color') {
colorEffects.push(effect);
}
else if (effect.type === 'backgroundColor') {
backgroundColorEffects.push(effect);
}
else if (effect.type === 'textShadow') {
textShadowEffects.push(effect);
}
else if (effect.type === 'fontSize') {
fontSizeEffects.push(effect);
}
else if (effect.type === 'letterSpacing') {
letterSpacingEffects.push(effect);
}
else if ([
'padding',
'paddingTop',
'paddingRight',
'paddingBottom',
'paddingLeft',
'margin',
'marginTop',
'marginRight',
'marginBottom',
'marginLeft',
'borderRadius'
].includes(effect.type)) {
layoutEffects.push(effect);
}
else if (['backdropFilter', 'boxShadow', 'filter'].includes(effect.type)) {
visualEffects.push(effect);
}
else if ([
'lineHeight',
'fontWeight',
'border',
'borderWidth',
'borderColor',
'borderStyle',
'width',
'minWidth',
'maxWidth',
'height',
'minHeight',
'maxHeight'
].includes(effect.type)) {
advancedEffects.push(effect);
}
else {
const transforms = getTransformValues(effect);
if (transforms.from) {
combinedTransformFrom += transforms.from + ' ';
}
if (transforms.to) {
combinedTransformTo += transforms.to + ' ';
}
}
}
});
return {
hasFadeIn,
transformFrom: combinedTransformFrom.trim(),
transformTo: combinedTransformTo.trim(),
colorEffects,
backgroundColorEffects,
textShadowEffects,
fontSizeEffects,
letterSpacingEffects,
layoutEffects,
visualEffects,
advancedEffects
};
};
// Convert easing to CSS timing function
const getTimingFunction = (easing) => {
const timings = {
ease: 'ease',
'ease-in': 'ease-in',
'ease-out': 'ease-out',
'ease-in-out': 'ease-in-out',
linear: 'linear'
};
return timings[easing];
};
// ONE SINGLE ROBUST ANIMATION FUNCTION - handles everything including color, text shadow, font size, and letter spacing effects
export const generateAnimationStyles = (effects, duration, delayBefore = 0, easing = 'ease-out', isAnimated) => {
const { hasFadeIn, transformFrom, transformTo, colorEffects, backgroundColorEffects, textShadowEffects, fontSizeEffects, letterSpacingEffects, layoutEffects, visualEffects, advancedEffects } = getComposedEffectValues(effects);
const timingFunction = getTimingFunction(easing);
if (isAnimated) {
// ANIMATED STATE
const style = {};
// Handle opacity transitions
if (hasFadeIn) {
style.opacity = 1;
}
// Handle transform transitions - CRITICAL FIX: Always apply transform when present
if (transformTo) {
style.transform = transformTo;
}
// Handle color transitions
colorEffects.forEach(colorEffect => {
const property = colorEffect.property || 'color';
style[property] = colorEffect.to;
});
// Handle background color transitions
backgroundColorEffects.forEach(backgroundColorEffect => {
style.backgroundColor = backgroundColorEffect.to;
});
// Handle text shadow transitions
textShadowEffects.forEach(textShadowEffect => {
style.textShadow = textShadowEffect.to;
});
// Handle font size transitions
fontSizeEffects.forEach(fontSizeEffect => {
style.fontSize = fontSizeEffect.to;
});
// Handle letter spacing transitions
letterSpacingEffects.forEach(letterSpacingEffect => {
style.letterSpacing = letterSpacingEffect.to;
});
// Handle layout transitions
layoutEffects.forEach(layoutEffect => {
;
style[layoutEffect.type] = layoutEffect.to;
});
// Handle visual effects transitions
visualEffects.forEach(visualEffect => {
;
style[visualEffect.type] = visualEffect.to;
});
// Handle advanced property transitions
advancedEffects.forEach(advancedEffect => {
if (advancedEffect.type === 'lineHeight') {
style.lineHeight = advancedEffect.to;
}
else {
;
style[advancedEffect.type] = advancedEffect.to;
}
});
// Build transition string - use simple "all" to catch everything
if (hasFadeIn ||
transformTo ||
colorEffects.length > 0 ||
backgroundColorEffects.length > 0 ||
textShadowEffects.length > 0 ||
fontSizeEffects.length > 0 ||
letterSpacingEffects.length > 0 ||
layoutEffects.length > 0 ||
visualEffects.length > 0 ||
advancedEffects.length > 0) {
style.transition = `all ${duration}ms ${timingFunction} ${delayBefore}ms`;
}
return style;
}
else {
// INITIAL STATE
const style = {
transition: 'none'
};
// Set initial opacity
if (hasFadeIn) {
style.opacity = 0;
}
// Set initial transform - CRITICAL FIX: Always apply initial transform when present
if (transformFrom) {
style.transform = transformFrom;
}
// Set initial colors
colorEffects.forEach(colorEffect => {
const property = colorEffect.property || 'color';
style[property] = colorEffect.from;
});
// Set initial background colors
backgroundColorEffects.forEach(backgroundColorEffect => {
style.backgroundColor = backgroundColorEffect.from;
});
// Set initial text shadows
textShadowEffects.forEach(textShadowEffect => {
style.textShadow = textShadowEffect.from;
});
// Set initial font sizes
fontSizeEffects.forEach(fontSizeEffect => {
style.fontSize = fontSizeEffect.from;
});
// Set initial letter spacing
letterSpacingEffects.forEach(letterSpacingEffect => {
style.letterSpacing = letterSpacingEffect.from;
});
// Set initial layout properties
layoutEffects.forEach(layoutEffect => {
;
style[layoutEffect.type] = layoutEffect.from;
});
// Set initial visual properties
visualEffects.forEach(visualEffect => {
;
style[visualEffect.type] = visualEffect.from;
});
// Set initial advanced properties
advancedEffects.forEach(advancedEffect => {
if (advancedEffect.type === 'lineHeight') {
style.lineHeight = advancedEffect.from;
}
else {
;
style[advancedEffect.type] = advancedEffect.from;
}
});
return style;
}
};
// Wrapper for block animations (uses same function)
export const generateBlockAnimationStyles = (effects, duration, easing, shouldAnimate) => {
return generateAnimationStyles(effects, duration, 0, easing, shouldAnimate);
};