Skip to content

Commit 99c54a1

Browse files
committed
feat(plugin-reanimated): Add proper spring animation
1 parent b5e7254 commit 99c54a1

File tree

5 files changed

+419
-140
lines changed

5 files changed

+419
-140
lines changed

.changeset/every-towns-find.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@plextv/react-lightning-plugin-reanimated": patch
3+
---
4+
5+
feat(plugin-reanimated): Added proper spring animation

apps/react-native-lightning-example/src/pages/AnimationTest.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Button, StyleSheet, View } from 'react-native';
33
import Animated, {
44
useAnimatedStyle,
55
useSharedValue,
6+
withSpring,
67
withTiming,
78
} from 'react-native-reanimated';
89

@@ -25,15 +26,17 @@ const AnimationTest = () => {
2526

2627
const animatedStyles = useAnimatedStyle(
2728
() => ({
28-
top: withTiming(translateY.value),
2929
backgroundColor: withTiming(animColor.value),
30+
left: withSpring(translateX.value, {
31+
stiffness: 2000,
32+
damping: 200,
33+
mass: 50,
34+
}),
35+
top: withSpring(translateY.value, { duration: 500 }),
3036
transform: [
3137
{
3238
scale: withTiming(scale.value),
3339
},
34-
{
35-
translateX: withTiming(translateX.value),
36-
},
3740
],
3841
}),
3942
[translateX, translateY, animColor],

packages/plugin-reanimated/src/animation/spring.ts

Lines changed: 119 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -1,150 +1,147 @@
1+
// Extracted and adapted from Reanimated's spring implementation
2+
// See: https://github.com/software-mansion/react-native-reanimated/tree/main/packages/react-native-reanimated/src/animation/spring
13
import type { AnimationSettings } from '@lightningjs/renderer';
2-
import type { WithSpringConfig } from 'react-native-reanimated-original';
3-
import { ReduceMotion } from 'react-native-reanimated-original';
4-
5-
const MAX_SIM_FRAMES = 600; // ~2 seconds @ 60 FPS
6-
const TARGET_FPS = 1 / 60;
7-
const _SPRING_LENGTH = 0.2;
4+
import {
5+
GentleSpringConfig,
6+
GentleSpringConfigWithDuration,
7+
ReduceMotion,
8+
type WithSpringConfig,
9+
} from 'react-native-reanimated-original';
10+
import {
11+
calculateNewStiffnessToMatchDuration,
12+
checkIfConfigIsValid,
13+
criticallyDampedSpringCalculations,
14+
type DefaultSpringConfig,
15+
getEnergy,
16+
getEstimatedDuration,
17+
initialCalculations,
18+
isAnimationTerminatingCalculation,
19+
underDampedSpringCalculations,
20+
} from './springUtils';
821

922
const cache = new Map<string, AnimationSettings>();
10-
const timingFunctionCache = new Map<string, (delta: number) => number>();
1123

12-
const DefaultSpringConfig = {
13-
damping: 10,
14-
mass: 1,
15-
stiffness: 100,
24+
const DefaultConfig: DefaultSpringConfig = {
25+
...GentleSpringConfig,
26+
...GentleSpringConfigWithDuration,
1627
overshootClamping: false,
17-
restDisplacementThreshold: 0.01,
18-
restSpeedThreshold: 2,
28+
energyThreshold: 6e-9,
1929
velocity: 0,
20-
dampingRatio: 0.5,
21-
reduceMotion: ReduceMotion.System,
30+
reduceMotion: undefined,
31+
delay: 0,
32+
clamp: undefined,
2233
};
2334

24-
export function createSpringTimingFunction(
25-
config?: WithSpringConfig,
26-
): (delta: number) => number {
27-
const {
28-
mass,
29-
stiffness,
30-
damping,
31-
velocity: _velocity,
32-
restDisplacementThreshold,
33-
restSpeedThreshold,
34-
overshootClamping,
35-
} = { ...DefaultSpringConfig, ...config };
36-
37-
const key = JSON.stringify({
38-
mass,
39-
stiffness,
40-
damping,
41-
velocity: _velocity,
42-
restDisplacementThreshold,
43-
restSpeedThreshold,
44-
overshootClamping,
45-
});
46-
47-
const cachedResult = timingFunctionCache.get(key);
48-
if (cachedResult) {
49-
return cachedResult;
50-
}
35+
export function createSpringAnimation(
36+
userConfig?: WithSpringConfig,
37+
): AnimationSettings {
38+
const key = JSON.stringify(userConfig);
39+
const cached = cache.get(key);
5140

52-
const SPRING_TARGET = 1;
53-
let position = 0;
54-
let velocity = _velocity;
55-
const positions: number[] = [];
56-
let frame = 0;
41+
if (cached) {
42+
return cached;
43+
}
5744

58-
while (frame < MAX_SIM_FRAMES) {
59-
const springAmount = -stiffness * (position - SPRING_TARGET);
60-
const dampingAmount = -damping * velocity;
61-
const acceleration = (springAmount + dampingAmount) / mass;
45+
const config: DefaultSpringConfig = {
46+
...DefaultConfig,
47+
...userConfig,
48+
};
6249

63-
velocity += acceleration * TARGET_FPS;
64-
position += velocity * TARGET_FPS;
50+
if (
51+
config.reduceMotion === ReduceMotion.Always ||
52+
config.duration === 0 ||
53+
!checkIfConfigIsValid(config)
54+
) {
55+
const instant: AnimationSettings = {
56+
duration: config.duration,
57+
easing: 'ease-out',
58+
delay: config.delay,
59+
loop: false,
60+
repeat: 0,
61+
repeatDelay: 0,
62+
stopMethod: false,
63+
};
64+
65+
cache.set(key, instant);
66+
67+
return instant;
68+
}
6569

66-
if (overshootClamping && position > 1) {
67-
position = 1;
68-
velocity = 0;
69-
}
70+
const startValue = 0;
71+
const toValue = 1;
72+
const x0 = startValue - toValue;
73+
const useDuration =
74+
userConfig?.dampingRatio != null || userConfig?.duration != null;
7075

71-
positions.push(position);
72-
frame++;
76+
let current = startValue;
77+
let velocity = config.velocity;
7378

74-
const isAtRest =
75-
Math.abs(position - SPRING_TARGET) < restDisplacementThreshold &&
76-
Math.abs(velocity) < restSpeedThreshold;
79+
const { zeta, omega0, omega1 } = initialCalculations(useDuration, config);
7780

78-
if (isAtRest) {
79-
break;
80-
}
81+
if (useDuration) {
82+
config.stiffness = calculateNewStiffnessToMatchDuration(x0, config);
83+
} else {
84+
config.duration = getEstimatedDuration(zeta, omega0);
8185
}
8286

83-
const timingFunction = (delta: number): number => {
84-
if (delta < 0) {
85-
delta = 0;
86-
}
87-
88-
if (delta > 1) {
89-
delta = 1;
87+
const initialEnergy = getEnergy(
88+
x0,
89+
config.velocity,
90+
config.stiffness,
91+
config.mass,
92+
);
93+
94+
function easing(progress: number): number {
95+
const t = (progress * config.duration) / 1000;
96+
const v0 = velocity;
97+
const x0 = progress - toValue;
98+
99+
const { position: newPosition, velocity: newVelocity } =
100+
zeta < 1
101+
? underDampedSpringCalculations({
102+
zeta,
103+
v0,
104+
x0,
105+
omega0,
106+
omega1,
107+
t,
108+
})
109+
: criticallyDampedSpringCalculations({
110+
v0,
111+
x0,
112+
omega0,
113+
t,
114+
});
115+
116+
current = newPosition;
117+
velocity = newVelocity;
118+
119+
if (
120+
isAnimationTerminatingCalculation(
121+
startValue,
122+
toValue,
123+
current,
124+
velocity,
125+
initialEnergy,
126+
config,
127+
)
128+
) {
129+
velocity = 0;
130+
current = toValue;
90131
}
91132

92-
const index = Math.round(delta * (positions.length - 1));
93-
94-
return positions[index] ?? 0;
95-
};
96-
97-
timingFunctionCache.set(key, timingFunction);
98-
return timingFunction;
99-
}
100-
101-
// Temporary until https://github.com/lightning-js/renderer/pull/639 is landed in a beta
102-
export function createSpringAnimation(
103-
config?: WithSpringConfig,
104-
): AnimationSettings {
105-
const {
106-
mass,
107-
stiffness,
108-
damping,
109-
velocity: _velocity,
110-
restDisplacementThreshold,
111-
restSpeedThreshold,
112-
overshootClamping,
113-
} = { ...DefaultSpringConfig, ...config };
114-
115-
const key = JSON.stringify({
116-
mass,
117-
stiffness,
118-
damping,
119-
velocity: _velocity,
120-
restDisplacementThreshold,
121-
restSpeedThreshold,
122-
overshootClamping,
123-
});
124-
125-
const cachedResult = cache.get(key);
126-
if (cachedResult) {
127-
return cachedResult;
133+
return current;
128134
}
129135

130-
const timingFn = createSpringTimingFunction(config);
131-
132-
// Find the four points for the cubic bezier
133-
const y1 = timingFn(1 / 3);
134-
const y2 = timingFn(2 / 3);
135-
136-
const p1_y = (9 * y1 - 3 * y2) / 4;
137-
const p2_y = (6 * y2 - 3 * y1) / 4;
138-
139-
const animation = {
140-
duration: 350,
141-
easing: `cubic-bezier(${1 / 3}, ${p1_y}, ${2 / 3}, ${p2_y})`,
142-
delay: config?.delay ?? 0,
136+
const animation: AnimationSettings = {
137+
duration: config.duration,
138+
easing,
139+
delay: config.delay,
143140
loop: false,
144141
repeat: 0,
145142
repeatDelay: 0,
146143
stopMethod: false,
147-
} satisfies AnimationSettings;
144+
};
148145

149146
cache.set(key, animation);
150147

0 commit comments

Comments
 (0)