Skip to content
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

Add new spring feature - clamp #5195

Merged
merged 45 commits into from
Jan 16, 2024
Merged

Add new spring feature - clamp #5195

merged 45 commits into from
Jan 16, 2024

Conversation

Latropos
Copy link
Contributor

@Latropos Latropos commented Oct 6, 2023

Summary

Add possibility to define limits that won't be crossed by bouncing spring.
In simplification we can assume that range of bounces is related just with damping ratio,
therefore we recalculate new damping ratio, that satisfies our expectation - the spring moves within its range.

Simplified alghoritm:

  1. Calculate $\mathtt{zeta}$ (dampingRatio), $\mathtt{omega1}$ and $\mathtt{omega0}$
  2. Calculate such a two values for $\mathtt{zeta}$ that the extrema match both edges of the range exactly
  3. Compare the calculate $\mathtt{zeta}$ s with the provided damping ratio. Substitute the damping ratio with the biggest of the values
  4. Ignore the fact that now values for $\mathtt{omega0}$ and $\mathtt{omega1}$ are outdated
    • The spring still looks like a real physical one and seems very convincing
  5. We changed damping ratio, but the duration stays the same 🥳

Examples

Two examples with different arbitrary chosen config.

Example 1Example 2
Screen.Recording.2023-10-19.at.14.34.19.mov
Screen.Recording.2023-10-19.at.14.35.53.mov

Test plan

Resting example - code
import Animated, {
  useSharedValue,
  useAnimatedStyle,
  withSpring,
} from 'react-native-reanimated';
import { View, Text, Button, StyleSheet, ViewStyle } from 'react-native';
import React, { useState } from 'react';

const VIOLET = '#b58df1';
const BORDER_WIDTH = 4;
const FRAME_WIDTH = 300;

const LOWER_CLAMP = 120;
const UPPER_CLAMP = 220;
function renderFramedExample(testedStyle: ViewStyle, description: string) {
  return (
    <>
      <Text style={styles.text}>{description}</Text>
      <View
        style={{
          width: FRAME_WIDTH + 2 * BORDER_WIDTH,
          borderWidth: BORDER_WIDTH,
          borderColor: VIOLET,
        }}>
        <Animated.View
          style={[
            styles.clampMarker,
            { marginBottom: -40, width: UPPER_CLAMP },
          ]}
        />
        <Animated.View style={[styles.movingBox, testedStyle]} />
        <Animated.View
          style={[
            styles.clampMarker,
            {
              marginTop: -40,
              width: FRAME_WIDTH - LOWER_CLAMP,
              alignSelf: 'flex-end',
            },
          ]}
        />
      </View>
    </>
  );
}

export default function AnimatedStyleUpdateExample() {
  const randomWidth = useSharedValue(100);
  const [toggle, setToggle] = useState(false);

  const config = {
    duration: 5000,
    dampingRatio: 0.4,
  };

  const clampedStyle = useAnimatedStyle(() => {
    return {
      width: withSpring(randomWidth.value, {
        ...config,
        clamp: [LOWER_CLAMP, UPPER_CLAMP],
      }),
    };
  });
  const style = useAnimatedStyle(() => {
    return {
      width: withSpring(randomWidth.value, config),
    };
  });

  return (
    <View style={styles.container}>
      {renderFramedExample(clampedStyle, 'Clamp example')}

      {renderFramedExample(style, 'Default')}
      <Button
        title="toggle"
        onPress={() => {
          randomWidth.value = toggle ? 130 : 210;
          setToggle(!toggle);
        }}
      />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    flexDirection: 'column',
    padding: 40,
  },
  clampMarker: {
    margin: 0,
    opacity: 0.5,
    height: 40,
    backgroundColor: VIOLET,
  },
  movingBox: {
    height: 100,
    opacity: 0.5,
    backgroundColor: 'black',
  },
  text: {
    fontSize: 16,
    marginVertical: 4,
  },
});

Documentation

@Latropos Latropos changed the base branch from main to acynk/spring-zero-duration October 6, 2023 10:46
@Latropos Latropos marked this pull request as ready for review October 6, 2023 12:35
@piaskowyk piaskowyk self-requested a review October 7, 2023 08:29
Copy link
Member

@tomekzaw tomekzaw left a comment

Choose a reason for hiding this comment

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

Left some syntax-related comments.

src/reanimated2/animation/springUtils.ts Outdated Show resolved Hide resolved
src/reanimated2/animation/springUtils.ts Outdated Show resolved Hide resolved
) {
'worklet';
const { zeta, toValue, startValue } = animation;
if (Number(toValue) === startValue) {
Copy link
Member

Choose a reason for hiding this comment

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

Why do we convert toValue into a number here? Does it mean that we no longer support colors?

Copy link
Contributor Author

@Latropos Latropos Oct 10, 2023

Choose a reason for hiding this comment

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

I've just tested and it looks that color handling is a bit buggy with our main code 😱
Something like

 const style = useAnimatedStyle(() => {
    return {
      backgroundColor: withSpring(randomColor.value, config),
    };
  });

where randomColor is a colorname like 'red' or 'blue' doesn't always run (on main!)

Anyway toValue is expected to be a number, since in src/reanimated2/animation/util.ts we have a lot of functions like colorOnStart and numberOnStart that are expected to convert input to numbers and then join it again.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Also if toValue weren't number it would be difficult wo handle it, since value is always a number.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Btw. logic inside src/reanimated2/animation/util.ts is very important and complicated, I think I'd suggest splitting it into multiple files, WDYT?

src/reanimated2/animation/springUtils.ts Outdated Show resolved Hide resolved
src/reanimated2/animation/springUtils.ts Outdated Show resolved Hide resolved
src/reanimated2/animation/springUtils.ts Outdated Show resolved Hide resolved
@Latropos Latropos requested a review from tomekzaw October 11, 2023 11:51
@Latropos Latropos marked this pull request as draft October 12, 2023 13:13
Base automatically changed from acynk/spring-zero-duration to main October 13, 2023 11:59
@Latropos Latropos force-pushed the acynk/spring-clamp branch 3 times, most recently from 3a27ddd to 7b5f66f Compare October 19, 2023 13:02
@Latropos Latropos marked this pull request as ready for review October 19, 2023 13:25
app/src/examples/WithClampExample.tsx Show resolved Hide resolved
app/src/examples/WithClampExample.tsx Show resolved Hide resolved
app/src/examples/WithClampExample.tsx Show resolved Hide resolved
app/src/examples/WithClampExample.tsx Outdated Show resolved Hide resolved
app/src/examples/WithClampExample.tsx Outdated Show resolved Hide resolved
src/reanimated2/animation/spring.ts Show resolved Hide resolved
src/reanimated2/animation/springUtils.ts Outdated Show resolved Hide resolved
src/reanimated2/animation/springUtils.ts Show resolved Hide resolved
Aleksandra Cynk and others added 5 commits December 11, 2023 12:41
@Latropos Latropos requested a review from tomekzaw December 11, 2023 15:35
__tests__/spring.test.tsx Outdated Show resolved Hide resolved
__tests__/spring.test.tsx Outdated Show resolved Hide resolved
__tests__/spring.test.tsx Outdated Show resolved Hide resolved
src/reanimated2/animation/springUtils.ts Outdated Show resolved Hide resolved
tjzel and others added 2 commits December 18, 2023 10:01
This PR aims to fix the issue when Release builds would fail on Paper on
iOS.
The reason this regression happened is #5113.

XCode ships by default to new projects `DEBUG` macro for Debug
Configuration. Due to some issues (#4902) we decided to switch over to
`NDEBUG` macro. However, the `NDEBUG` is not provided by XCode out of
the box. Incidentally, it's provided by React Native which puts its
install script into project's Podfile - however, [only in
Fabric](facebook/react-native@421df9f).
What is more, some time ago React Native made it so `PRODUCTION=1 pod
install` is [no longer a necessary
step](facebook/react-native@93fdcba)
to make a release build, therefore we cannot rely on it. Until React
Native defines `NDEBUG` for Paper too we have to detect, based on
available options, whether we potentially have a Release build and this
pull request does this.

Thanks to
#5383 we
finally figures out how to do it the most agreeable way. We hardcode
that for a build config named `Release` NDEBUG=1 will be set. If the
user uses some custom release build config e.g. "MyRelease", NDEBUG=1
will not be set - in this situation we'll require the user to use
`PRODUCTION=1`.

Run all example apps (except TVOS cause I couldn't make it cooperate
with me, and Web) on both iOS and Android in both Release and Debug and
see if they work properly (e.g. add a throw in #ifndef fragment).
@Latropos Latropos requested a review from tomekzaw December 18, 2023 09:56
@piaskowyk
Copy link
Member

I've tested it and it seems to work 🎉

@piaskowyk piaskowyk merged commit c39f904 into main Jan 16, 2024
7 checks passed
@piaskowyk piaskowyk deleted the acynk/spring-clamp branch January 16, 2024 09:28
github-merge-queue bot pushed a commit that referenced this pull request Jan 16, 2024
<!-- Thanks for submitting a pull request! We appreciate you spending
the time to work on these changes. Please follow the template so that
the reviewers can easily understand what the code changes affect. -->

## Summary

> **warning**
> DO NOT MERGE BEFORE
#5195
GETS MERGED

This change is needed since we have implemented one more config option
for spring (clamp).
The clamp PR:
#5195
However documentation of spring config gets too cluttered.

My suggestions:
- Add minimum reanimated version supporting each config option
- Split config table into three smaller tables (properties for
duration-based spring, physical spring & common config properties)
- Make it easier for the user to read only the interesting part of
documentation (since it gets long). Maybe we can just add an example
config for users, who don't care too much about details, or highlight
the most important props.

<!-- Explain the motivation for this PR. Include "Fixes #<number>" if
applicable. -->

## Test plan

Thats how it looks:

![image](https://github.com/software-mansion/react-native-reanimated/assets/56199675/030da0b4-1d0b-4c42-8d89-f3373ae38edc)

<!-- Provide a minimal but complete code snippet that can be used to
test out this change along with instructions how to run it and a
description of the expected behavior. -->

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Aleksandra Cynk <aleksandracynk@Aleksandras-MacBook-Pro-3.local>
Co-authored-by: Kacper Kapuściak <39658211+kacperkapusciak@users.noreply.github.com>
Co-authored-by: joshlam <38872498+joshlam@users.noreply.github.com>
Co-authored-by: Andrey <44743245+andreysam@users.noreply.github.com>
Co-authored-by: Krzysztof Piaskowy <krzysztof.piaskowy@swmansion.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
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.

4 participants