Skip to content

refactor: withSpring end conditions #7803

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 16, 2025

Conversation

tjzel
Copy link
Collaborator

@tjzel tjzel commented Jul 15, 2025

Summary

Previous withSpring end conditions were based on two parameters: restDisplacementThreshold - how far the spring is from its resting point and restSpeedThreshold - the speed of the spring. These parameters were absolute and adjusted for animations in ranges greater than 0-1. For animations in a lesser range the user would need to overwrite the restDisplacementThreshold and restSpeedThreshold parameters so the animation wouldn't snap immediately after starting. However, since these parameters were absolute it wasn't so obvious what the values should've been.

Instead, we replace both these parameters with one energyThreshold which tells at what fraction of the initial energy the animation should snap. This value is relative, quadratic and bound to both position and speed of the spring. In my testing it proved much more reliable than the fixed absolute threshold and didn't need to be overridden in any case.

To make equations make sense I had to fix the coordinates system used in withSpring animation. Now the resting point is always at relative 0, which makes things more readable in my opinion. If there's an animation from 0 to 100, it gets translated to (-100, 0). If it's from 100 to 0, it gets translated to (100, 0). Thanks to this we don't have to compare the start with the end to figure out whether we should use a minus sign somewhere or not.

Test plan

I added an example that shows that the behavior of the spring works as expected.

ChatHeads example shows that chained withSpring animations work too.

@tjzel tjzel added this pull request to the merge queue Jul 16, 2025
Merged via the queue into main with commit 7819921 Jul 16, 2025
11 checks passed
@tjzel tjzel deleted the @tjzel/reanimated/with-spring-end-conditions branch July 16, 2025 15:42
previousAnimation?.startValue
: Number(animation.toValue) - value;
(previousAnimation?.startValue as number)
: value - (animation.toValue as number);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this changes actually breaks overshootClamping

assume we have a value 1000 which needs to go to 900
now x0 will bee 100, and in the isAnimationTerminatingCalculation condition (current > toValue && startValue < toValue)

current would be 999 which is bigger than 900 and startValue would be 100 (instead of 1000) which is also less than 900, so if the spring is not moving more than 50% it will terminate immediately.

which is breaking @gorhom/bottom-sheets for small bottom sheets

github-merge-queue bot pushed a commit that referenced this pull request Jul 31, 2025
## Summary

Fixes #7947

When fixing various thing in withSpring in #7803 I moved over to a
coherent coordinate system for the animation. This means that when
animating a value from 700 to 600, the animation internally animated
from 100 to 0. For 600 to 700 it's -100 to 0. I accidentally didn't
reflect that change for `overshootClamping` checks.

## Test plan

```tsx
import React, { useEffect, useState } from 'react';
import { Button, StyleSheet, Text, View } from 'react-native';
import Animated, {
  useAnimatedStyle,
  useSharedValue,
  withSpring,
} from 'react-native-reanimated';

export default function EmptyExample() {
  const [toggle, setToggle] = useState(false);
  const height = useSharedValue(600);

  const animatedStyle = useAnimatedStyle(() => {
    return {
      height: withSpring(height.value, {
        overshootClamping: true,
      }),
    };
  });

  return (
    <View style={styles.container}>
      <Animated.View
        style={[{ width: 100, backgroundColor: 'blue' }, animatedStyle]}
      />
      <Button
        title="Change Height"
        onPress={() => {
          const target = toggle ? 600 : 500;
          height.value = target;
          setToggle(!toggle);
        }}
      />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
  },
});
```


| Before | After |
|--------|--------|
| <video
src="https://github.com/user-attachments/assets/75ac8595-90dd-44f1-ab04-5c5126941e1e"/>
| <video
src="https://github.com/user-attachments/assets/45c6f4e8-0291-42be-a268-8ee43ee72fb7"/>
|
tjzel added a commit that referenced this pull request Jul 31, 2025
## Summary

Fixes #7947

When fixing various thing in withSpring in #7803 I moved over to a
coherent coordinate system for the animation. This means that when
animating a value from 700 to 600, the animation internally animated
from 100 to 0. For 600 to 700 it's -100 to 0. I accidentally didn't
reflect that change for `overshootClamping` checks.

## Test plan

```tsx
import React, { useEffect, useState } from 'react';
import { Button, StyleSheet, Text, View } from 'react-native';
import Animated, {
  useAnimatedStyle,
  useSharedValue,
  withSpring,
} from 'react-native-reanimated';

export default function EmptyExample() {
  const [toggle, setToggle] = useState(false);
  const height = useSharedValue(600);

  const animatedStyle = useAnimatedStyle(() => {
    return {
      height: withSpring(height.value, {
        overshootClamping: true,
      }),
    };
  });

  return (
    <View style={styles.container}>
      <Animated.View
        style={[{ width: 100, backgroundColor: 'blue' }, animatedStyle]}
      />
      <Button
        title="Change Height"
        onPress={() => {
          const target = toggle ? 600 : 500;
          height.value = target;
          setToggle(!toggle);
        }}
      />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
  },
});
```


| Before | After |
|--------|--------|
| <video
src="https://github.com/user-attachments/assets/75ac8595-90dd-44f1-ab04-5c5126941e1e"/>
| <video
src="https://github.com/user-attachments/assets/45c6f4e8-0291-42be-a268-8ee43ee72fb7"/>
|
tjzel added a commit that referenced this pull request Aug 1, 2025
## Summary

Fixes #7947

When fixing various thing in withSpring in #7803 I moved over to a
coherent coordinate system for the animation. This means that when
animating a value from 700 to 600, the animation internally animated
from 100 to 0. For 600 to 700 it's -100 to 0. I accidentally didn't
reflect that change for `overshootClamping` checks.

## Test plan

```tsx
import React, { useEffect, useState } from 'react';
import { Button, StyleSheet, Text, View } from 'react-native';
import Animated, {
  useAnimatedStyle,
  useSharedValue,
  withSpring,
} from 'react-native-reanimated';

export default function EmptyExample() {
  const [toggle, setToggle] = useState(false);
  const height = useSharedValue(600);

  const animatedStyle = useAnimatedStyle(() => {
    return {
      height: withSpring(height.value, {
        overshootClamping: true,
      }),
    };
  });

  return (
    <View style={styles.container}>
      <Animated.View
        style={[{ width: 100, backgroundColor: 'blue' }, animatedStyle]}
      />
      <Button
        title="Change Height"
        onPress={() => {
          const target = toggle ? 600 : 500;
          height.value = target;
          setToggle(!toggle);
        }}
      />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
  },
});
```


| Before | After |
|--------|--------|
| <video
src="https://github.com/user-attachments/assets/75ac8595-90dd-44f1-ab04-5c5126941e1e"/>
| <video
src="https://github.com/user-attachments/assets/45c6f4e8-0291-42be-a268-8ee43ee72fb7"/>
|
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants