Animation.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347
  1. /* eslint-disable max-depth */
  2. /* eslint-disable eqeqeq */
  3. /* eslint-disable max-lines-per-function */
  4. import Event from './utils/Event';
  5. import shouldStopAnimation from './shouldStopAnimation';
  6. import shouldUseBezier from './shouldUseBezier';
  7. import stripStyle from './stripStyle';
  8. import stepper from './stepper';
  9. import mapToZero from './mapToZero';
  10. import wrapValue from './wrapValue';
  11. const now = () => Date.now();
  12. const msPerFrame = 1000 / 60;
  13. /**
  14. * @summary
  15. *
  16. * Lifecycle hook:
  17. * start, pause, resume, stop, frame, rest
  18. *
  19. * Binding method:
  20. * const animation = new Animation (); animation.on ('start | frame | rest ', () => {});
  21. */
  22. export default class Animation extends Event {
  23. _config: Record<string, any>;
  24. _props: Record<string, any>;
  25. _from: Record<string, any>;
  26. _to: Record<string, any>;
  27. _delay: number;
  28. _currentVelocity: Record<string, any>;
  29. _currentStyle: Record<string, any>;
  30. _lastIdealStyle: Record<string, any>;
  31. _lastIdealVelocity: Record<string, any>;
  32. _frameCount: number;
  33. _prevTime: number;
  34. _timer: any;
  35. _startedTime: number;
  36. _ended: boolean;
  37. _stopped: boolean;
  38. _wasAnimating: boolean;
  39. _started: boolean;
  40. _paused: boolean;
  41. _accumulatedTime: Record<string, any>;
  42. _pausedTime: number;
  43. _destroyed: boolean;
  44. constructor(props = {}, config = {}) {
  45. super();
  46. this._props = { ...props };
  47. this._config = { ...config };
  48. this.initStates();
  49. }
  50. _wrapConfig(object: { [x: string]: any }, config: { delay?: string }) {
  51. config = config && typeof config === 'object' ? config : this._config;
  52. const ret = {};
  53. for (const key of Object.keys(object)) {
  54. ret[key] = wrapValue(object[key], config);
  55. }
  56. return ret;
  57. }
  58. initStates(props?: Record<string, any>, config?: Record<string, any>) {
  59. props = props && typeof props === 'object' ? props : this._props;
  60. config = config && typeof config === 'object' ? config : this._config;
  61. const { from, to } = props;
  62. this._from = {};
  63. if (from && typeof from) {
  64. for (const key of Object.keys(from)) {
  65. this._from[key] = typeof from[key] === 'object' && from[key].val ? from[key].val : from[key];
  66. }
  67. }
  68. this._to = this._wrapConfig(to, config);
  69. this._delay = parseInt(config.delay) || 0;
  70. const currentStyle = (this._from && stripStyle(this._from)) || stripStyle(this._to);
  71. const currentVelocity = mapToZero(currentStyle);
  72. this._currentStyle = { ...currentStyle };
  73. this._currentVelocity = { ...currentVelocity };
  74. this._lastIdealStyle = { ...currentStyle };
  75. this._lastIdealVelocity = { ...currentVelocity };
  76. this.resetPlayStates();
  77. this._frameCount = 0;
  78. this._prevTime = 0;
  79. }
  80. animate() {
  81. if (this._timer != null) {
  82. return;
  83. }
  84. this._timer = requestAnimationFrame(timestamp => {
  85. const nowTime = now();
  86. // stop animation and emit onRest event
  87. if (
  88. shouldStopAnimation(
  89. this._currentStyle,
  90. this._to,
  91. this._currentVelocity,
  92. this._startedTime || nowTime,
  93. nowTime
  94. ) ||
  95. this._ended ||
  96. this._stopped
  97. ) {
  98. if (this._wasAnimating && !this._ended && !this._stopped) {
  99. // should emit reset in settimeout for delay msPerframe
  100. this._timer = setTimeout(() => {
  101. clearTimeout(this._timer);
  102. this._timer = null;
  103. this._ended = true;
  104. this.emit('rest', this.getCurrentStates());
  105. }, msPerFrame);
  106. }
  107. this.resetPlayStates();
  108. return;
  109. }
  110. if (!this._started) {
  111. this._started = true;
  112. this.emit('start', this.getCurrentStates());
  113. }
  114. this._stopped = false;
  115. this._paused = false;
  116. this._wasAnimating = true;
  117. if (this._startedTime === 0) {
  118. this._startedTime = nowTime;
  119. }
  120. const currentTime = nowTime;
  121. const timeDelta = currentTime - this._prevTime;
  122. this._prevTime = currentTime;
  123. if (currentTime - this._startedTime < this._delay) {
  124. this._timer = null;
  125. this.animate();
  126. }
  127. const newLastIdealStyle = {};
  128. const newLastIdealVelocity = {};
  129. const newCurrentStyle = {};
  130. const newCurrentVelocity = {};
  131. const toKeys = (this._to && Object.keys(this._to)) || [];
  132. for (const key of toKeys) {
  133. const styleValue = this._to[key];
  134. this._accumulatedTime[key] =
  135. typeof this._accumulatedTime[key] !== 'number' ? timeDelta : this._accumulatedTime[key] + timeDelta;
  136. const from =
  137. this._from[key] != null && typeof this._from[key] === 'object'
  138. ? this._from[key].val
  139. : this._from[key];
  140. const to = styleValue.val;
  141. if (typeof styleValue === 'number') {
  142. newCurrentStyle[key] = styleValue;
  143. newCurrentVelocity[key] = 0;
  144. newLastIdealStyle[key] = styleValue;
  145. newLastIdealVelocity[key] = 0;
  146. } else {
  147. let newLastIdealStyleValue = this._lastIdealStyle[key];
  148. let newLastIdealVelocityValue = this._lastIdealVelocity[key];
  149. if (shouldUseBezier(this._config) || shouldUseBezier(styleValue)) {
  150. // easing
  151. const { easing, duration } = styleValue;
  152. newLastIdealStyleValue =
  153. from + easing((currentTime - this._startedTime) / duration) * (to - from);
  154. if (currentTime >= this._startedTime + duration) {
  155. newLastIdealStyleValue = to;
  156. styleValue.done = true;
  157. }
  158. newLastIdealStyle[key] = newLastIdealStyleValue;
  159. newCurrentStyle[key] = newLastIdealStyleValue;
  160. } else if (to != null && to === this._currentStyle[key]) {
  161. newCurrentStyle[key] = to;
  162. newCurrentVelocity[key] = 0;
  163. newLastIdealStyle[key] = to;
  164. newLastIdealVelocity[key] = 0;
  165. } else {
  166. // spring
  167. const currentFrameCompletion =
  168. (this._accumulatedTime[key] -
  169. Math.floor(this._accumulatedTime[key] / msPerFrame) * msPerFrame) /
  170. msPerFrame;
  171. const framesToCatchUp = Math.floor(this._accumulatedTime[key] / msPerFrame);
  172. for (let i = 0; i < framesToCatchUp; i++) {
  173. [newLastIdealStyleValue, newLastIdealVelocityValue] = stepper(
  174. msPerFrame / 1000,
  175. newLastIdealStyleValue,
  176. newLastIdealVelocityValue,
  177. styleValue.val,
  178. styleValue.tension,
  179. styleValue.friction,
  180. styleValue.precision
  181. );
  182. }
  183. const [nextIdealX, nextIdealV] = stepper(
  184. msPerFrame / 1000,
  185. newLastIdealStyleValue,
  186. newLastIdealVelocityValue,
  187. styleValue.val,
  188. styleValue.tension,
  189. styleValue.friction,
  190. styleValue.precision
  191. );
  192. newCurrentStyle[key] =
  193. newLastIdealStyleValue + (nextIdealX - newLastIdealStyleValue) * currentFrameCompletion;
  194. newCurrentVelocity[key] =
  195. newLastIdealVelocityValue +
  196. (nextIdealV - newLastIdealVelocityValue) * currentFrameCompletion;
  197. newLastIdealStyle[key] = newLastIdealStyleValue;
  198. newLastIdealVelocity[key] = newLastIdealVelocityValue;
  199. this._accumulatedTime[key] -= framesToCatchUp * msPerFrame;
  200. }
  201. }
  202. }
  203. this._timer = null;
  204. this._currentStyle = { ...newCurrentStyle };
  205. this._currentVelocity = { ...newCurrentVelocity };
  206. this._lastIdealStyle = { ...newLastIdealStyle };
  207. this._lastIdealVelocity = { ...newLastIdealVelocity };
  208. // console.log(newCurrentStyle);
  209. if (!this._destroyed) {
  210. this.emit('frame', this.getCurrentStates());
  211. this.animate();
  212. }
  213. });
  214. }
  215. start() {
  216. this._prevTime = now();
  217. this._startedTime = now();
  218. this.animate();
  219. }
  220. end() {
  221. if (!this._ended) {
  222. this._ended = true;
  223. this._currentStyle = this.getFinalStates();
  224. this.emit('frame', this.getFinalStates());
  225. this.emit('rest', this.getFinalStates());
  226. }
  227. this.destroy();
  228. }
  229. pause() {
  230. if (!this._paused) {
  231. this._pausedTime = now();
  232. this._paused = true;
  233. this.emit('pause', this.getCurrentStates());
  234. this.destroy();
  235. this._destroyed = false;
  236. }
  237. }
  238. resume() {
  239. if (this._started && this._paused) {
  240. const nowTime = now();
  241. const pausedDuration = nowTime - this._pausedTime;
  242. this._paused = false;
  243. // should add with pausedDuration
  244. this._startedTime += pausedDuration;
  245. this._prevTime += pausedDuration;
  246. this._pausedTime = 0;
  247. this.emit('resume', this.getCurrentStates());
  248. this.animate();
  249. }
  250. }
  251. stop() {
  252. this.destroy();
  253. if (!this._stopped) {
  254. this._stopped = true;
  255. // this.emit('frame', this.getInitialStates());
  256. this.emit('stop', this.getInitialStates());
  257. this.initStates();
  258. }
  259. }
  260. destroy() {
  261. cancelAnimationFrame(this._timer);
  262. clearTimeout(this._timer);
  263. this._timer = null;
  264. this._destroyed = true;
  265. }
  266. resetPlayStates() {
  267. this._started = false;
  268. this._stopped = false;
  269. this._ended = false;
  270. this._paused = false;
  271. this._destroyed = false;
  272. this._timer = null;
  273. this._wasAnimating = false;
  274. this._accumulatedTime = {};
  275. this._startedTime = 0;
  276. this._pausedTime = 0;
  277. }
  278. reset() {
  279. this.destroy();
  280. this.initStates();
  281. }
  282. reverse() {
  283. this.destroy();
  284. const props = { ...this._props };
  285. const [from, to] = [props.to, props.from];
  286. props.from = from;
  287. props.to = to;
  288. this._props = { ...props };
  289. this.initStates();
  290. }
  291. getCurrentStates() {
  292. return { ...this._currentStyle };
  293. }
  294. getInitialStates() {
  295. return { ...stripStyle(this._props.from) };
  296. }
  297. getFinalStates() {
  298. return { ...stripStyle(this._props.to) };
  299. }
  300. }