Animator.ts

  1. import {
  2. THRESHOLD,
  3. ALTERNATE, ALTERNATE_REVERSE, REVERSE, INFINITE, NORMAL,
  4. ITERATION_COUNT, DELAY, FILL_MODE, DIRECTION, PLAY_SPEED,
  5. DURATION, EASING, ITERATION_TIME, EASING_NAME, PAUSED,
  6. RUNNING, PLAY, TIMEUPDATE, ENDED, PLAY_STATE, PREV_TIME, TICK_TIME, CURRENT_TIME, ITERATION, OPTIONS, EASINGS
  7. } from "./consts";
  8. import EventTrigger from "./EventTrigger";
  9. import { bezier, steps } from "./easing";
  10. import { toFixed, getEasing } from "./utils";
  11. import { splitUnit, isString, camelize, requestAnimationFrame, isArray, cancelAnimationFrame } from "@daybrush/utils";
  12. import {
  13. IterationCountType, DirectionType, AnimatorState,
  14. EasingFunction, FillModeType, PlayStateType, EasingType, AnimatorOptions,
  15. } from "./types";
  16. import { toPropertyObject } from "./utils/property";
  17. function GetterSetter<T extends new (...args: any[]) => {}>(
  18. getter: string[], setter: string[], parent: string) {
  19. return (constructor: T) => {
  20. const prototype = constructor.prototype;
  21. getter.forEach(name => {
  22. prototype[camelize(`get ${name}`)] = function() {
  23. return this[parent][name];
  24. };
  25. });
  26. setter.forEach(name => {
  27. prototype[camelize(`set ${name}`)] = function(value: any) {
  28. this[parent][name] = value;
  29. return this;
  30. };
  31. });
  32. };
  33. }
  34. export function isDirectionReverse(iteration: number, iteraiontCount: IterationCountType, direction: DirectionType) {
  35. if (direction === REVERSE) {
  36. return true;
  37. } else if (iteraiontCount !== INFINITE && iteration === iteraiontCount && iteraiontCount % 1 === 0) {
  38. return direction === (iteration % 2 >= 1 ? ALTERNATE_REVERSE : ALTERNATE);
  39. }
  40. return direction === (iteration % 2 >= 1 ? ALTERNATE : ALTERNATE_REVERSE);
  41. }
  42. /**
  43. * @typedef {Object} AnimatorState The Animator options. Properties used in css animation.
  44. * @property {number} [duration] The duration property defines how long an animation should take to complete one cycle.
  45. * @property {"none"|"forwards"|"backwards"|"both"} [fillMode] The fillMode property specifies a style for the element when the animation is not playing (before it starts, after it ends, or both).
  46. * @property {"infinite"|number} [iterationCount] The iterationCount property specifies the number of times an animation should be played.
  47. * @property {array|function} [easing] The easing(timing-function) specifies the speed curve of an animation.
  48. * @property {number} [delay] The delay property specifies a delay for the start of an animation.
  49. * @property {"normal"|"reverse"|"alternate"|"alternate-reverse"} [direction] The direction property defines whether an animation should be played forwards, backwards or in alternate cycles.
  50. */
  51. const setters = ["id", ITERATION_COUNT, DELAY, FILL_MODE,
  52. DIRECTION, PLAY_SPEED, DURATION, PLAY_SPEED, ITERATION_TIME, PLAY_STATE];
  53. const getters = [...setters, EASING, EASING_NAME];
  54. /**
  55. * play video, animation, the others
  56. * @extends EventTrigger
  57. * @see {@link https://www.w3schools.com/css/css3_animations.asp|CSS3 Animation}
  58. */
  59. @GetterSetter(getters, setters, "state")
  60. class Animator
  61. <T extends AnimatorOptions = AnimatorOptions, U extends AnimatorState = AnimatorState> extends EventTrigger {
  62. public state: U;
  63. private timerId: number = 0;
  64. /**
  65. * @param - animator's options
  66. * @example
  67. const animator = new Animator({
  68. delay: 2,
  69. diretion: "alternate",
  70. duration: 2,
  71. fillMode: "forwards",
  72. iterationCount: 3,
  73. easing: Scene.easing.EASE,
  74. });
  75. */
  76. constructor(options?: Partial<T & AnimatorOptions>) {
  77. super();
  78. this.state = {
  79. id: "",
  80. easing: 0,
  81. easingName: "linear",
  82. iterationCount: 1,
  83. delay: 0,
  84. fillMode: "forwards",
  85. direction: NORMAL,
  86. playSpeed: 1,
  87. currentTime: 0,
  88. iterationTime: -1,
  89. iteration: 0,
  90. tickTime: 0,
  91. prevTime: 0,
  92. playState: PAUSED,
  93. duration: 0,
  94. } as U;
  95. this.setOptions(options);
  96. }
  97. /**
  98. * set animator's easing.
  99. * @param curverArray - The speed curve of an animation.
  100. * @return {Animator} An instance itself.
  101. * @example
  102. animator.({
  103. delay: 2,
  104. diretion: "alternate",
  105. duration: 2,
  106. fillMode: "forwards",
  107. iterationCount: 3,
  108. easing: Scene.easing.EASE,
  109. });
  110. */
  111. public setEasing(curveArray: string | number[] | EasingFunction): this {
  112. const easing: EasingType = getEasing(curveArray);
  113. const easingName = easing && easing[EASING_NAME] || "linear";
  114. const state = this.state;
  115. state[EASING] = easing;
  116. state[EASING_NAME] = easingName;
  117. return this;
  118. }
  119. /**
  120. * set animator's options.
  121. * @see {@link https://www.w3schools.com/css/css3_animations.asp|CSS3 Animation}
  122. * @param - animator's options
  123. * @return {Animator} An instance itself.
  124. * @example
  125. animator.({
  126. delay: 2,
  127. diretion: "alternate",
  128. duration: 2,
  129. fillMode: "forwards",
  130. iterationCount: 3,
  131. easing: Scene.eaasing.EASE,
  132. });
  133. */
  134. public setOptions(options: Partial<AnimatorOptions> = {}): this {
  135. for (const name in options) {
  136. const value = options[name];
  137. if (name === EASING) {
  138. this.setEasing(value);
  139. continue;
  140. } else if (name === DURATION) {
  141. value && this.setDuration(value);
  142. continue;
  143. }
  144. if (OPTIONS.indexOf(name as any) > -1) {
  145. this.state[name] = value;
  146. }
  147. }
  148. return this;
  149. }
  150. /**
  151. * Get the animator's total duration including delay
  152. * @return {number} Total duration
  153. * @example
  154. animator.getTotalDuration();
  155. */
  156. public getTotalDuration(): number {
  157. return this.getActiveDuration(true);
  158. }
  159. /**
  160. * Get the animator's total duration excluding delay
  161. * @return {number} Total duration excluding delay
  162. * @example
  163. animator.getActiveDuration();
  164. */
  165. public getActiveDuration(delay?: boolean): number {
  166. const state = this.state;
  167. const count = state[ITERATION_COUNT];
  168. if (count === INFINITE) {
  169. return Infinity;
  170. }
  171. return (delay ? state[DELAY] : 0) + this.getDuration() * count;
  172. }
  173. /**
  174. * Check if the animator has reached the end.
  175. * @return {boolean} ended
  176. * @example
  177. animator.isEnded(); // true or false
  178. */
  179. public isEnded(): boolean {
  180. if (this.state[TICK_TIME] === 0 && this.state[PLAY_STATE] === PAUSED) {
  181. return true;
  182. } else if (this.getTime() < this.getActiveDuration()) {
  183. return false;
  184. }
  185. return true;
  186. }
  187. /**
  188. *Check if the animator is paused:
  189. * @return {boolean} paused
  190. * @example
  191. animator.isPaused(); // true or false
  192. */
  193. public isPaused(): boolean {
  194. return this.state[PLAY_STATE] === PAUSED;
  195. }
  196. public start(delay: number = this.state[DELAY]): boolean {
  197. const state = this.state;
  198. state[PLAY_STATE] = RUNNING;
  199. if (state[TICK_TIME] >= delay) {
  200. /**
  201. * This event is fired when play animator.
  202. * @event Animator#play
  203. */
  204. this.trigger(PLAY);
  205. return true;
  206. }
  207. return false;
  208. }
  209. /**
  210. * play animator
  211. * @return {Animator} An instance itself.
  212. */
  213. public play(toTime?: number) {
  214. const state = this.state;
  215. const delay = state[DELAY];
  216. const currentTime = this.getTime();
  217. state[PLAY_STATE] = RUNNING;
  218. if (this.isEnded() && (currentTime === 0 || currentTime >= this.getActiveDuration())) {
  219. this.setTime(-delay, true);
  220. }
  221. this.timerId = requestAnimationFrame((time: number) => {
  222. state[PREV_TIME] = time;
  223. this.tick(time, toTime);
  224. });
  225. this.start();
  226. return this;
  227. }
  228. /**
  229. * pause animator
  230. * @return {Animator} An instance itself.
  231. */
  232. public pause(): this {
  233. const state = this.state;
  234. if (state[PLAY_STATE] !== PAUSED) {
  235. state[PLAY_STATE] = PAUSED;
  236. /**
  237. * This event is fired when animator is paused.
  238. * @event Animator#paused
  239. */
  240. this.trigger(PAUSED);
  241. }
  242. cancelAnimationFrame(this.timerId);
  243. return this;
  244. }
  245. /**
  246. * end animator
  247. * @return {Animator} An instance itself.
  248. */
  249. public finish() {
  250. this.setTime(0);
  251. this.state[TICK_TIME] = 0;
  252. this.end();
  253. return this;
  254. }
  255. /**
  256. * end animator
  257. * @return {Animator} An instance itself.
  258. */
  259. public end() {
  260. this.pause();
  261. /**
  262. * This event is fired when animator is ended.
  263. * @event Animator#ended
  264. */
  265. this.trigger(ENDED);
  266. return this;
  267. }
  268. /**
  269. * set currentTime
  270. * @param {Number|String} time - currentTime
  271. * @return {Animator} An instance itself.
  272. * @example
  273. animator.setTime("from"); // 0
  274. animator.setTime("to"); // 100%
  275. animator.setTime("50%");
  276. animator.setTime(10);
  277. animator.getTime() // 10
  278. */
  279. public setTime(time: number | string, isTick?: boolean, isParent?: boolean) {
  280. const activeDuration = this.getActiveDuration();
  281. const state = this.state;
  282. const prevTime = state[TICK_TIME];
  283. const delay = state[DELAY];
  284. let currentTime = isTick ? (time as number) : this.getUnitTime(time);
  285. state[TICK_TIME] = delay + currentTime;
  286. if (currentTime < 0) {
  287. currentTime = 0;
  288. } else if (currentTime > activeDuration) {
  289. currentTime = activeDuration;
  290. }
  291. state[CURRENT_TIME] = currentTime;
  292. this.calculate();
  293. if (isTick && !isParent) {
  294. const tickTime = state[TICK_TIME];
  295. if (prevTime < delay && time >= 0) {
  296. this.start(0);
  297. }
  298. if (tickTime < prevTime || this.isEnded()) {
  299. this.end();
  300. return;
  301. }
  302. }
  303. if (this.isDelay()) {
  304. return this;
  305. }
  306. /**
  307. * This event is fired when the animator updates the time.
  308. * @event Animator#timeupdate
  309. * @param {Object} param The object of data to be sent to an event.
  310. * @param {Number} param.currentTime The total time that the animator is running.
  311. * @param {Number} param.time The iteration time during duration that the animator is running.
  312. * @param {Number} param.iterationCount The iteration count that the animator is running.
  313. */
  314. this.trigger(TIMEUPDATE, {
  315. currentTime,
  316. time: this.getIterationTime(),
  317. iterationCount: state[ITERATION],
  318. });
  319. return this;
  320. }
  321. /**
  322. * Get the animator's current time
  323. * @return {number} current time
  324. * @example
  325. animator.getTime();
  326. */
  327. public getTime(): number {
  328. return this.state[CURRENT_TIME];
  329. }
  330. public getUnitTime(time: string | number) {
  331. if (isString(time)) {
  332. const duration = this.getDuration() || 100;
  333. if (time === "from") {
  334. return 0;
  335. } else if (time === "to") {
  336. return duration;
  337. }
  338. const { unit, value } = splitUnit(time);
  339. if (unit === "%") {
  340. !this.getDuration() && (this.setDuration(duration));
  341. return toFixed(parseFloat(time) / 100 * duration);
  342. } else if (unit === ">") {
  343. return value + THRESHOLD;
  344. } else {
  345. return value;
  346. }
  347. } else {
  348. return toFixed(time);
  349. }
  350. }
  351. /**
  352. * Check if the current state of animator is delayed.
  353. * @return {boolean} check delay state
  354. */
  355. public isDelay() {
  356. const state = this.state;
  357. const delay = state[DELAY];
  358. const tickTime = state[TICK_TIME];
  359. return delay > 0 && (tickTime < delay);
  360. }
  361. public setIteration(iterationCount: number): this {
  362. const state = this.state;
  363. const passIterationCount = Math.floor(iterationCount);
  364. const maxIterationCount = state[ITERATION_COUNT] === INFINITE ? Infinity : state[ITERATION_COUNT];
  365. if (state[ITERATION] < passIterationCount && passIterationCount < maxIterationCount) {
  366. /**
  367. * The event is fired when an iteration of an animation ends.
  368. * @event Animator#iteration
  369. * @param {Object} param The object of data to be sent to an event.
  370. * @param {Number} param.currentTime The total time that the animator is running.
  371. * @param {Number} param.iterationCount The iteration count that the animator is running.
  372. */
  373. this.trigger("iteration", {
  374. currentTime: state[CURRENT_TIME],
  375. iterationCount: passIterationCount,
  376. });
  377. }
  378. state[ITERATION] = iterationCount;
  379. return this;
  380. }
  381. protected calculate() {
  382. const state = this.state;
  383. const iterationCount = state[ITERATION_COUNT];
  384. const fillMode = state[FILL_MODE];
  385. const direction = state[DIRECTION];
  386. const duration = this.getDuration();
  387. const time = this.getTime();
  388. const iteration = duration === 0 ? 0 : time / duration;
  389. let currentIterationTime = duration ? time % duration : 0;
  390. if (!duration) {
  391. this.setIterationTime(0);
  392. return this;
  393. }
  394. this.setIteration(iteration);
  395. // direction : normal, reverse, alternate, alternate-reverse
  396. // fillMode : forwards, backwards, both, none
  397. const isReverse = isDirectionReverse(iteration, iterationCount, direction);
  398. const isFiniteDuration = isFinite(duration);
  399. if (isFiniteDuration && isReverse) {
  400. currentIterationTime = duration - currentIterationTime;
  401. }
  402. if (isFiniteDuration && iterationCount !== INFINITE) {
  403. const isForwards = fillMode === "both" || fillMode === "forwards";
  404. // fill forwards
  405. if (iteration >= iterationCount) {
  406. currentIterationTime = duration * (isForwards ? (iterationCount % 1) || 1 : 0);
  407. isReverse && (currentIterationTime = duration - currentIterationTime);
  408. }
  409. }
  410. this.setIterationTime(currentIterationTime);
  411. return this;
  412. }
  413. private tick(now: number, to?: number) {
  414. if (this.isPaused()) {
  415. return;
  416. }
  417. const state = this.state;
  418. const playSpeed = state[PLAY_SPEED];
  419. const prevTime = state[PREV_TIME];
  420. const delay = state[DELAY];
  421. const tickTime = state[TICK_TIME];
  422. const currentTime = tickTime + Math.min(1000, now - prevTime) / 1000 * playSpeed;
  423. state[PREV_TIME] = now;
  424. this.setTime(currentTime - delay, true);
  425. if (to && to * 1000 < now) {
  426. this.pause();
  427. }
  428. if (state[PLAY_STATE] === PAUSED) {
  429. return;
  430. }
  431. this.timerId = requestAnimationFrame((time: number) => {
  432. this.tick(time, to);
  433. });
  434. }
  435. }
  436. /**
  437. * Specifies the unique indicator of the animator
  438. * @method Animator#setId
  439. * @param {number | string} - String or number of id to be set in the animator
  440. * @return {Animator} An instance itself.
  441. */
  442. /**
  443. * Specifies the unique indicator of the animator
  444. * @method Animator#getId
  445. * @return {number | string} the indicator of the item.
  446. */
  447. /**
  448. * Get a delay for the start of an animation.
  449. * @method Animator#getDelay
  450. * @return {number} delay
  451. */
  452. /**
  453. * Set a delay for the start of an animation.
  454. * @method Animator#setDelay
  455. * @param {number} delay - delay
  456. * @return {Animator} An instance itself.
  457. */
  458. /**
  459. * Get fill mode for the item when the animation is not playing (before it starts, after it ends, or both)
  460. * @method Animator#getFillMode
  461. * @return {FillModeType} fillMode
  462. */
  463. /**
  464. * Set fill mode for the item when the animation is not playing (before it starts, after it ends, or both)
  465. * @method Animator#setFillMode
  466. * @param {FillModeType} fillMode - fillMode
  467. * @return {Animator} An instance itself.
  468. */
  469. /**
  470. * Get the number of times an animation should be played.
  471. * @method Animator#getIterationCount
  472. * @return {IterationCountType} iterationCount
  473. */
  474. /**
  475. * Set the number of times an animation should be played.
  476. * @method Animator#setIterationCount
  477. * @param {IterationCountType} iterationCount - iterationCount
  478. * @return {Animator} An instance itself.
  479. */
  480. /**
  481. * Get whether an animation should be played forwards, backwards or in alternate cycles.
  482. * @method Animator#getDirection
  483. * @return {DirectionType} direction
  484. */
  485. /**
  486. * Set whether an animation should be played forwards, backwards or in alternate cycles.
  487. * @method Animator#setDirection
  488. * @param {DirectionType} direction - direction
  489. * @return {Animator} An instance itself.
  490. */
  491. /**
  492. * Get whether the animation is running or paused.
  493. * @method Animator#getPlayState
  494. * @return {PlayStateType} playState
  495. */
  496. /**
  497. * Set whether the animation is running or paused.
  498. * @method Animator#setPlayState
  499. * @param {PlayStateType} playState - playState
  500. * @return {Animator} An instance itself.
  501. */
  502. /**
  503. * Get the animator's play speed
  504. * @method Animator#getPlaySpeed
  505. * @return {number} playSpeed
  506. */
  507. /**
  508. * Set the animator's play speed
  509. * @method Animator#setPlaySpeed
  510. * @param {number} playSpeed - playSpeed
  511. * @return {Animator} An instance itself.
  512. */
  513. /**
  514. * Get how long an animation should take to complete one cycle.
  515. * @method Animator#getDuration
  516. * @return {number} duration
  517. */
  518. /**
  519. * Set how long an animation should take to complete one cycle.
  520. * @method Animator#setDuration
  521. * @param {number} duration - duration
  522. * @return {Animator} An instance itself.
  523. */
  524. /**
  525. * Get the speed curve of an animation.
  526. * @method Animator#getEasing
  527. * @return {EasingType} easing
  528. */
  529. /**
  530. * Get the speed curve's name
  531. * @method Animator#getEasingName
  532. * @return {string} the curve's name.
  533. */
  534. /**
  535. * Get the animator's current iteration time
  536. * @method Animator#getIterationTime
  537. * @return {number} current iteration time
  538. * @example
  539. animator.getIterationTime();
  540. */
  541. // tslint:disable-next-line:interface-name
  542. interface Animator<T extends AnimatorOptions = AnimatorOptions, U extends AnimatorState = AnimatorState> {
  543. setId(id: number | string): this;
  544. getId(): number | string;
  545. getIterationTime(): number;
  546. setIterationTime(time: number): this;
  547. setDelay(delay: number): this;
  548. getDelay(): number;
  549. setFillMode(fillMode: FillModeType): this;
  550. getFillMode(): FillModeType;
  551. setIterationCount(iterationCount: IterationCountType): this;
  552. getIterationCount(): IterationCountType;
  553. setDirection(direction: DirectionType): this;
  554. getDirection(): DirectionType;
  555. setPlayState(playState: PlayStateType): this;
  556. getPlayState(): PlayStateType;
  557. setPlaySpeed(playSpeed: number): this;
  558. getPlaySpeed(): number;
  559. setDuration(duration: number): this;
  560. getDuration(): number;
  561. getEasing(): EasingType;
  562. getEasingName(): string;
  563. }
  564. export default Animator;