From 35fd7e972bafe8d83ddb726e5a67a643b3607875 Mon Sep 17 00:00:00 2001 From: danvervlad <32311513+danvervlad@users.noreply.github.com> Date: Fri, 29 Nov 2024 18:51:12 +0200 Subject: [PATCH] Allow to provide a canvas to be used for the game rendering instead of creating a new one (#7199) Only show in developer changelog --- .../runtimegame-pixi-renderer.ts | 46 +++++++++++---- GDJS/Runtime/runtimegame.ts | 5 +- GDJS/tests/tests/game-canvas.js | 59 +++++++++++++++++++ 3 files changed, 96 insertions(+), 14 deletions(-) create mode 100644 GDJS/tests/tests/game-canvas.js diff --git a/GDJS/Runtime/pixi-renderers/runtimegame-pixi-renderer.ts b/GDJS/Runtime/pixi-renderers/runtimegame-pixi-renderer.ts index 75c970afe09f..e4cfcb35e9b3 100644 --- a/GDJS/Runtime/pixi-renderers/runtimegame-pixi-renderer.ts +++ b/GDJS/Runtime/pixi-renderers/runtimegame-pixi-renderer.ts @@ -60,15 +60,29 @@ namespace gdjs { } /** - * Create a standard canvas inside canvasArea. + * Create the canvas on which the game will be rendered, inside the specified DOM element, and + * setup the rendering of the game. + * If you want to use your own canvas, use `initializeForCanvas` instead. * + * @param parentElement The parent element to which the canvas will be added. */ createStandardCanvas(parentElement: HTMLElement) { this._throwIfDisposed(); - let gameCanvas: HTMLCanvasElement; + const gameCanvas = document.createElement('canvas'); + parentElement.appendChild(gameCanvas); + + this.initializeForCanvas(gameCanvas); + } + + /** + * Setup the rendering of the game to use a canvas that was already created. + * @param gameCanvas The canvas to use. + */ + initializeForCanvas(gameCanvas: HTMLCanvasElement): void { + this._throwIfDisposed(); + if (typeof THREE !== 'undefined') { - gameCanvas = document.createElement('canvas'); this._threeRenderer = new THREE.WebGLRenderer({ canvas: gameCanvas, antialias: @@ -99,8 +113,6 @@ namespace gdjs { backgroundAlpha: 0, // TODO (3D): add a setting for pixel ratio (`resolution: window.devicePixelRatio`) }); - - gameCanvas = this._threeRenderer.domElement; } else { // Create the renderer and setup the rendering area. // "preserveDrawingBuffer: true" is needed to avoid flickering @@ -108,11 +120,10 @@ namespace gdjs { this._pixiRenderer = PIXI.autoDetectRenderer({ width: this._game.getGameResolutionWidth(), height: this._game.getGameResolutionHeight(), + view: gameCanvas, preserveDrawingBuffer: true, antialias: false, }) as PIXI.Renderer; - - gameCanvas = this._pixiRenderer.view as HTMLCanvasElement; } // Deactivating accessibility support in PixiJS renderer, as we want to be in control of this. @@ -121,7 +132,6 @@ namespace gdjs { delete this._pixiRenderer.plugins.accessibility; // Add the renderer view element to the DOM - parentElement.appendChild(gameCanvas); this._gameCanvas = gameCanvas; gameCanvas.style.position = 'absolute'; @@ -160,7 +170,7 @@ namespace gdjs { // but it seems not to affect us as the `domElementsContainer` has `pointerEvents` set to `none`. domElementsContainer.style['-webkit-user-select'] = 'none'; - parentElement.appendChild(domElementsContainer); + gameCanvas.parentNode?.appendChild(domElementsContainer); this._domElementsContainer = domElementsContainer; this._resizeCanvas(); @@ -938,14 +948,26 @@ namespace gdjs { } /** - * Dispose PixiRenderer, ThreeRenderer and remove canvas from DOM. + * Dispose the renderers (PixiJS and/or Three.js) as well as DOM elements + * used for the game (the canvas, if specified, and the additional DOM container + * created on top of it to allow display HTML elements, for example for text inputs). + * + * @param removeCanvas If true, the canvas will be removed from the DOM. */ - dispose() { - this._pixiRenderer?.destroy(true); + dispose(removeCanvas?: boolean) { + this._pixiRenderer?.destroy(); this._threeRenderer?.dispose(); this._pixiRenderer = null; this._threeRenderer = null; + + if (removeCanvas && this._gameCanvas) { + this._gameCanvas.parentNode?.removeChild(this._gameCanvas); + } + this._gameCanvas = null; + this._domElementsContainer?.parentNode?.removeChild( + this._domElementsContainer + ); this._domElementsContainer = null; this._wasDisposed = true; } diff --git a/GDJS/Runtime/runtimegame.ts b/GDJS/Runtime/runtimegame.ts index e940a22fe09d..14bd400bab81 100644 --- a/GDJS/Runtime/runtimegame.ts +++ b/GDJS/Runtime/runtimegame.ts @@ -976,11 +976,12 @@ namespace gdjs { /** * Stop game loop, unload all scenes, dispose renderer and resources. * After calling this method, the RuntimeGame should not be used anymore. + * @param removeCanvas If true, the canvas will be removed from the DOM. */ - dispose(): void { + dispose(removeCanvas?: boolean): void { this._renderer.stopGameLoop(); this._sceneStack.dispose(); - this._renderer.dispose(); + this._renderer.dispose(removeCanvas); this._resourcesLoader.dispose(); this._wasDisposed = true; diff --git a/GDJS/tests/tests/game-canvas.js b/GDJS/tests/tests/game-canvas.js new file mode 100644 index 000000000000..ead09bf8dbaf --- /dev/null +++ b/GDJS/tests/tests/game-canvas.js @@ -0,0 +1,59 @@ +describe('gdjs.RuntimeGameRenderer canvas tests', () => { + let runtimeGame; + let renderer; + let gameContainer; + + beforeEach(() => { + runtimeGame = gdjs.getPixiRuntimeGame(); + renderer = runtimeGame.getRenderer(); + gameContainer = document.createElement('div'); + }); + + it('should correctly create standard canvas and domElementsContainer', () => { + renderer.createStandardCanvas(gameContainer); + + const actualGameCanvas = renderer.getCanvas(); + const actualDomElementsContainer = renderer.getDomElementContainer(); + + expect(actualGameCanvas).to.not.be(null); + expect(actualDomElementsContainer).to.not.be(null); + expect(actualGameCanvas.parentElement).to.be(gameContainer); + expect(actualDomElementsContainer.parentElement).to.be(gameContainer); + }); + + it('should correctly initialize external canvas and create domElementsContainer', () => { + const gameCanvas = document.createElement('canvas'); + gameContainer.appendChild(gameCanvas); + renderer.initializeForCanvas(gameCanvas); + + const actualGameCanvas = renderer.getCanvas(); + const actualDomElementsContainer = renderer.getDomElementContainer(); + + expect(actualGameCanvas).to.not.be(null); + expect(actualDomElementsContainer).to.not.be(null); + expect(actualGameCanvas).to.be(gameCanvas); + expect(actualDomElementsContainer.parentElement).to.be(gameContainer); + }); + + it('should remove canvas and domElementsContainer on dispose', () => { + renderer.createStandardCanvas(gameContainer); + + const actualGameCanvas = renderer.getCanvas(); + const actualDomElementsContainer = renderer.getDomElementContainer(); + + expect(actualGameCanvas).to.not.be(null); + expect(actualDomElementsContainer).to.not.be(null); + expect(actualGameCanvas.parentElement).to.be(gameContainer); + expect(actualDomElementsContainer.parentElement).to.be(gameContainer); + + runtimeGame.dispose(true); + + const actualGameCanvasAfterDispose = renderer.getCanvas(); + const actualDomElementsContainerAfterDispose = renderer.getDomElementContainer(); + + expect(actualGameCanvasAfterDispose).to.be(null); + expect(actualDomElementsContainerAfterDispose).to.be(null); + + expect(gameContainer.childNodes.length).to.be(0); + }); +}); \ No newline at end of file