Skip to content

Commit e0b869e

Browse files
committed
fix(animate): preserve static truth in frame writes
Animation frame writes now use the transient path, so appear starts, wait frames, loop resets, and custom per-frame effects do not become static truth. MotionPath keeps the public endpoint contract by committing only at end unless commitOnEnd or saveOnEnd is explicitly false. Constraint: D3 static truth remains baseAttributes + resolvedStatePatch -> attribute Constraint: Do not restore normalAttrs snapshot/restore semantics Rejected: Make MotionPath transient by default | examples expect path endpoint Confidence: high Scope-risk: moderate Directive: Process frames must not call static setters without end commit semantics Tested: packages/vrender-animate touched eslint Tested: packages/vrender-animate rushx test --runInBand Tested: vrender-core static truth/state unit subset Tested: rush compile -t @visactor/vrender-animate Tested: rush build --to @visactor/vrender-animate Tested: VRender-only bar appear repro Not-tested: Full repository test matrix
1 parent 2b52e59 commit e0b869e

29 files changed

Lines changed: 567 additions & 85 deletions

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

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { createGroup, createRect } from '@visactor/vrender-core';
22
import { registerAnimate } from '../../src/register';
33
import { registerCustomAnimate } from '../../src/custom/register';
4+
import { AnimateExecutor } from '../../src/executor/animate-executor';
45
import { DefaultTimeline } from '../../src/timeline';
56
import { ManualTicker } from '../../src/ticker/manual-ticker';
67

@@ -159,6 +160,96 @@ describe('D3 pre-handoff animation runtime', () => {
159160
expect((rect as any).baseAttributes.x).toBe(80);
160161
});
161162

163+
test('wait step holds the previous frame without committing it to baseAttributes', () => {
164+
const { group, ticker, graphicService } = createStageHarness('wait-step');
165+
const rect = createAnimatedRect(graphicService);
166+
group.appendChild(rect);
167+
168+
rect.animate().to({ x: 100 }, 100, 'linear').wait(50).to({ x: 200 }, 100, 'linear');
169+
170+
tick(ticker, 100);
171+
expect(rect.attribute.x).toBeCloseTo(100, 5);
172+
expect((rect as any).baseAttributes.x).toBe(0);
173+
174+
tick(ticker, 1);
175+
expect(rect.attribute.x).toBeCloseTo(100, 5);
176+
expect((rect as any).baseAttributes.x).toBe(0);
177+
});
178+
179+
test('loop reset applies the start frame without overwriting current static truth', () => {
180+
const { group, ticker, graphicService } = createStageHarness('loop-reset');
181+
const rect = createAnimatedRect(graphicService);
182+
group.appendChild(rect);
183+
184+
rect.animate().to({ x: 100 }, 100, 'linear').loop(1);
185+
186+
tick(ticker, 50);
187+
expect(rect.attribute.x).toBeCloseTo(50, 5);
188+
rect.setAttribute('x', 20);
189+
expect((rect as any).baseAttributes.x).toBe(20);
190+
191+
tick(ticker, 50);
192+
expect(rect.attribute.x).toBe(0);
193+
expect((rect as any).baseAttributes.x).toBe(20);
194+
});
195+
196+
test('stop with an explicit target is a static commit API', () => {
197+
const { group, ticker, graphicService } = createStageHarness('stop-commit');
198+
const endRect = createAnimatedRect(graphicService);
199+
const startRect = createAnimatedRect(graphicService);
200+
const customRect = createAnimatedRect(graphicService);
201+
group.appendChild(endRect);
202+
group.appendChild(startRect);
203+
group.appendChild(customRect);
204+
205+
const endAnimate = endRect.animate().to({ x: 100 }, 100, 'linear');
206+
tick(ticker, 50);
207+
endAnimate.stop('end');
208+
expect(endRect.attribute.x).toBe(100);
209+
expect((endRect as any).baseAttributes.x).toBe(100);
210+
211+
const startAnimate = startRect.animate().to({ x: 100 }, 100, 'linear');
212+
tick(ticker, 50);
213+
startAnimate.stop('start');
214+
expect(startRect.attribute.x).toBe(0);
215+
expect((startRect as any).baseAttributes.x).toBe(0);
216+
217+
const customAnimate = customRect.animate().to({ x: 100 }, 100, 'linear');
218+
tick(ticker, 50);
219+
customAnimate.stop({ x: 12 });
220+
expect(customRect.attribute.x).toBe(12);
221+
expect((customRect as any).baseAttributes.x).toBe(12);
222+
});
223+
224+
test('executor attrOutChannel commits non-animated diff attrs as static truth', () => {
225+
const { group, ticker, graphicService } = createStageHarness('executor-attr-out-channel');
226+
const rect = createAnimatedRect(graphicService);
227+
(rect as any).context = {
228+
diffAttrs: {
229+
x: 80,
230+
fill: 'red'
231+
}
232+
};
233+
group.appendChild(rect);
234+
235+
new AnimateExecutor(rect).execute({
236+
channel: {
237+
x: { to: 80 }
238+
},
239+
duration: 100,
240+
easing: 'linear'
241+
});
242+
243+
expect((rect as any).baseAttributes.fill).toBe('red');
244+
expect((rect as any).baseAttributes.x).toBe(0);
245+
246+
tick(ticker, 50);
247+
expect(rect.attribute.x).toBeCloseTo(40, 5);
248+
expect(rect.attribute.fill).toBe('red');
249+
expect((rect as any).baseAttributes.fill).toBe('red');
250+
expect((rect as any).baseAttributes.x).toBe(0);
251+
});
252+
162253
test('switching states mid-animation restores to the new static truth and blocks late writes', () => {
163254
const { group, ticker, graphicService } = createStageHarness('state-conflict');
164255
const rect = createAnimatedRect(graphicService);
@@ -189,7 +280,7 @@ describe('D3 pre-handoff animation runtime', () => {
189280
expect(rect.attribute.opacity).toBeCloseTo(0.8, 5);
190281
});
191282

192-
test('state switch wins over an in-flight self-driven animation on the same attribute and restores current truth', () => {
283+
test('state switch wins over an in-flight self-driven animation on the same attribute', () => {
193284
const { group, ticker, graphicService } = createStageHarness('self-vs-state');
194285
const rect = createAnimatedRect(graphicService);
195286
group.appendChild(rect);

0 commit comments

Comments
 (0)