I'm trying to create an animation, where the text appears in the screen and have the option to delay the animation using a timeout.
The strange thing is, when the app first render on the browser, it works as intended, but if I manually refresh "f5", the delay is only visible if it's a longer timeout duration, any value lower than 500ms for example, the others the delay is no visible.
I've already tried to create a custom hook, use "useEffect" and "useLayoutEffect" but this strange behavior keep happening.
Here is the component.
const FadeInText = ({text, style, timeout, noScroll}) => {
const textRef = useRef(null)
const [visible, setVisible] = useState()
useLayoutEffect(() => {
setVisible(timeout !== undefined ? {animation: "none",
opacity: 0} : {})
let time = setTimeout(() => {
setVisible({})
}, timeout)
return () => clearTimeout(time)
},[timeout])
return <p ref={textRef} style={{...visible,...style}} className="fade-in-text">{text}</p>
}
export default FadeInText;
Here are the CSS rules applied to the text
#keyframes fade-slide-up {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.fade-in-text {
font-size: 1rem;
color: white;
animation: fade-slide-up 0.2s ease-in;
}
Here are some cases where I use this component, the only component where the delay is visible, is the one with the timeout set as 600
<FadeInText style={{fontSize: "5vw"}} text="R" timeout={200} />
<FadeInText style={{fontSize: "5vw"}} text="A" timeout={300} />
<FadeInText style={{fontSize: "5vw"}} text="N" timeout={400} />
<FadeInText style={{fontSize: "5vw"}} text="D" timeout={600} />
Use clearTimeout instead of clearInterval.
Make sure timeout value is in milliseconds (1000, 2000, 3500 ...)
Without css rules applied or value of variables, it's hard to help.
What I want to do is, I've an Image component which shows some loading state as placeholder colour (via css background-colour) till image loads then swap it with the actual image.
Css
.show-img {
opacity: 1;
transition: all 0.3s ease-in;
}
.hide-img {
background-color:#eee;
opacity: 0;
transition: all 0.3s ease-in;
}
Using state
const Image = () => {
const [loading, setLoading] = useState(false);
const onImgLoad = () => {
setLoading(true);
};
return (
<img
src="https://i.picsum.photos/id/498/200/300.jpg"
className={loading ? 'hide-img' : 'show-img'}
onLoad={onImgLoad}
/>
);
};
Using refs
const Image = () => {
const ref = useRef();
const onImgLoad = (e) => {
//if img is loaded
if (e.target.src && ref.current) {
ref.current?.classList?.remove('hide-img');
ref.current?.classList?.add('show-img');
}
};
return (
<img
ref={ref}
src="https://i.picsum.photos/id/498/200/300.jpg"
className="hide-img"
onLoad={onImgLoad}
/>
);
};
I want to know which one of the approach is more performant and why? I was thinking about avoiding re-rendering due to state update for such a basic task (maybe).
PS: I've this Image component inside the carousel and there are multiple carousels on the page.
Thank you,
Theoretically speaking, I would argue that using state is more performant as the state is handled by react and its virtual DOM which is way faster than updating the DOM directly with useRef. Additionally, using state warranties the component re-rendering but optimised by react, which means only when its needed. I used the following articles to reach to this conclusion.
https://medium.com/swlh/useref-explained-76c1151658e8
https://blog.logrocket.com/usestate-vs-useref/
Nonetheless you may have to implement a profiler to check if this is true, as for the final user it may not look as the fastest solution, due to other circumstances outside react, like the amount of images, the connection speed and whether the images are in a CDN or not, just to mention some.
We have a way to detect when an animation ends using JS:
const element = $('#animatable');
element.addClass('being-animated').on("animationend", (event) => {
console.log('Animation ended!');
});
#keyframes animateOpacity {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
#keyframes animatePosition {
0% {
transform: translate3d(0, 0, 0);
}
100% {
transform: translate3d(0, 15px, 0);
}
}
#animatable.being-animated {
animation: animateOpacity 1s ease 0s forwards, animatePosition 2s ease 0s forwards;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="animatable">I'm probably being animated.</div>
And as you can see, JS, rightfully, because I'm hooked to the animationend event tells me "yup, an animation is done" but isn't aware of what's coming after and I'm missing the second one.
Isn't there an animation queue? Surely CSS has to register these things somewhere in the system before they're fired and I could peak inside.
Disclaimer: I don't think jQuery is important to answer this question and would hurt both load and runtime performance if others choose to rely on this code after seeing this answer. So, I will be answering with vanilla JavaScript to help as many people as I can with this, but if you want to use jQuery, you can still apply the same concepts.
Answer: There isn't an animation queue, but you could make your own.
For example, you could link data about animations to your target element using a closure, and/or a Map (In the snippet below, I actually used a WeakMap in an attempt to help garbage collection). If you save animation states as true when they are completed, you could check and eventually fire a different callback when all are true, or dispatch a custom event of your own. I used the custom event approach, because it's more flexible (able to add multiple callbacks).
The following code should additionally help you avoid waiting for ALL animations in those cases where you only actually care about a couple specific ones. It should also let you handle animation events multiple times and for multiple individual elements (try running the snippet and clicking the boxes a few times)
const addAnimationEndAllEvent = (() => {
const weakMap = new WeakMap()
const initAnimationsObject = (element, expectedAnimations, eventName) => {
const events = weakMap.get(element)
const animationsCompleted = {}
for (const animation of expectedAnimations) {
animationsCompleted[animation] = false
}
events[eventName] = animationsCompleted
}
return (element, expectedAnimations, eventName = 'animationendall') => {
if (!weakMap.has(element)) weakMap.set(element, {})
if (expectedAnimations) {
initAnimationsObject(element, expectedAnimations, eventName)
}
// When any animation completes...
element.addEventListener('animationend', ({ target, animationName }) => {
const events = weakMap.get(target)
// Use all animations, if there were none provided earlier
if (!events[eventName]) {
initAnimationsObject(target, window.getComputedStyle(target).animationName.split(', '), eventName)
}
const animationsCompleted = events[eventName]
// Ensure this animation should be tracked
if (!(animationName in animationsCompleted)) return
// Mark the current animation as complete (true)
animationsCompleted[animationName] = true
// If every animation is now completed...
if (Object.values(animationsCompleted).every(
isCompleted => isCompleted === true
)) {
const animations = Object.keys(animationsCompleted)
// Fire the event
target.dispatchEvent(new CustomEvent(eventName, {
detail: { target, animations },
}))
// Reset for next time - set all animations to not complete (false)
initAnimationsObject(target, animations, eventName)
}
})
}
})()
const toggleAnimation = ({ target }) => {
target.classList.toggle('being-animated')
}
document.querySelectorAll('.animatable').forEach(element => {
// Wait for all animations before firing the default event "animationendall"
addAnimationEndAllEvent(element)
// Wait for the provided animations before firing the event "animationend2"
addAnimationEndAllEvent(element, [
'animateOpacity',
'animatePosition'
], 'animationend2')
// Listen for our added "animationendall" event
element.addEventListener('animationendall', ({detail: { target, animations }}) => {
console.log(`Animations: ${animations.join(', ')} - Complete`)
})
// Listen for our added "animationend2" event
element.addEventListener('animationend2', ({detail: { target, animations }}) => {
console.log(`Animations: ${animations.join(', ')} - Complete`)
})
// Just updated this to function on click, so we can test animation multiple times
element.addEventListener('click', toggleAnimation)
})
.animatable {
margin: 5px;
width: 100px;
height: 100px;
background: black;
}
#keyframes animateOpacity {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
#keyframes animatePosition {
0% {
transform: translate3d(0, 0, 0);
}
100% {
transform: translate3d(0, 15px, 0);
}
}
#keyframes animateRotation {
100% {
transform: rotate(360deg);
}
}
.animatable.being-animated {
animation:
animateOpacity 1s ease 0s forwards,
animatePosition 1.5s ease 0s forwards,
animateRotation 2s ease 0s forwards;
}
<div class="animatable"></div>
<div class="animatable"></div>
#BDawg's awesome snippet is more flexible and thorough, it certainly deserves to be the accepted answer. That said, I was inspired to see if a less verbose approach was feasible. Here's what I came up with.
It's pretty self-explanitory, but basically the concept is that all the animation properties' indexes correlate, and we can use that to find the name of the animation that finishes last.
const getFinalAnimationName = el => {
const style = window.getComputedStyle(el)
// get the combined duration of all timing properties
const [durations, iterations, delays] = ['Duration', 'IterationCount', 'Delay']
.map(prop => style[`animation${prop}`].split(', ')
.map(val => Number(val.replace(/[^0-9\.]/g, ''))))
const combinedDurations = durations.map((duration, idx) =>
duration * iterations[idx] + delays[idx])
// use the index of the longest duration to select the animation name
const finalAnimationIdx = combinedDurations
.findIndex(d => d === Math.max(...combinedDurations))
return style.animationName.split(', ')[finalAnimationIdx]
}
// pipe your element through this function to give it the ability to dispatch the 'animationendall' event
const addAnimationEndAllEvent = el => {
const animationendall = new CustomEvent('animationendall')
el.addEventListener('animationend', ({animationName}) =>
animationName === getFinalAnimationName(el) &&
el.dispatchEvent(animationendall))
return el
}
// example usage
const animatable = document.querySelector('.animatable')
addAnimationEndAllEvent(animatable)
.addEventListener('animationendall', () => console.log('All animations have finished'))
.animatable {
width: 50px;
height: 50px;
background-color: red;
position: relative;
left: 0;
animation: 1.5s slidein, 1s fadein;
}
#keyframes slidein {
0% { left: 100vw; }
100% { left: 0; }
}
#keyframes fadein {
0% { opacity: 0; }
100% { opacity: 1; }
}
<div class="animatable"></div>
First technique:
Add a class to an element, then handle every animation and wait for them to end, no matter what. This is the common way to do things where you trigger animations by classes.
As per Kaiido's comment and pointing out, this waits for every single animation, no matter how long to finish. This was the motivation behind all of this: create a nice animation and make JS aware of it (no matter how complex / long) finishing it so you could then chain other things.
If you don't do this, you might have a nice animation running and suddenly being cut by something else and...that's bad.
const triggerAnimationWithClass = (classToAdd, toWhat) => {
const element = document.querySelector(toWhat);
/**
* Initialize the count with 1, because you'll always have at least one animation no matter what.
*/
let animationCount = 1;
return new Promise((resolve, reject) => {
element.addEventListener('animationend', (event) => {
if((window.getComputedStyle(element).animationName).split(',').length - animationCount === 0) {
/**
* Remove the current function being hooked once we're done. When a class gets added that contains any N number of animations,
* we're running in a synchronous environment. There is virtually no way for another animation to happen at this point, so, we're
* surgically looking at animations that only happen when our classToAdd gets applied then hooking off to not create conflicts.
*/
element.removeEventListener('animationend', this);
const animationsDonePackage = {
'animatedWithClass': classToAdd,
'animatedElement': toWhat,
'animatedDoneTime': new Date().getTime()
};
resolve(animationsDonePackage);
} else {
animationCount++;
}
});
element.classList.add(classToAdd);
});
}
This handles multiple classes being added. Let's assume that from the outside, someone adds yet another class at the same time (weird, but, let's say it happens) you've added yours. All the animations on that element are then treated as one and the function will wait for all of them to finish.
Second technique:
Based on #B-Dawg's answer. Handle a set of animations, based on name (CSS animation names), not class, please read the after-word:
const onAnimationsComplete = ({element, animationsToLookFor}) => {
const animationsMap = new WeakMap();
if(!animationsMap.has(element)) {
const animationsCompleted = {};
for(const animation of animationsToLookFor) {
animationsCompleted[animation] = false;
}
animationsMap.set(element, animationsCompleted);
}
return new Promise((resolve, reject) => {
// When any animation completes...
element.addEventListener('animationend', ({target, animationName, elapsedTime}) => {
const animationsCompleted = animationsMap.get(target);
animationsCompleted[animationName] = true;
// If every animation is now completed...
if(Object.values(animationsCompleted).every(isCompleted => isCompleted === true)) {
const animations = Object.keys(animationsCompleted);
// Reset for next time - set all animations to not complete (false)
animations.forEach(animation => animationsCompleted[animation] = false);
//Remove the listener once we're done.
element.removeEventListener('animationend', this);
resolve({
'animationsDone': animationsToLookFor
});
}
});
});
};
This has a bug. Assuming that a new animation comes from, say, maybe a new class, if it's not put in the animationsToLookFor list, this never resolves.
Trying to fix it but if we're talking about a precise list of animations you're looking for, this is the go-to.
I been trying to make a Masonry gallery with a sequential fade-in effect so that the pictures fade in one by one. And there is also a shuffle feature which will randomize the images and they fade in again after being shuffled.
here is the demo and the code:
https://tuo1t.csb.app/
https://codesandbox.io/s/objective-swartz-tuo1t
When first visiting the page, the animation is correct. However once we click on the shuffle button, something weird happened: There are often some pictures don't fade-in sequentially after the image before them faded in, there is even no fade-in animation on them, they just show up out of order.
The way I achieved this animation is by adding a delay transition based on the index of the image, and use ref to track images.
first I initialize the ref
let refs = {};
for (let i = 0; i < images.length; i++) {
refs[i] = useRef(null);
}
and I render the gallery
<Mansory gap={"1em"} minWidth={minWidth}>
{imgs.map((img, i) => {
return (
<PicContainer
index={img.index}
selected={isSelected}
key={img.index}
>
<Enlarger
src={img.url}
index={img.index}
setIsSelected={setIsSelected}
onLoad={() => {
refs[i].current.toggleOpacity(1); <--- start with zero opacity images till those are loaded
}}
ref={refs[i]}
realIndex={i}
/>
</PicContainer>
);
})}
</Mansory>
for the every image component
class ZoomImg extends React.Component {
state = { zoomed: false, opacity: 0 };
toggleOpacity = o => {
console.log("here");
this.setState({ opacity: o }); <-- a setter function to change the opacity state via refs:
};
render() {
const {
realIndex,
index,
src,
enlargedSrc,
setIsSelected,
onLoad
} = this.props;
return (
<div style={{ margin: "0.25rem" }} onLoad={onLoad}>
<Image
style={{
opacity: this.state.opacity,
transition: "opacity 0.5s cubic-bezier(0.25,0.46,0.45,0.94)",
transitionDelay: `${realIndex * 0.1}s` <--- add a delay transition based on the index of the image.
}}
zoomed={this.state.zoomed}
src={src}
enlargedSrc={enlargedSrc}
onClick={() => {
this.setState({ zoomed: true });
setIsSelected(index);
}}
onRequestClose={() => {
this.setState({ zoomed: false });
setIsSelected(null);
}}
renderLoading={
<div
style={{
position: "absolute",
top: "50%",
color: "white",
left: "50%",
transform: "translateY(-50%} translateX(-50%)"
}}
>
Loading!
</div>
}
/>
</div>
);
}
}
I used console.log("here"); in the setter function, which will be called for changing the opacity state via refs. There are 16 images, so initially it is called 16 times. But when I clicked on the shuffle button, you can see that it is called fewer than 16 times because some of the pictures show up directly without fading in.
I been struggling with this problem for days and really hope someone can give me some hints.
The problem is that your are adding only some new images in the shuffle method, one approach is to apply 0 opacity to all refs first, then wait a few ms to add 1 opacity again, like here.
But, I would recommend a better approach for animation, I love shifty and its Tweenable module.
I'll try making my question as clear as possible.
Generally speaking, how can I get a component location attribute: left, top etc') if it changes constantly?
Specifically:
I've created a component in react-native that is animating left and top properties of his son styles (this is an the code for the component animating the left position of the object):
class MovementAnimation extends Component {
state = {
moveAnim: new Animated.Value(100), // Initial value for left: 100
}
componentDidMount() { //Call the animation
this.runMovementAnimation()
}
runMovementAnimation(){
var firstPosRand = parseInt(randomize('0', 3)) % 300;
var secondPosRand = parseInt(randomize('0', 3)) % 300;
var firstTimeRand = (parseInt(randomize('0',5)) % 3000) + 3000; //Stupid temporary solution to set a number between 500-3500
var secondTimeRand = (parseInt(randomize('0',5))% 3000) + 3000;
Animated.sequence([
Animated.timing(this.state.moveAnim, {
toValue: firstPosRand, // Animation
duration: firstTimeRand
}),
Animated.timing(this.state.moveAnim, {
toValue: secondPosRand,
duration: secondTimeRand
})
]).start(() => this.runMovementAnimation()) // repeat the animation
}
render() {
let { moveAnim } = this.state;
return (
<Animated.View ref='myElement' // Special animatable View
style={{
...this.props.style,
left: moveAnim, // Bind opacity to animated value
}}
>
{this.props.children}
</Animated.View>
);
}
}
Now, in my mainPage I'm controlling a circle component I've made with this animation component, and it works great. BUT, I want to use this specific circle 'left' attribute to control a different one, how can I access it from a different class?
Thanks in advance!
Amit