Source: animation.js

  1. /*!
  2. * Module Animation
  3. */
  4. /**
  5. * @namespace Animation
  6. */
  7. const MODULE_NAME = 'Animation';
  8. //###[ IMPORTS ]########################################################################################################
  9. import {hasValue, isPlainObject, isEmpty, isNaN, isElement, orDefault, assert, Deferred} from './basic.js';
  10. import {warn} from './logging.js';
  11. import {pschedule, countermand, waitForRepaint} from './timers.js';
  12. import {applyStyles} from './css.js';
  13. //###[ DATA ]###########################################################################################################
  14. const RUNNING_TRANSITIONS = new WeakMap();
  15. //###[ EXPORTS ]########################################################################################################
  16. /**
  17. * @namespace Animation:EasingFunctions
  18. */
  19. /**
  20. * A collection of all basic easing functions to be used in animations.
  21. * All functions here take a float parameter between 0 and 1 and return a mapped value between 0 and 1.
  22. *
  23. * Taken from: https://gist.github.com/gre/1650294
  24. *
  25. * Available functions:
  26. * - linear
  27. * - easeInQuad
  28. * - easeOutQuad
  29. * - easeInOutQuad
  30. * - easeInCubic
  31. * - easeOutCubic
  32. * - easeInOutCubic
  33. * - easeInQuart
  34. * - easeOutQuart
  35. * - easeInOutQuart
  36. * - easeInQuint
  37. * - easeOutQuint
  38. * - easeInOutQuint
  39. *
  40. * @memberof Animation:EasingFunctions
  41. * @alias EasingFunctions
  42. * @example
  43. * scrollTo(window, 1000, 0, EasingFunctions.easeInOutQuint);
  44. */
  45. export const EasingFunctions = {
  46. // no easing, no acceleration
  47. linear : t => t,
  48. // accelerating from zero velocity
  49. easeInQuad : t => t*t,
  50. // decelerating to zero velocity
  51. easeOutQuad : t => t*(2-t),
  52. // acceleration until halfway, then deceleration
  53. easeInOutQuad : t => t<.5 ? 2*t*t : -1+(4-2*t)*t,
  54. // accelerating from zero velocity
  55. easeInCubic : t => t*t*t,
  56. // decelerating to zero velocity
  57. easeOutCubic : t => (--t)*t*t+1,
  58. // acceleration until halfway, then deceleration
  59. easeInOutCubic : t => t<.5 ? 4*t*t*t : (t-1)*(2*t-2)*(2*t-2)+1,
  60. // accelerating from zero velocity
  61. easeInQuart : t => t*t*t*t,
  62. // decelerating to zero velocity
  63. easeOutQuart : t => 1-(--t)*t*t*t,
  64. // acceleration until halfway, then deceleration
  65. easeInOutQuart : t => t<.5 ? 8*t*t*t*t : 1-8*(--t)*t*t*t,
  66. // accelerating from zero velocity
  67. easeInQuint : t => t*t*t*t*t,
  68. // decelerating to zero velocity
  69. easeOutQuint : t => 1+(--t)*t*t*t*t,
  70. // acceleration until halfway, then deceleration
  71. easeInOutQuint : t => t<.5 ? 16*t*t*t*t*t : 1+16*(--t)*t*t*t*t
  72. };
  73. /**
  74. * @namespace Animation:transition
  75. */
  76. /**
  77. * This method offers the possibility to apply CSS transitions via classes and/or styles and wait for the transition
  78. * to finish, which results in the resolution of a Deferred.
  79. *
  80. * In general, this method remedies the pain of having to manage transitions manually in JS, entering precise ms for
  81. * timers waiting on conclusion of transitions.
  82. *
  83. * The general principle of this is the parsing of transition CSS attributes, which may contain transition
  84. * timings (transition and transition-duration) and looks for the longest currently running transition.
  85. * Values are excepted as milliseconds or seconds (int or float notation).
  86. *
  87. * Why would you do this, if there is something like the `animationend` event, you ask? Well, the problem is, that,
  88. * if the animation is interrupted or never finishes for any other reason, the event never fires. For that, there is
  89. * the `animationcancel` event, but that is not really robustly supported at the moment. So, in cases of complex
  90. * style changes, where we definitively want to have a callback when the animation has been (or would have been)
  91. * finished, this is still the safer option. But, for simple and small cases I'd strongly recommend using the native
  92. * `AnimationEvent` API.
  93. *
  94. * Calling this method successively on the same element replaces the currently running transition, normally
  95. * resulting in premature resolution of the Deferred and application of the newly provided changes.
  96. *
  97. * Be advised, that legacy browsers like IE11 and Edge <= 18 have problems connecting interrupted transitions,
  98. * especially when transition-durations change during animation, resulting in skipped or choppy animations. If you
  99. * experience this, try to keep timings stable during animation and chain animations without overlap.
  100. *
  101. * @param {Element} element - the element to transition, by applying class and/or style changes
  102. * @param {?Object} [classChanges=null] - plain object containing class changes to apply, add classes via the "add" key, remove them via the "remove" key (add has precedence over remove); values may be standard CSS class string notation or an array of standard CSS class notations
  103. * @param {?Object} [styleChanges=null] - plain object containing style changes to apply (via applyStyles)
  104. * @param {?Boolean} [rejectOnInterruption=false] - if a new transition is applied using this function while a previous transition is still running the Deferred would normally be resolved before continuing, set this to true to let the Deferred reject in that case (the rejection message is "interrupted", access the element using "element)
  105. * @return {Basic.Deferred} resolves on transition completion or repeated call on the same element, with the resolution value being the element, rejects on repeated call on same element if rejectOnInterruption is true (the rejection message is "interrupted", access the element using "element")
  106. *
  107. * @memberof Animation:transition
  108. * @alias transition
  109. * @see applyStyles
  110. * @see https://developer.mozilla.org/en-US/docs/Web/API/Element/animationend_event
  111. * @see https://developer.mozilla.org/en-US/docs/Web/API/Element/animationcancel_event
  112. * @example
  113. * transition(element, {add : 'foobar'}).then(element => { return transition(element, {remove : 'foobar'}); }).then(() => { console.log('finished'); });
  114. * transition(element, null, {top : 0, left : 0, background : 'pink', transition : 'all 1500ms'}).then(() => { console.log('finished'); });
  115. * transition(element, {add : 'foobar'}).then(() => { console.log('finished'); }).catch(error => { console.log('cancelled'); });
  116. */
  117. export function transition(element, classChanges=null, styleChanges=null, rejectOnInterruption=false){
  118. const __methodName__ = 'cssTransition';
  119. classChanges = orDefault(classChanges, {});
  120. styleChanges = orDefault(styleChanges, {});
  121. rejectOnInterruption = orDefault(rejectOnInterruption, false, 'bool');
  122. assert(isElement(element), `${MODULE_NAME}:${__methodName__} | element is not usable`);
  123. assert(isPlainObject(classChanges), `${MODULE_NAME}:${__methodName__} | classChanges is not a plain object`);
  124. assert(isPlainObject(styleChanges), `${MODULE_NAME}:${__methodName__} | styleChanges is not a plain object`);
  125. const
  126. deferred = new Deferred(),
  127. runningTransition = RUNNING_TRANSITIONS.get(element)
  128. ;
  129. if( hasValue(runningTransition) ){
  130. countermand(runningTransition.timer);
  131. if( !rejectOnInterruption ){
  132. runningTransition.deferred.resolve(element);
  133. } else {
  134. const error = new Error('interrupted');
  135. error.element = element;
  136. runningTransition.deferred.reject(error);
  137. }
  138. }
  139. RUNNING_TRANSITIONS.delete(element);
  140. const
  141. transitionDurationProperties = [
  142. 'transition-duration',
  143. '-webkit-transition-duration',
  144. '-moz-transition-duration',
  145. '-o-transition-duration'
  146. ],
  147. transitionProperties = [
  148. 'transition',
  149. '-webkit-transition',
  150. '-moz-transition',
  151. '-o-transition'
  152. ],
  153. timingProperties = [
  154. ...transitionDurationProperties,
  155. ...transitionProperties
  156. ],
  157. transitionDefinition = {
  158. property : null,
  159. value : null
  160. }
  161. ;
  162. if( !isEmpty(styleChanges) ){
  163. let vendorPropertiesAdded;
  164. [transitionDurationProperties, transitionProperties].forEach(properties => {
  165. vendorPropertiesAdded = false;
  166. properties.forEach(property => {
  167. const transitionValue = styleChanges[property];
  168. if( !vendorPropertiesAdded && hasValue(transitionValue) ){
  169. vendorPropertiesAdded = true;
  170. properties.forEach(property => {
  171. styleChanges[property] = transitionValue;
  172. });
  173. }
  174. });
  175. });
  176. applyStyles(element, styleChanges);
  177. }
  178. if( !isEmpty(classChanges?.remove) ){
  179. [].concat(classChanges.remove).forEach(removeClass => {
  180. `${removeClass}`.split(' ').forEach(removeClass => {
  181. element.classList.remove(removeClass.trim());
  182. });
  183. });
  184. }
  185. if( !isEmpty(classChanges?.add) ){
  186. [].concat(classChanges.add).forEach(addClass => {
  187. `${addClass}`.split(' ').forEach(addClass => {
  188. element.classList.add(addClass.trim());
  189. });
  190. });
  191. }
  192. waitForRepaint(() => {
  193. const elementStyles = getComputedStyle(element);
  194. timingProperties.forEach(timingProperty => {
  195. if( !hasValue(transitionDefinition.value) && hasValue(elementStyles[timingProperty]) ){
  196. transitionDefinition.property = timingProperty;
  197. transitionDefinition.value = elementStyles[timingProperty];
  198. }
  199. });
  200. if( !hasValue(transitionDefinition.value) ){
  201. warn(`${MODULE_NAME}:${__methodName__} | no usable transitions on element "${element}"`);
  202. deferred.resolve(element);
  203. } else {
  204. const
  205. sTimings = transitionDefinition.value.match(/(^|\s)(\d+(\.\d+)?)s(\s|,|$)/g),
  206. msTimings = transitionDefinition.value.match(/(^|\s)(\d+)ms(\s|,|$)/g)
  207. ;
  208. let longestTiming = 0;
  209. (sTimings ?? []).forEach(timing => {
  210. timing = parseFloat(timing);
  211. if( !isNaN(timing) ){
  212. timing = Math.floor(timing * 1000);
  213. if( timing > longestTiming ){
  214. longestTiming = timing;
  215. }
  216. }
  217. });
  218. (msTimings ?? []).forEach(timing => {
  219. timing = parseInt(timing, 10);
  220. if( !isNaN(timing) && (timing > longestTiming) ){
  221. longestTiming = timing;
  222. }
  223. });
  224. RUNNING_TRANSITIONS.set(element, {
  225. deferred,
  226. timer : pschedule(longestTiming, () => {
  227. waitForRepaint(() => {
  228. deferred.resolve(element);
  229. RUNNING_TRANSITIONS.delete(element);
  230. });
  231. })
  232. });
  233. }
  234. });
  235. return deferred;
  236. }