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,
+};