hero.js 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. const raf =
  2. (typeof window !== "undefined" && window.requestAnimationFrame) || setTimeout;
  3. const nextFrame = function (fn) {
  4. raf(function () {
  5. raf(fn);
  6. });
  7. };
  8. function setNextFrame(obj, prop, val) {
  9. nextFrame(function () {
  10. obj[prop] = val;
  11. });
  12. }
  13. function getTextNodeRect(textNode) {
  14. let rect;
  15. if (document.createRange) {
  16. const range = document.createRange();
  17. range.selectNodeContents(textNode);
  18. if (range.getBoundingClientRect) {
  19. rect = range.getBoundingClientRect();
  20. }
  21. }
  22. return rect;
  23. }
  24. function calcTransformOrigin(isTextNode, textRect, boundingRect) {
  25. if (isTextNode) {
  26. if (textRect) {
  27. // calculate pixels to center of text from left edge of bounding box
  28. const relativeCenterX =
  29. textRect.left + textRect.width / 2 - boundingRect.left;
  30. const relativeCenterY =
  31. textRect.top + textRect.height / 2 - boundingRect.top;
  32. return `${relativeCenterX}px ${relativeCenterY}px`;
  33. }
  34. }
  35. return "0 0"; // top left
  36. }
  37. function getTextDx(oldTextRect, newTextRect) {
  38. if (oldTextRect && newTextRect) {
  39. return (
  40. oldTextRect.left +
  41. oldTextRect.width / 2 -
  42. (newTextRect.left + newTextRect.width / 2)
  43. );
  44. }
  45. return 0;
  46. }
  47. function getTextDy(oldTextRect, newTextRect) {
  48. if (oldTextRect && newTextRect) {
  49. return (
  50. oldTextRect.top +
  51. oldTextRect.height / 2 -
  52. (newTextRect.top + newTextRect.height / 2)
  53. );
  54. }
  55. return 0;
  56. }
  57. function isTextElement(elm) {
  58. return elm.childNodes.length === 1 && elm.childNodes[0].nodeType === 3;
  59. }
  60. let removed, created;
  61. function pre() {
  62. removed = {};
  63. created = [];
  64. }
  65. function create(oldVnode, vnode) {
  66. const hero = vnode.data.hero;
  67. if (hero && hero.id) {
  68. created.push(hero.id);
  69. created.push(vnode);
  70. }
  71. }
  72. function destroy(vnode) {
  73. const hero = vnode.data.hero;
  74. if (hero && hero.id) {
  75. const elm = vnode.elm;
  76. vnode.isTextNode = isTextElement(elm); // is this a text node?
  77. vnode.boundingRect = elm.getBoundingClientRect(); // save the bounding rectangle to a new property on the vnode
  78. vnode.textRect = vnode.isTextNode
  79. ? getTextNodeRect(elm.childNodes[0])
  80. : null; // save bounding rect of inner text node
  81. const computedStyle = window.getComputedStyle(elm, undefined); // get current styles (includes inherited properties)
  82. vnode.savedStyle = JSON.parse(JSON.stringify(computedStyle)); // save a copy of computed style values
  83. removed[hero.id] = vnode;
  84. }
  85. }
  86. function post() {
  87. let i,
  88. id,
  89. newElm,
  90. oldVnode,
  91. oldElm,
  92. hRatio,
  93. wRatio,
  94. oldRect,
  95. newRect,
  96. dx,
  97. dy,
  98. origTransform,
  99. origTransition,
  100. newStyle,
  101. oldStyle,
  102. newComputedStyle,
  103. isTextNode,
  104. newTextRect,
  105. oldTextRect;
  106. for (i = 0; i < created.length; i += 2) {
  107. id = created[i];
  108. newElm = created[i + 1].elm;
  109. oldVnode = removed[id];
  110. if (oldVnode) {
  111. isTextNode = oldVnode.isTextNode && isTextElement(newElm); // Are old & new both text?
  112. newStyle = newElm.style;
  113. newComputedStyle = window.getComputedStyle(newElm, undefined); // get full computed style for new element
  114. oldElm = oldVnode.elm;
  115. oldStyle = oldElm.style;
  116. // Overall element bounding boxes
  117. newRect = newElm.getBoundingClientRect();
  118. oldRect = oldVnode.boundingRect; // previously saved bounding rect
  119. // Text node bounding boxes & distances
  120. if (isTextNode) {
  121. newTextRect = getTextNodeRect(newElm.childNodes[0]);
  122. oldTextRect = oldVnode.textRect;
  123. dx = getTextDx(oldTextRect, newTextRect);
  124. dy = getTextDy(oldTextRect, newTextRect);
  125. } else {
  126. // Calculate distances between old & new positions
  127. dx = oldRect.left - newRect.left;
  128. dy = oldRect.top - newRect.top;
  129. }
  130. hRatio = newRect.height / Math.max(oldRect.height, 1);
  131. wRatio = isTextNode ? hRatio : newRect.width / Math.max(oldRect.width, 1); // text scales based on hRatio
  132. // Animate new element
  133. origTransform = newStyle.transform;
  134. origTransition = newStyle.transition;
  135. if (newComputedStyle.display === "inline") {
  136. // inline elements cannot be transformed
  137. newStyle.display = "inline-block"; // this does not appear to have any negative side effects
  138. }
  139. newStyle.transition = origTransition + "transform 0s";
  140. newStyle.transformOrigin = calcTransformOrigin(
  141. isTextNode,
  142. newTextRect,
  143. newRect
  144. );
  145. newStyle.opacity = "0";
  146. newStyle.transform = `${origTransform}translate(${dx}px, ${dy}px) scale(${
  147. 1 / wRatio
  148. }, ${1 / hRatio})`;
  149. setNextFrame(newStyle, "transition", origTransition);
  150. setNextFrame(newStyle, "transform", origTransform);
  151. setNextFrame(newStyle, "opacity", "1");
  152. // Animate old element
  153. for (const key in oldVnode.savedStyle) {
  154. // re-apply saved inherited properties
  155. if (String(parseInt(key)) !== key) {
  156. const ms = key.substring(0, 2) === "ms";
  157. const moz = key.substring(0, 3) === "moz";
  158. const webkit = key.substring(0, 6) === "webkit";
  159. if (!ms && !moz && !webkit) {
  160. // ignore prefixed style properties
  161. oldStyle[key] = oldVnode.savedStyle[key];
  162. }
  163. }
  164. }
  165. oldStyle.position = "absolute";
  166. oldStyle.top = `${oldRect.top}px`; // start at existing position
  167. oldStyle.left = `${oldRect.left}px`;
  168. oldStyle.width = `${oldRect.width}px`; // Needed for elements who were sized relative to their parents
  169. oldStyle.height = `${oldRect.height}px`; // Needed for elements who were sized relative to their parents
  170. oldStyle.margin = "0"; // Margin on hero element leads to incorrect positioning
  171. oldStyle.transformOrigin = calcTransformOrigin(
  172. isTextNode,
  173. oldTextRect,
  174. oldRect
  175. );
  176. oldStyle.transform = "";
  177. oldStyle.opacity = "1";
  178. document.body.appendChild(oldElm);
  179. setNextFrame(
  180. oldStyle,
  181. "transform",
  182. `translate(${-dx}px, ${-dy}px) scale(${wRatio}, ${hRatio})`
  183. ); // scale must be on far right for translate to be correct
  184. setNextFrame(oldStyle, "opacity", "0");
  185. oldElm.addEventListener("transitionend", function (ev) {
  186. if (ev.propertyName === "transform") {
  187. document.body.removeChild(ev.target);
  188. }
  189. });
  190. }
  191. }
  192. removed = created = undefined;
  193. }
  194. export const heroModule = { pre, create, destroy, post };