diff --git a/Example/App.js b/Example/App.js index 6a82d33..d642d07 100644 --- a/Example/App.js +++ b/Example/App.js @@ -16,6 +16,7 @@ import Rotations from './rotations'; import Imperative from './imperative'; import PanRotateAndZoom from './PanRotateAndZoom'; import ProgressBar from './progressBar'; +import DifferentSpringConfigs from './differentSpringConfigs'; import TransitionsSequence from './transitions/sequence'; import TransitionsShuffle from './transitions/shuffle'; import TransitionsProgress from './transitions/progress'; @@ -56,6 +57,10 @@ const SCREENS = { screen: ProgressBar, title: 'Progress bar', }, + differentSpringConfigs: { + screen: DifferentSpringConfigs, + title: 'Different Spring Configs', + }, transitionsSequence: { screen: TransitionsSequence, title: 'Transitions sequence', diff --git a/Example/differentSpringConfigs/index.js b/Example/differentSpringConfigs/index.js new file mode 100644 index 0000000..aa4a209 --- /dev/null +++ b/Example/differentSpringConfigs/index.js @@ -0,0 +1,102 @@ +import React, { Component } from 'react'; +import { StyleSheet, View } from 'react-native'; +import Animated from 'react-native-reanimated'; + +const { + block, + set, + cond, + eq, + spring, + startClock, + Value, + Clock, + SpringUtils, +} = Animated; + +function runSpring(clock, value, config) { + const state = { + finished: new Value(1), + velocity: new Value(0), + position: new Value(0), + time: new Value(0), + }; + + return block([ + cond(state.finished, [ + set(state.finished, 0), + set(state.position, value), + set(config.toValue, cond(eq(config.toValue, 100), -100, 100)), + startClock(clock), + ]), + spring(clock, state, config), + state.position, + ]); +} + +class Snappable extends Component { + constructor(props) { + super(props); + const transX = new Value(); + const clock = new Clock(); + this._transX = runSpring(clock, transX, props.config); + } + render() { + const { children } = this.props; + return ( + + {children} + + ); + } +} + +const configA = SpringUtils.makeDefaultConfig(); +const configB = SpringUtils.makeConfigFromBouncinessAndSpeed({ + ...SpringUtils.makeDefaultConfig(), + bounciness: 10, + speed: 8, +}); +const configC = SpringUtils.makeConfigFromOrigamiTensionAndFriction({ + ...SpringUtils.makeDefaultConfig(), + tension: 10, + friction: new Value(4), +}); + +export default class Example extends React.Component { + render() { + return ( + + + + + + + + + + + + ); + } +} + +const CIRCLE_SIZE = 70; + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, + box: { + backgroundColor: 'tomato', + marginLeft: -(CIRCLE_SIZE / 2), + marginTop: -(CIRCLE_SIZE / 2), + width: CIRCLE_SIZE, + height: CIRCLE_SIZE, + margin: CIRCLE_SIZE, + borderRadius: CIRCLE_SIZE / 2, + borderColor: '#000', + }, +}); diff --git a/README.md b/README.md index 1d67dfb..187604b 100644 --- a/README.md +++ b/README.md @@ -729,6 +729,31 @@ spring(clock, { finished, position, velocity, time }, { damping, mass, stiffness When evaluated, updates `position` and `velocity` nodes by running a single step of spring based animation. Check the original `Animated` API docs to learn about the config parameters like `damping`, `mass`, `stiffness`, `overshootClamping`, `restSpeedThreshold` and `restDisplacementThreshold`. The `finished` state updates to `1` when the `position` reaches the destination set by `toValue`. The `time` state variable also updates when the node evaluates and it represents the clock value at the time when the node got evaluated for the last time. It is expected that `time` variable is reset before spring animation can be restarted. +### `SpringUtils` +For developers' convenience, it's possible to use a different way of configuring `spring` animation which follows behavior known from React Native core. + +#### `SpringUtils.makeDefaultConfig()` + Returns an object filled with default config of animation: + ```js + { + stiffness: new Value(100), + mass: new Value(1), + damping: new Value(10), + overshootClamping: false, + restSpeedThreshold: 0.001, + restDisplacementThreshold: 0.001, + toValue: new Value(0), + } +``` + +#### `SpringUtils.makeConfigFromBouncinessAndSpeed(prevConfig)` +Transforms an object with `bounciness` and `speed` params into config expected by the `spring` node. `bounciness` and `speed` might be nodes or numbers. + +#### `SpringUtils.makeConfigFromOrigamiTensionAndFriction(prevConfig)` +Transforms an object with `tension` and `friction` params into config expected by the `spring` node. `tension` and `friction` might be nodes or numbers. + +See an [Example of different configs](https://github.com/kmagiera/react-native-reanimated/blob/master/Example/colors/differentSpringConfigs.js). + ## Running animations ### Declarative API diff --git a/react-native-reanimated.d.ts b/react-native-reanimated.d.ts index 3c5f3da..90fdc29 100644 --- a/react-native-reanimated.d.ts +++ b/react-native-reanimated.d.ts @@ -113,6 +113,34 @@ declare module 'react-native-reanimated' { toValue: Adaptable; } + interface SpringConfigWithOrigamiTensionAndFriction { + tension: Adaptable; + mass: Adaptable; + friction: Adaptable; + overshootClamping: Adaptable | boolean; + restSpeedThreshold: Adaptable; + restDisplacementThreshold: Adaptable; + toValue: Adaptable; + } + + interface SpringConfigWithBouncinessAndSpeed { + bounciness: Adaptable; + mass: Adaptable; + speed: Adaptable; + overshootClamping: Adaptable | boolean; + restSpeedThreshold: Adaptable; + restDisplacementThreshold: Adaptable; + toValue: Adaptable; + } + + type SprintUtils = { + makeDefaultConfig: () => SpringConfig; + makeConfigFromBouncinessAndSpeed: (prevConfig: SpringConfigWithBouncinessAndSpeed) => SpringConfig; + makeConfigFromOrigamiTensionAndFriction: (prevConfig: SpringConfigWithOrigamiTensionAndFriction) => SpringConfig + } + + export const SprintUtils: SprintUtils + type AnimateStyle = { [K in keyof S]: S[K] extends ReadonlyArray ? ReadonlyArray> diff --git a/src/Animated.js b/src/Animated.js index cb921a8..d8cea3b 100644 --- a/src/Animated.js +++ b/src/Animated.js @@ -23,6 +23,7 @@ import { Transitioning, createTransitioningComponent, } from './Transitioning'; +import SpringUtils from './animations/SpringUtils'; const Animated = { // components @@ -46,6 +47,7 @@ const Animated = { decay: backwardCompatibleAnimWrapper(decay, DecayAnimation), timing: backwardCompatibleAnimWrapper(timing, TimingAnimation), spring: backwardCompatibleAnimWrapper(spring, SpringAnimation), + SpringUtils, // configuration addWhitelistedNativeProps, diff --git a/src/animations/SpringUtils.js b/src/animations/SpringUtils.js new file mode 100644 index 0000000..92588d4 --- /dev/null +++ b/src/animations/SpringUtils.js @@ -0,0 +1,199 @@ +import { + cond, + sub, + divide, + multiply, + add, + pow, + lessOrEq, + and, + greaterThan, +} from './../base'; +import AnimatedValue from './../core/InternalAnimatedValue'; + +function stiffnessFromOrigamiValue(oValue) { + return (oValue - 30) * 3.62 + 194; +} + +function dampingFromOrigamiValue(oValue) { + return (oValue - 8) * 3 + 25; +} + +function stiffnessFromOrigamiNode(oValue) { + return add(multiply(sub(oValue, 30), 3.62), 194); +} + +function dampingFromOrigamiNode(oValue) { + return add(multiply(sub(oValue, 8), 3), 25); +} + +function makeConfigFromOrigamiTensionAndFriction(prevConfig) { + const { tension, friction, ...rest } = prevConfig; + return { + ...rest, + stiffness: + typeof tension === 'number' + ? stiffnessFromOrigamiValue(tension) + : stiffnessFromOrigamiNode(tension), + damping: + typeof friction === 'number' + ? dampingFromOrigamiValue(friction) + : dampingFromOrigamiNode(friction), + }; +} + +function makeConfigFromBouncinessAndSpeed(prevConfig) { + const { bounciness, speed, ...rest } = prevConfig; + if (typeof bounciness === 'number' && typeof speed === 'number') { + return fromBouncinessAndSpeedNumbers(bounciness, speed, rest); + } + return fromBouncinessAndSpeedNodes(bounciness, speed, rest); +} + +function fromBouncinessAndSpeedNodes(bounciness, speed, rest) { + function normalize(value, startValue, endValue) { + return divide(sub(value, startValue), sub(endValue, startValue)); + } + + function projectNormal(n, start, end) { + return add(start, multiply(n, sub(end, start))); + } + + function linearInterpolation(t, start, end) { + return add(multiply(t, end), multiply(sub(1, t), start)); + } + + function quadraticOutInterpolation(t, start, end) { + return linearInterpolation(sub(multiply(2, t), multiply(t, t)), start, end); + } + + function b3Friction1(x) { + return add( + sub(multiply(0.0007, pow(x, 3)), multiply(0.031, pow(x, 2))), + multiply(0.64, x), + 1.28 + ); + } + + function b3Friction2(x) { + return add( + sub(multiply(0.000044, pow(x, 3)), multiply(0.006, pow(x, 2))), + multiply(0.36, x), + 2 + ); + } + + function b3Friction3(x) { + return add( + sub(multiply(0.00000045, pow(x, 3)), multiply(0.000332, pow(x, 2))), + multiply(0.1078, x), + 5.84 + ); + } + + function b3Nobounce(tension) { + return cond( + lessOrEq(tension, 18), + b3Friction1(tension), + cond( + and(greaterThan(tension, 18), lessOrEq(tension, 44)), + b3Friction2(tension), + b3Friction3(tension) + ) + ); + } + + let b = normalize(divide(bounciness, 1.7), 0, 20); + b = projectNormal(b, 0, 0.8); + let s = normalize(divide(speed, 1.7), 0, 20); + let bouncyTension = projectNormal(s, 0.5, 200); + let bouncyFriction = quadraticOutInterpolation( + b, + b3Nobounce(bouncyTension), + 0.01 + ); + return { + ...rest, + stiffness: stiffnessFromOrigamiNode(bouncyTension), + damping: dampingFromOrigamiNode(bouncyFriction), + }; +} + +function fromBouncinessAndSpeedNumbers(bounciness, speed, rest) { + function normalize(value, startValue, endValue) { + return (value - startValue) / (endValue - startValue); + } + + function projectNormal(n, start, end) { + return start + n * (end - start); + } + + function linearInterpolation(t, start, end) { + return t * end + (1 - t) * start; + } + + function quadraticOutInterpolation(t, start, end) { + return linearInterpolation(2 * t - t * t, start, end); + } + + function b3Friction1(x) { + return 0.0007 * Math.pow(x, 3) - 0.031 * Math.pow(x, 2) + 0.64 * x + 1.28; + } + + function b3Friction2(x) { + return 0.000044 * Math.pow(x, 3) - 0.006 * Math.pow(x, 2) + 0.36 * x + 2; + } + + function b3Friction3(x) { + return ( + 0.00000045 * Math.pow(x, 3) - + 0.000332 * Math.pow(x, 2) + + 0.1078 * x + + 5.84 + ); + } + + function b3Nobounce(tension) { + if (tension <= 18) { + return b3Friction1(tension); + } else if (tension > 18 && tension <= 44) { + return b3Friction2(tension); + } else { + return b3Friction3(tension); + } + } + + let b = normalize(bounciness / 1.7, 0, 20); + b = projectNormal(b, 0, 0.8); + const s = normalize(speed / 1.7, 0, 20); + const bouncyTension = projectNormal(s, 0.5, 200); + const bouncyFriction = quadraticOutInterpolation( + b, + b3Nobounce(bouncyTension), + 0.01 + ); + + return { + ...rest, + stiffness: stiffnessFromOrigamiValue(bouncyTension), + damping: dampingFromOrigamiValue(bouncyFriction), + }; +} + +function makeDefaultConfig() { + return { + stiffness: new AnimatedValue(100), + mass: new AnimatedValue(1), + damping: new AnimatedValue(10), + overshootClamping: false, + restSpeedThreshold: 0.001, + restDisplacementThreshold: 0.001, + toValue: new AnimatedValue(0), + }; +} + +export default { + makeDefaultConfig, + makeConfigFromBouncinessAndSpeed, + makeConfigFromOrigamiTensionAndFriction, +};