How to animate many elements React-Native Reanimated 2? - javascript

I'm trying to just make 20 - 50 stars "twinkle" (opacity change with random interval), but it's slowing the app to a crawl.
I'm using reanimated 2.2.0.
I tried sharing the opacity value but only 1 element had the styles applied, which I think may be because of this: "Animated styles cannot be shared between views."
This is my entire component
function TwinklingStar({ position, size }) {
const starOpacity = useSharedValue(1);
useEffect(() => {
setInterval(() => {
starOpacity.value = withSpring(starOpacity.value ? 0 : 1)
}, getRandomInt(500, 1000))
}, []);
const twinkling = useAnimatedStyle(() => ({
opacity: starOpacity.value
}));
return (
<Animated.Image
source={sparkleImage}
resizeMode="cover"
style={[{
width: size,
height: size,
position: 'absolute',
top: position.y,
left: position.x
}, twinkling]}
/>
)
}
There are perhaps 5 of them per card, and a random amount of cards, 5-20 lets say.
I've read over the docs but don't think I'm understanding them properly cause I cant figure out why app is crawling/freezing/crashing. It's like all the animations are blocking the js thread, but they should be running on ui thread since the opacity changes are "captured" by the useAnimatedStyle hook which is a "worklet"?
Confused...

Related

Matching setState interval with css animation

I am rotating the items in an array of images inside an interval of 5 seconds. Then I have a css animation with styled components that eases in the gallery images with a fade that occurs at the same time interval.
const fadeIn = keyframes`
5%, 95% { opacity: 1 }
100% { opacity: 0 }
`
export const Gallery = styled.div<{ lapse: number }>`
position: relative;
margin: 0 auto;
max-width: 1064px;
opacity: 0;
animation: ${fadeIn} ease-in-out ${({ lapse }) => `${lapse}s`} infinite;
`
The problem is that when I change the state even thou at first it seems in sync, eventually the setState takes a bit longer
import React, { useState, useEffect } from 'react'
const [images, setImages] = useState<string[]>([])
// Time in seconds for each image swap, fading in between
const lapse = 5
...
useEffect(() => {
// Clone the images array
const imgs = [...images]
// Time interval same as css animation to fade in
const interval = setInterval(() => {
// Take the first element and put it at the end
imgs.push(...imgs.splice(0, 1))
// Update the state, this seems to desync as time passes
setImages(imgs)
}, lapse * 1000)
return () => clearInterval(interval)
}, [images])
return (
<Gallery lapse={lapse}>
<Portal>
<img src={imgs/${images[0]}`}
</Portal>
<Thumbnails>
<Thumbwrapper>
<img src={imgs/${images[1]}`}
</Thumbwrapper>
<Thumbwrapper>
<img src={imgs/${images[2]}`}
</Thumbwrapper>
</Thumbnails>
</Gallery>
)
Is there a way I can make sure the swapping happends smoothly?
Make the component class based,
constructor(props) {
super(props);
this.state = {
interval: null,
images: ["1.jpg", "2.jpg", "3.jpg"],
timeChanged: null
};
}
// setInterval when component is mounted
componentDidMount() {
var interval = setInterval(()=> {
// use callback argument for setState
// when new value (images) depends on old value
this.setState((state)=>({
timeChanged: (new Date()).toString(),
images: state.images.slice(1).concat(state.images[0])
// add 1st img at the end
});
}, 3000);
this.setState({interval: interval});
}
// clearInterval before component is unmounted
componentWillUnmount() {
clearInterval(this.state.interval);
}
When the state is updated using its old value the callback argument should be used.
(See https://reactjs.org/docs/state-and-lifecycle.html#state-updates-may-be-asynchronous)
slice returns a portion of an array.
Also see how to update state arrays in React.
If you still wish to use hooks. Just call setImages with
(images)=>images.slice(1).concat(images[0]).
I'm not sure where the clearTimeout should be, when using hooks.
It would be great if we can have example in codesandbox. Also I would like to ask you, maybe I am missing something, but how do you determine when to stop using effect, since it depends on image, which is, I assume, part of the useSate hook, and in that same effect you are updating that same image, which will lead to another effect usage(leading to infinity loop)?
UPDATED ANSWER
I made similar example on codesandbox to look it on my own. It looks to me that main problem here is sync between applied CSS and actual update in your react component. When using setInterval with 5000ms it does not mean that it will be triggered right after 5000ms, but CSS animation on the other side is always right on time, so it stuck when two of those become unsynced.
It looks like the only way to solve this is to recreate img elements on each reorder, by introducing timestamp state and use it as part of the each img key in order to force img recreation on each update. Also you would need to change animation in simpler way.
CSS would besomething like this:
#keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.image {
position: relative;
margin: 0 auto;
max-width: 1064px;
animation: fadeIn 3s;
opacity: 1;
}
And functional component would be something like this(hook part only):
const [images, setImages] = useState(["1.jpg", "2.jpg", "3.jpg"]);
const [dateTimeWhenChanged, setDatetimeWhenChanged] = useState("");
useEffect(() => {
const imgs = [...images];
// Time interval same as css animation to fade in
const interval = setInterval(() => {
// Take the first element and but it to the end
imgs.push(...imgs.splice(0, 1));
// Update the state, this seems to desync as time passes
setImages(imgs);
setDatetimeWhenChanged(new Date().toString());
}, 5 * 1000);
return (
<div>
{images.map((img, ind) => (
<img
key={`${ind}-${dateTimeWhenChanged}`}
alt={img}
src={`images/${img}`}
className="imgg"
/>
))}
</div>
);
I came to the conclusion that having two 'clocks' to keep the time, one being react setState and the other css animation, was at the core of the problem.
So I am now keeping a single interval with a setTimeout to make sure it goes in order then substract the time taken on the timeouts from the interval and toggle the class for css
export const Gallery = styled.div<{ fadeIn: boolean }>`
position: relative;
margin: 0 auto;
max-width: 1064px;
transition: opacity 0.3s;
opacity: ${({ fadeIn }) => (fadeIn ? 1 : 0)};
`
import React, { useState, useEffect } from 'react'
const [images, setImages] = useState<string[]>([])
const [fadeIn, setFadeIn] = useState<boolean>(true)
useEffect(() => {
let interval: ReturnType<typeof setInterval>
if (images.length) {
interval = setInterval(() => {
setFadeIn(false)
setTimeout(() => {
setImages(images => images.slice(1).concat(images[0]) )
setFadeIn(true)
}, 400)
}, (lapse - 0.4) * 1000)
}
return () => clearInterval(interval)
}, [images])
const getImage = (i: number) => <img src={imgs/${images[i]}`} />
<Gallery fadeIn={fadeIn}>
<Portal>{getImage(0)}</Portal>
<Thumbnails>
<Thumbwrapper>{getImage(1)}</Thumbwrapper>
<Thumbwrapper>{getImage(2)}</Thumbwrapper>
</Thumbnails>
</Gallery>
I did however used #Nice Books better setState aproach

How do I get this progress bar from react native?

So I want to implement a progress bar like this in react native. I have found a library which is https://www.npmjs.com/package/react-native-progress, but this library does not have a progress bar like this. The issue is the circle part. Is there a library which has a progress bar like this one? If there isn't, then any idea on how to implement this would be appreciated. So as a fellow commenter got confused whether it is a slider or progress bar, I will include the full screen image. There is a timer and the progress bar or progress slider reflects that in real time.
We can still exploit react-native-community/slider for this purpose. This would make sense since we can allow the user to fast forward. If you do not want allow user interaction, you can still disable it via the slider component props.
For a smooth sliding, the slider changes its value at a higher tick rate. You might want to experiment with it a little bit to make it fit your needs.
Here is an example on how I did it.
const [progress, setProgress] = useState(-1)
// takes time in seconds
function interpolate(time) {
return time * 600
}
// runs 10 seconds, you can take any other value
const [time, setTime] = useState(interpolate(10))
useEffect(() => {
if (progress < time && progress > -1) {
const timer = setTimeout(() => setProgress(progress + 10), 10)
return () => clearTimeout(timer)
}
}, [progress, time])
const startTimer = React.useCallback(() => {
setProgress(0)
}, [])
return (
<SafeAreaView style={{ margin: 30 }}>
<Slider
style={{ width: 300, height: 40, backgroundColor: "red" }}
value={progress}
minimumValue={0}
maximumValue={time}
minimumTrackTintColor="#FFFFFF"
maximumTrackTintColor="#FFFFFF"
/>
<Pressable style={{ marginTop: 10 }} onPress={() => startTimer()}>
<Text>Toggle timer</Text>
</Pressable>
</SafeAreaView>
)
The above is a dummy implementation which will run a timer for 10 seconds and the slider will move automatically like a progress bar.

Animate when scrollY is equal to 0.52 - Next.js ( with framer-motion )

I have this landing page, and there's this part of the page where i want to generate a simple animation with opacity using framer-motion.
The problem, is that those types of animations only happen when you first go to the page, but i want it to happen based on scroll position, how can i make that happen?
This is what i've tried so far
const [scrollY, setScrollY] = useState(0);
const { scrollYProgress } = useViewportScroll();
useEffect(() => {
scrollYProgress.onChange(number => setScrollY(number));
}, [scrollYProgress, scrollY]);
// Framer motion - PopUp
const PopUp = {
hidden: { opacity: 0 },
visible: { opacity: 1, transition: { duration: 0.5 } }
};
return (
<DescriptionHero
variants={Math.round(scrollY * 100) / 100 === 0.52 && PopUp}
initial="hidden"
animate="visible"
>
</DescriptionHero>
)
So basically, I'm trying to see the scrollY progress in scrollY using useState and useViewportScroll from framer motion, and, in the actual component i've made an animation using variants and all that jazz.
The problem, is that, Math.round(scrollY * 100) / 100 , when the value of that expressin is equal to 0.52, i want the component execute the animation, but it just doesn't work, and i'm having a hard time trying to animate based on scroll position, what can i do ?
I think you need to change animate prop, not variants
return (
<DescriptionHero
variants={PopUp}
initial="hidden"
animate={Math.round(scrollY * 100) / 100 === 0.52 ? "visible" : "hidden"}
>
</DescriptionHero>
Although it is still quite bad way to trigger animation, what whould happen when you scroll is not equal 0.52? What happens on 0.53?

React: Trying to achieve a sequential fade-in effect with a Masonry layout but it's out of order

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.

Accessing dynamic style variables (location) in react native

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

Categories

Resources