|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237 |
- /* @flow */
- import mapToZero from './mapToZero';
- import stripStyle from './stripStyle';
- import stepper from './stepper';
- import defaultNow from 'performance-now';
- import defaultRaf from 'raf';
- import shouldStopAnimation from './shouldStopAnimation';
- import React from 'react';
- import PropTypes from 'prop-types';
-
- import type {ReactElement, PlainStyle, Style, Velocity, MotionProps} from './Types';
-
- const msPerFrame = 1000 / 60;
-
- type MotionState = {
- currentStyle: PlainStyle,
- currentVelocity: Velocity,
- lastIdealStyle: PlainStyle,
- lastIdealVelocity: Velocity,
- };
-
- export default class Motion extends React.Component<MotionProps, MotionState> {
- static propTypes = {
- // TOOD: warn against putting a config in here
- defaultStyle: PropTypes.objectOf(PropTypes.number),
- style: PropTypes.objectOf(PropTypes.oneOfType([
- PropTypes.number,
- PropTypes.object,
- ])).isRequired,
- children: PropTypes.func.isRequired,
- onRest: PropTypes.func,
- };
-
- constructor(props: MotionProps) {
- super(props);
- this.state = this.defaultState();
- }
-
- wasAnimating: boolean = false;
- animationID: ?number = null;
- prevTime: number = 0;
- accumulatedTime: number = 0;
-
- defaultState(): MotionState {
- const {defaultStyle, style} = this.props;
- const currentStyle = defaultStyle || stripStyle(style);
- const currentVelocity = mapToZero(currentStyle);
- return {
- currentStyle,
- currentVelocity,
- lastIdealStyle: currentStyle,
- lastIdealVelocity: currentVelocity,
- };
- }
-
- // it's possible that currentStyle's value is stale: if props is immediately
- // changed from 0 to 400 to spring(0) again, the async currentStyle is still
- // at 0 (didn't have time to tick and interpolate even once). If we naively
- // compare currentStyle with destVal it'll be 0 === 0 (no animation, stop).
- // In reality currentStyle should be 400
- unreadPropStyle: ?Style = null;
- // after checking for unreadPropStyle != null, we manually go set the
- // non-interpolating values (those that are a number, without a spring
- // config)
- clearUnreadPropStyle = (destStyle: Style): void => {
- let dirty = false;
- let {currentStyle, currentVelocity, lastIdealStyle, lastIdealVelocity} = this.state;
-
- for (let key in destStyle) {
- if (!Object.prototype.hasOwnProperty.call(destStyle, key)) {
- continue;
- }
-
- const styleValue = destStyle[key];
- if (typeof styleValue === 'number') {
- if (!dirty) {
- dirty = true;
- currentStyle = {...currentStyle};
- currentVelocity = {...currentVelocity};
- lastIdealStyle = {...lastIdealStyle};
- lastIdealVelocity = {...lastIdealVelocity};
- }
-
- currentStyle[key] = styleValue;
- currentVelocity[key] = 0;
- lastIdealStyle[key] = styleValue;
- lastIdealVelocity[key] = 0;
- }
- }
-
- if (dirty) {
- this.setState({currentStyle, currentVelocity, lastIdealStyle, lastIdealVelocity});
- }
- };
-
- startAnimationIfNecessary = (): void => {
- // TODO: when config is {a: 10} and dest is {a: 10} do we raf once and
- // call cb? No, otherwise accidental parent rerender causes cb trigger
- this.animationID = defaultRaf((timestamp) => {
- // check if we need to animate in the first place
- const propsStyle: Style = this.props.style;
- if (shouldStopAnimation(
- this.state.currentStyle,
- propsStyle,
- this.state.currentVelocity,
- )) {
- if (this.wasAnimating && this.props.onRest) {
- this.props.onRest();
- }
-
- // no need to cancel animationID here; shouldn't have any in flight
- this.animationID = null;
- this.wasAnimating = false;
- this.accumulatedTime = 0;
- return;
- }
-
- this.wasAnimating = true;
-
- const currentTime = timestamp || defaultNow();
- const timeDelta = currentTime - this.prevTime;
- this.prevTime = currentTime;
- this.accumulatedTime = this.accumulatedTime + timeDelta;
- // more than 10 frames? prolly switched browser tab. Restart
- if (this.accumulatedTime > msPerFrame * 10) {
- this.accumulatedTime = 0;
- }
-
- if (this.accumulatedTime === 0) {
- // no need to cancel animationID here; shouldn't have any in flight
- this.animationID = null;
- this.startAnimationIfNecessary();
- return;
- }
-
- let currentFrameCompletion =
- (this.accumulatedTime - Math.floor(this.accumulatedTime / msPerFrame) * msPerFrame) / msPerFrame;
- const framesToCatchUp = Math.floor(this.accumulatedTime / msPerFrame);
-
- let newLastIdealStyle: PlainStyle = {};
- let newLastIdealVelocity: Velocity = {};
- let newCurrentStyle: PlainStyle = {};
- let newCurrentVelocity: Velocity = {};
-
- for (let key in propsStyle) {
- if (!Object.prototype.hasOwnProperty.call(propsStyle, key)) {
- continue;
- }
-
- const styleValue = propsStyle[key];
- if (typeof styleValue === 'number') {
- newCurrentStyle[key] = styleValue;
- newCurrentVelocity[key] = 0;
- newLastIdealStyle[key] = styleValue;
- newLastIdealVelocity[key] = 0;
- } else {
- let newLastIdealStyleValue = this.state.lastIdealStyle[key];
- let newLastIdealVelocityValue = this.state.lastIdealVelocity[key];
- for (let i = 0; i < framesToCatchUp; i++) {
- [newLastIdealStyleValue, newLastIdealVelocityValue] = stepper(
- msPerFrame / 1000,
- newLastIdealStyleValue,
- newLastIdealVelocityValue,
- styleValue.val,
- styleValue.stiffness,
- styleValue.damping,
- styleValue.precision,
- );
- }
- const [nextIdealX, nextIdealV] = stepper(
- msPerFrame / 1000,
- newLastIdealStyleValue,
- newLastIdealVelocityValue,
- styleValue.val,
- styleValue.stiffness,
- styleValue.damping,
- styleValue.precision,
- );
-
- newCurrentStyle[key] =
- newLastIdealStyleValue +
- (nextIdealX - newLastIdealStyleValue) * currentFrameCompletion;
- newCurrentVelocity[key] =
- newLastIdealVelocityValue +
- (nextIdealV - newLastIdealVelocityValue) * currentFrameCompletion;
- newLastIdealStyle[key] = newLastIdealStyleValue;
- newLastIdealVelocity[key] = newLastIdealVelocityValue;
- }
- }
-
- this.animationID = null;
- // the amount we're looped over above
- this.accumulatedTime -= framesToCatchUp * msPerFrame;
-
- this.setState({
- currentStyle: newCurrentStyle,
- currentVelocity: newCurrentVelocity,
- lastIdealStyle: newLastIdealStyle,
- lastIdealVelocity: newLastIdealVelocity,
- });
-
- this.unreadPropStyle = null;
-
- this.startAnimationIfNecessary();
- });
- };
-
- componentDidMount() {
- this.prevTime = defaultNow();
- this.startAnimationIfNecessary();
- }
-
- componentWillReceiveProps(props: MotionProps) {
- if (this.unreadPropStyle != null) {
- // previous props haven't had the chance to be set yet; set them here
- this.clearUnreadPropStyle(this.unreadPropStyle);
- }
-
- this.unreadPropStyle = props.style;
- if (this.animationID == null) {
- this.prevTime = defaultNow();
- this.startAnimationIfNecessary();
- }
- }
-
- componentWillUnmount() {
- if (this.animationID != null) {
- defaultRaf.cancel(this.animationID);
- this.animationID = null;
- }
- }
-
- render(): ReactElement {
- const renderedChildren = this.props.children(this.state.currentStyle);
- return renderedChildren && React.Children.only(renderedChildren);
- }
- }
|