ani.js 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. function each(object, callback) {
  2. Object.keys(object).forEach((key) => {
  3. callback(object[key], key);
  4. });
  5. }
  6. const EASINGS = {
  7. linear: (t) => t,
  8. inQuad: (t) => t * t,
  9. outQuad: (t) => t * (2 - t),
  10. inOutQuad: (t) => t < .5 ? 2 * t * t : -1 + (4 - 2 * t) * t,
  11. inCubic: (t) => t * t * t,
  12. outCubic: (t) => (--t) * t * t + 1,
  13. inOutCubic: (t) => t < .5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1,
  14. inQuart: (t) => t * t * t * t,
  15. outQuart: (t) => 1 - (--t) * t * t * t,
  16. inOutQuart: (t) => t < .5 ? 8 * t * t * t * t : 1 - 8 * (--t) * t * t * t,
  17. inQuint: (t) => t * t * t * t * t,
  18. outQuint: (t) => 1 + (--t) * t * t * t * t,
  19. inOutQuint: (t) => t < .5 ? 16 * t * t * t * t * t : 1 + 16 * (--t) * t * t * t * t,
  20. inSine: (t) => 1 - Math.cos(t * (Math.PI / 2)),
  21. outSine: (t) => Math.sin(t * (Math.PI / 2)),
  22. inOutSine: (t) => .5 - .5 * Math.cos(Math.PI * t),
  23. inExpo: (t) => Math.pow(2, 10 * (t - 1)),
  24. outExpo: (t) => 1 - Math.pow(2, -10 * t),
  25. inOutExpo: (t) => (t = t * 2 - 1) < 0 ? .5 * Math.pow(2, 10 * t) : 1 - .5 * Math.pow(2, -10 * t),
  26. inCirc: (t) => 1 - Math.sqrt(1 - t * t),
  27. outCirc: (t) => Math.sqrt(1 - (t - 1) * (t - 1)),
  28. inOutCirc: (t) => (t *= 2) < 1 ? .5 - .5 * Math.sqrt(1 - t * t) : .5 + .5 * Math.sqrt(1 - (t -= 2) * t)
  29. };
  30. /**
  31. * @summary Interpolation helper for animations
  32. * @memberOf PSV
  33. * @description
  34. * Implements the Promise API with an additional "cancel" and "finally" methods.
  35. * The promise is resolved when the animation is complete and rejected if the animation is cancelled.
  36. * @example
  37. * new Animation({
  38. * properties: {
  39. * width: {start: 100, end: 200}
  40. * },
  41. * duration: 5000,
  42. * onTick: (properties) => element.style.width = `${properties.width}px`;
  43. * })
  44. */
  45. export default class Animation {
  46. constructor(options) {
  47. this.__cancelled = false;
  48. this.__resolved = false;
  49. this.__promise = new Promise((resolve, reject) => {
  50. this.__resolve = resolve;
  51. this.__reject = reject;
  52. });
  53. if (options) {
  54. if (!options.easing || typeof options.easing === 'string') {
  55. options.easing = EASINGS[options.easing || 'linear'];
  56. }
  57. this.__start = null;
  58. this.options = options;
  59. if (options.delay) {
  60. this.__delayTimeout = setTimeout(() => {
  61. this.__delayTimeout = null;
  62. window.requestAnimationFrame(t => this.__run(t));
  63. }, options.delay);
  64. } else {
  65. window.requestAnimationFrame(t => this.__run(t));
  66. }
  67. }
  68. }
  69. /**
  70. * @summary Main loop for the animation
  71. * @param {number} timestamp
  72. * @private
  73. */
  74. __run(timestamp) {
  75. // the animation has been cancelled
  76. if (this.__cancelled) {
  77. return;
  78. }
  79. // first iteration
  80. if (this.__start === null) {
  81. this.__start = timestamp;
  82. }
  83. // compute progress
  84. const progress = (timestamp - this.__start) / this.options.duration;
  85. const current = {};
  86. if (progress < 1.0) {
  87. // interpolate properties
  88. each(this.options.properties, (prop, name) => {
  89. if (prop) {
  90. current[name] = prop.start + (prop.end - prop.start) * this.options.easing(progress);
  91. }
  92. });
  93. this.options.onTick(current, progress);
  94. window.requestAnimationFrame(t => this.__run(t));
  95. } else {
  96. // call onTick one last time with final values
  97. each(this.options.properties, (prop, name) => {
  98. if (prop) {
  99. current[name] = prop.end;
  100. }
  101. });
  102. this.options.onTick(current, 1.0);
  103. window.requestAnimationFrame(() => {
  104. this.__resolved = true;
  105. this.__resolve();
  106. });
  107. }
  108. }
  109. /**
  110. * @summary Animation chaining
  111. * @param {Function} [onFulfilled] - Called when the animation is complete, can return a new animation
  112. * @param {Function} [onRejected] - Called when the animation is cancelled
  113. * @returns {PSV.Animation}
  114. */
  115. then(onFulfilled = null, onRejected = null) {
  116. const p = new Animation();
  117. // Allow cancellation to climb up the promise chain
  118. p.__promise.then(null, () => this.cancel());
  119. this.__promise.then(
  120. () => p.__resolve(onFulfilled ? onFulfilled() : undefined),
  121. () => p.__reject(onRejected ? onRejected() : undefined)
  122. );
  123. return p;
  124. }
  125. /**
  126. * @summary Alias to `.then(null, onRejected)`
  127. * @param {Function} onRejected - Called when the animation has been cancelled
  128. * @returns {PSV.Animation}
  129. */
  130. catch (onRejected) {
  131. return this.then(undefined, onRejected);
  132. }
  133. /**
  134. * @summary Alias to `.then(onFinally, onFinally)`
  135. * @param {Function} onFinally - Called when the animation is either complete or cancelled
  136. * @returns {PSV.Animation}
  137. */
  138. finally(onFinally) {
  139. return this.then(onFinally, onFinally);
  140. }
  141. /**
  142. * @summary Cancels the animation
  143. */
  144. cancel() {
  145. if (!this.__cancelled && !this.__resolved) {
  146. this.__cancelled = true;
  147. this.__reject();
  148. if (this.__delayTimeout) {
  149. window.cancelAnimationFrame(this.__delayTimeout);
  150. this.__delayTimeout = null;
  151. }
  152. }
  153. }
  154. /**
  155. * @summary Returns a resolved animation promise
  156. * @returns {PSV.Animation}
  157. */
  158. static resolve() {
  159. const p = Promise.resolve();
  160. p.cancel = () => {};
  161. p.finally = (onFinally) => {
  162. return p.then(onFinally, onFinally);
  163. };
  164. return p;
  165. }
  166. }