Skip to content

Fix/aniamtion state #4039

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Jun 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
238 changes: 118 additions & 120 deletions packages/vchart-schema/vchart.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions packages/vchart/src/component/player/player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,7 @@ export class Player extends BaseComponent<IPlayer> implements IComponent {
// 自动播放
this._option.globalInstance.on(ChartEvent.rendered, () => {
if (this._spec?.auto) {
this._playerComponent.pause();
this._playerComponent.play();
}
});
Expand Down
11 changes: 5 additions & 6 deletions packages/vchart/src/core/vchart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -793,7 +793,6 @@ export class VChart implements IVChart {
if (!this._beforeRender(option)) {
return self;
}
this._updateAnimateState(true);
// 填充数据绘图
this._compiler?.render(option.morphConfig);
this._updateAnimateState(false);
Expand All @@ -811,7 +810,7 @@ export class VChart implements IVChart {
const updateGraphicAnimationState = (graphic: IMarkGraphic) => {
const diffState = graphic.context?.diffState;
if (initial) {
return diffState === 'exit' ? undefined : AnimationStateEnum.appear;
return diffState === 'exit' ? AnimationStateEnum.none : AnimationStateEnum.appear;
}
return diffState;
};
Expand Down Expand Up @@ -939,13 +938,12 @@ export class VChart implements IVChart {
if (this._chart) {
this._chart.updateData(id, data, true, parserOptions);

// after layout
this._compiler.render();

if (userUpdateOptions?.reAnimate) {
this.stopAnimation();
this._updateAnimateState(true);
}
this._compiler.render();

return this as unknown as IVChart;
}
this._spec.data = array(this._spec.data);
Expand All @@ -968,11 +966,12 @@ export class VChart implements IVChart {
if (this._chart) {
this._chart.updateFullData(data);
if (reRender) {
this._compiler.render();
if (userUpdateOptions?.reAnimate) {
this.stopAnimation();
this._updateAnimateState(true);
}

this._compiler.render();
}
return this as unknown as IVChart;
}
Expand Down
133 changes: 71 additions & 62 deletions packages/vchart/src/mark/base/base-mark.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ import type {
IMarkStateManager,
StateValueType
} from '../../compile/mark/interface';
import { array, degreeToRadian, has, isArray, isBoolean, isFunction, isNil, isObject, isValid } from '@visactor/vutils';
import { array, degreeToRadian, isArray, isBoolean, isFunction, isNil, isObject, isValid } from '@visactor/vutils';
import { curveTypeTransform, groupData, runEncoder } from '../utils/common';
import type { ICompilableInitOption } from '../../compile/interface';
import { LayoutState } from '../../compile/interface';
Expand All @@ -67,7 +67,8 @@ import type { ICompilableData } from '../../compile/data/interface';
import type { IAnimationConfig } from '../../animation/interface';
import { AnimationStateEnum, type MarkAnimationSpec } from '../../animation/interface';
import { CompilableData } from '../../compile/data/compilable-data';
import { log } from '../../util';
import { getDiffAttributesOfGraphic } from '../../util/mark';
import { log } from '../../util/debug';
import { morph as runMorph } from '../../compile/morph';

export type ExChannelCall = (
Expand Down Expand Up @@ -1074,6 +1075,7 @@ export class BaseMark<T extends ICommonSpec> extends GrammarItem implements IMar
this._dataByKey = (mark as any)._dataByKey;
this._prevDataByKey = (mark as any)._prevDataByKey;
this.needClear = (mark as any).needClear;
this._aniamtionStateCallback = (mark as any)._aniamtionStateCallback;
}

private _parseProgressiveContext(data: Datum[]) {
Expand Down Expand Up @@ -1145,12 +1147,18 @@ export class BaseMark<T extends ICommonSpec> extends GrammarItem implements IMar
// TODO 因为数据的覆盖特点,无动画的时候新的更新一定会覆盖前一次的旧值,所以默认都是后面的动画覆盖前面的动画
// TODO 但是如果用户定义了一个动画数组,他的预期是动画不会覆盖,通过priority为INfinity来控制不覆盖
if (Array.isArray(config)) {
config = config.map((item: any, index: number) => ({
return config.map((item: any, index: number) => ({
...item,
priority: item.priority ?? Infinity
}));
}
return config;
return config
? {
...config,
// 循环动画的优先级定为最高,不会被屏蔽掉
priority: type === 'normal' ? config.priority ?? Infinity : config.priority
}
: config;
}

/**
Expand All @@ -1177,6 +1185,7 @@ export class BaseMark<T extends ICommonSpec> extends GrammarItem implements IMar
if (!this._animationConfig || graphics.length === 0) {
return;
}

if (this.tryRunMorphing(graphics)) {
return;
}
Expand Down Expand Up @@ -1262,10 +1271,29 @@ export class BaseMark<T extends ICommonSpec> extends GrammarItem implements IMar
}
}

protected _setAnimationState(g: IMarkGraphic) {
const customizedState = this._aniamtionStateCallback ? this._aniamtionStateCallback(g) : undefined;

g.context.animationState = customizedState ?? g.context.diffState;

// 复用exit的图元,需要设置属性为最初的属性
if (g.context.animationState === DiffState.exit) {
// 表示正在被复用,后续需要重设属性的
g.context.reusing = true;
// 停止所有动画,
// TODO:属性可能回不去了(如果enter和exit不是一个动画),所以在encode阶段要获取finalAttribute,设置上去
(g as any).animates && (g as any).animates.forEach((a: any) => a.stop());
// force element to stop exit animation if it is reentered
// todo animaiton
// const animators = this.animate?.getElementAnimators(element, DiffState.exit);
// animators && animators.forEach(animator => animator.stop('start'));
}
}

protected _runJoin(data: Datum[]) {
const newGroupedData = this._getDataByKey(data);
const prevGroupedData = this._prevDataByKey;
const newGraphics: IMarkGraphic[] = [];
const allGraphics: IMarkGraphic[] = [];

const enterGraphics = new Set<IMarkGraphic>(this._graphics.filter(g => g.context.diffState === DiffState.enter));

Expand All @@ -1289,32 +1317,20 @@ export class BaseMark<T extends ICommonSpec> extends GrammarItem implements IMar
diffState = DiffState.enter;
g.isExiting = false;

// 复用exit的图元,需要设置属性为最初的属性
if (g.context?.diffState === DiffState.exit) {
// 表示正在被复用,后续需要重设属性的
g.context.reusing = true;
// 停止所有动画,
// TODO:属性可能回不去了(如果enter和exit不是一个动画),所以在encode阶段要获取finalAttribute,设置上去
(g as any).animates && (g as any).animates.forEach((a: any) => a.stop());
// force element to stop exit animation if it is reentered
// todo animaiton
// const animators = this.animate?.getElementAnimators(element, DiffState.exit);
// animators && animators.forEach(animator => animator.stop('start'));
}

this._graphicMap.set(key, g as IMarkGraphic);
newGraphics.push(g as IMarkGraphic);
allGraphics.push(g as IMarkGraphic);
} else {
// update
g = this._graphicMap.get(key);

if (g) {
diffState = DiffState.update;
newGraphics.push(g as IMarkGraphic);
allGraphics.push(g);
}
}

if (g) {
enterGraphics.delete(g);
g.context = {
...this._getCommonContext(),
diffState,
Expand All @@ -1328,7 +1344,6 @@ export class BaseMark<T extends ICommonSpec> extends GrammarItem implements IMar
fieldX: g.context?.fieldX,
// 从旧context中继承
fieldY: g.context?.fieldY,
animationState: diffState,
// TODO 如果newData为空,则使用旧的data,避免exit图元找不到data
data: newData ?? g.context?.data,
uniqueKey: key,
Expand All @@ -1338,7 +1353,7 @@ export class BaseMark<T extends ICommonSpec> extends GrammarItem implements IMar
indexKey: '__VCHART_DEFAULT_DATA_INDEX',
stateAnimateConfig: this.getAnimationConfig()?.state
};
enterGraphics.delete(g);
this._setAnimationState(g);
}
return g;
};
Expand All @@ -1363,13 +1378,15 @@ export class BaseMark<T extends ICommonSpec> extends GrammarItem implements IMar
const g = callback(key, newGroupedData.data.get(key), null);
if (g) {
g.context.animationState = AnimationStateEnum.appear;
// this._setAnimationState(g);
}
});
} else if (prevGroupedData) {
prevGroupedData.keys.forEach(key => {
// disappear
const g = callback(key, null, prevGroupedData.data.get(key));
g.context.animationState = AnimationStateEnum.disappear;
// this._setAnimationState(g);
});
}

Expand All @@ -1383,13 +1400,13 @@ export class BaseMark<T extends ICommonSpec> extends GrammarItem implements IMar

(g as IMarkGraphic).release();
});
const graphicCount = newGraphics.length;
newGraphics.forEach((g, index) => {
const graphicCount = allGraphics.length;
allGraphics.forEach((g, index) => {
g.context.graphicCount = graphicCount;
g.context.graphicIndex = index;
});
this._dataByKey = newGroupedData;
this._graphics = newGraphics;
this._graphics = allGraphics;
this.needClear = true;
}

Expand Down Expand Up @@ -1543,14 +1560,7 @@ export class BaseMark<T extends ICommonSpec> extends GrammarItem implements IMar
this._graphicMap.set(g.context.uniqueKey, g);
}
} else {
// diff一下,获取差异的属性
const prevAttrs: Record<string, any> = g.getAttributes(true);
const diffAttrs: Record<string, any> = {};
Object.keys(finalAttrs).forEach(key => {
if (prevAttrs[key] !== finalAttrs[key]) {
diffAttrs[key] = finalAttrs[key];
}
});
const diffAttrs = getDiffAttributesOfGraphic(g, finalAttrs);
g.context.diffAttrs = diffAttrs;
if (g.context.reusing) {
// 表示正在被复用,需要重设属性的
Expand Down Expand Up @@ -1759,29 +1769,30 @@ export class BaseMark<T extends ICommonSpec> extends GrammarItem implements IMar
}
};
this._graphicMap.forEach((g, key) => {
if (g.context.diffState !== DiffState.exit || g.isExiting) {
return;
}
// 避免重复执行退场动画
if (g.context.diffState === DiffState.exit && !g.isExiting) {
if (this.hasAnimationByState('exit')) {
g.isExiting = true;
// 执行exit动画
const animationConfig = this.getAnimationConfig();
if ((animationConfig as any).exit && (animationConfig as any).exit.length) {
const exitConfigList = (animationConfig as any).exit.map((item: any, index: number) => ({
name: `exit_${index}`,
animation: {
...item,
customParameters: g.context
}
}));
g.applyAnimationState(['exit'], [exitConfigList.length === 1 ? exitConfigList[0] : exitConfigList], () => {
// 有可能又被复用了,所以这里需要判断,如果还是在exiting阶段的话才删除
// TODO 这里如果频繁执行的话,可能会误判
doRemove(g, key);
});
}
} else {
doRemove(g, key);
if (g.context.animationState === DiffState.exit && this.hasAnimationByState('exit')) {
g.isExiting = true;
// 执行exit动画
const animationConfig = this.getAnimationConfig();
if ((animationConfig as any).exit && (animationConfig as any).exit.length) {
const exitConfigList = (animationConfig as any).exit.map((item: any, index: number) => ({
name: `exit_${index}`,
animation: {
...item,
customParameters: g.context
}
}));
g.applyAnimationState(['exit'], [exitConfigList.length === 1 ? exitConfigList[0] : exitConfigList], () => {
// 有可能又被复用了,所以这里需要判断,如果还是在exiting阶段的话才删除
// TODO 这里如果频繁执行的话,可能会误判
doRemove(g, key);
});
}
} else {
doRemove(g, key);
}
});
}
Expand Down Expand Up @@ -1966,19 +1977,17 @@ export class BaseMark<T extends ICommonSpec> extends GrammarItem implements IMar
}
}

protected _aniamtionStateCallback: (g: IMarkGraphic) => AnimationStateValues;

updateAnimationState(callback: (graphic: IMarkGraphic) => AnimationStateValues) {
if (this._graphics && this._graphics.length) {
this._graphics.forEach(g => {
g.context.animationState = callback(g);
});
}
this._aniamtionStateCallback = callback;
}

hasAnimationByState(state: keyof MarkAnimationSpec) {
if (!state || !this._animationConfig || !this._animationConfig[state]) {
hasAnimationByState(state: AnimationStateValues) {
if (!state || !this._animationConfig || !(this._animationConfig as any)[state]) {
return false;
}
const stateAnimationConfig = this._animationConfig[state];
const stateAnimationConfig = (this._animationConfig as any)[state];
return (stateAnimationConfig as IAnimationConfig[]).length > 0 || isObject(stateAnimationConfig);
}

Expand Down
46 changes: 37 additions & 9 deletions packages/vchart/src/mark/group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,22 @@ import type { Maybe } from '../typings';
import { log, warn } from '../util/debug';
import type { IGroupMarkSpec } from '../typings/visual';
import { BaseMark } from './base/base-mark';
import type { AnimationStateValues, IGroupMark, IMark, IMarkGraphic, MarkType } from './interface';
import {
DiffState,
type AnimationStateValues,
type IGroupMark,
type IMark,
type IMarkGraphic,
type MarkType
} from './interface';
// eslint-disable-next-line no-duplicate-imports
import { MarkTypeEnum } from './interface/type';
import { type IMarkCompileOption } from '../compile/mark';
import type { IGroup, IGroupGraphicAttribute } from '@visactor/vrender-core';
import { registerGroup, registerShadowRoot } from '@visactor/vrender-kits';
import { isNil } from '@visactor/vutils';
import { traverseGroupMark } from '../compile/util';
import { LayoutState } from '../compile/interface';
import { getDiffAttributesOfGraphic } from '../util/mark';

export class GroupMark extends BaseMark<IGroupMarkSpec> implements IGroupMark {
static readonly type = MarkTypeEnum.group;
Expand Down Expand Up @@ -110,9 +117,33 @@ export class GroupMark extends BaseMark<IGroupMarkSpec> implements IGroupMark {
}

const style = this._simpleStyle ?? this.getAttributesOfState({});
const prevState = this._product.context?.diffState;

this._product.context = {
...this._product.context,
...this._getCommonContext(),
diffState: prevState ? DiffState.update : DiffState.enter
};
this._setAnimationState(this._product as unknown as IMarkGraphic);
const newAttrs = this._getAttrsFromConfig(style);

// TODO: 需要优化,现在group mark 走了一些特殊逻辑
if (this._product.context.diffState === DiffState.update) {
const hasAnimation = this.hasAnimation();
const diffAttrs = getDiffAttributesOfGraphic(this._product as unknown as IMarkGraphic, newAttrs);
this._product.context.diffAttrs = diffAttrs;

if (!this.hasAnimationByState(this._product.context.animationState)) {
hasAnimation ? this._product.setAttributesAndPreventAnimate(diffAttrs) : this._product.setAttributes(diffAttrs);
}

if (hasAnimation) {
this._product.setFinalAttributes(newAttrs);
}
} else {
this._product.setAttributes(newAttrs);
}

this._product.context = { ...this._product.context, ...this._getCommonContext() };
this._product.setAttributes(this._getAttrsFromConfig(style));
this.needClear = true;
}

Expand All @@ -129,11 +160,8 @@ export class GroupMark extends BaseMark<IGroupMarkSpec> implements IGroupMark {
}

updateAnimationState(callback: (g: IMarkGraphic) => AnimationStateValues) {
this.getGraphics().forEach(g => {
if (g) {
g.context = { ...g.context, animationState: callback(g) };
}
});
super.updateAnimationState(callback);

this.getMarks().forEach(mark => {
mark.updateAnimationState(callback);
});
Expand Down
Loading
Loading