UNPKG

@lightningjs/renderer

Version:
365 lines 13.3 kB
/* * If not stated otherwise in this file or this component's LICENSE file the * following copyright and licenses apply: * * Copyright 2023 Comcast Cable Communications Management, LLC. * * Licensed under the Apache License, Version 2.0 (the License); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { ManualCountTextureUsageTracker, } from './texture-usage-trackers/ManualCountTextureUsageTracker.js'; import { FinalizationRegistryTextureUsageTracker } from './texture-usage-trackers/FinalizationRegistryTextureUsageTracker.js'; import { EventEmitter } from '../common/EventEmitter.js'; import { Inspector } from './Inspector.js'; import { santizeCustomDataMap } from '../render-drivers/utils.js'; import { isProductionEnvironment } from '../utils.js'; /** * The Renderer Main API * * @remarks * This is the primary class used to configure and operate the Renderer. * * It is used to create and destroy Nodes, as well as Texture and Shader * references. * * Example: * ```ts * import { RendererMain, MainCoreDriver } from '@lightningjs/renderer'; * * // Initialize the Renderer * const renderer = new RendererMain( * { * appWidth: 1920, * appHeight: 1080 * }, * 'app', * new MainCoreDriver(), * ); * ``` */ export class RendererMain extends EventEmitter { root = null; driver; canvas; settings; inspector = null; nodes = new Map(); nextTextureId = 1; /** * Texture Usage Tracker for Usage Based Texture Garbage Collection * * @remarks * For internal use only. DO NOT ACCESS. */ textureTracker; /** * Constructs a new Renderer instance * * @param settings Renderer settings * @param target Element ID or HTMLElement to insert the canvas into * @param driver Core Driver to use */ constructor(settings, target, driver) { super(); const resolvedSettings = { appWidth: settings.appWidth || 1920, appHeight: settings.appHeight || 1080, txMemByteThreshold: settings.txMemByteThreshold || 124e6, boundsMargin: settings.boundsMargin || 0, deviceLogicalPixelRatio: settings.deviceLogicalPixelRatio || 1, devicePhysicalPixelRatio: settings.devicePhysicalPixelRatio || window.devicePixelRatio, clearColor: settings.clearColor ?? 0x00000000, coreExtensionModule: settings.coreExtensionModule || null, experimental_FinalizationRegistryTextureUsageTracker: settings.experimental_FinalizationRegistryTextureUsageTracker ?? false, textureCleanupOptions: settings.textureCleanupOptions || {}, fpsUpdateInterval: settings.fpsUpdateInterval || 0, numImageWorkers: settings.numImageWorkers !== undefined ? settings.numImageWorkers : 2, enableContextSpy: settings.enableContextSpy ?? false, enableInspector: settings.enableInspector ?? false, }; this.settings = resolvedSettings; const { appWidth, appHeight, deviceLogicalPixelRatio, devicePhysicalPixelRatio, enableInspector, } = resolvedSettings; const releaseCallback = (textureId) => { this.driver.releaseTexture(textureId); }; const useFinalizationRegistryTracker = resolvedSettings.experimental_FinalizationRegistryTextureUsageTracker && typeof FinalizationRegistry === 'function'; this.textureTracker = useFinalizationRegistryTracker ? new FinalizationRegistryTextureUsageTracker(releaseCallback) : new ManualCountTextureUsageTracker(releaseCallback, this.settings.textureCleanupOptions); const deviceLogicalWidth = appWidth * deviceLogicalPixelRatio; const deviceLogicalHeight = appHeight * deviceLogicalPixelRatio; this.driver = driver; const canvas = document.createElement('canvas'); this.canvas = canvas; canvas.width = deviceLogicalWidth * devicePhysicalPixelRatio; canvas.height = deviceLogicalHeight * devicePhysicalPixelRatio; canvas.style.width = `${deviceLogicalWidth}px`; canvas.style.height = `${deviceLogicalHeight}px`; let targetEl; if (typeof target === 'string') { targetEl = document.getElementById(target); } else { targetEl = target; } if (!targetEl) { throw new Error('Could not find target element'); } // Hook up the driver's callbacks driver.onCreateNode = (node) => { this.nodes.set(node.id, node); }; driver.onBeforeDestroyNode = (node) => { this.nodes.delete(node.id); }; driver.onFpsUpdate = (fpsData) => { this.emit('fpsUpdate', fpsData); }; driver.onFrameTick = (frameTickData) => { this.emit('frameTick', frameTickData); }; driver.onIdle = () => { this.emit('idle'); }; targetEl.appendChild(canvas); if (enableInspector && !isProductionEnvironment()) { this.inspector = new Inspector(canvas, resolvedSettings); } } /** * Initialize the renderer * * @remarks * This method must be called and resolved asyncronously before any other * methods are called. */ async init() { await this.driver.init(this, this.settings, this.canvas); // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion this.root = this.driver.getRootNode(); } /** * Create a new scene graph node * * @remarks * A node is the main graphical building block of the Renderer scene graph. It * can be a container for other nodes, or it can be a leaf node that renders a * solid color, gradient, image, or specific texture, using a specific shader. * * To create a text node, see {@link createTextNode}. * * See {@link INode} for more details. * * @param props * @returns */ createNode(props) { if (this.inspector) { return this.inspector.createNode(this.driver, this.resolveNodeDefaults(props)); } return this.driver.createNode(this.resolveNodeDefaults(props)); } /** * Create a new scene graph text node * * @remarks * A text node is the second graphical building block of the Renderer scene * graph. It renders text using a specific text renderer that is automatically * chosen based on the font requested and what type of fonts are installed * into an app via a CoreExtension. * * See {@link ITextNode} for more details. * * @param props * @returns */ createTextNode(props) { const fontSize = props.fontSize ?? 16; const data = { ...this.resolveNodeDefaults(props), text: props.text ?? '', textRendererOverride: props.textRendererOverride ?? null, fontSize, fontFamily: props.fontFamily ?? 'sans-serif', fontStyle: props.fontStyle ?? 'normal', fontWeight: props.fontWeight ?? 'normal', fontStretch: props.fontStretch ?? 'normal', textAlign: props.textAlign ?? 'left', contain: props.contain ?? 'none', scrollable: props.scrollable ?? false, scrollY: props.scrollY ?? 0, offsetY: props.offsetY ?? 0, letterSpacing: props.letterSpacing ?? 0, lineHeight: props.lineHeight ?? fontSize, maxLines: props.maxLines ?? 0, textBaseline: props.textBaseline ?? 'alphabetic', verticalAlign: props.verticalAlign ?? 'top', overflowSuffix: props.overflowSuffix ?? '...', debug: props.debug ?? {}, }; if (this.inspector) { return this.inspector.createTextNode(this.driver, data); } return this.driver.createTextNode(data); } /** * Resolves the default property values for a Node * * @remarks * This method is used internally by the RendererMain to resolve the default * property values for a Node. It is exposed publicly so that it can be used * by Core Driver implementations. * * @param props * @returns */ resolveNodeDefaults(props) { const color = props.color ?? 0xffffffff; const colorTl = props.colorTl ?? props.colorTop ?? props.colorLeft ?? color; const colorTr = props.colorTr ?? props.colorTop ?? props.colorRight ?? color; const colorBl = props.colorBl ?? props.colorBottom ?? props.colorLeft ?? color; const colorBr = props.colorBr ?? props.colorBottom ?? props.colorRight ?? color; const data = santizeCustomDataMap(props.data ?? {}); return { x: props.x ?? 0, y: props.y ?? 0, width: props.width ?? 0, height: props.height ?? 0, alpha: props.alpha ?? 1, autosize: props.autosize ?? false, clipping: props.clipping ?? false, color, colorTop: props.colorTop ?? color, colorBottom: props.colorBottom ?? color, colorLeft: props.colorLeft ?? color, colorRight: props.colorRight ?? color, colorBl, colorBr, colorTl, colorTr, zIndex: props.zIndex ?? 0, zIndexLocked: props.zIndexLocked ?? 0, parent: props.parent ?? null, texture: props.texture ?? null, shader: props.shader ?? null, // Since setting the `src` will trigger a texture load, we need to set it after // we set the texture. Otherwise, problems happen. src: props.src ?? '', scale: props.scale ?? null, scaleX: props.scaleX ?? props.scale ?? 1, scaleY: props.scaleY ?? props.scale ?? 1, mount: props.mount ?? 0, mountX: props.mountX ?? props.mount ?? 0, mountY: props.mountY ?? props.mount ?? 0, pivot: props.pivot ?? 0.5, pivotX: props.pivotX ?? props.pivot ?? 0.5, pivotY: props.pivotY ?? props.pivot ?? 0.5, rotation: props.rotation ?? 0, data: data, }; } /** * Destroy a node * * @remarks * This method destroys a node but does not destroy its children. * * @param node * @returns */ destroyNode(node) { if (this.inspector) { this.inspector.destroyNode(node); } return this.driver.destroyNode(node); } /** * Create a new texture reference * * @remarks * This method creates a new reference to a texture. The texture is not * loaded until it is used on a node. * * It can be assigned to a node's `texture` property, or it can be used * when creating a SubTexture. * * @param textureType * @param props * @param options * @returns */ createTexture(textureType, props, options) { const id = this.nextTextureId++; const desc = { descType: 'texture', txType: textureType, props, options: { ...options, // This ID is used to identify the texture in the CoreTextureManager's // ID Texture Map cache. id, }, }; this.textureTracker.registerTexture(desc); return desc; } /** * Create a new shader reference * * @remarks * This method creates a new reference to a shader. The shader is not * loaded until it is used on a Node. * * It can be assigned to a Node's `shader` property. * * @param shaderType * @param props * @returns */ createShader(shaderType, props) { return this.driver.createShaderController({ descType: 'shader', shType: shaderType, props: props, }); } /** * Get a Node by its ID * * @param id * @returns */ getNodeById(id) { return this.nodes.get(id) || null; } toggleFreeze() { throw new Error('Not implemented'); } advanceFrame() { throw new Error('Not implemented'); } /** * Re-render the current frame without advancing any running animations. * * @remarks * Any state changes will be reflected in the re-rendered frame. Useful for * debugging. * * May not do anything if the render loop is running on a separate worker. */ rerender() { throw new Error('Not implemented'); } } //# sourceMappingURL=RendererMain.js.map