Skip to content
Open
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
6 changes: 4 additions & 2 deletions examples/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import { installFonts } from './common/installFonts.js';
import { MemMonitor } from './common/MemMonitor.js';
import { setupMathRandom } from './common/setupMathRandom.js';
import { installShaders } from './common/installShaders.js';
import { CoreAnimationManager } from '../dist/src/core/animations/AnimationManager.js';

interface TestModule {
default: (settings: ExampleSettings) => Promise<void>;
Expand Down Expand Up @@ -276,6 +277,7 @@ async function initRenderer(
inspector,
platform: platformMap[platform] || WebPlatform,
renderEngine: renderMode === 'webgl' ? WebGlRenderer : CanvasRenderer,
animationManager: CoreAnimationManager,
fontEngines: [SdfTextRenderer, CanvasTextRenderer],
textureProcessingTimeLimit: textureProcessingTimeLimit,
...customSettings,
Expand Down Expand Up @@ -304,7 +306,7 @@ async function initRenderer(
let fpsSamplesLeft = fpsSampleCount;
renderer.on(
'fpsUpdate',
(target: RendererMain, fpsData: FpsUpdatePayload) => {
(target: RendererMain<CoreAnimationManager>, fpsData: FpsUpdatePayload) => {
const captureSample = fpsSampleIndex >= fpsSampleSkipCount;
if (captureSample) {
samples.add('fps', fpsData.fps);
Expand Down Expand Up @@ -489,7 +491,7 @@ async function runAutomation(
}
}

function waitForRendererIdle(renderer: RendererMain) {
function waitForRendererIdle(renderer: RendererMain<CoreAnimationManager>) {
return new Promise<void>((resolve) => {
let timeout: NodeJS.Timeout | undefined;
const startTimeout = () => {
Expand Down
14 changes: 5 additions & 9 deletions src/core/CoreNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,6 @@ import {
} from './lib/utils.js';
import { Matrix3d } from './lib/Matrix3d.js';
import { RenderCoords } from './lib/RenderCoords.js';
import type { AnimationSettings } from './animations/CoreAnimation.js';
import type { IAnimationController } from '../common/IAnimationController.js';
import type { CoreShaderNode } from './renderers/CoreShaderNode.js';
import { AutosizeMode, Autosizer } from './Autosizer.js';
import { bucketSortByZIndex, removeChild } from './lib/collectionUtils.js';
Expand Down Expand Up @@ -832,10 +830,15 @@ export class CoreNode extends EventEmitter {
parentAutosizer: Autosizer | null = null;

public destroyed = false;
public animate: (
props: Record<string, any>,
settings?: Record<string, any>,
) => any;

constructor(readonly stage: Stage, props: CoreNodeProps) {
super();
const p = (this.props = {} as CoreNodeProps);
this.animate = stage.animationManager.animateNode;

// Initialize the renderOpTextures array with a capacity of 16 (typical max textures)
this.renderOpTextures = [];
Expand Down Expand Up @@ -2778,13 +2781,6 @@ export class CoreNode extends EventEmitter {
this.parent?.setRTTUpdates(type);
}

animate(
props: Partial<CoreNodeAnimateProps>,
settings: Partial<AnimationSettings>,
): IAnimationController {
return this.stage.animationManager.createAnimation(this, props, settings);
}

flush() {
// no-op
}
Expand Down
13 changes: 9 additions & 4 deletions src/core/Stage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
*/

import { assertTruthy, setPremultiplyMode } from '../utils.js';
import { AnimationManager } from './animations/AnimationManager.js';
import { type AnimationManager } from './animations/AnimationManager.js';
import {
UpdateType,
CoreNode,
Expand Down Expand Up @@ -59,13 +59,18 @@ import type { WebPlatform } from './platforms/web/WebPlatform.js';
import type { RendererMainSettings } from '../main-api/Renderer.js';

export type StageOptions = Omit<
RendererMainSettings,
'inspector' | 'platform' | 'maxRetryCount' | keyof PlatformSettings
RendererMainSettings<AnimationManager>,
| 'inspector'
| 'platform'
| 'maxRetryCount'
| 'animationManager'
| keyof PlatformSettings
> & {
textureMemory: TextureMemoryManagerSettings;
fpsUpdateInterval: number;
eventBus: EventEmitter;
platform: Platform | WebPlatform;
animationManager: AnimationManager;
inspector: boolean;
maxRetryCount: number;
enableClear: boolean;
Expand Down Expand Up @@ -197,7 +202,7 @@ export class Stage {

this.txMemManager = new TextureMemoryManager(this, textureMemory);

this.animationManager = new AnimationManager();
this.animationManager = options.animationManager;
this.contextSpy = enableContextSpy ? new ContextSpy() : null;

let bm = [0, 0, 0, 0] as [number, number, number, number];
Expand Down
102 changes: 20 additions & 82 deletions src/core/animations/AnimationManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
*/

import { describe, expect, it, vi } from 'vitest';
import { AnimationManager } from './AnimationManager.js';
import { CoreAnimationManager as AnimationManager } from './AnimationManager.js';
import type { CoreNode } from '../CoreNode.js';
import type { IAnimationController } from '../../common/IAnimationController.js';

Expand Down Expand Up @@ -48,16 +48,8 @@ describe('AnimationManager', () => {
const manager = new AnimationManager();
const node = createMockNode();

const controller1 = manager.createAnimation(
node,
{ x: 100 },
{ duration: 1000 },
);
const controller2 = manager.createAnimation(
node,
{ y: 200 },
{ duration: 1000 },
);
const controller1 = manager.animate(node, { x: 100 }, { duration: 1000 });
const controller2 = manager.animate(node, { y: 200 }, { duration: 1000 });

// Should be different controller instances
expect(controller1).not.toBe(controller2);
Expand All @@ -68,23 +60,15 @@ describe('AnimationManager', () => {
const node = createMockNode();

// Create and start an animation
const controller1 = manager.createAnimation(
node,
{ x: 100 },
{ duration: 1000 },
);
const controller1 = manager.animate(node, { x: 100 }, { duration: 1000 });
controller1.start();

// Stop it -- should release to pool
controller1.stop();

// Pool should now have objects
// Create another animation -- should reuse from pool
const controller2 = manager.createAnimation(
node,
{ y: 200 },
{ duration: 1000 },
);
const controller2 = manager.animate(node, { y: 200 }, { duration: 1000 });

// The controller instance should be reused (same object reference)
expect(controller2).toBe(controller1);
Expand All @@ -95,20 +79,12 @@ describe('AnimationManager', () => {
const node = createMockNode();

// Create, start, and stop
const controller1 = manager.createAnimation(
node,
{ x: 100 },
{ duration: 500 },
);
const controller1 = manager.animate(node, { x: 100 }, { duration: 500 });
controller1.start();
controller1.stop();

// Reuse from pool with different props
const controller2 = manager.createAnimation(
node,
{ y: 200 },
{ duration: 1000 },
);
const controller2 = manager.animate(node, { y: 200 }, { duration: 1000 });

// Should be stopped state (reinitialized)
expect(controller2.state).toBe('stopped');
Expand All @@ -123,11 +99,7 @@ describe('AnimationManager', () => {
const node = createMockNode();

// Create and add a user listener
const controller1 = manager.createAnimation(
node,
{ x: 100 },
{ duration: 1000 },
);
const controller1 = manager.animate(node, { x: 100 }, { duration: 1000 });
const stoppedSpy = vi.fn();
controller1.on('stopped', stoppedSpy);
controller1.start();
Expand All @@ -137,11 +109,7 @@ describe('AnimationManager', () => {
expect(stoppedSpy).toHaveBeenCalledTimes(1);

// Reuse from pool
const controller2 = manager.createAnimation(
node,
{ y: 200 },
{ duration: 1000 },
);
const controller2 = manager.animate(node, { y: 200 }, { duration: 1000 });

// Old listener should not fire on the recycled controller
const newStoppedSpy = vi.fn();
Expand All @@ -158,22 +126,14 @@ describe('AnimationManager', () => {
const manager = new AnimationManager();
const node = createMockNode();

const controller = manager.createAnimation(
node,
{ x: 100 },
{ duration: 0 },
);
const controller = manager.animate(node, { x: 100 }, { duration: 0 });
controller.start();

// Duration is 0 so animation finishes immediately on first update
manager.update(0);

// Create another -- should reuse from pool
const controller2 = manager.createAnimation(
node,
{ y: 200 },
{ duration: 1000 },
);
const controller2 = manager.animate(node, { y: 200 }, { duration: 1000 });

expect(controller2).toBe(controller);
});
Expand All @@ -182,23 +142,15 @@ describe('AnimationManager', () => {
const manager = new AnimationManager();
const node = createMockNode();

const controller = manager.createAnimation(
node,
{ x: 100 },
{ duration: 1000 },
);
const controller = manager.animate(node, { x: 100 }, { duration: 1000 });
controller.start();

// Simulate node destruction
(node as unknown as Record<string, unknown>).destroyed = true;
manager.update(16);

// Create another -- should reuse from pool
const controller2 = manager.createAnimation(
node,
{ y: 200 },
{ duration: 1000 },
);
const controller2 = manager.animate(node, { y: 200 }, { duration: 1000 });

expect(controller2).toBe(controller);
});
Expand All @@ -207,7 +159,7 @@ describe('AnimationManager', () => {
const manager = new AnimationManager();
const node = createMockNode();

const controller = manager.createAnimation(
const controller = manager.animate(
node,
{ x: 100 },
{ duration: 100, loop: true },
Expand All @@ -218,11 +170,7 @@ describe('AnimationManager', () => {
manager.update(200);

// Create another -- should NOT reuse the looping controller
const controller2 = manager.createAnimation(
node,
{ y: 200 },
{ duration: 1000 },
);
const controller2 = manager.animate(node, { y: 200 }, { duration: 1000 });

expect(controller2).not.toBe(controller);
});
Expand All @@ -232,22 +180,18 @@ describe('AnimationManager', () => {
const node = createMockNode();

// Cycle 1
const c1 = manager.createAnimation(node, { x: 100 }, { duration: 1000 });
const c1 = manager.animate(node, { x: 100 }, { duration: 1000 });
c1.start();
c1.stop();

// Cycle 2 -- reuses c1
const c2 = manager.createAnimation(node, { y: 200 }, { duration: 1000 });
const c2 = manager.animate(node, { y: 200 }, { duration: 1000 });
expect(c2).toBe(c1);
c2.start();
c2.stop();

// Cycle 3 -- reuses again
const c3 = manager.createAnimation(
node,
{ alpha: 0 },
{ duration: 1000 },
);
const c3 = manager.animate(node, { alpha: 0 }, { duration: 1000 });
expect(c3).toBe(c1);
});

Expand All @@ -258,11 +202,7 @@ describe('AnimationManager', () => {
// Create 3 concurrent animations
const controllers: IAnimationController[] = [];
for (let i = 0; i < 3; i++) {
const c = manager.createAnimation(
node,
{ x: i * 100 },
{ duration: 1000 },
);
const c = manager.animate(node, { x: i * 100 }, { duration: 1000 });
c.start();
controllers.push(c);
}
Expand All @@ -279,9 +219,7 @@ describe('AnimationManager', () => {
// Create 3 more -- should all reuse from pool
const reused: IAnimationController[] = [];
for (let i = 0; i < 3; i++) {
reused.push(
manager.createAnimation(node, { y: i * 100 }, { duration: 1000 }),
);
reused.push(manager.animate(node, { y: i * 100 }, { duration: 1000 }));
}

// Each should be one of the original controllers (pool is LIFO)
Expand Down
Loading
Loading