Animate rotation to the nearest value in React Native - react-native

Given, interpolation, that has to rotate a car marker:
this.rotation.interpolate({
inputRange: [0, 360],
outputRange: ['0deg', '360deg'],
extrapolate: 'clamp'
})
GIF below represents the following bearings:
1. 337.38055647641903
2. 335.202973299875
3. 13.03761189748721
4. 13.042704554861551
5. 358.77805501498045
Green line is how I want it to work (rotate to the nearest value). Red line is how it works currently (on GIF).
Is there a way to animate rotation to the nearest value? (in our case from 335 to 13 clockwise). Maybe there's a better way instead of interpolation?

let heading = this.state.heading;
let oldHeading = this.state.oldHeading;
const duration = 200;
if (oldHeading - heading > 180) {
Animated.timing(
this.state.spinAnim,
{
toValue: 360,
duration: duration,
easing: Easing.linear,
useNativeDriver: true
}).start(() => this.state.spinAnim.setValue(0));
} else if (oldHeading - heading < -180) {
Animated.timing(
this.state.spinAnim,
{
toValue: 0,
duration: duration,
easing: Easing.linear,
useNativeDriver: true
}).start(() => this.state.spinAnim.setValue(360));
} else {
Animated.timing(
this.state.spinAnim,
{
toValue: heading,
duration: duration,
easing: Easing.linear,
useNativeDriver: true
}).start();
}
I had the same problem so my solution was to keep every time the exactly previous heading and if the distance is bigger than 180 i use two separate animations. One that animates from x to 0 and then sets value to 360 (or x to 360 and then sets value to 0) and the one that is the "default" and runs every time.

This is equivalent to avoiding rollbacks in CSS rotate transitions.
A way to do this is to recalculate the next angle based on the amplitude of the previous one. You still have to use interpolation, what changes is that the value fed to the animation will be the result of this function (credits), which adds the difference to the previous value and performs some modulo checks to guess the closest value:
function closestEquivalentAngle(from, to) {
var delta = ((((to - from) % 360) + 540) % 360) - 180;
return from + delta;
}
So for example, next time you move forward from 335 deg to 13 deg, the result will be 373, that is the original value plus a difference of 38 degrees. That works backwards as well. Just remember to store the value of the resulting angle somewhere, so you can use it on the next iteration as prevAngle.
Pulling it all together:
const RotateComp = (props) => {
const rotationValue = React.useRef(new Animated.Value(0)).current;
// Animation interpolation
const rotation = rotationValue.interpolate({
inputRange: [-360, 360],
outputRange: ['-360deg', '360deg'],
});
React.useEffect(() => {
// Obtain prevAngle and nextAngle, then...
Animated.timing(
rotationValue,
{
toValue: closestEquivalentAngle(prevAngle, nextAngle),
duration: 200,
useNativeDriver: true,
},
).start();
}, [rotationValue])
return (
// ...
<Animated.View
style={{
...props.style,
transform: [{rotate: rotation}],
}}
>
{props.children}
</Animated.View>
);
}

Related

Repeating a repeating animation with delay with reanimated

Using react-native-reanimated, I'm trying to repeat infinitely an animation which is also a repeated animation, with a delay.
With my code, the delay and the nested repeat animation are triggered but not the inifinite one.
Any ideas ?
useEffect(() => {
progress.value = withRepeat(
withDelay(2000, withRepeat(withTiming(0, { duration: 500 }), 6, true)),
-1,
true
);
}, []);
Like #Abe pointed out in his answer, reverse property of withRepeat is not supported when wrapped with other animation modifiers.
But, you can do this without setInterval - making use of withSequence to simulate the `reverse animation
// starting delay of 2000ms
// play animation 6 times
// repeat
progress.value =
withRepeat(
withDelay(2000, withRepeat(
withSequence(
// split duration of 500ms to 250ms
withTiming(goToValue, {duration: 250, easing: Easing.inOut(Easing.ease)}),
withTiming(initialValue, {duration: 250, easing: Easing.inOut(Easing.ease)}),
)
, 6))
, -1)
You've set the outer withRepeat to 1, so it should only repeat once, is that intended? Use a negative number to repeat indefinitely.
The docs for withRepeat say that withRepeat with the third argument (reverse) set to true doesn't work properly for the withDelay and withSequence animations, that could also be causing an issue. You might want to try reversing the animations manually in a withSequence call and repeating that.
No solution found with Reanimated, but as suggested by #Abe,a simple setInterval does the trick
useEffect(() => {
progress.value = withRepeat(withTiming(0, { duration: 400 }), 6, true);
const interval = setInterval(() => {
progress.value = withRepeat(withTiming(0, { duration: 400 }), 6, true);
}, 6000);
return () => clearInterval(interval);
}, []);
You can achieve that without setInterval, put withDelay on each animation.
withRepeat(
withSequence(
withDelay(
2000,
withTiming(0, {
duration: 300,
easing: Easing.inOut(Easing.ease),
}),
),
withDelay(
2000,
withTiming(1, {
duration: 300,
easing: Easing.inOut(Easing.ease),
}),
),
),
-1,
);
You can achieve this by using Animated.sequence
This code basically works by re-run the function when the animation done
function fadeAnimation() {
Animated.sequence([
Animated.timing(progress.value, {
toValue: 0,
duration: 500,
delay: 1000,
useNativeDriver: true,
}),
Animated.timing(progress.value, {
toValue: 1,
duration: 500,
useNativeDriver: true,
}),
]).start(() => {
fadeAnimation();
});
}
useEffect(() => {
fadeAnimation()
}, []);

How to set on a new random to x, y when gsap is repeated

Hello i use vue3 and gsap
I want to give new x and y whenever gsap repeats.
Repeatdelay works well randomly, but without refresh, x and y are always given the same value.
This is my code.
gsap.set(array.value[i], {
xPercent: gsap.utils.random(0, 900),
yPercent: gsap.utils.random(0, 300)
})
gsap.to(array.value[i], {
opacity: 0,
scale: 2,
duration: 0.3,
yoyo: true,
onComplete () {
gsap.delayedCall(gsap.utils.random(2, 5, 0.1), () => this.restart())
}
})
I want to set new random values of x and y whenever gsap is repeated.
Is there a good way for me?
Please let me know if you don't have enough explanation.
Thank for your help.
You can set a new random value for x and y before the animation gets restarted.
const setRandomPosition = item => {
gsap.set(item, {
xPercent: gsap.utils.random(0, 900),
yPercent: gsap.utils.random(0, 300)
})
}
setRandomPosition(array.value[i]);
gsap.to(array.value[i], {
opacity: 0,
scale: 2,
duration: 0.3,
yoyo: true,
onComplete(){
gsap.delayedCall(gsap.utils.random(2, 5, 0.1), () => {
setRandomPosition(array.value[i]);
this.restart()
})
}
})

React Native (Animations and setState)

I am facing a particular issue related to react native animations with setState (functional component), I have a countdown made with setInterval and each second I make a setState, whenever I have a setState the animation resets and restarts and I don't know why, I am using also useRef like this
const opacity = new Animated.Value(0) const animatedValueRef = useRef(opacity), the animations is looping like this (each 250millis)
Animated.timing(animatedValueRef.current, {
toValue: 1,
duration: 220,
easing: Easing.ease,
useNativeDriver: true,
}).start((event) => {
if(event.finished) {
opacity.setValue(0);
second();
}
});
}
Thank you!
Edit: this is how I implemented the countdown:
function step1(max, onoff) {
let intervalId;
let varCounter = 1;
setAnimation(true); //animation starts
irrigation(); //animation call
let counter = function () {
if (varCounter < max) {
varCounter= varCounter + 1;
setCounter(varCounter + " " + onoff)
} else {
clearInterval(intervalId);
setCounter(" ");
setAnimation(false);
}
};
intervalId = setInterval(()=>{counter()}, 1000);
}
(The code needs to be refactored)
Basically your component is re- render every time your component’s state changes.
The component gets the updated state and React decides if it should re-render the component.
By default React re-renders everything all the time.
all you have to do is chain your animation. Your animated value will go from 0 to 1, then the second animation will make the value from 1 to 0. You just need to start the animation again after 1 second in your counter function. I suggest making the useNativeDriver: false. No need to setState at all.
Animated.timing(animatedValueRef.current, {
toValue: 1,
duration: 220,
easing: Easing.ease,
useNativeDriver: true,
}).start(() => {
Animated.timing(animatedValueRef.current, {
toValue: 0,
duration: 220,
easing: Easing.ease,
useNativeDriver: true,
}).start(())
});
}

How to get current Animated.value when nativeDriver is true?

It is possible to get the current animated value using ._value while nativeDriver is false.
But is it possible to get current value when nativeDriver is turned to true ?
Do we have any work arounds ?
use addListener like so:
this._animatedValue = new Animated.Value(0);
this._animatedValue.addListener(({value}) => this._value = value);
Animated.timing(this._animatedValue, {
toValue: 100,
duration: 500
}).start();
I figured out a workaround for scenarios like this, where the native driver spoils our offset and gives a jump in our animation towards the end.
Create a pseudo animated value (which will not be used for any animation) and keep updating it alongside your original animated value. Finally when your animation is over (like decay, spring, etc.) set the offsets and current angle with the pseudo value as below:
const angle = useRef(new Animated.Value(0)).current; //original value used in animation; will run on native side
const psuedoAngle = useRef(new Animated.Value(0)).current; //only to track the value on JS side
//using PanResponder
...
onPanResponderMove: (evt, gestureState) => {
angle.setValue(some_value);
psuedoAngle.setValue(same_value);
},
onPanResponderRelease: (evt, {vx, vy}) => {
// calculate your velocity
...
const decay1 = Animated.decay(angle,{
velocity: velocity,
deceleration: 0.997,
useNativeDriver: true
}
);
const decay2 = Animated.decay(psuedoAngle,{
velocity: velocity,
deceleration: 0.997,
useNativeDriver: false //this will keep your pseudo value intact
}
);
//set offset and current angle in callback as below
Animated.parallel([decay1,decay2]).start(() => {
psuedoAngle.flattenOffset();
angle.setOffset(psuedoAngle._value); //correct value passed to original value
psuedoAngle.setOffset(psuedoAngle._value);
angle.setValue(0);
psuedoAngle.setValue(0);
});
}

React Native Animated Loop start callback

I am struggling with React Native Animations here. The outcome is simple, I have an Animated.Image which I want to spin.
All good, until I want to loop thru the animation for n times and do something when it stops.
I have the following code .
Animated.loop(
Animated.timing(this.state.spin, {
toValue: 360,
duration: 1000,
easing: Easing.linear,
useNativeDriver: true,
}), {
iterations: 3
}
).start(() => {
console.log('done');
});
It spins 3 times as per loop iteration but no callback was fired when animation ends.
Here the Expo which replicates this: https://snack.expo.io/S1PjnfB9-
Try the code below
Animated.loop(
Animated.timing(this.state.spin, {
toValue: 360,
duration: 1000,
easing: Easing.linear,
useNativeDriver: true,
}), {
iterations: 3
}
).start(event => {
if (event.finished) {
console.log('finished');
}
});
I just added a check on the event response.
Hope it helps.
Found the answer.
It seems that if you take out useNativeDriver it works as it should be, and the callback is called.
This is weird...