Skip to content

Commit a42cee0

Browse files
committed
feat: fix bug with parent clip
1 parent f2baf69 commit a42cee0

2 files changed

Lines changed: 250 additions & 0 deletions

File tree

packages/vrender-animate/__tests__/unit/animation-runtime-attribute.test.ts

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -958,6 +958,138 @@ describe('D3 pre-handoff animation runtime', () => {
958958
expect(rect.getFinalAttribute().width).toBeCloseTo(threeItemLayout.width, 5);
959959
});
960960

961+
test('executor update keeps parent clip path in sync with child transient layout', () => {
962+
const { group, ticker, graphicService } = createStageHarness('executor-update-clip-path-sync');
963+
const currentLayout = {
964+
x: 266.8696428571429,
965+
y: 404,
966+
y1: 808,
967+
width: 142.39017857142858,
968+
visible: true
969+
};
970+
const finalLayout = {
971+
x: 257.01875,
972+
y: 404,
973+
y1: 808,
974+
width: 112.8375,
975+
visible: true
976+
};
977+
const rect = createRect(currentLayout);
978+
const clipRect = createRect(finalLayout);
979+
bindGraphicService(rect as any, graphicService);
980+
bindGraphicService(clipRect as any, graphicService);
981+
rect.setFinalAttributes(currentLayout);
982+
clipRect.setFinalAttributes(finalLayout);
983+
group.appendChild(rect);
984+
group.setAttributes({
985+
clip: true,
986+
path: [clipRect]
987+
});
988+
989+
(rect as any).context = {
990+
animationState: 'update',
991+
data: [{ value: 10 }],
992+
diffAttrs: {
993+
x: finalLayout.x,
994+
width: finalLayout.width
995+
},
996+
finalAttrs: finalLayout
997+
};
998+
rect.setFinalAttributes(finalLayout);
999+
1000+
new AnimateExecutor(group).execute({
1001+
type: 'update',
1002+
duration: 300,
1003+
easing: 'linear'
1004+
});
1005+
1006+
expect(clipRect.attribute.x).toBeCloseTo(currentLayout.x, 5);
1007+
expect(clipRect.attribute.width).toBeCloseTo(currentLayout.width, 5);
1008+
expect((clipRect as any).baseAttributes.x).toBeCloseTo(finalLayout.x, 5);
1009+
expect((clipRect as any).baseAttributes.width).toBeCloseTo(finalLayout.width, 5);
1010+
1011+
tick(ticker, 20);
1012+
expect(rect.attribute.x).toBeLessThan(currentLayout.x);
1013+
expect(rect.attribute.x).toBeGreaterThan(finalLayout.x);
1014+
expect(rect.attribute.width).toBeLessThan(currentLayout.width);
1015+
expect(rect.attribute.width).toBeGreaterThan(finalLayout.width);
1016+
expect(clipRect.attribute.x).toBeCloseTo(rect.attribute.x, 5);
1017+
expect(clipRect.attribute.width).toBeCloseTo(rect.attribute.width, 5);
1018+
expect((clipRect as any).baseAttributes.x).toBeCloseTo(finalLayout.x, 5);
1019+
expect((clipRect as any).baseAttributes.width).toBeCloseTo(finalLayout.width, 5);
1020+
1021+
tick(ticker, 280);
1022+
expect(rect.attribute.x).toBeCloseTo(finalLayout.x, 5);
1023+
expect(rect.attribute.width).toBeCloseTo(finalLayout.width, 5);
1024+
expect(clipRect.attribute.x).toBeCloseTo(finalLayout.x, 5);
1025+
expect(clipRect.attribute.width).toBeCloseTo(finalLayout.width, 5);
1026+
expect((clipRect as any).baseAttributes.x).toBeCloseTo(finalLayout.x, 5);
1027+
expect((clipRect as any).baseAttributes.width).toBeCloseTo(finalLayout.width, 5);
1028+
expect(clipRect.getFinalAttribute().x).toBeCloseTo(finalLayout.x, 5);
1029+
expect(clipRect.getFinalAttribute().width).toBeCloseTo(finalLayout.width, 5);
1030+
});
1031+
1032+
test('executor update does not sync unrelated parent clip paths', () => {
1033+
const { group, ticker, graphicService } = createStageHarness('executor-update-unrelated-clip-path');
1034+
const currentLayout = {
1035+
x: 100,
1036+
y: 0,
1037+
y1: 100,
1038+
width: 80,
1039+
visible: true
1040+
};
1041+
const finalLayout = {
1042+
x: 120,
1043+
y: 0,
1044+
y1: 100,
1045+
width: 40,
1046+
visible: true
1047+
};
1048+
const unrelatedClipLayout = {
1049+
x: 0,
1050+
y: 0,
1051+
y1: 100,
1052+
width: 300,
1053+
visible: true
1054+
};
1055+
const rect = createRect(currentLayout);
1056+
const clipRect = createRect(unrelatedClipLayout);
1057+
bindGraphicService(rect as any, graphicService);
1058+
bindGraphicService(clipRect as any, graphicService);
1059+
rect.setFinalAttributes(currentLayout);
1060+
clipRect.setFinalAttributes(unrelatedClipLayout);
1061+
group.appendChild(rect);
1062+
group.setAttributes({
1063+
clip: true,
1064+
path: [clipRect]
1065+
});
1066+
1067+
(rect as any).context = {
1068+
animationState: 'update',
1069+
data: [{ value: 10 }],
1070+
diffAttrs: {
1071+
x: finalLayout.x,
1072+
width: finalLayout.width
1073+
},
1074+
finalAttrs: finalLayout
1075+
};
1076+
rect.setFinalAttributes(finalLayout);
1077+
1078+
new AnimateExecutor(group).execute({
1079+
type: 'update',
1080+
duration: 100,
1081+
easing: 'linear'
1082+
});
1083+
1084+
tick(ticker, 50);
1085+
expect(rect.attribute.x).toBeCloseTo(110, 5);
1086+
expect(rect.attribute.width).toBeCloseTo(60, 5);
1087+
expect(clipRect.attribute.x).toBeCloseTo(unrelatedClipLayout.x, 5);
1088+
expect(clipRect.attribute.width).toBeCloseTo(unrelatedClipLayout.width, 5);
1089+
expect((clipRect as any).baseAttributes.x).toBeCloseTo(unrelatedClipLayout.x, 5);
1090+
expect((clipRect as any).baseAttributes.width).toBeCloseTo(unrelatedClipLayout.width, 5);
1091+
});
1092+
9611093
test('switching states mid-animation restores to the new static truth and blocks late writes', () => {
9621094
const { group, ticker, graphicService } = createStageHarness('state-conflict');
9631095
const rect = createAnimatedRect(graphicService);

packages/vrender-animate/src/custom/update.ts

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
import { AttributeUpdateType, type EasingType, type IAnimate, type IStep } from '@visactor/vrender-core';
22
import { ACustomAnimate } from './custom-animate';
3+
import { applyAnimationTransientAttributes } from './transient';
4+
5+
const clipPathGeometryAttrs: Record<string, true> = {
6+
x: true,
7+
y: true,
8+
x1: true,
9+
y1: true,
10+
width: true,
11+
height: true
12+
};
313

414
export interface IUpdateAnimationOptions {
515
diffAttrs: Record<string, any>;
@@ -16,6 +26,10 @@ export interface IUpdateAnimationOptions {
1626
export class Update extends ACustomAnimate<Record<string, number>> {
1727
declare valid: boolean;
1828
// params: IUpdateAnimationOptions;
29+
private clipPathSyncKeys: string[] | null = null;
30+
private clipPathSyncParent: any = null;
31+
private clipPathSyncChildIndex: number = -1;
32+
private clipPathSyncDisabled: boolean = false;
1933

2034
constructor(from: null, to: null, duration: number, easing: EasingType, params?: IUpdateAnimationOptions) {
2135
super(from, to, duration, easing, params);
@@ -35,6 +49,9 @@ export class Update extends ACustomAnimate<Record<string, number>> {
3549
}
3650

3751
this.props = diffAttrs;
52+
this.clipPathSyncKeys = Object.keys(diffAttrs).filter(key => clipPathGeometryAttrs[key]);
53+
this.clipPathSyncDisabled = !this.clipPathSyncKeys.length;
54+
this.syncParentClipPathToTarget();
3855
}
3956

4057
private getStaticCommitAttrs(): Record<string, any> | null {
@@ -78,6 +95,7 @@ export class Update extends ACustomAnimate<Record<string, number>> {
7895
if (commitAttrs) {
7996
this.target.setAttributes(commitAttrs, false, { type: AttributeUpdateType.ANIMATE_END });
8097
}
98+
this.syncParentClipPathToTarget();
8199
super.onEnd();
82100
}
83101

@@ -100,6 +118,106 @@ export class Update extends ACustomAnimate<Record<string, number>> {
100118
const toValue = this.props[key];
101119
func(key, fromValue, toValue, easedRatio, this, this.target);
102120
});
121+
this.syncParentClipPathToTarget();
103122
this.onUpdate(end, easedRatio, out);
104123
}
124+
125+
private syncParentClipPathToTarget(): void {
126+
if (this.clipPathSyncDisabled) {
127+
return;
128+
}
129+
130+
const target = this.target as any;
131+
const parent = target.parent as any;
132+
const path = parent?.attribute?.path;
133+
if (!parent?.attribute?.clip || !Array.isArray(path) || !path.length) {
134+
return;
135+
}
136+
137+
const childIndex = this.getClipPathSyncChildIndex(parent);
138+
if (childIndex < 0 || childIndex >= path.length) {
139+
return;
140+
}
141+
142+
const clipGraphic = path[childIndex] as any;
143+
if (!clipGraphic?.attribute || clipGraphic.type !== target.type || !this.isClipPathStaticTarget(clipGraphic)) {
144+
this.clipPathSyncDisabled = true;
145+
return;
146+
}
147+
148+
const syncAttrs = this.buildClipPathTransientAttrs(clipGraphic);
149+
if (syncAttrs) {
150+
applyAnimationTransientAttributes(clipGraphic, syncAttrs, AttributeUpdateType.ANIMATE_UPDATE);
151+
}
152+
}
153+
154+
private getClipPathSyncChildIndex(parent: any): number {
155+
if (this.clipPathSyncParent === parent && this.clipPathSyncChildIndex >= 0) {
156+
return this.clipPathSyncChildIndex;
157+
}
158+
159+
const target = this.target as any;
160+
let childIndex = -1;
161+
parent.forEachChildren?.((child: unknown, index: number) => {
162+
if (child === target) {
163+
childIndex = index;
164+
return true;
165+
}
166+
return false;
167+
});
168+
169+
this.clipPathSyncParent = parent;
170+
this.clipPathSyncChildIndex = childIndex;
171+
return childIndex;
172+
}
173+
174+
private isClipPathStaticTarget(clipGraphic: any): boolean {
175+
const target = this.target as any;
176+
const targetFinalAttrs = this.getTargetFinalAttrs();
177+
const clipGraphicFinalAttrs =
178+
typeof clipGraphic.getFinalAttribute === 'function'
179+
? clipGraphic.getFinalAttribute()
180+
: clipGraphic.finalAttribute;
181+
const clipFinalAttrs = clipGraphicFinalAttrs ?? clipGraphic.baseAttributes ?? clipGraphic.attribute;
182+
const keys = this.clipPathSyncKeys ?? [];
183+
if (!keys.length || !targetFinalAttrs || !clipFinalAttrs) {
184+
return false;
185+
}
186+
187+
return keys.every(key =>
188+
this.isSameClipPathValue(clipFinalAttrs[key], targetFinalAttrs[key] ?? target.attribute?.[key])
189+
);
190+
}
191+
192+
private getTargetFinalAttrs(): Record<string, any> | null {
193+
const target = this.target as any;
194+
return (
195+
target.context?.finalAttrs ??
196+
(typeof target.getFinalAttribute === 'function' ? target.getFinalAttribute() : target.finalAttribute) ??
197+
null
198+
);
199+
}
200+
201+
private isSameClipPathValue(a: any, b: any): boolean {
202+
if (typeof a === 'number' && typeof b === 'number') {
203+
return Math.abs(a - b) < 1e-8;
204+
}
205+
return a === b;
206+
}
207+
208+
private buildClipPathTransientAttrs(clipGraphic: any): Record<string, any> | null {
209+
const target = this.target as any;
210+
const attrs: Record<string, any> = {};
211+
(this.clipPathSyncKeys ?? []).forEach(key => {
212+
const nextValue = target.attribute?.[key];
213+
if (
214+
Object.prototype.hasOwnProperty.call(clipGraphic.attribute, key) &&
215+
nextValue !== undefined &&
216+
!this.isSameClipPathValue(clipGraphic.attribute[key], nextValue)
217+
) {
218+
attrs[key] = nextValue;
219+
}
220+
});
221+
return Object.keys(attrs).length ? attrs : null;
222+
}
105223
}

0 commit comments

Comments
 (0)