Add SpringUtils.js (#40)

Motivation

API known form RN core is much more flexible when it comes to being flexible.

We wanted to make using spring animation easier.

Changes
We didn't want to modify existing spring object so I have added SpringUtils object which is a set of methods for easier spring's config manipulations.
This commit is contained in:
Michał Osadnik
2019-04-25 15:15:31 +02:00
committed by GitHub
parent 1ed19e5255
commit fd11ce21f2
6 changed files with 361 additions and 0 deletions

View File

@@ -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',

View File

@@ -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 (
<Animated.View style={{ transform: [{ translateX: this._transX }] }}>
{children}
</Animated.View>
);
}
}
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 (
<View style={styles.container}>
<Snappable config={configA}>
<View style={styles.box} />
</Snappable>
<Snappable config={configB}>
<View style={styles.box} />
</Snappable>
<Snappable config={configC}>
<View style={styles.box} />
</Snappable>
</View>
);
}
}
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',
},
});

View File

@@ -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

View File

@@ -113,6 +113,34 @@ declare module 'react-native-reanimated' {
toValue: Adaptable<number>;
}
interface SpringConfigWithOrigamiTensionAndFriction {
tension: Adaptable<number>;
mass: Adaptable<number>;
friction: Adaptable<number>;
overshootClamping: Adaptable<number> | boolean;
restSpeedThreshold: Adaptable<number>;
restDisplacementThreshold: Adaptable<number>;
toValue: Adaptable<number>;
}
interface SpringConfigWithBouncinessAndSpeed {
bounciness: Adaptable<number>;
mass: Adaptable<number>;
speed: Adaptable<number>;
overshootClamping: Adaptable<number> | boolean;
restSpeedThreshold: Adaptable<number>;
restDisplacementThreshold: Adaptable<number>;
toValue: Adaptable<number>;
}
type SprintUtils = {
makeDefaultConfig: () => SpringConfig;
makeConfigFromBouncinessAndSpeed: (prevConfig: SpringConfigWithBouncinessAndSpeed) => SpringConfig;
makeConfigFromOrigamiTensionAndFriction: (prevConfig: SpringConfigWithOrigamiTensionAndFriction) => SpringConfig
}
export const SprintUtils: SprintUtils
type AnimateStyle<S extends object> = {
[K in keyof S]: S[K] extends ReadonlyArray<any>
? ReadonlyArray<AnimateStyle<S[K][0]>>

View File

@@ -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,

View File

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