morphPath.ts 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894
  1. import PathProxy from '../core/PathProxy';
  2. import { cubicSubdivide } from '../core/curve';
  3. import Path from '../graphic/Path';
  4. import Element, { ElementAnimateConfig } from '../Element';
  5. import { defaults, map } from '../core/util';
  6. import { lerp } from '../core/vector';
  7. import Group, { GroupLike } from '../graphic/Group';
  8. import { clonePath } from './path';
  9. import { MatrixArray } from '../core/matrix';
  10. import Transformable from '../core/Transformable';
  11. import { ZRenderType } from '../zrender';
  12. import { split } from './dividePath';
  13. import { pathToBezierCurves } from './convertPath';
  14. function alignSubpath(subpath1: number[], subpath2: number[]): [number[], number[]] {
  15. const len1 = subpath1.length;
  16. const len2 = subpath2.length;
  17. if (len1 === len2) {
  18. return [subpath1, subpath2];
  19. }
  20. const tmpSegX: number[] = [];
  21. const tmpSegY: number[] = [];
  22. const shorterPath = len1 < len2 ? subpath1 : subpath2;
  23. const shorterLen = Math.min(len1, len2);
  24. // Should divide excatly
  25. const diff = Math.abs(len2 - len1) / 6;
  26. const shorterBezierCount = (shorterLen - 2) / 6;
  27. // Add `diff` number of beziers
  28. const eachCurveSubDivCount = Math.ceil(diff / shorterBezierCount) + 1;
  29. const newSubpath = [shorterPath[0], shorterPath[1]];
  30. let remained = diff;
  31. for (let i = 2; i < shorterLen;) {
  32. let x0 = shorterPath[i - 2];
  33. let y0 = shorterPath[i - 1];
  34. let x1 = shorterPath[i++];
  35. let y1 = shorterPath[i++];
  36. let x2 = shorterPath[i++];
  37. let y2 = shorterPath[i++];
  38. let x3 = shorterPath[i++];
  39. let y3 = shorterPath[i++];
  40. if (remained <= 0) {
  41. newSubpath.push(x1, y1, x2, y2, x3, y3);
  42. continue;
  43. }
  44. let actualSubDivCount = Math.min(remained, eachCurveSubDivCount - 1) + 1;
  45. for (let k = 1; k <= actualSubDivCount; k++) {
  46. const p = k / actualSubDivCount;
  47. cubicSubdivide(x0, x1, x2, x3, p, tmpSegX);
  48. cubicSubdivide(y0, y1, y2, y3, p, tmpSegY);
  49. // tmpSegX[3] === tmpSegX[4]
  50. x0 = tmpSegX[3];
  51. y0 = tmpSegY[3];
  52. newSubpath.push(tmpSegX[1], tmpSegY[1], tmpSegX[2], tmpSegY[2], x0, y0);
  53. x1 = tmpSegX[5];
  54. y1 = tmpSegY[5];
  55. x2 = tmpSegX[6];
  56. y2 = tmpSegY[6];
  57. // The last point (x3, y3) is still the same.
  58. }
  59. remained -= actualSubDivCount - 1;
  60. }
  61. return shorterPath === subpath1 ? [newSubpath, subpath2] : [subpath1, newSubpath];
  62. }
  63. function createSubpath(lastSubpathSubpath: number[], otherSubpath: number[]) {
  64. const len = lastSubpathSubpath.length;
  65. const lastX = lastSubpathSubpath[len - 2];
  66. const lastY = lastSubpathSubpath[len - 1];
  67. const newSubpath: number[] = [];
  68. for (let i = 0; i < otherSubpath.length;) {
  69. newSubpath[i++] = lastX;
  70. newSubpath[i++] = lastY;
  71. }
  72. return newSubpath;
  73. }
  74. /**
  75. * Make two bezier arrays aligns on structure. To have better animation.
  76. *
  77. * It will:
  78. * Make two bezier arrays have same number of subpaths.
  79. * Make each subpath has equal number of bezier curves.
  80. *
  81. * array is the convert result of pathToBezierCurves.
  82. */
  83. export function alignBezierCurves(array1: number[][], array2: number[][]) {
  84. let lastSubpath1;
  85. let lastSubpath2;
  86. let newArray1 = [];
  87. let newArray2 = [];
  88. for (let i = 0; i < Math.max(array1.length, array2.length); i++) {
  89. const subpath1 = array1[i];
  90. const subpath2 = array2[i];
  91. let newSubpath1;
  92. let newSubpath2;
  93. if (!subpath1) {
  94. newSubpath1 = createSubpath(lastSubpath1 || subpath2, subpath2);
  95. newSubpath2 = subpath2;
  96. }
  97. else if (!subpath2) {
  98. newSubpath2 = createSubpath(lastSubpath2 || subpath1, subpath1);
  99. newSubpath1 = subpath1;
  100. }
  101. else {
  102. [newSubpath1, newSubpath2] = alignSubpath(subpath1, subpath2);
  103. lastSubpath1 = newSubpath1;
  104. lastSubpath2 = newSubpath2;
  105. }
  106. newArray1.push(newSubpath1);
  107. newArray2.push(newSubpath2);
  108. }
  109. return [newArray1, newArray2];
  110. }
  111. interface MorphingPath extends Path {
  112. __morphT: number;
  113. }
  114. export interface CombineMorphingPath extends Path {
  115. childrenRef(): (CombineMorphingPath | Path)[]
  116. __isCombineMorphing: boolean;
  117. }
  118. export function centroid(array: number[]) {
  119. // https://en.wikipedia.org/wiki/Centroid#Of_a_polygon
  120. let signedArea = 0;
  121. let cx = 0;
  122. let cy = 0;
  123. const len = array.length;
  124. // Polygon should been closed.
  125. for (let i = 0, j = len - 2; i < len; j = i, i += 2) {
  126. const x0 = array[j];
  127. const y0 = array[j + 1];
  128. const x1 = array[i];
  129. const y1 = array[i + 1];
  130. const a = x0 * y1 - x1 * y0;
  131. signedArea += a;
  132. cx += (x0 + x1) * a;
  133. cy += (y0 + y1) * a;
  134. }
  135. if (signedArea === 0) {
  136. return [array[0] || 0, array[1] || 0];
  137. }
  138. return [cx / signedArea / 3, cy / signedArea / 3, signedArea];
  139. }
  140. /**
  141. * Offset the points to find the nearest morphing distance.
  142. * Return beziers count needs to be offset.
  143. */
  144. function findBestRingOffset(
  145. fromSubBeziers: number[],
  146. toSubBeziers: number[],
  147. fromCp: number[],
  148. toCp: number[]
  149. ) {
  150. const bezierCount = (fromSubBeziers.length - 2) / 6;
  151. let bestScore = Infinity;
  152. let bestOffset = 0;
  153. const len = fromSubBeziers.length;
  154. const len2 = len - 2;
  155. for (let offset = 0; offset < bezierCount; offset++) {
  156. const cursorOffset = offset * 6;
  157. let score = 0;
  158. for (let k = 0; k < len; k += 2) {
  159. let idx = k === 0 ? cursorOffset : ((cursorOffset + k - 2) % len2 + 2);
  160. const x0 = fromSubBeziers[idx] - fromCp[0];
  161. const y0 = fromSubBeziers[idx + 1] - fromCp[1];
  162. const x1 = toSubBeziers[k] - toCp[0];
  163. const y1 = toSubBeziers[k + 1] - toCp[1];
  164. const dx = x1 - x0;
  165. const dy = y1 - y0;
  166. score += dx * dx + dy * dy;
  167. }
  168. if (score < bestScore) {
  169. bestScore = score;
  170. bestOffset = offset;
  171. }
  172. }
  173. return bestOffset;
  174. }
  175. function reverse(array: number[]) {
  176. const newArr: number[] = [];
  177. const len = array.length;
  178. for (let i = 0; i < len; i += 2) {
  179. newArr[i] = array[len - i - 2];
  180. newArr[i + 1] = array[len - i - 1];
  181. }
  182. return newArr;
  183. }
  184. type MorphingData = {
  185. from: number[];
  186. to: number[];
  187. fromCp: number[];
  188. toCp: number[];
  189. rotation: number;
  190. }[];
  191. /**
  192. * If we interpolating between two bezier curve arrays.
  193. * It will have many broken effects during the transition.
  194. * So we try to apply an extra rotation which can make each bezier curve morph as small as possible.
  195. */
  196. function findBestMorphingRotation(
  197. fromArr: number[][],
  198. toArr: number[][],
  199. searchAngleIteration: number,
  200. searchAngleRange: number
  201. ): MorphingData {
  202. const result = [];
  203. let fromNeedsReverse: boolean;
  204. for (let i = 0; i < fromArr.length; i++) {
  205. let fromSubpathBezier = fromArr[i];
  206. const toSubpathBezier = toArr[i];
  207. const fromCp = centroid(fromSubpathBezier);
  208. const toCp = centroid(toSubpathBezier);
  209. if (fromNeedsReverse == null) {
  210. // Reverse from array if two have different directions.
  211. // Determine the clockwise based on the first subpath.
  212. // Reverse all subpaths or not. Avoid winding rule changed.
  213. fromNeedsReverse = fromCp[2] < 0 !== toCp[2] < 0;
  214. }
  215. const newFromSubpathBezier: number[] = [];
  216. const newToSubpathBezier: number[] = [];
  217. let bestAngle = 0;
  218. let bestScore = Infinity;
  219. let tmpArr: number[] = [];
  220. const len = fromSubpathBezier.length;
  221. if (fromNeedsReverse) {
  222. // Make sure clockwise
  223. fromSubpathBezier = reverse(fromSubpathBezier);
  224. }
  225. const offset = findBestRingOffset(fromSubpathBezier, toSubpathBezier, fromCp, toCp) * 6;
  226. const len2 = len - 2;
  227. for (let k = 0; k < len2; k += 2) {
  228. // Not include the start point.
  229. const idx = (offset + k) % len2 + 2;
  230. newFromSubpathBezier[k + 2] = fromSubpathBezier[idx] - fromCp[0];
  231. newFromSubpathBezier[k + 3] = fromSubpathBezier[idx + 1] - fromCp[1];
  232. }
  233. newFromSubpathBezier[0] = fromSubpathBezier[offset] - fromCp[0];
  234. newFromSubpathBezier[1] = fromSubpathBezier[offset + 1] - fromCp[1];
  235. if (searchAngleIteration > 0) {
  236. const step = searchAngleRange / searchAngleIteration;
  237. for (let angle = -searchAngleRange / 2; angle <= searchAngleRange / 2; angle += step) {
  238. const sa = Math.sin(angle);
  239. const ca = Math.cos(angle);
  240. let score = 0;
  241. for (let k = 0; k < fromSubpathBezier.length; k += 2) {
  242. const x0 = newFromSubpathBezier[k];
  243. const y0 = newFromSubpathBezier[k + 1];
  244. const x1 = toSubpathBezier[k] - toCp[0];
  245. const y1 = toSubpathBezier[k + 1] - toCp[1];
  246. // Apply rotation on the target point.
  247. const newX1 = x1 * ca - y1 * sa;
  248. const newY1 = x1 * sa + y1 * ca;
  249. tmpArr[k] = newX1;
  250. tmpArr[k + 1] = newY1;
  251. const dx = newX1 - x0;
  252. const dy = newY1 - y0;
  253. // Use dot product to have min direction change.
  254. // const d = Math.sqrt(x0 * x0 + y0 * y0);
  255. // score += x0 * dx / d + y0 * dy / d;
  256. score += dx * dx + dy * dy;
  257. }
  258. if (score < bestScore) {
  259. bestScore = score;
  260. bestAngle = angle;
  261. // Copy.
  262. for (let m = 0; m < tmpArr.length; m++) {
  263. newToSubpathBezier[m] = tmpArr[m];
  264. }
  265. }
  266. }
  267. }
  268. else {
  269. for (let i = 0; i < len; i += 2) {
  270. newToSubpathBezier[i] = toSubpathBezier[i] - toCp[0];
  271. newToSubpathBezier[i + 1] = toSubpathBezier[i + 1] - toCp[1];
  272. }
  273. }
  274. result.push({
  275. from: newFromSubpathBezier,
  276. to: newToSubpathBezier,
  277. fromCp,
  278. toCp,
  279. rotation: -bestAngle
  280. });
  281. }
  282. return result;
  283. }
  284. export function isCombineMorphing(path: Element): path is CombineMorphingPath {
  285. return (path as CombineMorphingPath).__isCombineMorphing;
  286. }
  287. export function isMorphing(el: Element) {
  288. return (el as MorphingPath).__morphT >= 0;
  289. }
  290. const SAVED_METHOD_PREFIX = '__mOriginal_';
  291. function saveAndModifyMethod<T extends object, M extends keyof T>(
  292. obj: T,
  293. methodName: M,
  294. modifiers: { replace?: T[M], after?: T[M], before?: T[M] }
  295. ) {
  296. const savedMethodName = SAVED_METHOD_PREFIX + methodName;
  297. const originalMethod = (obj as any)[savedMethodName] || obj[methodName];
  298. if (!(obj as any)[savedMethodName]) {
  299. (obj as any)[savedMethodName] = obj[methodName];
  300. }
  301. const replace = modifiers.replace;
  302. const after = modifiers.after;
  303. const before = modifiers.before;
  304. (obj as any)[methodName] = function () {
  305. const args = arguments;
  306. let res;
  307. before && (before as unknown as Function).apply(this, args);
  308. // Still call the original method if not replacement.
  309. if (replace) {
  310. res = (replace as unknown as Function).apply(this, args);
  311. }
  312. else {
  313. res = originalMethod.apply(this, args);
  314. }
  315. after && (after as unknown as Function).apply(this, args);
  316. return res;
  317. };
  318. }
  319. function restoreMethod<T extends object>(
  320. obj: T,
  321. methodName: keyof T
  322. ) {
  323. const savedMethodName = SAVED_METHOD_PREFIX + methodName;
  324. if ((obj as any)[savedMethodName]) {
  325. obj[methodName] = (obj as any)[savedMethodName];
  326. (obj as any)[savedMethodName] = null;
  327. }
  328. }
  329. function applyTransformOnBeziers(bezierCurves: number[][], mm: MatrixArray) {
  330. for (let i = 0; i < bezierCurves.length; i++) {
  331. const subBeziers = bezierCurves[i];
  332. for (let k = 0; k < subBeziers.length;) {
  333. const x = subBeziers[k];
  334. const y = subBeziers[k + 1];
  335. subBeziers[k++] = mm[0] * x + mm[2] * y + mm[4];
  336. subBeziers[k++] = mm[1] * x + mm[3] * y + mm[5];
  337. }
  338. }
  339. }
  340. function prepareMorphPath(
  341. fromPath: Path,
  342. toPath: Path
  343. ) {
  344. const fromPathProxy = fromPath.getUpdatedPathProxy();
  345. const toPathProxy = toPath.getUpdatedPathProxy();
  346. const [fromBezierCurves, toBezierCurves] =
  347. alignBezierCurves(pathToBezierCurves(fromPathProxy), pathToBezierCurves(toPathProxy));
  348. const fromPathTransform = fromPath.getComputedTransform();
  349. const toPathTransform = toPath.getComputedTransform();
  350. function updateIdentityTransform(this: Transformable) {
  351. this.transform = null;
  352. }
  353. fromPathTransform && applyTransformOnBeziers(fromBezierCurves, fromPathTransform);
  354. toPathTransform && applyTransformOnBeziers(toBezierCurves, toPathTransform);
  355. // Just ignore transform
  356. saveAndModifyMethod(toPath, 'updateTransform', { replace: updateIdentityTransform });
  357. toPath.transform = null;
  358. const morphingData = findBestMorphingRotation(fromBezierCurves, toBezierCurves, 10, Math.PI);
  359. const tmpArr: number[] = [];
  360. saveAndModifyMethod(toPath, 'buildPath', { replace(path: PathProxy) {
  361. const t = (toPath as MorphingPath).__morphT;
  362. const onet = 1 - t;
  363. const newCp: number[] = [];
  364. for (let i = 0; i < morphingData.length; i++) {
  365. const item = morphingData[i];
  366. const from = item.from;
  367. const to = item.to;
  368. const angle = item.rotation * t;
  369. const fromCp = item.fromCp;
  370. const toCp = item.toCp;
  371. const sa = Math.sin(angle);
  372. const ca = Math.cos(angle);
  373. lerp(newCp, fromCp, toCp, t);
  374. for (let m = 0; m < from.length; m += 2) {
  375. const x0 = from[m];
  376. const y0 = from[m + 1];
  377. const x1 = to[m];
  378. const y1 = to[m + 1];
  379. const x = x0 * onet + x1 * t;
  380. const y = y0 * onet + y1 * t;
  381. tmpArr[m] = (x * ca - y * sa) + newCp[0];
  382. tmpArr[m + 1] = (x * sa + y * ca) + newCp[1];
  383. }
  384. let x0 = tmpArr[0];
  385. let y0 = tmpArr[1];
  386. path.moveTo(x0, y0);
  387. for (let m = 2; m < from.length;) {
  388. const x1 = tmpArr[m++];
  389. const y1 = tmpArr[m++];
  390. const x2 = tmpArr[m++];
  391. const y2 = tmpArr[m++];
  392. const x3 = tmpArr[m++];
  393. const y3 = tmpArr[m++];
  394. // Is a line.
  395. if (x0 === x1 && y0 === y1 && x2 === x3 && y2 === y3) {
  396. path.lineTo(x3, y3);
  397. }
  398. else {
  399. path.bezierCurveTo(x1, y1, x2, y2, x3, y3);
  400. }
  401. x0 = x3;
  402. y0 = y3;
  403. }
  404. }
  405. } });
  406. }
  407. /**
  408. * Morphing from old path to new path.
  409. */
  410. export function morphPath(
  411. fromPath: Path,
  412. toPath: Path,
  413. animationOpts: ElementAnimateConfig
  414. ): Path {
  415. if (!fromPath || !toPath) {
  416. return toPath;
  417. }
  418. const oldDone = animationOpts.done;
  419. // const oldAborted = animationOpts.aborted;
  420. const oldDuring = animationOpts.during;
  421. prepareMorphPath(fromPath, toPath);
  422. (toPath as MorphingPath).__morphT = 0;
  423. function restoreToPath() {
  424. restoreMethod(toPath, 'buildPath');
  425. restoreMethod(toPath, 'updateTransform');
  426. // Mark as not in morphing
  427. (toPath as MorphingPath).__morphT = -1;
  428. // Cleanup.
  429. toPath.createPathProxy();
  430. toPath.dirtyShape();
  431. }
  432. toPath.animateTo({
  433. __morphT: 1
  434. } as any, defaults({
  435. during(p) {
  436. toPath.dirtyShape();
  437. oldDuring && oldDuring(p);
  438. },
  439. done() {
  440. restoreToPath();
  441. oldDone && oldDone();
  442. }
  443. // NOTE: Don't do restore if aborted.
  444. // Because all status was just set when animation started.
  445. // aborted() {
  446. // oldAborted && oldAborted();
  447. // }
  448. } as ElementAnimateConfig, animationOpts));
  449. return toPath;
  450. }
  451. // https://github.com/mapbox/earcut/blob/master/src/earcut.js#L437
  452. // https://jsfiddle.net/pissang/2jk7x145/
  453. // function zOrder(x: number, y: number, minX: number, minY: number, maxX: number, maxY: number) {
  454. // // Normalize coords to 0 - 1
  455. // // The transformed into non-negative 15-bit integer range
  456. // x = (maxX === minX) ? 0 : Math.round(32767 * (x - minX) / (maxX - minX));
  457. // y = (maxY === minY) ? 0 : Math.round(32767 * (y - minY) / (maxY - minY));
  458. // x = (x | (x << 8)) & 0x00FF00FF;
  459. // x = (x | (x << 4)) & 0x0F0F0F0F;
  460. // x = (x | (x << 2)) & 0x33333333;
  461. // x = (x | (x << 1)) & 0x55555555;
  462. // y = (y | (y << 8)) & 0x00FF00FF;
  463. // y = (y | (y << 4)) & 0x0F0F0F0F;
  464. // y = (y | (y << 2)) & 0x33333333;
  465. // y = (y | (y << 1)) & 0x55555555;
  466. // return x | (y << 1);
  467. // }
  468. // https://github.com/w8r/hilbert/blob/master/hilbert.js#L30
  469. // https://jsfiddle.net/pissang/xdnbzg6v/
  470. function hilbert(x: number, y: number, minX: number, minY: number, maxX: number, maxY: number) {
  471. const bits = 16;
  472. x = (maxX === minX) ? 0 : Math.round(32767 * (x - minX) / (maxX - minX));
  473. y = (maxY === minY) ? 0 : Math.round(32767 * (y - minY) / (maxY - minY));
  474. let d = 0;
  475. let tmp: number;
  476. for (let s = (1 << bits) / 2; s > 0; s /= 2) {
  477. let rx = 0;
  478. let ry = 0;
  479. if ((x & s) > 0) {
  480. rx = 1;
  481. }
  482. if ((y & s) > 0) {
  483. ry = 1;
  484. }
  485. d += s * s * ((3 * rx) ^ ry);
  486. if (ry === 0) {
  487. if (rx === 1) {
  488. x = s - 1 - x;
  489. y = s - 1 - y;
  490. }
  491. tmp = x;
  492. x = y;
  493. y = tmp;
  494. }
  495. }
  496. return d;
  497. }
  498. // Sort paths on hilbert. Not using z-order because it may still have large cross.
  499. // So the left most source can animate to the left most target, not right most target.
  500. // Hope in this way. We can make sure each element is animated to the proper target. Not the farthest.
  501. function sortPaths(pathList: Path[]): Path[] {
  502. let xMin = Infinity;
  503. let yMin = Infinity;
  504. let xMax = -Infinity;
  505. let yMax = -Infinity;
  506. const cps = map(pathList, path => {
  507. const rect = path.getBoundingRect();
  508. const m = path.getComputedTransform();
  509. const x = rect.x + rect.width / 2 + (m ? m[4] : 0);
  510. const y = rect.y + rect.height / 2 + (m ? m[5] : 0);
  511. xMin = Math.min(x, xMin);
  512. yMin = Math.min(y, yMin);
  513. xMax = Math.max(x, xMax);
  514. yMax = Math.max(y, yMax);
  515. return [x, y];
  516. });
  517. const items = map(cps, (cp, idx) => {
  518. return {
  519. cp,
  520. z: hilbert(cp[0], cp[1], xMin, yMin, xMax, yMax),
  521. path: pathList[idx]
  522. };
  523. });
  524. return items.sort((a, b) => a.z - b.z).map(item => item.path);
  525. }
  526. export interface DividePathParams {
  527. path: Path,
  528. count: number
  529. };
  530. export interface DividePath {
  531. (params: DividePathParams): Path[]
  532. }
  533. export interface IndividualDelay {
  534. (index: number, count: number, fromPath: Path, toPath: Path): number
  535. }
  536. function defaultDividePath(param: DividePathParams) {
  537. return split(param.path, param.count);
  538. }
  539. export interface CombineConfig extends ElementAnimateConfig {
  540. /**
  541. * Transform of returned will be ignored.
  542. */
  543. dividePath?: DividePath
  544. /**
  545. * delay of each individual.
  546. * Because individual are sorted on z-order. The index is also sorted top-left / right-down.
  547. */
  548. individualDelay?: IndividualDelay
  549. /**
  550. * If sort splitted paths so the movement between them can be more natural
  551. */
  552. // sort?: boolean
  553. }
  554. function createEmptyReturn() {
  555. return {
  556. fromIndividuals: [] as Path[],
  557. toIndividuals: [] as Path[],
  558. count: 0
  559. };
  560. }
  561. /**
  562. * Make combine morphing from many paths to one.
  563. * Will return a group to replace the original path.
  564. */
  565. export function combineMorph(
  566. fromList: (CombineMorphingPath | Path)[],
  567. toPath: Path,
  568. animationOpts: CombineConfig
  569. ) {
  570. let fromPathList: Path[] = [];
  571. function addFromPath(fromList: Element[]) {
  572. for (let i = 0; i < fromList.length; i++) {
  573. const from = fromList[i];
  574. if (isCombineMorphing(from)) {
  575. addFromPath((from as GroupLike).childrenRef());
  576. }
  577. else if (from instanceof Path) {
  578. fromPathList.push(from);
  579. }
  580. }
  581. }
  582. addFromPath(fromList);
  583. const separateCount = fromPathList.length;
  584. // fromPathList.length is 0.
  585. if (!separateCount) {
  586. return createEmptyReturn();
  587. }
  588. const dividePath = animationOpts.dividePath || defaultDividePath;
  589. let toSubPathList = dividePath({
  590. path: toPath, count: separateCount
  591. });
  592. if (toSubPathList.length !== separateCount) {
  593. console.error('Invalid morphing: unmatched splitted path');
  594. return createEmptyReturn();
  595. }
  596. fromPathList = sortPaths(fromPathList);
  597. toSubPathList = sortPaths(toSubPathList);
  598. const oldDone = animationOpts.done;
  599. // const oldAborted = animationOpts.aborted;
  600. const oldDuring = animationOpts.during;
  601. const individualDelay = animationOpts.individualDelay;
  602. const identityTransform = new Transformable();
  603. for (let i = 0; i < separateCount; i++) {
  604. const from = fromPathList[i];
  605. const to = toSubPathList[i];
  606. to.parent = toPath as unknown as Group;
  607. // Ignore transform in each subpath.
  608. to.copyTransform(identityTransform);
  609. // Will do morphPath for each individual if individualDelay is set.
  610. if (!individualDelay) {
  611. prepareMorphPath(from, to);
  612. }
  613. }
  614. (toPath as CombineMorphingPath).__isCombineMorphing = true;
  615. (toPath as CombineMorphingPath).childrenRef = function () {
  616. return toSubPathList;
  617. };
  618. function addToSubPathListToZr(zr: ZRenderType) {
  619. for (let i = 0; i < toSubPathList.length; i++) {
  620. toSubPathList[i].addSelfToZr(zr);
  621. }
  622. }
  623. saveAndModifyMethod(toPath, 'addSelfToZr', {
  624. after(zr) {
  625. addToSubPathListToZr(zr);
  626. }
  627. });
  628. saveAndModifyMethod(toPath, 'removeSelfFromZr', {
  629. after(zr) {
  630. for (let i = 0; i < toSubPathList.length; i++) {
  631. toSubPathList[i].removeSelfFromZr(zr);
  632. }
  633. }
  634. });
  635. function restoreToPath() {
  636. (toPath as CombineMorphingPath).__isCombineMorphing = false;
  637. // Mark as not in morphing
  638. (toPath as MorphingPath).__morphT = -1;
  639. (toPath as CombineMorphingPath).childrenRef = null;
  640. restoreMethod(toPath, 'addSelfToZr');
  641. restoreMethod(toPath, 'removeSelfFromZr');
  642. }
  643. const toLen = toSubPathList.length;
  644. if (individualDelay) {
  645. let animating = toLen;
  646. const eachDone = () => {
  647. animating--;
  648. if (animating === 0) {
  649. restoreToPath();
  650. oldDone && oldDone();
  651. }
  652. };
  653. // Animate each element individually.
  654. for (let i = 0; i < toLen; i++) {
  655. // TODO only call during once?
  656. const indivdualAnimationOpts = individualDelay ? defaults({
  657. delay: (animationOpts.delay || 0) + individualDelay(i, toLen, fromPathList[i], toSubPathList[i]),
  658. done: eachDone
  659. } as ElementAnimateConfig, animationOpts) : animationOpts;
  660. morphPath(fromPathList[i], toSubPathList[i], indivdualAnimationOpts);
  661. }
  662. }
  663. else {
  664. (toPath as MorphingPath).__morphT = 0;
  665. toPath.animateTo({
  666. __morphT: 1
  667. } as any, defaults({
  668. during(p) {
  669. for (let i = 0; i < toLen; i++) {
  670. const child = toSubPathList[i] as MorphingPath;
  671. child.__morphT = (toPath as MorphingPath).__morphT;
  672. child.dirtyShape();
  673. }
  674. oldDuring && oldDuring(p);
  675. },
  676. done() {
  677. restoreToPath();
  678. for (let i = 0; i < fromList.length; i++) {
  679. restoreMethod(fromList[i], 'updateTransform');
  680. }
  681. oldDone && oldDone();
  682. }
  683. } as ElementAnimateConfig, animationOpts));
  684. }
  685. if (toPath.__zr) {
  686. addToSubPathListToZr(toPath.__zr);
  687. }
  688. return {
  689. fromIndividuals: fromPathList,
  690. toIndividuals: toSubPathList,
  691. count: toLen
  692. };
  693. }
  694. export interface SeparateConfig extends ElementAnimateConfig {
  695. dividePath?: DividePath
  696. individualDelay?: IndividualDelay
  697. /**
  698. * If sort splitted paths so the movement between them can be more natural
  699. */
  700. // sort?: boolean
  701. // // If the from path of separate animation is doing combine animation.
  702. // // And the paths number is not same with toPathList. We need to do enter/leave animation
  703. // // on the missing/spare paths.
  704. // enter?: (el: Path) => void
  705. // leave?: (el: Path) => void
  706. }
  707. /**
  708. * Make separate morphing from one path to many paths.
  709. * Make the MorphingKind of `toPath` become `'ONE_ONE'`.
  710. */
  711. export function separateMorph(
  712. fromPath: Path,
  713. toPathList: Path[],
  714. animationOpts: SeparateConfig
  715. ) {
  716. const toLen = toPathList.length;
  717. let fromPathList: Path[] = [];
  718. const dividePath = animationOpts.dividePath || defaultDividePath;
  719. function addFromPath(fromList: Element[]) {
  720. for (let i = 0; i < fromList.length; i++) {
  721. const from = fromList[i];
  722. if (isCombineMorphing(from)) {
  723. addFromPath((from as GroupLike).childrenRef());
  724. }
  725. else if (from instanceof Path) {
  726. fromPathList.push(from);
  727. }
  728. }
  729. }
  730. // This case most happen when a combining path is called to reverse the animation
  731. // to its original separated state.
  732. if (isCombineMorphing(fromPath)) {
  733. addFromPath(fromPath.childrenRef());
  734. const fromLen = fromPathList.length;
  735. if (fromLen < toLen) {
  736. let k = 0;
  737. for (let i = fromLen; i < toLen; i++) {
  738. // Create a clone
  739. fromPathList.push(clonePath(fromPathList[k++ % fromLen]));
  740. }
  741. }
  742. // Else simply remove if fromLen > toLen
  743. fromPathList.length = toLen;
  744. }
  745. else {
  746. fromPathList = dividePath({ path: fromPath, count: toLen });
  747. const fromPathTransform = fromPath.getComputedTransform();
  748. for (let i = 0; i < fromPathList.length; i++) {
  749. // Force use transform of source path.
  750. fromPathList[i].setLocalTransform(fromPathTransform);
  751. }
  752. if (fromPathList.length !== toLen) {
  753. console.error('Invalid morphing: unmatched splitted path');
  754. return createEmptyReturn();
  755. }
  756. }
  757. fromPathList = sortPaths(fromPathList);
  758. toPathList = sortPaths(toPathList);
  759. const individualDelay = animationOpts.individualDelay;
  760. for (let i = 0; i < toLen; i++) {
  761. const indivdualAnimationOpts = individualDelay ? defaults({
  762. delay: (animationOpts.delay || 0) + individualDelay(i, toLen, fromPathList[i], toPathList[i])
  763. } as ElementAnimateConfig, animationOpts) : animationOpts;
  764. morphPath(fromPathList[i], toPathList[i], indivdualAnimationOpts);
  765. }
  766. return {
  767. fromIndividuals: fromPathList,
  768. toIndividuals: toPathList,
  769. count: toPathList.length
  770. };
  771. }
  772. export { split as defaultDividePath };