You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

238 lines
7.6 KiB

  1. /* @flow */
  2. import mapToZero from './mapToZero';
  3. import stripStyle from './stripStyle';
  4. import stepper from './stepper';
  5. import defaultNow from 'performance-now';
  6. import defaultRaf from 'raf';
  7. import shouldStopAnimation from './shouldStopAnimation';
  8. import React from 'react';
  9. import PropTypes from 'prop-types';
  10. import type {ReactElement, PlainStyle, Style, Velocity, MotionProps} from './Types';
  11. const msPerFrame = 1000 / 60;
  12. type MotionState = {
  13. currentStyle: PlainStyle,
  14. currentVelocity: Velocity,
  15. lastIdealStyle: PlainStyle,
  16. lastIdealVelocity: Velocity,
  17. };
  18. export default class Motion extends React.Component<MotionProps, MotionState> {
  19. static propTypes = {
  20. // TOOD: warn against putting a config in here
  21. defaultStyle: PropTypes.objectOf(PropTypes.number),
  22. style: PropTypes.objectOf(PropTypes.oneOfType([
  23. PropTypes.number,
  24. PropTypes.object,
  25. ])).isRequired,
  26. children: PropTypes.func.isRequired,
  27. onRest: PropTypes.func,
  28. };
  29. constructor(props: MotionProps) {
  30. super(props);
  31. this.state = this.defaultState();
  32. }
  33. wasAnimating: boolean = false;
  34. animationID: ?number = null;
  35. prevTime: number = 0;
  36. accumulatedTime: number = 0;
  37. defaultState(): MotionState {
  38. const {defaultStyle, style} = this.props;
  39. const currentStyle = defaultStyle || stripStyle(style);
  40. const currentVelocity = mapToZero(currentStyle);
  41. return {
  42. currentStyle,
  43. currentVelocity,
  44. lastIdealStyle: currentStyle,
  45. lastIdealVelocity: currentVelocity,
  46. };
  47. }
  48. // it's possible that currentStyle's value is stale: if props is immediately
  49. // changed from 0 to 400 to spring(0) again, the async currentStyle is still
  50. // at 0 (didn't have time to tick and interpolate even once). If we naively
  51. // compare currentStyle with destVal it'll be 0 === 0 (no animation, stop).
  52. // In reality currentStyle should be 400
  53. unreadPropStyle: ?Style = null;
  54. // after checking for unreadPropStyle != null, we manually go set the
  55. // non-interpolating values (those that are a number, without a spring
  56. // config)
  57. clearUnreadPropStyle = (destStyle: Style): void => {
  58. let dirty = false;
  59. let {currentStyle, currentVelocity, lastIdealStyle, lastIdealVelocity} = this.state;
  60. for (let key in destStyle) {
  61. if (!Object.prototype.hasOwnProperty.call(destStyle, key)) {
  62. continue;
  63. }
  64. const styleValue = destStyle[key];
  65. if (typeof styleValue === 'number') {
  66. if (!dirty) {
  67. dirty = true;
  68. currentStyle = {...currentStyle};
  69. currentVelocity = {...currentVelocity};
  70. lastIdealStyle = {...lastIdealStyle};
  71. lastIdealVelocity = {...lastIdealVelocity};
  72. }
  73. currentStyle[key] = styleValue;
  74. currentVelocity[key] = 0;
  75. lastIdealStyle[key] = styleValue;
  76. lastIdealVelocity[key] = 0;
  77. }
  78. }
  79. if (dirty) {
  80. this.setState({currentStyle, currentVelocity, lastIdealStyle, lastIdealVelocity});
  81. }
  82. };
  83. startAnimationIfNecessary = (): void => {
  84. // TODO: when config is {a: 10} and dest is {a: 10} do we raf once and
  85. // call cb? No, otherwise accidental parent rerender causes cb trigger
  86. this.animationID = defaultRaf((timestamp) => {
  87. // check if we need to animate in the first place
  88. const propsStyle: Style = this.props.style;
  89. if (shouldStopAnimation(
  90. this.state.currentStyle,
  91. propsStyle,
  92. this.state.currentVelocity,
  93. )) {
  94. if (this.wasAnimating && this.props.onRest) {
  95. this.props.onRest();
  96. }
  97. // no need to cancel animationID here; shouldn't have any in flight
  98. this.animationID = null;
  99. this.wasAnimating = false;
  100. this.accumulatedTime = 0;
  101. return;
  102. }
  103. this.wasAnimating = true;
  104. const currentTime = timestamp || defaultNow();
  105. const timeDelta = currentTime - this.prevTime;
  106. this.prevTime = currentTime;
  107. this.accumulatedTime = this.accumulatedTime + timeDelta;
  108. // more than 10 frames? prolly switched browser tab. Restart
  109. if (this.accumulatedTime > msPerFrame * 10) {
  110. this.accumulatedTime = 0;
  111. }
  112. if (this.accumulatedTime === 0) {
  113. // no need to cancel animationID here; shouldn't have any in flight
  114. this.animationID = null;
  115. this.startAnimationIfNecessary();
  116. return;
  117. }
  118. let currentFrameCompletion =
  119. (this.accumulatedTime - Math.floor(this.accumulatedTime / msPerFrame) * msPerFrame) / msPerFrame;
  120. const framesToCatchUp = Math.floor(this.accumulatedTime / msPerFrame);
  121. let newLastIdealStyle: PlainStyle = {};
  122. let newLastIdealVelocity: Velocity = {};
  123. let newCurrentStyle: PlainStyle = {};
  124. let newCurrentVelocity: Velocity = {};
  125. for (let key in propsStyle) {
  126. if (!Object.prototype.hasOwnProperty.call(propsStyle, key)) {
  127. continue;
  128. }
  129. const styleValue = propsStyle[key];
  130. if (typeof styleValue === 'number') {
  131. newCurrentStyle[key] = styleValue;
  132. newCurrentVelocity[key] = 0;
  133. newLastIdealStyle[key] = styleValue;
  134. newLastIdealVelocity[key] = 0;
  135. } else {
  136. let newLastIdealStyleValue = this.state.lastIdealStyle[key];
  137. let newLastIdealVelocityValue = this.state.lastIdealVelocity[key];
  138. for (let i = 0; i < framesToCatchUp; i++) {
  139. [newLastIdealStyleValue, newLastIdealVelocityValue] = stepper(
  140. msPerFrame / 1000,
  141. newLastIdealStyleValue,
  142. newLastIdealVelocityValue,
  143. styleValue.val,
  144. styleValue.stiffness,
  145. styleValue.damping,
  146. styleValue.precision,
  147. );
  148. }
  149. const [nextIdealX, nextIdealV] = stepper(
  150. msPerFrame / 1000,
  151. newLastIdealStyleValue,
  152. newLastIdealVelocityValue,
  153. styleValue.val,
  154. styleValue.stiffness,
  155. styleValue.damping,
  156. styleValue.precision,
  157. );
  158. newCurrentStyle[key] =
  159. newLastIdealStyleValue +
  160. (nextIdealX - newLastIdealStyleValue) * currentFrameCompletion;
  161. newCurrentVelocity[key] =
  162. newLastIdealVelocityValue +
  163. (nextIdealV - newLastIdealVelocityValue) * currentFrameCompletion;
  164. newLastIdealStyle[key] = newLastIdealStyleValue;
  165. newLastIdealVelocity[key] = newLastIdealVelocityValue;
  166. }
  167. }
  168. this.animationID = null;
  169. // the amount we're looped over above
  170. this.accumulatedTime -= framesToCatchUp * msPerFrame;
  171. this.setState({
  172. currentStyle: newCurrentStyle,
  173. currentVelocity: newCurrentVelocity,
  174. lastIdealStyle: newLastIdealStyle,
  175. lastIdealVelocity: newLastIdealVelocity,
  176. });
  177. this.unreadPropStyle = null;
  178. this.startAnimationIfNecessary();
  179. });
  180. };
  181. componentDidMount() {
  182. this.prevTime = defaultNow();
  183. this.startAnimationIfNecessary();
  184. }
  185. componentWillReceiveProps(props: MotionProps) {
  186. if (this.unreadPropStyle != null) {
  187. // previous props haven't had the chance to be set yet; set them here
  188. this.clearUnreadPropStyle(this.unreadPropStyle);
  189. }
  190. this.unreadPropStyle = props.style;
  191. if (this.animationID == null) {
  192. this.prevTime = defaultNow();
  193. this.startAnimationIfNecessary();
  194. }
  195. }
  196. componentWillUnmount() {
  197. if (this.animationID != null) {
  198. defaultRaf.cancel(this.animationID);
  199. this.animationID = null;
  200. }
  201. }
  202. render(): ReactElement {
  203. const renderedChildren = this.props.children(this.state.currentStyle);
  204. return renderedChildren && React.Children.only(renderedChildren);
  205. }
  206. }