morphing.js 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. import { convertToPath, Path, } from '@antv/g';
  2. import { copyAttributes } from '../utils/helper';
  3. import { attributeKeys, attributeOf, effectTiming } from './utils';
  4. function localBBoxOf(shape) {
  5. const { min, max } = shape.getLocalBounds();
  6. const [x0, y0] = min;
  7. const [x1, y1] = max;
  8. const height = y1 - y0;
  9. const width = x1 - x0;
  10. return [x0, y0, width, height];
  11. }
  12. function d(bbox) {
  13. const [x, y, width, height] = bbox;
  14. return `
  15. M ${x} ${y}
  16. L ${x + width} ${y}
  17. L ${x + width} ${y + height}
  18. L ${x} ${y + height}
  19. Z
  20. `;
  21. }
  22. function pack(shape, count) {
  23. const [x0, y0, width, height] = localBBoxOf(shape);
  24. const aspect = height / width;
  25. const col = Math.ceil(Math.sqrt(count / aspect));
  26. const row = Math.ceil(count / col);
  27. const B = [];
  28. const h = height / row;
  29. let j = 0;
  30. let n = count;
  31. while (n > 0) {
  32. const c = Math.min(n, col);
  33. const w = width / c;
  34. for (let i = 0; i < c; i++) {
  35. const x = x0 + i * w;
  36. const y = y0 + j * h;
  37. B.push(d([x, y, w, h]));
  38. }
  39. n -= c;
  40. j += 1;
  41. }
  42. return B;
  43. }
  44. function normalizeSplit(split = 'pack') {
  45. if (typeof split == 'function')
  46. return split;
  47. return pack;
  48. }
  49. /**
  50. * Translate and scale.
  51. */
  52. function shapeToShape(from, to, timeEffect) {
  53. const [x0, y0, w0, h0] = localBBoxOf(from);
  54. const { transform: fromTransform } = from.style;
  55. const { transform: toTransform } = to.style;
  56. // Replace first to get right bbox after mounting.
  57. replaceChild(to, from);
  58. // Apply translate and scale transform.
  59. const [x1, y1, w1, h1] = localBBoxOf(to);
  60. const dx = x0 - x1;
  61. const dy = y0 - y1;
  62. const sx = w0 / w1;
  63. const sy = h0 / h1;
  64. const keyframes = [
  65. Object.assign({ transform: `${fromTransform ? fromTransform + ' ' : ''}translate(${dx}, ${dy}) scale(${sx}, ${sy})` }, attributeOf(from, attributeKeys)),
  66. Object.assign({ transform: `${toTransform ? toTransform + ' ' : ''}translate(0, 0) scale(1, 1)` }, attributeOf(to, attributeKeys)),
  67. ];
  68. const animation = to.animate(keyframes, timeEffect);
  69. return animation;
  70. }
  71. /**
  72. * Replace object and copy className and __data__
  73. */
  74. function replaceChild(newChild, oldChild) {
  75. newChild['__data__'] = oldChild['__data__'];
  76. newChild.className = oldChild.className;
  77. oldChild.parentNode.replaceChild(newChild, oldChild);
  78. }
  79. /**
  80. * Replace element with a path shape.
  81. */
  82. function maybePath(node, d) {
  83. const { nodeName } = node;
  84. if (nodeName === 'path')
  85. return node;
  86. const path = new Path({
  87. style: Object.assign(Object.assign({}, attributeOf(node, attributeKeys)), { d }),
  88. });
  89. replaceChild(path, node);
  90. return path;
  91. }
  92. function hasUniqueString(search, pattern) {
  93. const first = search.indexOf(pattern);
  94. const last = search.lastIndexOf(pattern);
  95. return first === last;
  96. }
  97. // Path definition with multiple m and M command has sub path.
  98. // eg. 'M10,10...M20,20', 'm10,10...m20,20'
  99. function hasSubPath(path) {
  100. return !hasUniqueString(path, 'm') || !hasUniqueString(path, 'M');
  101. }
  102. function shape2path(shape) {
  103. const path = convertToPath(shape);
  104. if (!path)
  105. return;
  106. // Path definition with sub path can't do path morphing animation,
  107. // so skip this kind of path.
  108. if (hasSubPath(path))
  109. return;
  110. return path;
  111. }
  112. function oneToOne(shape, from, to, timeEffect) {
  113. // If the nodeTypes of from and to are equal,
  114. // or non of them can convert to path,
  115. // the apply shape to shape animation.
  116. const { nodeName: fromName } = from;
  117. const { nodeName: toName } = to;
  118. const fromPath = shape2path(from);
  119. const toPath = shape2path(to);
  120. const isSameNodes = fromName === toName && fromName !== 'path';
  121. const hasNonPathNode = fromPath === undefined || toPath === undefined;
  122. if (isSameNodes || hasNonPathNode)
  123. return shapeToShape(from, to, timeEffect);
  124. const pathShape = maybePath(shape, fromPath);
  125. // Convert Path will take transform, anchor, etc into account,
  126. // so there is no need to specify these attributes in keyframes.
  127. const keyframes = [
  128. Object.assign({ path: fromPath }, attributeOf(from, attributeKeys)),
  129. Object.assign({ path: toPath }, attributeOf(to, attributeKeys)),
  130. ];
  131. const animation = pathShape.animate(keyframes, timeEffect);
  132. animation.onfinish = () => {
  133. copyAttributes(pathShape, to);
  134. };
  135. // Remove transform because it already applied in path
  136. // converted by convertToPath.
  137. // @todo Remove this scale(1, 1)
  138. pathShape.style.transform = 'scale(1, 1)';
  139. pathShape.style.transform = 'none';
  140. return animation;
  141. }
  142. function oneToMultiple(from, to, timeEffect, split) {
  143. // Hide the shape to be split before being removing.
  144. from.style.visibility = 'hidden';
  145. const D = split(from, to.length);
  146. return to.map((shape, i) => {
  147. const path = new Path({
  148. style: Object.assign({ path: D[i] }, attributeOf(from, attributeKeys)),
  149. });
  150. return oneToOne(shape, path, shape, timeEffect);
  151. });
  152. }
  153. function multipleToOne(from, to, timeEffect, split) {
  154. const D = split(to, from.length);
  155. const { fillOpacity = 1, strokeOpacity = 1, opacity = 1 } = to.style;
  156. const keyframes = [
  157. { fillOpacity: 0, strokeOpacity: 0, opacity: 0 },
  158. { fillOpacity: 0, strokeOpacity: 0, opacity: 0, offset: 0.99 },
  159. {
  160. fillOpacity,
  161. strokeOpacity,
  162. opacity,
  163. },
  164. ];
  165. const animation = to.animate(keyframes, timeEffect);
  166. const animations = from.map((shape, i) => {
  167. const path = new Path({
  168. style: {
  169. path: D[i],
  170. fill: to.style.fill,
  171. },
  172. });
  173. return oneToOne(shape, shape, path, timeEffect);
  174. });
  175. return [...animations, animation];
  176. }
  177. /**
  178. * Morphing animations.
  179. * @todo Support more split function.
  180. */
  181. export const Morphing = (options) => {
  182. return (from, to, value, coordinate, defaults) => {
  183. const split = normalizeSplit(options.split);
  184. const timeEffect = effectTiming(defaults, value, options);
  185. const { length: fl } = from;
  186. const { length: tl } = to;
  187. if ((fl === 1 && tl === 1) || (fl > 1 && tl > 1)) {
  188. const [f] = from;
  189. const [t] = to;
  190. return oneToOne(f, f, t, timeEffect);
  191. }
  192. if (fl === 1 && tl > 1) {
  193. const [f] = from;
  194. return oneToMultiple(f, to, timeEffect, split);
  195. }
  196. if (fl > 1 && tl === 1) {
  197. const [t] = to;
  198. return multipleToOne(from, t, timeEffect, split);
  199. }
  200. return null;
  201. };
  202. };
  203. Morphing.props = {};
  204. //# sourceMappingURL=morphing.js.map