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
JavaScript
/**
* @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;