import PathProxy from '../core/PathProxy';
|
import { cubicSubdivide } from '../core/curve';
|
import Path from '../graphic/Path';
|
import Element, { ElementAnimateConfig } from '../Element';
|
import { defaults, map } from '../core/util';
|
import { lerp } from '../core/vector';
|
import Group, { GroupLike } from '../graphic/Group';
|
import { clonePath } from './path';
|
import { MatrixArray } from '../core/matrix';
|
import Transformable from '../core/Transformable';
|
import { ZRenderType } from '../zrender';
|
import { split } from './dividePath';
|
import { pathToBezierCurves } from './convertPath';
|
|
function alignSubpath(subpath1: number[], subpath2: number[]): [number[], number[]] {
|
const len1 = subpath1.length;
|
const len2 = subpath2.length;
|
if (len1 === len2) {
|
return [subpath1, subpath2];
|
}
|
const tmpSegX: number[] = [];
|
const tmpSegY: number[] = [];
|
|
const shorterPath = len1 < len2 ? subpath1 : subpath2;
|
const shorterLen = Math.min(len1, len2);
|
// Should divide excatly
|
const diff = Math.abs(len2 - len1) / 6;
|
const shorterBezierCount = (shorterLen - 2) / 6;
|
// Add `diff` number of beziers
|
const eachCurveSubDivCount = Math.ceil(diff / shorterBezierCount) + 1;
|
|
const newSubpath = [shorterPath[0], shorterPath[1]];
|
let remained = diff;
|
|
for (let i = 2; i < shorterLen;) {
|
let x0 = shorterPath[i - 2];
|
let y0 = shorterPath[i - 1];
|
let x1 = shorterPath[i++];
|
let y1 = shorterPath[i++];
|
let x2 = shorterPath[i++];
|
let y2 = shorterPath[i++];
|
let x3 = shorterPath[i++];
|
let y3 = shorterPath[i++];
|
|
if (remained <= 0) {
|
newSubpath.push(x1, y1, x2, y2, x3, y3);
|
continue;
|
}
|
|
let actualSubDivCount = Math.min(remained, eachCurveSubDivCount - 1) + 1;
|
for (let k = 1; k <= actualSubDivCount; k++) {
|
const p = k / actualSubDivCount;
|
|
cubicSubdivide(x0, x1, x2, x3, p, tmpSegX);
|
cubicSubdivide(y0, y1, y2, y3, p, tmpSegY);
|
|
// tmpSegX[3] === tmpSegX[4]
|
x0 = tmpSegX[3];
|
y0 = tmpSegY[3];
|
|
newSubpath.push(tmpSegX[1], tmpSegY[1], tmpSegX[2], tmpSegY[2], x0, y0);
|
x1 = tmpSegX[5];
|
y1 = tmpSegY[5];
|
x2 = tmpSegX[6];
|
y2 = tmpSegY[6];
|
// The last point (x3, y3) is still the same.
|
}
|
remained -= actualSubDivCount - 1;
|
}
|
|
return shorterPath === subpath1 ? [newSubpath, subpath2] : [subpath1, newSubpath];
|
}
|
|
function createSubpath(lastSubpathSubpath: number[], otherSubpath: number[]) {
|
const len = lastSubpathSubpath.length;
|
const lastX = lastSubpathSubpath[len - 2];
|
const lastY = lastSubpathSubpath[len - 1];
|
|
const newSubpath: number[] = [];
|
for (let i = 0; i < otherSubpath.length;) {
|
newSubpath[i++] = lastX;
|
newSubpath[i++] = lastY;
|
}
|
return newSubpath;
|
}
|
|
/**
|
* Make two bezier arrays aligns on structure. To have better animation.
|
*
|
* It will:
|
* Make two bezier arrays have same number of subpaths.
|
* Make each subpath has equal number of bezier curves.
|
*
|
* array is the convert result of pathToBezierCurves.
|
*/
|
export function alignBezierCurves(array1: number[][], array2: number[][]) {
|
|
let lastSubpath1;
|
let lastSubpath2;
|
|
let newArray1 = [];
|
let newArray2 = [];
|
|
for (let i = 0; i < Math.max(array1.length, array2.length); i++) {
|
const subpath1 = array1[i];
|
const subpath2 = array2[i];
|
|
let newSubpath1;
|
let newSubpath2;
|
|
if (!subpath1) {
|
newSubpath1 = createSubpath(lastSubpath1 || subpath2, subpath2);
|
newSubpath2 = subpath2;
|
}
|
else if (!subpath2) {
|
newSubpath2 = createSubpath(lastSubpath2 || subpath1, subpath1);
|
newSubpath1 = subpath1;
|
}
|
else {
|
[newSubpath1, newSubpath2] = alignSubpath(subpath1, subpath2);
|
lastSubpath1 = newSubpath1;
|
lastSubpath2 = newSubpath2;
|
}
|
|
newArray1.push(newSubpath1);
|
newArray2.push(newSubpath2);
|
}
|
|
return [newArray1, newArray2];
|
}
|
|
interface MorphingPath extends Path {
|
__morphT: number;
|
}
|
|
export interface CombineMorphingPath extends Path {
|
childrenRef(): (CombineMorphingPath | Path)[]
|
__isCombineMorphing: boolean;
|
}
|
|
export function centroid(array: number[]) {
|
// https://en.wikipedia.org/wiki/Centroid#Of_a_polygon
|
let signedArea = 0;
|
let cx = 0;
|
let cy = 0;
|
const len = array.length;
|
// Polygon should been closed.
|
for (let i = 0, j = len - 2; i < len; j = i, i += 2) {
|
const x0 = array[j];
|
const y0 = array[j + 1];
|
const x1 = array[i];
|
const y1 = array[i + 1];
|
const a = x0 * y1 - x1 * y0;
|
signedArea += a;
|
cx += (x0 + x1) * a;
|
cy += (y0 + y1) * a;
|
}
|
|
if (signedArea === 0) {
|
return [array[0] || 0, array[1] || 0];
|
}
|
|
return [cx / signedArea / 3, cy / signedArea / 3, signedArea];
|
}
|
|
/**
|
* Offset the points to find the nearest morphing distance.
|
* Return beziers count needs to be offset.
|
*/
|
function findBestRingOffset(
|
fromSubBeziers: number[],
|
toSubBeziers: number[],
|
fromCp: number[],
|
toCp: number[]
|
) {
|
const bezierCount = (fromSubBeziers.length - 2) / 6;
|
let bestScore = Infinity;
|
let bestOffset = 0;
|
|
const len = fromSubBeziers.length;
|
const len2 = len - 2;
|
for (let offset = 0; offset < bezierCount; offset++) {
|
const cursorOffset = offset * 6;
|
let score = 0;
|
|
for (let k = 0; k < len; k += 2) {
|
let idx = k === 0 ? cursorOffset : ((cursorOffset + k - 2) % len2 + 2);
|
|
const x0 = fromSubBeziers[idx] - fromCp[0];
|
const y0 = fromSubBeziers[idx + 1] - fromCp[1];
|
const x1 = toSubBeziers[k] - toCp[0];
|
const y1 = toSubBeziers[k + 1] - toCp[1];
|
|
const dx = x1 - x0;
|
const dy = y1 - y0;
|
score += dx * dx + dy * dy;
|
}
|
if (score < bestScore) {
|
bestScore = score;
|
bestOffset = offset;
|
}
|
}
|
|
return bestOffset;
|
}
|
|
function reverse(array: number[]) {
|
const newArr: number[] = [];
|
const len = array.length;
|
for (let i = 0; i < len; i += 2) {
|
newArr[i] = array[len - i - 2];
|
newArr[i + 1] = array[len - i - 1];
|
}
|
return newArr;
|
}
|
|
type MorphingData = {
|
from: number[];
|
to: number[];
|
fromCp: number[];
|
toCp: number[];
|
rotation: number;
|
}[];
|
|
/**
|
* If we interpolating between two bezier curve arrays.
|
* It will have many broken effects during the transition.
|
* So we try to apply an extra rotation which can make each bezier curve morph as small as possible.
|
*/
|
function findBestMorphingRotation(
|
fromArr: number[][],
|
toArr: number[][],
|
searchAngleIteration: number,
|
searchAngleRange: number
|
): MorphingData {
|
const result = [];
|
|
let fromNeedsReverse: boolean;
|
|
for (let i = 0; i < fromArr.length; i++) {
|
let fromSubpathBezier = fromArr[i];
|
const toSubpathBezier = toArr[i];
|
|
const fromCp = centroid(fromSubpathBezier);
|
const toCp = centroid(toSubpathBezier);
|
|
if (fromNeedsReverse == null) {
|
// Reverse from array if two have different directions.
|
// Determine the clockwise based on the first subpath.
|
// Reverse all subpaths or not. Avoid winding rule changed.
|
fromNeedsReverse = fromCp[2] < 0 !== toCp[2] < 0;
|
}
|
|
const newFromSubpathBezier: number[] = [];
|
const newToSubpathBezier: number[] = [];
|
let bestAngle = 0;
|
let bestScore = Infinity;
|
let tmpArr: number[] = [];
|
|
const len = fromSubpathBezier.length;
|
if (fromNeedsReverse) {
|
// Make sure clockwise
|
fromSubpathBezier = reverse(fromSubpathBezier);
|
}
|
const offset = findBestRingOffset(fromSubpathBezier, toSubpathBezier, fromCp, toCp) * 6;
|
|
const len2 = len - 2;
|
for (let k = 0; k < len2; k += 2) {
|
// Not include the start point.
|
const idx = (offset + k) % len2 + 2;
|
newFromSubpathBezier[k + 2] = fromSubpathBezier[idx] - fromCp[0];
|
newFromSubpathBezier[k + 3] = fromSubpathBezier[idx + 1] - fromCp[1];
|
}
|
newFromSubpathBezier[0] = fromSubpathBezier[offset] - fromCp[0];
|
newFromSubpathBezier[1] = fromSubpathBezier[offset + 1] - fromCp[1];
|
|
if (searchAngleIteration > 0) {
|
const step = searchAngleRange / searchAngleIteration;
|
for (let angle = -searchAngleRange / 2; angle <= searchAngleRange / 2; angle += step) {
|
const sa = Math.sin(angle);
|
const ca = Math.cos(angle);
|
let score = 0;
|
|
for (let k = 0; k < fromSubpathBezier.length; k += 2) {
|
const x0 = newFromSubpathBezier[k];
|
const y0 = newFromSubpathBezier[k + 1];
|
const x1 = toSubpathBezier[k] - toCp[0];
|
const y1 = toSubpathBezier[k + 1] - toCp[1];
|
|
// Apply rotation on the target point.
|
const newX1 = x1 * ca - y1 * sa;
|
const newY1 = x1 * sa + y1 * ca;
|
|
tmpArr[k] = newX1;
|
tmpArr[k + 1] = newY1;
|
|
const dx = newX1 - x0;
|
const dy = newY1 - y0;
|
|
// Use dot product to have min direction change.
|
// const d = Math.sqrt(x0 * x0 + y0 * y0);
|
// score += x0 * dx / d + y0 * dy / d;
|
score += dx * dx + dy * dy;
|
}
|
|
if (score < bestScore) {
|
bestScore = score;
|
bestAngle = angle;
|
// Copy.
|
for (let m = 0; m < tmpArr.length; m++) {
|
newToSubpathBezier[m] = tmpArr[m];
|
}
|
}
|
}
|
}
|
else {
|
for (let i = 0; i < len; i += 2) {
|
newToSubpathBezier[i] = toSubpathBezier[i] - toCp[0];
|
newToSubpathBezier[i + 1] = toSubpathBezier[i + 1] - toCp[1];
|
}
|
}
|
|
result.push({
|
from: newFromSubpathBezier,
|
to: newToSubpathBezier,
|
fromCp,
|
toCp,
|
rotation: -bestAngle
|
});
|
}
|
return result;
|
}
|
|
export function isCombineMorphing(path: Element): path is CombineMorphingPath {
|
return (path as CombineMorphingPath).__isCombineMorphing;
|
}
|
|
export function isMorphing(el: Element) {
|
return (el as MorphingPath).__morphT >= 0;
|
}
|
|
const SAVED_METHOD_PREFIX = '__mOriginal_';
|
function saveAndModifyMethod<T extends object, M extends keyof T>(
|
obj: T,
|
methodName: M,
|
modifiers: { replace?: T[M], after?: T[M], before?: T[M] }
|
) {
|
const savedMethodName = SAVED_METHOD_PREFIX + methodName;
|
const originalMethod = (obj as any)[savedMethodName] || obj[methodName];
|
if (!(obj as any)[savedMethodName]) {
|
(obj as any)[savedMethodName] = obj[methodName];
|
}
|
const replace = modifiers.replace;
|
const after = modifiers.after;
|
const before = modifiers.before;
|
|
(obj as any)[methodName] = function () {
|
const args = arguments;
|
let res;
|
before && (before as unknown as Function).apply(this, args);
|
// Still call the original method if not replacement.
|
if (replace) {
|
res = (replace as unknown as Function).apply(this, args);
|
}
|
else {
|
res = originalMethod.apply(this, args);
|
}
|
after && (after as unknown as Function).apply(this, args);
|
return res;
|
};
|
}
|
function restoreMethod<T extends object>(
|
obj: T,
|
methodName: keyof T
|
) {
|
const savedMethodName = SAVED_METHOD_PREFIX + methodName;
|
if ((obj as any)[savedMethodName]) {
|
obj[methodName] = (obj as any)[savedMethodName];
|
(obj as any)[savedMethodName] = null;
|
}
|
}
|
|
function applyTransformOnBeziers(bezierCurves: number[][], mm: MatrixArray) {
|
for (let i = 0; i < bezierCurves.length; i++) {
|
const subBeziers = bezierCurves[i];
|
for (let k = 0; k < subBeziers.length;) {
|
const x = subBeziers[k];
|
const y = subBeziers[k + 1];
|
|
subBeziers[k++] = mm[0] * x + mm[2] * y + mm[4];
|
subBeziers[k++] = mm[1] * x + mm[3] * y + mm[5];
|
}
|
}
|
}
|
|
function prepareMorphPath(
|
fromPath: Path,
|
toPath: Path
|
) {
|
const fromPathProxy = fromPath.getUpdatedPathProxy();
|
const toPathProxy = toPath.getUpdatedPathProxy();
|
|
const [fromBezierCurves, toBezierCurves] =
|
alignBezierCurves(pathToBezierCurves(fromPathProxy), pathToBezierCurves(toPathProxy));
|
|
const fromPathTransform = fromPath.getComputedTransform();
|
const toPathTransform = toPath.getComputedTransform();
|
function updateIdentityTransform(this: Transformable) {
|
this.transform = null;
|
}
|
fromPathTransform && applyTransformOnBeziers(fromBezierCurves, fromPathTransform);
|
toPathTransform && applyTransformOnBeziers(toBezierCurves, toPathTransform);
|
// Just ignore transform
|
saveAndModifyMethod(toPath, 'updateTransform', { replace: updateIdentityTransform });
|
toPath.transform = null;
|
|
const morphingData = findBestMorphingRotation(fromBezierCurves, toBezierCurves, 10, Math.PI);
|
|
const tmpArr: number[] = [];
|
|
saveAndModifyMethod(toPath, 'buildPath', { replace(path: PathProxy) {
|
const t = (toPath as MorphingPath).__morphT;
|
const onet = 1 - t;
|
|
const newCp: number[] = [];
|
|
for (let i = 0; i < morphingData.length; i++) {
|
const item = morphingData[i];
|
const from = item.from;
|
const to = item.to;
|
const angle = item.rotation * t;
|
const fromCp = item.fromCp;
|
const toCp = item.toCp;
|
const sa = Math.sin(angle);
|
const ca = Math.cos(angle);
|
|
lerp(newCp, fromCp, toCp, t);
|
|
for (let m = 0; m < from.length; m += 2) {
|
const x0 = from[m];
|
const y0 = from[m + 1];
|
const x1 = to[m];
|
const y1 = to[m + 1];
|
|
const x = x0 * onet + x1 * t;
|
const y = y0 * onet + y1 * t;
|
|
tmpArr[m] = (x * ca - y * sa) + newCp[0];
|
tmpArr[m + 1] = (x * sa + y * ca) + newCp[1];
|
}
|
|
let x0 = tmpArr[0];
|
let y0 = tmpArr[1];
|
|
path.moveTo(x0, y0);
|
|
for (let m = 2; m < from.length;) {
|
const x1 = tmpArr[m++];
|
const y1 = tmpArr[m++];
|
const x2 = tmpArr[m++];
|
const y2 = tmpArr[m++];
|
const x3 = tmpArr[m++];
|
const y3 = tmpArr[m++];
|
|
// Is a line.
|
if (x0 === x1 && y0 === y1 && x2 === x3 && y2 === y3) {
|
path.lineTo(x3, y3);
|
}
|
else {
|
path.bezierCurveTo(x1, y1, x2, y2, x3, y3);
|
}
|
x0 = x3;
|
y0 = y3;
|
}
|
}
|
} });
|
}
|
|
/**
|
* Morphing from old path to new path.
|
*/
|
export function morphPath(
|
fromPath: Path,
|
toPath: Path,
|
animationOpts: ElementAnimateConfig
|
): Path {
|
if (!fromPath || !toPath) {
|
return toPath;
|
}
|
|
const oldDone = animationOpts.done;
|
// const oldAborted = animationOpts.aborted;
|
const oldDuring = animationOpts.during;
|
|
prepareMorphPath(fromPath, toPath);
|
|
(toPath as MorphingPath).__morphT = 0;
|
|
function restoreToPath() {
|
restoreMethod(toPath, 'buildPath');
|
restoreMethod(toPath, 'updateTransform');
|
// Mark as not in morphing
|
(toPath as MorphingPath).__morphT = -1;
|
// Cleanup.
|
toPath.createPathProxy();
|
toPath.dirtyShape();
|
}
|
|
toPath.animateTo({
|
__morphT: 1
|
} as any, defaults({
|
during(p) {
|
toPath.dirtyShape();
|
oldDuring && oldDuring(p);
|
},
|
done() {
|
restoreToPath();
|
oldDone && oldDone();
|
}
|
// NOTE: Don't do restore if aborted.
|
// Because all status was just set when animation started.
|
// aborted() {
|
// oldAborted && oldAborted();
|
// }
|
} as ElementAnimateConfig, animationOpts));
|
|
return toPath;
|
}
|
|
// https://github.com/mapbox/earcut/blob/master/src/earcut.js#L437
|
// https://jsfiddle.net/pissang/2jk7x145/
|
// function zOrder(x: number, y: number, minX: number, minY: number, maxX: number, maxY: number) {
|
// // Normalize coords to 0 - 1
|
// // The transformed into non-negative 15-bit integer range
|
// x = (maxX === minX) ? 0 : Math.round(32767 * (x - minX) / (maxX - minX));
|
// y = (maxY === minY) ? 0 : Math.round(32767 * (y - minY) / (maxY - minY));
|
|
// x = (x | (x << 8)) & 0x00FF00FF;
|
// x = (x | (x << 4)) & 0x0F0F0F0F;
|
// x = (x | (x << 2)) & 0x33333333;
|
// x = (x | (x << 1)) & 0x55555555;
|
|
// y = (y | (y << 8)) & 0x00FF00FF;
|
// y = (y | (y << 4)) & 0x0F0F0F0F;
|
// y = (y | (y << 2)) & 0x33333333;
|
// y = (y | (y << 1)) & 0x55555555;
|
|
// return x | (y << 1);
|
// }
|
|
// https://github.com/w8r/hilbert/blob/master/hilbert.js#L30
|
// https://jsfiddle.net/pissang/xdnbzg6v/
|
function hilbert(x: number, y: number, minX: number, minY: number, maxX: number, maxY: number) {
|
const bits = 16;
|
x = (maxX === minX) ? 0 : Math.round(32767 * (x - minX) / (maxX - minX));
|
y = (maxY === minY) ? 0 : Math.round(32767 * (y - minY) / (maxY - minY));
|
|
let d = 0;
|
let tmp: number;
|
for (let s = (1 << bits) / 2; s > 0; s /= 2) {
|
let rx = 0;
|
let ry = 0;
|
|
if ((x & s) > 0) {
|
rx = 1;
|
}
|
if ((y & s) > 0) {
|
ry = 1;
|
}
|
|
d += s * s * ((3 * rx) ^ ry);
|
|
if (ry === 0) {
|
if (rx === 1) {
|
x = s - 1 - x;
|
y = s - 1 - y;
|
}
|
tmp = x;
|
x = y;
|
y = tmp;
|
}
|
}
|
return d;
|
}
|
|
// Sort paths on hilbert. Not using z-order because it may still have large cross.
|
// So the left most source can animate to the left most target, not right most target.
|
// Hope in this way. We can make sure each element is animated to the proper target. Not the farthest.
|
function sortPaths(pathList: Path[]): Path[] {
|
let xMin = Infinity;
|
let yMin = Infinity;
|
let xMax = -Infinity;
|
let yMax = -Infinity;
|
const cps = map(pathList, path => {
|
const rect = path.getBoundingRect();
|
const m = path.getComputedTransform();
|
const x = rect.x + rect.width / 2 + (m ? m[4] : 0);
|
const y = rect.y + rect.height / 2 + (m ? m[5] : 0);
|
xMin = Math.min(x, xMin);
|
yMin = Math.min(y, yMin);
|
xMax = Math.max(x, xMax);
|
yMax = Math.max(y, yMax);
|
return [x, y];
|
});
|
|
const items = map(cps, (cp, idx) => {
|
return {
|
cp,
|
z: hilbert(cp[0], cp[1], xMin, yMin, xMax, yMax),
|
path: pathList[idx]
|
};
|
});
|
|
return items.sort((a, b) => a.z - b.z).map(item => item.path);
|
}
|
|
export interface DividePathParams {
|
path: Path,
|
count: number
|
};
|
export interface DividePath {
|
(params: DividePathParams): Path[]
|
}
|
|
export interface IndividualDelay {
|
(index: number, count: number, fromPath: Path, toPath: Path): number
|
}
|
|
function defaultDividePath(param: DividePathParams) {
|
return split(param.path, param.count);
|
}
|
export interface CombineConfig extends ElementAnimateConfig {
|
/**
|
* Transform of returned will be ignored.
|
*/
|
dividePath?: DividePath
|
/**
|
* delay of each individual.
|
* Because individual are sorted on z-order. The index is also sorted top-left / right-down.
|
*/
|
individualDelay?: IndividualDelay
|
/**
|
* If sort splitted paths so the movement between them can be more natural
|
*/
|
// sort?: boolean
|
}
|
|
function createEmptyReturn() {
|
return {
|
fromIndividuals: [] as Path[],
|
toIndividuals: [] as Path[],
|
count: 0
|
};
|
}
|
/**
|
* Make combine morphing from many paths to one.
|
* Will return a group to replace the original path.
|
*/
|
export function combineMorph(
|
fromList: (CombineMorphingPath | Path)[],
|
toPath: Path,
|
animationOpts: CombineConfig
|
) {
|
let fromPathList: Path[] = [];
|
|
function addFromPath(fromList: Element[]) {
|
for (let i = 0; i < fromList.length; i++) {
|
const from = fromList[i];
|
if (isCombineMorphing(from)) {
|
addFromPath((from as GroupLike).childrenRef());
|
}
|
else if (from instanceof Path) {
|
fromPathList.push(from);
|
}
|
}
|
}
|
addFromPath(fromList);
|
|
const separateCount = fromPathList.length;
|
|
// fromPathList.length is 0.
|
if (!separateCount) {
|
return createEmptyReturn();
|
}
|
|
const dividePath = animationOpts.dividePath || defaultDividePath;
|
|
let toSubPathList = dividePath({
|
path: toPath, count: separateCount
|
});
|
if (toSubPathList.length !== separateCount) {
|
console.error('Invalid morphing: unmatched splitted path');
|
return createEmptyReturn();
|
}
|
|
fromPathList = sortPaths(fromPathList);
|
toSubPathList = sortPaths(toSubPathList);
|
|
const oldDone = animationOpts.done;
|
// const oldAborted = animationOpts.aborted;
|
const oldDuring = animationOpts.during;
|
const individualDelay = animationOpts.individualDelay;
|
|
const identityTransform = new Transformable();
|
|
for (let i = 0; i < separateCount; i++) {
|
const from = fromPathList[i];
|
const to = toSubPathList[i];
|
|
to.parent = toPath as unknown as Group;
|
|
// Ignore transform in each subpath.
|
to.copyTransform(identityTransform);
|
|
// Will do morphPath for each individual if individualDelay is set.
|
if (!individualDelay) {
|
prepareMorphPath(from, to);
|
}
|
}
|
|
(toPath as CombineMorphingPath).__isCombineMorphing = true;
|
(toPath as CombineMorphingPath).childrenRef = function () {
|
return toSubPathList;
|
};
|
|
function addToSubPathListToZr(zr: ZRenderType) {
|
for (let i = 0; i < toSubPathList.length; i++) {
|
toSubPathList[i].addSelfToZr(zr);
|
}
|
}
|
saveAndModifyMethod(toPath, 'addSelfToZr', {
|
after(zr) {
|
addToSubPathListToZr(zr);
|
}
|
});
|
saveAndModifyMethod(toPath, 'removeSelfFromZr', {
|
after(zr) {
|
for (let i = 0; i < toSubPathList.length; i++) {
|
toSubPathList[i].removeSelfFromZr(zr);
|
}
|
}
|
});
|
|
function restoreToPath() {
|
(toPath as CombineMorphingPath).__isCombineMorphing = false;
|
// Mark as not in morphing
|
(toPath as MorphingPath).__morphT = -1;
|
(toPath as CombineMorphingPath).childrenRef = null;
|
|
restoreMethod(toPath, 'addSelfToZr');
|
restoreMethod(toPath, 'removeSelfFromZr');
|
}
|
|
const toLen = toSubPathList.length;
|
|
if (individualDelay) {
|
let animating = toLen;
|
const eachDone = () => {
|
animating--;
|
if (animating === 0) {
|
restoreToPath();
|
oldDone && oldDone();
|
}
|
};
|
// Animate each element individually.
|
for (let i = 0; i < toLen; i++) {
|
// TODO only call during once?
|
const indivdualAnimationOpts = individualDelay ? defaults({
|
delay: (animationOpts.delay || 0) + individualDelay(i, toLen, fromPathList[i], toSubPathList[i]),
|
done: eachDone
|
} as ElementAnimateConfig, animationOpts) : animationOpts;
|
morphPath(fromPathList[i], toSubPathList[i], indivdualAnimationOpts);
|
}
|
}
|
else {
|
(toPath as MorphingPath).__morphT = 0;
|
toPath.animateTo({
|
__morphT: 1
|
} as any, defaults({
|
during(p) {
|
for (let i = 0; i < toLen; i++) {
|
const child = toSubPathList[i] as MorphingPath;
|
child.__morphT = (toPath as MorphingPath).__morphT;
|
child.dirtyShape();
|
}
|
oldDuring && oldDuring(p);
|
},
|
done() {
|
restoreToPath();
|
for (let i = 0; i < fromList.length; i++) {
|
restoreMethod(fromList[i], 'updateTransform');
|
}
|
oldDone && oldDone();
|
}
|
} as ElementAnimateConfig, animationOpts));
|
}
|
|
if (toPath.__zr) {
|
addToSubPathListToZr(toPath.__zr);
|
}
|
|
return {
|
fromIndividuals: fromPathList,
|
toIndividuals: toSubPathList,
|
count: toLen
|
};
|
}
|
export interface SeparateConfig extends ElementAnimateConfig {
|
dividePath?: DividePath
|
individualDelay?: IndividualDelay
|
/**
|
* If sort splitted paths so the movement between them can be more natural
|
*/
|
// sort?: boolean
|
// // If the from path of separate animation is doing combine animation.
|
// // And the paths number is not same with toPathList. We need to do enter/leave animation
|
// // on the missing/spare paths.
|
// enter?: (el: Path) => void
|
// leave?: (el: Path) => void
|
}
|
|
/**
|
* Make separate morphing from one path to many paths.
|
* Make the MorphingKind of `toPath` become `'ONE_ONE'`.
|
*/
|
export function separateMorph(
|
fromPath: Path,
|
toPathList: Path[],
|
animationOpts: SeparateConfig
|
) {
|
const toLen = toPathList.length;
|
let fromPathList: Path[] = [];
|
|
const dividePath = animationOpts.dividePath || defaultDividePath;
|
|
function addFromPath(fromList: Element[]) {
|
for (let i = 0; i < fromList.length; i++) {
|
const from = fromList[i];
|
if (isCombineMorphing(from)) {
|
addFromPath((from as GroupLike).childrenRef());
|
}
|
else if (from instanceof Path) {
|
fromPathList.push(from);
|
}
|
}
|
}
|
// This case most happen when a combining path is called to reverse the animation
|
// to its original separated state.
|
if (isCombineMorphing(fromPath)) {
|
addFromPath(fromPath.childrenRef());
|
|
const fromLen = fromPathList.length;
|
if (fromLen < toLen) {
|
let k = 0;
|
for (let i = fromLen; i < toLen; i++) {
|
// Create a clone
|
fromPathList.push(clonePath(fromPathList[k++ % fromLen]));
|
}
|
}
|
// Else simply remove if fromLen > toLen
|
fromPathList.length = toLen;
|
}
|
else {
|
fromPathList = dividePath({ path: fromPath, count: toLen });
|
const fromPathTransform = fromPath.getComputedTransform();
|
for (let i = 0; i < fromPathList.length; i++) {
|
// Force use transform of source path.
|
fromPathList[i].setLocalTransform(fromPathTransform);
|
}
|
if (fromPathList.length !== toLen) {
|
console.error('Invalid morphing: unmatched splitted path');
|
return createEmptyReturn();
|
}
|
}
|
|
fromPathList = sortPaths(fromPathList);
|
toPathList = sortPaths(toPathList);
|
|
const individualDelay = animationOpts.individualDelay;
|
for (let i = 0; i < toLen; i++) {
|
const indivdualAnimationOpts = individualDelay ? defaults({
|
delay: (animationOpts.delay || 0) + individualDelay(i, toLen, fromPathList[i], toPathList[i])
|
} as ElementAnimateConfig, animationOpts) : animationOpts;
|
morphPath(fromPathList[i], toPathList[i], indivdualAnimationOpts);
|
}
|
|
return {
|
fromIndividuals: fromPathList,
|
toIndividuals: toPathList,
|
count: toPathList.length
|
};
|
}
|
|
export { split as defaultDividePath };
|