Skip to content
This repository has been archived by the owner on Oct 7, 2023. It is now read-only.

Commit

Permalink
refactor: refactor axis animation
Browse files Browse the repository at this point in the history
  • Loading branch information
Aarebecca committed Feb 16, 2023
1 parent 6e0f692 commit 2c723df
Show file tree
Hide file tree
Showing 10 changed files with 226 additions and 46 deletions.
2 changes: 2 additions & 0 deletions __tests__/integration/animation/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './transition-1';
export * from './transition-2';
24 changes: 24 additions & 0 deletions __tests__/integration/animation/transition-1.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Group, Rect } from '@antv/g';
import { transition } from '../../../src/animation';

export function Transition1() {
const group = new Group();

const rect = group.appendChild(
new Rect({
style: {
x: 0,
y: 0,
width: 100,
height: 100,
fill: 'red',
},
})
);

setTimeout(() => {
transition(rect, { x: 100, y: 100, fill: 'green', width: 50, height: 50 }, { duration: 1000, fill: 'both' });
});

return group;
}
63 changes: 63 additions & 0 deletions __tests__/integration/animation/transition-2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { Group, Rect, Circle, Text } from '@antv/g';
import { transitionShape } from '../../../src/animation';

export function Transition2() {
const group = new Group();

const rect1 = group.appendChild(
new Rect({
style: {
x: 0,
y: 0,
width: 100,
height: 100,
fill: 'red',
},
})
);

const circle = group.appendChild(
new Circle({
style: {
cx: 200,
cy: 50,
r: 50,
fill: 'green',
},
})
);

setTimeout(async () => {
const animations1 = transitionShape(rect1, circle, { duration: 1000 });
await animations1.slice(-1)[0]!.finished;
const rect2 = group.appendChild(
new Rect({
style: {
x: 150,
y: 150,
width: 100,
height: 100,
fill: 'blue',
},
})
);
const animations2 = transitionShape(circle, rect2, { duration: 1000 });

await animations2.slice(-1)[0]!.finished;

const text = group.appendChild(
new Text({
style: {
x: 0,
y: 150,
text: 'Animation',
fontSize: 20,
},
})
);

transitionShape(rect2, text, { duration: 1000 });
});

return group;
}
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,5 @@ export const AxisLinearLabelOverlapMultiple = () => {
});
return group;
};

AxisLinearLabelOverlapMultiple.wait = 1000;
5 changes: 4 additions & 1 deletion __tests__/integration/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import { Renderer as SVGRenderer } from '@antv/g-svg';
import { Select, Tag } from 'antd';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import ReactDOM from 'react-dom';
import * as cases from './charts';
import * as staticCases from './charts';
import * as animationCases from './animation';

const cases = { ...staticCases, ...animationCases };

const { Option } = Select;

Expand Down
Binary file modified __tests__/integration/snapshots/AxisLinearLabelOverlapMultiple.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src/animation/fadeIn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export default function (element: DisplayObject, options: GenericAnimation) {
const opacity = element.attr('opacity') || 1;
if (!options) {
element.attr('opacity', 0);
return { finished: Promise.resolve() };
return null;
}
return element.animate([{ opacity: 0 }, { opacity }], options);
}
68 changes: 68 additions & 0 deletions src/animation/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import type { DisplayObject } from '@antv/g';
import { isNil } from '@antv/util';
import type { GUI } from '../core';
import { visibility } from '../util';
import type { AnimationOption, AnimationResult, GenericAnimation, StandardAnimationOption } from './types';

export function parseAnimationOption(option: AnimationOption): StandardAnimationOption {
Expand Down Expand Up @@ -49,6 +50,73 @@ export function animate(target: DisplayObject | GUI<any>, keyframes: Keyframe[],
return target.animate(keyframes, options);
}

/**
* transition source shape to target shape
* @param source
* @param target
* @param options
* @param after destroy or hide source shape after transition
*/
export function transitionShape(
source: DisplayObject,
target: DisplayObject,
options: GenericAnimation,
after: 'destroy' | 'hide' = 'destroy'
) {
const afterTransition = () => {
if (after === 'destroy') source.destroy();
else if (after === 'hide') visibility(source, false);
visibility(target, true);
};
if (!options) {
afterTransition();
return [null];
}
const { duration = 0, delay = 0 } = options;
const middle = Math.ceil(+duration / 2);
const offset = +duration / 4;

const getPosition = (shape: DisplayObject) => {
if (shape.nodeName === 'circle') {
const [cx, cy] = shape.getLocalPosition();
const r = shape.attr('r');
return [cx - r, cy - r];
}
return shape.getLocalPosition();
};

const [sx, sy] = getPosition(source);
const [ex, ey] = getPosition(target);
const [mx, my] = [(sx + ex) / 2 - sx, (sy + ey) / 2 - sy];

const sourceAnimation = source.animate(
[
{ opacity: 1, transform: 'translate(0, 0)' },
{ opacity: 0, transform: `translate(${mx}, ${my})` },
],
{
fill: 'both',
...options,
duration: delay + middle + offset,
}
);
const targetAnimation = target.animate(
[
{ opacity: 0, transform: `translate(${-mx}, ${-my})`, offset: 0.01 },
{ opacity: 1, transform: 'translate(0, 0)' },
],
{
fill: 'both',
...options,
duration: middle + offset,
delay: delay + middle - offset,
}
);

onAnimateFinished(targetAnimation, afterTransition);
return [sourceAnimation, targetAnimation];
}

/**
* execute transition animation on element
* @description in the current stage, only support the following properties:
Expand Down
99 changes: 59 additions & 40 deletions src/ui/axis/guides/labels.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import type { DisplayObject, Text } from '@antv/g';
import type { DisplayObject, IAnimation, Text } from '@antv/g';
import { vec2 } from '@antv/matrix-util';
import { get, isFunction, memoize } from '@antv/util';
import { RequiredStyleProps } from 'src/core';
import {
fadeOut,
onAnimateFinished,
transition,
type GenericAnimation,
transitionShape,
type StandardAnimationOption,
} from '../../../animation';
import type { Vector2 } from '../../../types';
Expand Down Expand Up @@ -177,8 +177,7 @@ function createLabel(
index: number,
data: AxisDatum[],
attr: RequiredStyleProps<AxisStyleProps>,
style: AxisLabelStyleProps['style'],
animate: GenericAnimation
style: AxisLabelStyleProps['style']
) {
// 1. set style
// 2. set position
Expand All @@ -197,38 +196,11 @@ function createLabel(
...labelStyle,
});

this.attr(groupStyle);
this.attr({ ...groupStyle, ...getLabelPos(datum, index, data, attr) });

percentTransform(this, transform);
const rotate = getLabelRotation(datum, this, attr);
setRotateAndAdjustLabelAlign(rotate, this, attr);

const animation = transition(this, getLabelPos(datum, index, data, attr), animate);
return animation;
}

function createLabels(
container: Selection,
element: Selection,
data: any[],
attr: RequiredStyleProps<AxisStyleProps>,
style: AxisLabelStyleProps,
animate: GenericAnimation
) {
const elements = get(element, '_elements') as _Element[];
if (elements.length === 0) return null;
const transitions = get(element, '_transitions');
const animations = elements.map((el: any) => createLabel.call(el, el.__data__, 0, data, attr, style.style, animate));

animations.forEach((a, i) => (transitions[i] = a));
// to avoid async manipulations
if (animations.filter((a) => !!a).length === 0) overlapHandler.call(container, attr);
else {
Promise.all(animations).then(() => {
overlapHandler.call(container, attr);
});
}
return animations;
}

export function renderLabels(
Expand All @@ -241,26 +213,73 @@ export function renderLabels(
element: formatter(datum, index, arr, attr),
...datum,
}));
const style = subStyleProps<AxisLabelStyleProps>(attr, 'label');
const { style } = subStyleProps<AxisLabelStyleProps>(attr, 'label');

return container
.selectAll(CLASS_NAMES.label.class)
.data(finalData, (d, i) => `${d.value}-${d.label}`)
.data(finalData, (d, i) => i)
.join(
(enter) =>
enter
.append('g')
.attr('className', CLASS_NAMES.label.name)
.call((element) => {
createLabels(container, element, finalData, attr, style, false);
.transition(function (datum, index) {
const label = select(this).append(datum.element).attr('className', CLASS_NAMES.labelItem.name).node();
const [labelStyle, { transform, ...groupStyle }] = styleSeparator(
getCallbackStyle(style, [datum, index, data])
);
percentTransform(this, transform);
const rotate = getLabelRotation(datum, this, attr);
this.setLocalEulerAngles(+rotate);

label?.nodeName === 'text' &&
label.attr({
fontSize: 12,
fontFamily: 'sans-serif',
fontWeight: 'normal',
textAlign: getLabelAlign(datum.value, rotate, attr),
textBaseline: 'middle',
...labelStyle,
});

this.attr({ ...groupStyle, ...getLabelPos(datum, index, data, attr) });
return null;
})
.call(() => {
overlapHandler.call(container, attr);
}),
(update) =>
update
.each(function () {
select(this).node().removeChildren();
.transition(function (datum, index) {
const prevLabel = this.querySelector(CLASS_NAMES.labelItem.class);
const label = select(this).append(datum.element).attr('className', CLASS_NAMES.labelItem.name).node();
const [labelStyle, { transform, ...groupStyle }] = styleSeparator(
getCallbackStyle(style, [datum, index, data])
);
percentTransform(this, transform);

const rotate = getLabelRotation(datum, this, attr);
this.setLocalEulerAngles(+rotate);
label?.nodeName === 'text' &&
label.attr({
fontSize: 12,
fontFamily: 'sans-serif',
fontWeight: 'normal',
textAlign: getLabelAlign(datum.value, rotate, attr),
textBaseline: 'middle',
...labelStyle,
});
this.attr(groupStyle);

const shapeAnimation = transitionShape(prevLabel, label, animate.update);
const animation = transition(this, getLabelPos(datum, index, data, attr), animate.update);
return [...shapeAnimation, animation];
})
.call((element) => {
createLabels(container, element, finalData, attr, style, animate.update);
.call((selection) => {
const transitions = get(selection, '_transitions') as (null | IAnimation)[];
Promise.all(transitions.filter((t) => !!t).map((t) => t?.finished)).then(() => {
overlapHandler.call(container, attr);
});
}),
(exit) =>
exit.transition(function () {
Expand Down
7 changes: 3 additions & 4 deletions src/util/selection.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// @ts-nocheck
import { Circle, Ellipse, Group, HTML, IDocument, Image, Line, Path, Polygon, Polyline, Rect, Text } from '@antv/g';
import type { BaseStyleProps as BP, DisplayObject } from '@antv/g';
import type { BaseStyleProps as BP, DisplayObject, IAnimation } from '@antv/g';
import { group } from 'd3-array';
import type { AnimationResult } from '../animation';

Expand Down Expand Up @@ -359,8 +359,7 @@ export class Selection<T = any> {
});
}

transition(value: any): Selection<T> {
const callback = typeof value !== 'function' ? () => value : value;
transition(callback?: (datum: T, index: number) => (null | IAnimation) | (null | IAnimation)[]): Selection<T> {
const { _transitions: T } = this;
return this.each(function (d, i) {
T[i] = callback.call(this, d, i);
Expand Down Expand Up @@ -388,7 +387,7 @@ export class Selection<T = any> {
}

transitions() {
return this._transitions;
return this._transitions.filter((t) => !!t);
}

parent(): DisplayObject {
Expand Down

0 comments on commit 2c723df

Please sign in to comment.