UNPKG

phaser

Version:

A fast, free and fun HTML5 Game Framework for Desktop and Mobile web browsers from the team at Phaser Studio Inc.

901 lines (753 loc) 28 kB
/** * @author Richard Davey <rich@phaser.io> * @author Felipe Alfonso <@bitnenfer> * @copyright 2013-2025 Phaser Studio Inc. * @license {@link https://opensource.org/licenses/MIT|MIT License} */ var CameraEvents = require('../../cameras/2d/events'); var CanvasSnapshot = require('../snapshot/CanvasSnapshot'); var Class = require('../../utils/Class'); var CONST = require('../../const'); var EventEmitter = require('eventemitter3'); var Events = require('../events'); var GetBlendModes = require('./utils/GetBlendModes'); var ScaleEvents = require('../../scale/events'); var TextureEvents = require('../../textures/events'); var GameEvents = require('../../core/events'); var TransformMatrix = require('../../gameobjects/components/TransformMatrix'); /** * @classdesc * The Canvas Renderer is responsible for managing 2D canvas rendering contexts, * including the one used by the Games canvas. It tracks the internal state of a * given context and can renderer textured Game Objects to it, taking into * account alpha, blending, and scaling. * * @class CanvasRenderer * @extends Phaser.Events.EventEmitter * @memberof Phaser.Renderer.Canvas * @constructor * @since 3.0.0 * * @param {Phaser.Game} game - The Phaser Game instance that owns this renderer. */ var CanvasRenderer = new Class({ Extends: EventEmitter, initialize: function CanvasRenderer (game) { EventEmitter.call(this); var gameConfig = game.config; /** * The local configuration settings of the CanvasRenderer. * * @name Phaser.Renderer.Canvas.CanvasRenderer#config * @type {object} * @since 3.0.0 */ this.config = { clearBeforeRender: gameConfig.clearBeforeRender, backgroundColor: gameConfig.backgroundColor, antialias: gameConfig.antialias, roundPixels: gameConfig.roundPixels, transparent: gameConfig.transparent }; /** * The Phaser Game instance that owns this renderer. * * @name Phaser.Renderer.Canvas.CanvasRenderer#game * @type {Phaser.Game} * @since 3.0.0 */ this.game = game; /** * A constant which allows the renderer to be easily identified as a Canvas Renderer. * * @name Phaser.Renderer.Canvas.CanvasRenderer#type * @type {number} * @since 3.0.0 */ this.type = CONST.CANVAS; /** * The total number of Game Objects which were rendered in a frame. * * @name Phaser.Renderer.Canvas.CanvasRenderer#drawCount * @type {number} * @default 0 * @since 3.0.0 */ this.drawCount = 0; /** * The width of the canvas being rendered to. * * @name Phaser.Renderer.Canvas.CanvasRenderer#width * @type {number} * @since 3.0.0 */ this.width = 0; /** * The height of the canvas being rendered to. * * @name Phaser.Renderer.Canvas.CanvasRenderer#height * @type {number} * @since 3.0.0 */ this.height = 0; /** * The canvas element which the Game uses. * * @name Phaser.Renderer.Canvas.CanvasRenderer#gameCanvas * @type {HTMLCanvasElement} * @since 3.0.0 */ this.gameCanvas = game.canvas; var contextOptions = { alpha: gameConfig.transparent, desynchronized: gameConfig.desynchronized, willReadFrequently: false }; /** * The canvas context used to render all Cameras in all Scenes during the game loop. * * @name Phaser.Renderer.Canvas.CanvasRenderer#gameContext * @type {CanvasRenderingContext2D} * @since 3.0.0 */ this.gameContext = (gameConfig.context) ? gameConfig.context : this.gameCanvas.getContext('2d', contextOptions); /** * The canvas context currently used by the CanvasRenderer for all rendering operations. * * @name Phaser.Renderer.Canvas.CanvasRenderer#currentContext * @type {CanvasRenderingContext2D} * @since 3.0.0 */ this.currentContext = this.gameContext; /** * Should the Canvas use Image Smoothing or not when drawing Sprites? * * @name Phaser.Renderer.Canvas.CanvasRenderer#antialias * @type {boolean} * @since 3.20.0 */ this.antialias = gameConfig.antialias; /** * The blend modes supported by the Canvas Renderer. * * This object maps the {@link Phaser.BlendModes} to canvas compositing operations. * * @name Phaser.Renderer.Canvas.CanvasRenderer#blendModes * @type {array} * @since 3.0.0 */ this.blendModes = GetBlendModes(); /** * Details about the currently scheduled snapshot. * * If a non-null `callback` is set in this object, a snapshot of the canvas will be taken after the current frame is fully rendered. * * @name Phaser.Renderer.Canvas.CanvasRenderer#snapshotState * @type {Phaser.Types.Renderer.Snapshot.SnapshotState} * @since 3.16.0 */ this.snapshotState = { x: 0, y: 0, width: 1, height: 1, getPixel: false, callback: null, type: 'image/png', encoder: 0.92 }; /** * A temporary Transform Matrix, re-used internally during batching. * * @name Phaser.Renderer.Canvas.CanvasRenderer#_tempMatrix1 * @private * @type {Phaser.GameObjects.Components.TransformMatrix} * @since 3.11.0 */ this._tempMatrix1 = new TransformMatrix(); /** * A temporary Transform Matrix, re-used internally during batching. * * @name Phaser.Renderer.Canvas.CanvasRenderer#_tempMatrix2 * @private * @type {Phaser.GameObjects.Components.TransformMatrix} * @since 3.11.0 */ this._tempMatrix2 = new TransformMatrix(); /** * A temporary Transform Matrix, re-used internally during batching. * * @name Phaser.Renderer.Canvas.CanvasRenderer#_tempMatrix3 * @private * @type {Phaser.GameObjects.Components.TransformMatrix} * @since 3.11.0 */ this._tempMatrix3 = new TransformMatrix(); /** * Has this renderer fully booted yet? * * @name Phaser.Renderer.Canvas.CanvasRenderer#isBooted * @type {boolean} * @since 3.50.0 */ this.isBooted = false; this.init(); }, /** * Prepares the game canvas for rendering. * * @method Phaser.Renderer.Canvas.CanvasRenderer#init * @since 3.0.0 */ init: function () { var game = this.game; game.events.once(GameEvents.BOOT, function () { var config = this.config; if (!config.transparent) { var ctx = this.gameContext; var gameCanvas = this.gameCanvas; ctx.fillStyle = config.backgroundColor.rgba; ctx.fillRect(0, 0, gameCanvas.width, gameCanvas.height); } }, this); game.textures.once(TextureEvents.READY, this.boot, this); }, /** * Internal boot handler. * * @method Phaser.Renderer.Canvas.CanvasRenderer#boot * @private * @since 3.50.0 */ boot: function () { var game = this.game; var baseSize = game.scale.baseSize; this.width = baseSize.width; this.height = baseSize.height; this.isBooted = true; game.scale.on(ScaleEvents.RESIZE, this.onResize, this); this.resize(baseSize.width, baseSize.height); }, /** * The event handler that manages the `resize` event dispatched by the Scale Manager. * * @method Phaser.Renderer.Canvas.CanvasRenderer#onResize * @since 3.16.0 * * @param {Phaser.Structs.Size} gameSize - The default Game Size object. This is the un-modified game dimensions. * @param {Phaser.Structs.Size} baseSize - The base Size object. The game dimensions multiplied by the resolution. The canvas width / height values match this. */ onResize: function (gameSize, baseSize) { // Has the underlying canvas size changed? if (baseSize.width !== this.width || baseSize.height !== this.height) { this.resize(baseSize.width, baseSize.height); } }, /** * Resize the main game canvas. * * @method Phaser.Renderer.Canvas.CanvasRenderer#resize * @fires Phaser.Renderer.Events#RESIZE * @since 3.0.0 * * @param {number} [width] - The new width of the renderer. * @param {number} [height] - The new height of the renderer. */ resize: function (width, height) { this.width = width; this.height = height; this.emit(Events.RESIZE, width, height); }, /** * Resets the transformation matrix of the current context to the identity matrix, thus resetting any transformation. * * @method Phaser.Renderer.Canvas.CanvasRenderer#resetTransform * @since 3.0.0 */ resetTransform: function () { this.currentContext.setTransform(1, 0, 0, 1, 0, 0); }, /** * Sets the blend mode (compositing operation) of the current context. * * @method Phaser.Renderer.Canvas.CanvasRenderer#setBlendMode * @since 3.0.0 * * @param {string} blendMode - The new blend mode which should be used. * * @return {this} This CanvasRenderer object. */ setBlendMode: function (blendMode) { this.currentContext.globalCompositeOperation = blendMode; return this; }, /** * Changes the Canvas Rendering Context that all draw operations are performed against. * * @method Phaser.Renderer.Canvas.CanvasRenderer#setContext * @since 3.12.0 * * @param {?CanvasRenderingContext2D} [ctx] - The new Canvas Rendering Context to draw everything to. Leave empty to reset to the Game Canvas. * * @return {this} The Canvas Renderer instance. */ setContext: function (ctx) { this.currentContext = (ctx) ? ctx : this.gameContext; return this; }, /** * Sets the global alpha of the current context. * * @method Phaser.Renderer.Canvas.CanvasRenderer#setAlpha * @since 3.0.0 * * @param {number} alpha - The new alpha to use, where 0 is fully transparent and 1 is fully opaque. * * @return {this} This CanvasRenderer object. */ setAlpha: function (alpha) { this.currentContext.globalAlpha = alpha; return this; }, /** * Called at the start of the render loop. * * @method Phaser.Renderer.Canvas.CanvasRenderer#preRender * @fires Phaser.Renderer.Events#PRE_RENDER_CLEAR * @fires Phaser.Renderer.Events#PRE_RENDER * @since 3.0.0 */ preRender: function () { var ctx = this.gameContext; var config = this.config; var width = this.width; var height = this.height; ctx.globalAlpha = 1; ctx.globalCompositeOperation = 'source-over'; ctx.setTransform(1, 0, 0, 1, 0, 0); this.emit(Events.PRE_RENDER_CLEAR); if (config.clearBeforeRender) { ctx.clearRect(0, 0, width, height); if (!config.transparent) { ctx.fillStyle = config.backgroundColor.rgba; ctx.fillRect(0, 0, width, height); } } ctx.save(); this.drawCount = 0; this.emit(Events.PRE_RENDER); }, /** * The core render step for a Scene Camera. * * Iterates through the given array of Game Objects and renders them with the given Camera. * * This is called by the `CameraManager.render` method. The Camera Manager instance belongs to a Scene, and is invoked * by the Scene Systems.render method. * * This method is not called if `Camera.visible` is `false`, or `Camera.alpha` is zero. * * @method Phaser.Renderer.Canvas.CanvasRenderer#render * @fires Phaser.Renderer.Events#RENDER * @since 3.0.0 * * @param {Phaser.Scene} scene - The Scene to render. * @param {Phaser.GameObjects.GameObject[]} children - An array of filtered Game Objects that can be rendered by the given Camera. * @param {Phaser.Cameras.Scene2D.Camera} camera - The Scene Camera to render with. */ render: function (scene, children, camera) { var childCount = children.length; this.emit(Events.RENDER, scene, camera); var cx = camera.x; var cy = camera.y; var cw = camera.width; var ch = camera.height; var ctx = (camera.renderToTexture) ? camera.context : scene.sys.context; // Save context pre-clip ctx.save(); if (this.game.scene.customViewports) { ctx.beginPath(); ctx.rect(cx, cy, cw, ch); ctx.clip(); } camera.emit(CameraEvents.PRE_RENDER, camera); this.currentContext = ctx; var mask = camera.mask; if (mask) { mask.preRenderCanvas(this, null, camera._maskCamera); } if (!camera.transparent) { ctx.fillStyle = camera.backgroundColor.rgba; ctx.fillRect(cx, cy, cw, ch); } ctx.globalAlpha = camera.alpha; ctx.globalCompositeOperation = 'source-over'; this.drawCount += childCount; if (camera.renderToTexture) { camera.emit(CameraEvents.PRE_RENDER, camera); } camera.matrix.copyToContext(ctx); for (var i = 0; i < childCount; i++) { var child = children[i]; if (child.mask) { child.mask.preRenderCanvas(this, child, camera); } child.renderCanvas(this, child, camera); if (child.mask) { child.mask.postRenderCanvas(this, child, camera); } } ctx.setTransform(1, 0, 0, 1, 0, 0); ctx.globalCompositeOperation = 'source-over'; ctx.globalAlpha = 1; camera.flashEffect.postRenderCanvas(ctx); camera.fadeEffect.postRenderCanvas(ctx); camera.dirty = false; if (mask) { mask.postRenderCanvas(this); } // Restore pre-clip context ctx.restore(); if (camera.renderToTexture) { camera.emit(CameraEvents.POST_RENDER, camera); if (camera.renderToGame) { scene.sys.context.drawImage(camera.canvas, cx, cy); } } camera.emit(CameraEvents.POST_RENDER, camera); }, /** * Restores the game context's global settings and takes a snapshot if one is scheduled. * * The post-render step happens after all Cameras in all Scenes have been rendered. * * @method Phaser.Renderer.Canvas.CanvasRenderer#postRender * @fires Phaser.Renderer.Events#POST_RENDER * @since 3.0.0 */ postRender: function () { var ctx = this.gameContext; ctx.restore(); this.emit(Events.POST_RENDER); var state = this.snapshotState; if (state.callback) { CanvasSnapshot(this.gameCanvas, state); state.callback = null; } }, /** * Takes a snapshot of the given area of the given canvas. * * Unlike the other snapshot methods, this one is processed immediately and doesn't wait for the next render. * * Snapshots work by creating an Image object from the canvas data, this is a blocking process, which gets * more expensive the larger the canvas size gets, so please be careful how you employ this in your game. * * @method Phaser.Renderer.Canvas.CanvasRenderer#snapshotCanvas * @since 3.19.0 * * @param {HTMLCanvasElement} canvas - The canvas to grab from. * @param {Phaser.Types.Renderer.Snapshot.SnapshotCallback} callback - The Function to invoke after the snapshot image is created. * @param {boolean} [getPixel=false] - Grab a single pixel as a Color object, or an area as an Image object? * @param {number} [x=0] - The x coordinate to grab from. * @param {number} [y=0] - The y coordinate to grab from. * @param {number} [width=canvas.width] - The width of the area to grab. * @param {number} [height=canvas.height] - The height of the area to grab. * @param {string} [type='image/png'] - The format of the image to create, usually `image/png` or `image/jpeg`. * @param {number} [encoderOptions=0.92] - The image quality, between 0 and 1. Used for image formats with lossy compression, such as `image/jpeg`. * * @return {this} This Canvas Renderer. */ snapshotCanvas: function (canvas, callback, getPixel, x, y, width, height, type, encoderOptions) { if (getPixel === undefined) { getPixel = false; } this.snapshotArea(x, y, width, height, callback, type, encoderOptions); var state = this.snapshotState; state.getPixel = getPixel; CanvasSnapshot(canvas, state); state.callback = null; return this; }, /** * Schedules a snapshot of the entire game viewport to be taken after the current frame is rendered. * * To capture a specific area see the `snapshotArea` method. To capture a specific pixel, see `snapshotPixel`. * * Only one snapshot can be active _per frame_. If you have already called `snapshotPixel`, for example, then * calling this method will override it. * * Snapshots work by creating an Image object from the canvas data, this is a blocking process, which gets * more expensive the larger the canvas size gets, so please be careful how you employ this in your game. * * @method Phaser.Renderer.Canvas.CanvasRenderer#snapshot * @since 3.0.0 * * @param {Phaser.Types.Renderer.Snapshot.SnapshotCallback} callback - The Function to invoke after the snapshot image is created. * @param {string} [type='image/png'] - The format of the image to create, usually `image/png` or `image/jpeg`. * @param {number} [encoderOptions=0.92] - The image quality, between 0 and 1. Used for image formats with lossy compression, such as `image/jpeg`. * * @return {this} This WebGL Renderer. */ snapshot: function (callback, type, encoderOptions) { return this.snapshotArea(0, 0, this.gameCanvas.width, this.gameCanvas.height, callback, type, encoderOptions); }, /** * Schedules a snapshot of the given area of the game viewport to be taken after the current frame is rendered. * * To capture the whole game viewport see the `snapshot` method. To capture a specific pixel, see `snapshotPixel`. * * Only one snapshot can be active _per frame_. If you have already called `snapshotPixel`, for example, then * calling this method will override it. * * Snapshots work by creating an Image object from the canvas data, this is a blocking process, which gets * more expensive the larger the canvas size gets, so please be careful how you employ this in your game. * * @method Phaser.Renderer.Canvas.CanvasRenderer#snapshotArea * @since 3.16.0 * * @param {number} x - The x coordinate to grab from. * @param {number} y - The y coordinate to grab from. * @param {number} width - The width of the area to grab. * @param {number} height - The height of the area to grab. * @param {Phaser.Types.Renderer.Snapshot.SnapshotCallback} callback - The Function to invoke after the snapshot image is created. * @param {string} [type='image/png'] - The format of the image to create, usually `image/png` or `image/jpeg`. * @param {number} [encoderOptions=0.92] - The image quality, between 0 and 1. Used for image formats with lossy compression, such as `image/jpeg`. * * @return {this} This WebGL Renderer. */ snapshotArea: function (x, y, width, height, callback, type, encoderOptions) { var state = this.snapshotState; state.callback = callback; state.type = type; state.encoder = encoderOptions; state.getPixel = false; state.x = x; state.y = y; state.width = Math.min(width, this.gameCanvas.width); state.height = Math.min(height, this.gameCanvas.height); return this; }, /** * Schedules a snapshot of the given pixel from the game viewport to be taken after the current frame is rendered. * * To capture the whole game viewport see the `snapshot` method. To capture a specific area, see `snapshotArea`. * * Only one snapshot can be active _per frame_. If you have already called `snapshotArea`, for example, then * calling this method will override it. * * Unlike the other two snapshot methods, this one will return a `Color` object containing the color data for * the requested pixel. It doesn't need to create an internal Canvas or Image object, so is a lot faster to execute, * using less memory. * * @method Phaser.Renderer.Canvas.CanvasRenderer#snapshotPixel * @since 3.16.0 * * @param {number} x - The x coordinate of the pixel to get. * @param {number} y - The y coordinate of the pixel to get. * @param {Phaser.Types.Renderer.Snapshot.SnapshotCallback} callback - The Function to invoke after the snapshot pixel data is extracted. * * @return {this} This WebGL Renderer. */ snapshotPixel: function (x, y, callback) { this.snapshotArea(x, y, 1, 1, callback); this.snapshotState.getPixel = true; return this; }, /** * Takes a Sprite Game Object, or any object that extends it, and draws it to the current context. * * @method Phaser.Renderer.Canvas.CanvasRenderer#batchSprite * @since 3.12.0 * * @param {Phaser.GameObjects.GameObject} sprite - The texture based Game Object to draw. * @param {Phaser.Textures.Frame} frame - The frame to draw, doesn't have to be that owned by the Game Object. * @param {Phaser.Cameras.Scene2D.Camera} camera - The Camera to use for the rendering transform. * @param {Phaser.GameObjects.Components.TransformMatrix} [parentTransformMatrix] - The transform matrix of the parent container, if set. */ batchSprite: function (sprite, frame, camera, parentTransformMatrix) { var alpha = camera.alpha * sprite.alpha; if (alpha === 0) { // Nothing to see, so abort early return; } var ctx = this.currentContext; var camMatrix = this._tempMatrix1; var spriteMatrix = this._tempMatrix2; var cd = frame.canvasData; var frameX = cd.x; var frameY = cd.y; var frameWidth = frame.cutWidth; var frameHeight = frame.cutHeight; var customPivot = frame.customPivot; var res = frame.source.resolution; var displayOriginX = sprite.displayOriginX; var displayOriginY = sprite.displayOriginY; var x = -displayOriginX + frame.x; var y = -displayOriginY + frame.y; if (sprite.isCropped) { var crop = sprite._crop; if (crop.flipX !== sprite.flipX || crop.flipY !== sprite.flipY) { frame.updateCropUVs(crop, sprite.flipX, sprite.flipY); } frameWidth = crop.cw; frameHeight = crop.ch; frameX = crop.cx; frameY = crop.cy; x = -displayOriginX + crop.x; y = -displayOriginY + crop.y; if (sprite.flipX) { if (x >= 0) { x = -(x + frameWidth); } else if (x < 0) { x = (Math.abs(x) - frameWidth); } } if (sprite.flipY) { if (y >= 0) { y = -(y + frameHeight); } else if (y < 0) { y = (Math.abs(y) - frameHeight); } } } var flipX = 1; var flipY = 1; if (sprite.flipX) { if (!customPivot) { x += (-frame.realWidth + (displayOriginX * 2)); } flipX = -1; } // Auto-invert the flipY if this is coming from a GLTexture if (sprite.flipY) { if (!customPivot) { y += (-frame.realHeight + (displayOriginY * 2)); } flipY = -1; } var gx = sprite.x; var gy = sprite.y; if (camera.roundPixels) { gx = Math.floor(gx); gy = Math.floor(gy); } spriteMatrix.applyITRS(gx, gy, sprite.rotation, sprite.scaleX * flipX, sprite.scaleY * flipY); camMatrix.copyFrom(camera.matrix); if (parentTransformMatrix) { // Multiply the camera by the parent matrix camMatrix.multiplyWithOffset(parentTransformMatrix, -camera.scrollX * sprite.scrollFactorX, -camera.scrollY * sprite.scrollFactorY); // Undo the camera scroll spriteMatrix.e = gx; spriteMatrix.f = gy; } else { spriteMatrix.e -= camera.scrollX * sprite.scrollFactorX; spriteMatrix.f -= camera.scrollY * sprite.scrollFactorY; } // Multiply by the Sprite matrix camMatrix.multiply(spriteMatrix); if (camera.renderRoundPixels) { camMatrix.e = Math.floor(camMatrix.e + 0.5); camMatrix.f = Math.floor(camMatrix.f + 0.5); } ctx.save(); camMatrix.setToContext(ctx); ctx.globalCompositeOperation = this.blendModes[sprite.blendMode]; ctx.globalAlpha = alpha; ctx.imageSmoothingEnabled = !frame.source.scaleMode; if (sprite.mask) { sprite.mask.preRenderCanvas(this, sprite, camera); } if (frameWidth > 0 && frameHeight > 0) { var fw = frameWidth / res; var fh = frameHeight / res; if (camera.roundPixels) { x = Math.floor(x + 0.5); y = Math.floor(y + 0.5); fw += 0.5; fh += 0.5; } ctx.drawImage( frame.source.image, frameX, frameY, frameWidth, frameHeight, x, y, fw, fh ); } if (sprite.mask) { sprite.mask.postRenderCanvas(this, sprite, camera); } ctx.restore(); }, /** * Destroys all object references in the Canvas Renderer. * * @method Phaser.Renderer.Canvas.CanvasRenderer#destroy * @since 3.0.0 */ destroy: function () { this.removeAllListeners(); this.game = null; this.gameCanvas = null; this.gameContext = null; } }); module.exports = CanvasRenderer;