diff --git a/examples/index.ts b/examples/index.ts index d8398acb..56c58223 100644 --- a/examples/index.ts +++ b/examples/index.ts @@ -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; @@ -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, @@ -304,7 +306,7 @@ async function initRenderer( let fpsSamplesLeft = fpsSampleCount; renderer.on( 'fpsUpdate', - (target: RendererMain, fpsData: FpsUpdatePayload) => { + (target: RendererMain, fpsData: FpsUpdatePayload) => { const captureSample = fpsSampleIndex >= fpsSampleSkipCount; if (captureSample) { samples.add('fps', fpsData.fps); @@ -489,7 +491,7 @@ async function runAutomation( } } -function waitForRendererIdle(renderer: RendererMain) { +function waitForRendererIdle(renderer: RendererMain) { return new Promise((resolve) => { let timeout: NodeJS.Timeout | undefined; const startTimeout = () => { diff --git a/src/core/CoreNode.ts b/src/core/CoreNode.ts index 8a460658..eac384af 100644 --- a/src/core/CoreNode.ts +++ b/src/core/CoreNode.ts @@ -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'; @@ -832,10 +830,15 @@ export class CoreNode extends EventEmitter { parentAutosizer: Autosizer | null = null; public destroyed = false; + public animate: ( + props: Record, + settings?: Record, + ) => 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 = []; @@ -2778,13 +2781,6 @@ export class CoreNode extends EventEmitter { this.parent?.setRTTUpdates(type); } - animate( - props: Partial, - settings: Partial, - ): IAnimationController { - return this.stage.animationManager.createAnimation(this, props, settings); - } - flush() { // no-op } diff --git a/src/core/Stage.ts b/src/core/Stage.ts index 525408bd..ba9d8981 100644 --- a/src/core/Stage.ts +++ b/src/core/Stage.ts @@ -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, @@ -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, + | 'inspector' + | 'platform' + | 'maxRetryCount' + | 'animationManager' + | keyof PlatformSettings > & { textureMemory: TextureMemoryManagerSettings; fpsUpdateInterval: number; eventBus: EventEmitter; platform: Platform | WebPlatform; + animationManager: AnimationManager; inspector: boolean; maxRetryCount: number; enableClear: boolean; @@ -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]; diff --git a/src/core/animations/AnimationManager.test.ts b/src/core/animations/AnimationManager.test.ts index 9ae7e16d..a9af177f 100644 --- a/src/core/animations/AnimationManager.test.ts +++ b/src/core/animations/AnimationManager.test.ts @@ -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'; @@ -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); @@ -68,11 +60,7 @@ 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 @@ -80,11 +68,7 @@ describe('AnimationManager', () => { // 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); @@ -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'); @@ -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(); @@ -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(); @@ -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); }); @@ -182,11 +142,7 @@ 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 @@ -194,11 +150,7 @@ describe('AnimationManager', () => { 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); }); @@ -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 }, @@ -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); }); @@ -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); }); @@ -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); } @@ -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) diff --git a/src/core/animations/AnimationManager.ts b/src/core/animations/AnimationManager.ts index a843ac12..60620f81 100644 --- a/src/core/animations/AnimationManager.ts +++ b/src/core/animations/AnimationManager.ts @@ -22,7 +22,35 @@ import { CoreAnimation, type AnimationSettings } from './CoreAnimation.js'; import { CoreAnimationController } from './CoreAnimationController.js'; import type { IAnimationController } from '../../common/IAnimationController.js'; -export class AnimationManager { +export interface AnimationManager { + /** + * function that updates all active animations, called by the stage on each frame + * + * @param dt delta time + */ + update(dt: number): void; + /** + * Animate a target object with the given properties and settings. This function is used to create an animation controller for the target object. + * @param target any object + * @param props any object + * @param settings optional object + */ + animate( + target: Record, + props: Record, + settings?: Record, + ): any; + /** + * This function is bound to the CoreNode instance and is used to animate the a CoreNode specifically + * + * @param this - The node to animate + * @param props The properties to animate + * @param settings Optional animation settings + */ + animateNode(props: Record, settings?: Record): any; +} + +export class CoreAnimationManager implements AnimationManager { private activeAnimations: CoreAnimation[] = []; /** @@ -77,7 +105,7 @@ export class AnimationManager { * Objects are returned to the pool when the controller reaches a terminal * state (stopped via finish, manual stop, or node destruction). */ - createAnimation( + animate( node: CoreNode, props: Partial, settings: Partial, @@ -119,4 +147,24 @@ export class AnimationManager { this.animationPool.push(animation); this.controllerPool.push(controller); } + + /** + * Animate a node, this function is bound to the CoreNode instance and is used to animate the a CoreNode specifically + * + * @param this - The node to animate + * @param props - The animation properties + * @param settings - The animation settings + * @returns The animation controller + */ + animateNode = function ( + this: CoreNode, + props: Partial, + settings: Partial, + ): IAnimationController { + return this.stage.animationManager.animate( + this, + props, + settings, + ) as IAnimationController; + }; } diff --git a/src/core/animations/CoreAnimationController.test.ts b/src/core/animations/CoreAnimationController.test.ts index 718f83f3..fe58bf30 100644 --- a/src/core/animations/CoreAnimationController.test.ts +++ b/src/core/animations/CoreAnimationController.test.ts @@ -19,7 +19,7 @@ import { describe, expect, it, vi, beforeEach } from 'vitest'; import { CoreAnimationController } from './CoreAnimationController.js'; -import { AnimationManager } from './AnimationManager.js'; +import { CoreAnimationManager as AnimationManager } from './AnimationManager.js'; import { CoreAnimation } from './CoreAnimation.js'; import { mock } from 'vitest-mock-extended'; import { EventEmitter } from '../../common/EventEmitter.js'; diff --git a/src/core/animations/CoreAnimationController.ts b/src/core/animations/CoreAnimationController.ts index 2c09607e..563aaa16 100644 --- a/src/core/animations/CoreAnimationController.ts +++ b/src/core/animations/CoreAnimationController.ts @@ -21,7 +21,7 @@ import type { AnimationControllerState, IAnimationController, } from '../../common/IAnimationController.js'; -import type { AnimationManager } from './AnimationManager.js'; +import type { CoreAnimationManager } from './AnimationManager.js'; import type { CoreAnimation } from './CoreAnimation.js'; import { EventEmitter } from '../../common/EventEmitter.js'; @@ -35,7 +35,7 @@ export class CoreAnimationController */ stoppedResolve: (() => void) | null = null; state: AnimationControllerState; - private manager!: AnimationManager; + private manager!: CoreAnimationManager; private animation!: CoreAnimation; // Pre-allocated tick payload -- reused every frame to avoid per-frame {} allocation @@ -57,7 +57,7 @@ export class CoreAnimationController * Initialize (or reinitialize) this controller with new dependencies. * Called both on first use and when recycled from the pool. */ - init(manager: AnimationManager, animation: CoreAnimation): void { + init(manager: CoreAnimationManager, animation: CoreAnimation): void { this.manager = manager; this.animation = animation; this.state = 'stopped'; diff --git a/src/main-api/INode.ts b/src/main-api/INode.ts index f97bb82b..8fc83405 100644 --- a/src/main-api/INode.ts +++ b/src/main-api/INode.ts @@ -16,15 +16,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import type { IAnimationController } from '../common/IAnimationController.js'; import { CoreNode, type CoreNodeAnimateProps, type CoreNodeProps, } from '../core/CoreNode.js'; import type { CoreTextNode, CoreTextNodeProps } from '../core/CoreTextNode.js'; -import type { AnimationSettings } from '../core/animations/CoreAnimation.js'; import type { CoreShaderNode } from '../core/renderers/CoreShaderNode.js'; +import type { AnimationManager } from '../core/animations/AnimationManager.js'; /** * A visual Node in the Renderer scene graph. @@ -44,14 +43,13 @@ import type { CoreShaderNode } from '../core/renderers/CoreShaderNode.js'; * Users of the Renderer API, should generally interact with INode objects * instead of CoreNode objects. */ -export interface INode - extends Omit { +export interface INode< + A extends AnimationManager, + ShaderNode extends CoreShaderNode = CoreShaderNode, +> extends Omit { shader: ShaderNode; - animate( - props: Partial>, - settings: Partial, - ): IAnimationController; - parent: INode | null; + animate: A['animateNode']; + parent: INode | null; } /** @@ -66,10 +64,12 @@ export interface INodeAnimateProps< /** * Properties used to create a new Node */ -export interface INodeProps - extends Omit { +export interface INodeProps< + A extends AnimationManager, + ShNode extends CoreShaderNode = CoreShaderNode, +> extends Omit { shader: ShNode; - parent: INode | null; + parent: INode | null; } /** @@ -84,17 +84,16 @@ export interface INodeProps * Users of the Renderer API, should generally interact with ITextNode objects * instead of CoreTextNode objects. */ -export interface ITextNode extends Omit { - animate( - props: Partial>, - settings: Partial, - ): IAnimationController; - parent: INode | null; +export interface ITextNode + extends Omit { + animate: A['animateNode']; + parent: INode | null; } /** * Properties used to create a new Text Node */ -export interface ITextNodeProps extends Omit { - parent: INode | null; +export interface ITextNodeProps + extends Omit { + parent: INode | null; } diff --git a/src/main-api/Inspector.ts b/src/main-api/Inspector.ts index 239e5e3b..8b9eba22 100644 --- a/src/main-api/Inspector.ts +++ b/src/main-api/Inspector.ts @@ -13,6 +13,7 @@ import { isProductionEnvironment } from '../utils.js'; import { CoreTextNode, type CoreTextNodeProps } from '../core/CoreTextNode.js'; import type { Texture } from '../core/textures/Texture.js'; import { TextureType } from '../core/textures/Texture.js'; +import type { AnimationManager } from '../core/animations/AnimationManager.js'; /** * Inspector Options @@ -290,7 +291,10 @@ export class Inspector { // Animation stats printing timer private animationStatsTimer: number | null = null; - constructor(canvas: HTMLCanvasElement, settings: RendererMainSettings) { + constructor( + canvas: HTMLCanvasElement, + settings: RendererMainSettings, + ) { if (isProductionEnvironment === true) return; if (!settings) { @@ -898,15 +902,16 @@ export class Inspector { // Define traps for each property in knownProperties knownProperties.forEach((property) => { let proto: CoreNode | ObjectConstructor | null = node; - let originalProp: PropertyDescriptor | undefined | null = Object.getOwnPropertyDescriptor(proto, property); + let originalProp: PropertyDescriptor | undefined | null = + Object.getOwnPropertyDescriptor(proto, property); // Search the prototype chain for the property descriptor - while(originalProp === undefined) { - proto = Object.getPrototypeOf(proto) as ObjectConstructor; - if (proto === null) { - return; - } - originalProp = Object.getOwnPropertyDescriptor(proto, property); + while (originalProp === undefined) { + proto = Object.getPrototypeOf(proto) as ObjectConstructor; + if (proto === null) { + return; + } + originalProp = Object.getOwnPropertyDescriptor(proto, property); } if (property === 'text') { diff --git a/src/main-api/Renderer.test.ts b/src/main-api/Renderer.test.ts index 88561518..09d499e9 100644 --- a/src/main-api/Renderer.test.ts +++ b/src/main-api/Renderer.test.ts @@ -25,6 +25,7 @@ import type { Platform } from '../core/platforms/Platform.js'; import type { Inspector } from './Inspector.js'; import type { CanvasRenderer } from '../core/renderers/canvas/CanvasRenderer.js'; import type { WebGlRenderer } from '../core/renderers/webgl/WebGlRenderer.js'; +import type { AnimationManager } from '../core/animations/AnimationManager.js'; // Mock isProductionEnvironment so the Inspector gets initialized vi.mock('../utils.js', async (importOriginal) => { @@ -74,12 +75,17 @@ function makeRenderer( }> = {}, ) { const MockPlatform = makeMockPlatform(); + const MockAnimationManager = vi.fn(() => ({ + update: vi.fn(), + animateNode: vi.fn(), + })) as unknown as new () => AnimationManager; return new RendererMain( { appWidth: 1920, appHeight: 1080, inspector: extra.inspector ?? false, platform: MockPlatform as unknown as typeof Platform, + animationManager: MockAnimationManager, renderEngine: {} as unknown as | typeof CanvasRenderer | typeof WebGlRenderer, diff --git a/src/main-api/Renderer.ts b/src/main-api/Renderer.ts index ac9f0f0e..c6e42d6d 100644 --- a/src/main-api/Renderer.ts +++ b/src/main-api/Renderer.ts @@ -37,6 +37,7 @@ import type { } from '../core/CoreShaderManager.js'; import { WebPlatform } from '../core/platforms/web/WebPlatform.js'; import { Platform, type PlatformSettings } from '../core/platforms/Platform.js'; +import type { AnimationManager } from '../core/animations/AnimationManager.js'; /** * FPS Update Event Data @@ -300,126 +301,137 @@ export interface RendererRuntimeSettings { /** * Configuration settings for {@link RendererMain} */ -export type RendererMainSettings = RendererRuntimeSettings & - Required & { - /** - * Include context call (i.e. WebGL) information in FPS updates - * - * @remarks - * When enabled the number of calls to each context method over the - * `fpsUpdateInterval` will be included in the FPS update payload's - * `contextSpyData` property. - * - * Enabling the context spy has a serious impact on performance so only use it - * when you need to extract context call information. - * - * @defaultValue `false` (disabled) - */ - enableContextSpy: boolean; - - /** - * Renderer Engine - * - * @remarks - * The renderer engine to use. Spawns a WebGL or Canvas renderer. - * WebGL is more performant and supports more features. Canvas is - * supported on most platforms. - * - * Note: When using CanvasCoreRenderer you can only use - * CanvasTextRenderer. The WebGLCoreRenderer supports - * both CanvasTextRenderer and SdfTextRenderer for Text Rendering. - * - */ - renderEngine: typeof CanvasRenderer | typeof WebGlRenderer; - - /** - * Quad buffer size in bytes - * - * @defaultValue 4 * 1024 * 1024 - */ - quadBufferSize: number; - - /** - * Font Engines - * - * @remarks - * The font engines to use for text rendering. CanvasTextRenderer is supported - * on all platforms. SdfTextRenderer is a more performant renderer. - * When using `renderEngine=CanvasCoreRenderer` you can only use `CanvasTextRenderer`. - * The `renderEngine=WebGLCoreRenderer` supports both `CanvasTextRenderer` and `SdfTextRenderer`. - * - * This setting is used to enable tree shaking of unused font engines. Please - * import your font engine(s) as follows: - * ``` - * import { CanvasTextRenderer } from '@lightning/renderer/canvas'; - * import { SdfTextRenderer } from '@lightning/renderer/webgl'; - * ``` - * - * If both CanvasTextRenderer and SdfTextRenderer are provided, the first renderer - * provided will be asked first if it can render the font. If it cannot render the - * font, the next renderer will be asked. If no renderer can render the font, the - * text will not be rendered. - * - * **Note** that if you have fonts available in both engines the second font engine - * will not be used. This is because the first font engine will always be asked first. - * - * @defaultValue '[]' - * - * - */ - fontEngines: TextRenderer[]; - - /** - * createImageBitmap support for the runtime - * - * @remarks - * This is used to determine if and which version of the createImageBitmap API - * is supported by the runtime. This is used to determine if the renderer can - * use createImageBitmap to load images. - * - * Options supported - * - Auto - Automatically determine the supported version - * - Basic - Supports createImageBitmap(image) - * - Options - Supports createImageBitmap(image, options) - * - Full - Supports createImageBitmap(image, sx, sy, sw, sh, options) - * - * Note with auto detection, the renderer will attempt to use the most advanced - * version of the API available. If the API is not available, the renderer will - * fall back to the next available version. - * - * This will affect startup performance as the renderer will need to determine - * the supported version of the API. - * - * @defaultValue `full` - */ - createImageBitmapSupport: 'auto' | 'basic' | 'options' | 'full'; - - /** - * Provide an alternative platform abstraction layer - * - * @remarks - * By default the Lightning 3 renderer will load a webplatform, assuming it runs - * inside a web browsr. However for special cases there might be a need to provide - * an abstracted platform layer to run on non-web or non-standard JS engines - * - * @defaultValue `null` - */ - platform: typeof Platform | null; - - /** - * Number of times to retry loading a failed texture - * - * @remarks - * When a texture fails to load, Lightning will retry up to this many times - * before permanently giving up. Each retry will clear the texture ownership - * and then re-establish it to trigger a new load attempt. - * - * Set to null to disable retries. Set to 0 to always try once and never retry. - * This is typically only used on ImageTexture instances. - * - */ - maxRetryCount?: number; - }; +export type RendererMainSettings = + RendererRuntimeSettings & + Required & { + /** + * Include context call (i.e. WebGL) information in FPS updates + * + * @remarks + * When enabled the number of calls to each context method over the + * `fpsUpdateInterval` will be included in the FPS update payload's + * `contextSpyData` property. + * + * Enabling the context spy has a serious impact on performance so only use it + * when you need to extract context call information. + * + * @defaultValue `false` (disabled) + */ + enableContextSpy: boolean; + + /** + * Renderer Engine + * + * @remarks + * The renderer engine to use. Spawns a WebGL or Canvas renderer. + * WebGL is more performant and supports more features. Canvas is + * supported on most platforms. + * + * Note: When using CanvasCoreRenderer you can only use + * CanvasTextRenderer. The WebGLCoreRenderer supports + * both CanvasTextRenderer and SdfTextRenderer for Text Rendering. + * + */ + renderEngine: typeof CanvasRenderer | typeof WebGlRenderer; + + /** + * Quad buffer size in bytes + * + * @defaultValue 4 * 1024 * 1024 + */ + quadBufferSize: number; + + /** + * Font Engines + * + * @remarks + * The font engines to use for text rendering. CanvasTextRenderer is supported + * on all platforms. SdfTextRenderer is a more performant renderer. + * When using `renderEngine=CanvasCoreRenderer` you can only use `CanvasTextRenderer`. + * The `renderEngine=WebGLCoreRenderer` supports both `CanvasTextRenderer` and `SdfTextRenderer`. + * + * This setting is used to enable tree shaking of unused font engines. Please + * import your font engine(s) as follows: + * ``` + * import { CanvasTextRenderer } from '@lightning/renderer/canvas'; + * import { SdfTextRenderer } from '@lightning/renderer/webgl'; + * ``` + * + * If both CanvasTextRenderer and SdfTextRenderer are provided, the first renderer + * provided will be asked first if it can render the font. If it cannot render the + * font, the next renderer will be asked. If no renderer can render the font, the + * text will not be rendered. + * + * **Note** that if you have fonts available in both engines the second font engine + * will not be used. This is because the first font engine will always be asked first. + * + * @defaultValue '[]' + * + * + */ + fontEngines: TextRenderer[]; + + /** + * createImageBitmap support for the runtime + * + * @remarks + * This is used to determine if and which version of the createImageBitmap API + * is supported by the runtime. This is used to determine if the renderer can + * use createImageBitmap to load images. + * + * Options supported + * - Auto - Automatically determine the supported version + * - Basic - Supports createImageBitmap(image) + * - Options - Supports createImageBitmap(image, options) + * - Full - Supports createImageBitmap(image, sx, sy, sw, sh, options) + * + * Note with auto detection, the renderer will attempt to use the most advanced + * version of the API available. If the API is not available, the renderer will + * fall back to the next available version. + * + * This will affect startup performance as the renderer will need to determine + * the supported version of the API. + * + * @defaultValue `full` + */ + createImageBitmapSupport: 'auto' | 'basic' | 'options' | 'full'; + + /** + * Provide an alternative platform abstraction layer + * + * @remarks + * By default the Lightning 3 renderer will load a webplatform, assuming it runs + * inside a web browsr. However for special cases there might be a need to provide + * an abstracted platform layer to run on non-web or non-standard JS engines + * + * @defaultValue `null` + */ + platform: typeof Platform | null; + + /** + * Provide a custom animation manager implementation + * + * @remarks + * This allows you to supply your own animation manager that conforms to the AnimationManager interface. + * + * @defaultValue `null` + */ + animationManager: (new () => A) | null; + + /** + * Number of times to retry loading a failed texture + * + * @remarks + * When a texture fails to load, Lightning will retry up to this many times + * before permanently giving up. Each retry will clear the texture ownership + * and then re-establish it to trigger a new load attempt. + * + * Set to null to disable retries. Set to 0 to always try once and never retry. + * This is typically only used on ImageTexture instances. + * + */ + maxRetryCount?: number; + }; /** * The Renderer Main API @@ -472,8 +484,8 @@ export type RendererMainSettings = RendererRuntimeSettings & * @fires RendererMain#criticalCleanup * @fires RendererMain#criticalCleanupFailed */ -export class RendererMain extends EventEmitter { - readonly root: INode; +export class RendererMain extends EventEmitter { + readonly root: INode; readonly canvas: HTMLCanvasElement; readonly stage: Stage; private inspector: Inspector | null = null; @@ -486,7 +498,7 @@ export class RendererMain extends EventEmitter { * @param driver Core Driver to use */ constructor( - settings: Partial, + settings: Partial>, target?: string | HTMLElement, ) { super(); @@ -520,6 +532,7 @@ export class RendererMain extends EventEmitter { canvas: settings.canvas, createImageBitmapSupport: settings.createImageBitmapSupport || 'full', platform: settings.platform || WebPlatform, + animationManager: settings.animationManager || null, maxRetryCount: settings.maxRetryCount ?? 5, }; @@ -529,7 +542,7 @@ export class RendererMain extends EventEmitter { deviceLogicalPixelRatio, devicePhysicalPixelRatio, inspector, - } = settings as RendererMainSettings; + } = settings as RendererMainSettings; assertTruthy( settings.platform, @@ -538,6 +551,13 @@ export class RendererMain extends EventEmitter { const platform = new (settings.platform as typeof WebPlatform)(settings); + assertTruthy( + settings.animationManager, + 'An animation manager must be provided in settings.animationManager', + ); + + const animationManager = new settings.animationManager(); + const deviceLogicalWidth = appWidth * deviceLogicalPixelRatio; const deviceLogicalHeight = appHeight * deviceLogicalPixelRatio; @@ -572,11 +592,12 @@ export class RendererMain extends EventEmitter { textureProcessingTimeLimit: settings.textureProcessingTimeLimit!, createImageBitmapSupport: settings.createImageBitmapSupport!, platform, + animationManager: animationManager, maxRetryCount: settings.maxRetryCount ?? 5, }); // Extract the root node - this.root = this.stage.root as unknown as INode; + this.root = this.stage.root as unknown as INode; // Get the target element and attach the canvas to it if (target) { @@ -602,7 +623,7 @@ export class RendererMain extends EventEmitter { if (inspector && isProductionEnvironment === false) { this.inspector = new inspector( this.canvas, - settings as RendererMainSettings, + settings as RendererMainSettings, ); } } @@ -661,15 +682,15 @@ export class RendererMain extends EventEmitter { * @returns */ createNode>( - props: Partial>, - ): INode { + props: Partial>, + ): INode { const node = this.stage.createNode(props as Partial); if (this.inspector) { - return this.inspector.createNode(node) as unknown as INode; + return this.inspector.createNode(node) as unknown as INode; } - return node as unknown as INode; + return node as unknown as INode; } /** @@ -686,14 +707,14 @@ export class RendererMain extends EventEmitter { * @param props * @returns */ - createTextNode(props: Partial): ITextNode { + createTextNode(props: Partial>): ITextNode { const textNode = this.stage.createTextNode(props as CoreTextNodeProps); if (this.inspector) { - return this.inspector.createTextNode(textNode) as unknown as ITextNode; + return this.inspector.createTextNode(textNode) as unknown as ITextNode; } - return textNode as unknown as ITextNode; + return textNode as unknown as ITextNode; } /** @@ -705,7 +726,7 @@ export class RendererMain extends EventEmitter { * @param node * @returns */ - destroyNode(node: INode) { + destroyNode(node: INode) { if (this.inspector) { this.inspector.destroyNode(node.id); } @@ -880,7 +901,7 @@ export class RendererMain extends EventEmitter { ) { this.inspector = new options.inspector( this.canvas, - stage.options as unknown as RendererMainSettings, + stage.options as unknown as RendererMainSettings, ); this.inspector?.createNodes(this.root as unknown as CoreNode); }