import { ResizeObserver as ResizeObserverPolyfill } from '@juggle/resize-observer'; import { EventEmitter } from "./EventEmitter"; const ResizeObserver = window.ResizeObserver || ResizeObserverPolyfill; interface WebGLCanvasOption { /** * Canvas element for outputting renderer images. * If a string is passed in: * Here will try `document.getElementById` and then `document.querySelector` * until a valid canvas element is found. */ canvas?: HTMLCanvasElement | OffscreenCanvas | string; /** * Initialize with WebGL context. */ context?: WebGLRenderingContext | WebGL2RenderingContext; /** * Parent node of canvas. */ container?: HTMLElement | string; /** * Automatically adjust width and height to parent node width and height. */ autoResize?: boolean; /** * Width of Canvas. * It will become invalid when autoResize is set. */ width?: number; /** * Height of Canvas. * It will become invalid when autoResize is set. */ height?: number; /** * Whether to use offline canvas. */ isOffScreen?: boolean; /** * WebGL context attributes. */ contextAttributes?: WebGLContextAttributes; } type WebGLCanvasEventMap = { /** * Emit When the canvas size changes. */ "resize": void; }; class WebGLCanvas extends EventEmitter { public readonly element: HTMLCanvasElement | OffscreenCanvas; public readonly context: WebGLRenderingContext | WebGL2RenderingContext; public readonly glVersion: 0 | 1 | 2; public readonly isOffScreen: boolean; public constructor(userOption?: WebGLCanvasOption) { super(); const option: WebGLCanvasOption = userOption ?? {}; let targetCanvas: HTMLCanvasElement | OffscreenCanvas | undefined = void 0; let targetContext: WebGLRenderingContext | WebGL2RenderingContext | undefined = void 0; let targetGLVersion: 0 | 1 | 2 = 0; let targetIsOffScreen: boolean = false; if (option.canvas instanceof HTMLCanvasElement || option.canvas instanceof OffscreenCanvas) { targetCanvas = option.canvas; } else if (typeof option.canvas === "string") { let findTarget: HTMLElement | null = null; findTarget = document.getElementById(option.canvas); if (findTarget instanceof HTMLCanvasElement) { targetCanvas = findTarget; } else { findTarget = document.querySelector(option.canvas); if (findTarget instanceof HTMLCanvasElement) { targetCanvas = findTarget; } else { console.warn( `[ray-lab Renderer] Unable to search any valid canvas through "${option.canvas}". \r\n` + "Created internal canvas instead..." ); } } } if (option.context) { if (targetCanvas && targetCanvas !== option.context.canvas) { console.warn( "[ray-lab Renderer] The `canvas` element does not match the `context.canvas`: \r\n" + "The `canvas` option will be ignored. \r\n" + "Please do not use `canvas` and `context` at the same time!" ); } targetCanvas = option.context.canvas; targetContext = option.context; } if (targetCanvas) { if (targetCanvas instanceof HTMLCanvasElement) { if (option.isOffScreen) { if (OffscreenCanvas) { targetCanvas = targetCanvas.transferControlToOffscreen(); targetIsOffScreen = true; } else { targetIsOffScreen = false; console.warn( "[ray-lab Renderer] The target environment does not support OffscreenCanvas \r\n" + "`isOffScreen` is ignored!" ); } } else { targetIsOffScreen = false; } } else if (targetCanvas instanceof OffscreenCanvas) { targetIsOffScreen = true; } } else { if (option.isOffScreen) { if (OffscreenCanvas) { targetIsOffScreen = true; targetCanvas = new OffscreenCanvas(option.width ?? 300, option.height ?? 150); } else { targetIsOffScreen = false; targetCanvas = document.createElement("canvas"); console.warn( "[ray-lab Renderer] The target environment does not support OffscreenCanvas \r\n" + "`isOffScreen` is ignored!" ); } } else { targetIsOffScreen = false; targetCanvas = document.createElement("canvas"); } } if (targetContext) { if (targetCanvas instanceof WebGL2RenderingContext) { targetGLVersion = 2; } else if (targetCanvas instanceof WebGLRenderingContext) { targetGLVersion = 1; } } else { let getContext: OffscreenRenderingContext | RenderingContext | null; getContext = targetCanvas.getContext("webgl2", option.contextAttributes); if (!getContext) { getContext = targetCanvas.getContext("webgl", option.contextAttributes); } if (WebGL2RenderingContext && (getContext instanceof WebGL2RenderingContext)) { targetContext = getContext; targetGLVersion = 2; } else if (getContext instanceof WebGLRenderingContext) { targetContext = getContext; targetGLVersion = 1; } else { throw new Error("[ray-lab Renderer] The target environment does not support WebGL :("); } } this.element = targetCanvas; this.context = targetContext; this.glVersion = targetGLVersion; this.isOffScreen = targetIsOffScreen; if (option.autoResize) { this.enableAutoResize(option.container); } else { this.resize(option.width, option.height); } } /** * Change the canvas size. * @param width - canvas width. * @param height - canvas height. */ public resize(width?: number, height?: number) { let hasResized = false; if (width !== void 0) { this.element.width = width; hasResized = true; } if (height !== void 0) { this.element.height = height; hasResized = true; } if (hasResized) { this.emit("resize"); } } private resizeObserver: ResizeObserver | undefined; /** * enable auto resize. * @param container - Parent node of canvas. */ public enableAutoResize(container?: HTMLElement | string) { let targetcontainer: HTMLElement | null = null; if (container instanceof HTMLElement) { targetcontainer = container; } else if (typeof container === "string") { targetcontainer = document.getElementById(container); if (targetcontainer === null) { targetcontainer = document.querySelector(container); if (targetcontainer === null) { console.warn( `[ray-lab Renderer] Unable to search any valid container through "${container}".` ); } } } if (targetcontainer === null && this.element instanceof HTMLCanvasElement) { targetcontainer = this.element.parentElement; } if (targetcontainer === null) { console.warn( `[ray-lab Renderer] Unable to enable auto resize. \r\n` + `Because a suitable container could not be found.` ); } else { const resizeObserver = new ResizeObserver((entries) => { for (const entry of entries) { if (entry.contentRect) { this.resize(entry.contentRect.width, entry.contentRect.height); } } }); if (this.resizeObserver) { this.resizeObserver.disconnect(); } this.resizeObserver = resizeObserver; if (this.element instanceof HTMLElement) { this.element.style.width = "100%"; this.element.style.height = "100%"; this.element.style.boxSizing = "border-box"; this.element.style.display = "block"; } resizeObserver.observe(targetcontainer); this.resize(targetcontainer.offsetWidth, targetcontainer.offsetHeight); } } /** * disable auto resize. */ public disableAutoResize() { if (this.resizeObserver) { this.resizeObserver.disconnect(); } this.resizeObserver = void 0; } } export { WebGLCanvas }; export type { WebGLCanvasOption };