phaser
Version:
A fast, free and fun HTML5 Game Framework for Desktop and Mobile web browsers.
762 lines (647 loc) • 25 kB
JavaScript
/**
* @author Richard Davey <rich@photonstorm.com>
* @author Felipe Alfonso <@bitnenfer>
* @copyright 2019 Photon Storm Ltd.
* @license {@link https://opensource.org/licenses/MIT|MIT License}
*/
var CanvasSnapshot = require('../snapshot/CanvasSnapshot');
var CameraEvents = require('../../cameras/2d/events');
var Class = require('../../utils/Class');
var CONST = require('../../const');
var GetBlendModes = require('./utils/GetBlendModes');
var ScaleModes = require('../ScaleModes');
var Smoothing = require('../../display/canvas/Smoothing');
var TransformMatrix = require('../../gameobjects/components/TransformMatrix');
/**
* @classdesc
* The Canvas Renderer is responsible for managing 2D canvas rendering contexts, including the one used by the Game's 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
* @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({
initialize:
function CanvasRenderer (game)
{
/**
* 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 {integer}
* @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 {integer}
* @since 3.0.0
*/
this.width = 0;
/**
* The height of the canvas being rendered to.
*
* @name Phaser.Renderer.Canvas.CanvasRenderer#height
* @type {integer}
* @since 3.0.0
*/
this.height = 0;
/**
* The local configuration settings of the CanvasRenderer.
*
* @name Phaser.Renderer.Canvas.CanvasRenderer#config
* @type {object}
* @since 3.0.0
*/
this.config = {
clearBeforeRender: game.config.clearBeforeRender,
backgroundColor: game.config.backgroundColor,
resolution: game.config.resolution,
antialias: game.config.antialias,
roundPixels: game.config.roundPixels
};
/**
* The scale mode which should be used by the CanvasRenderer.
*
* @name Phaser.Renderer.Canvas.CanvasRenderer#scaleMode
* @type {integer}
* @since 3.0.0
*/
this.scaleMode = (game.config.antialias) ? ScaleModes.LINEAR : ScaleModes.NEAREST;
/**
* The canvas element which the Game uses.
*
* @name Phaser.Renderer.Canvas.CanvasRenderer#gameCanvas
* @type {HTMLCanvasElement}
* @since 3.0.0
*/
this.gameCanvas = game.canvas;
/**
* 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 = (this.game.config.context) ? this.game.config.context : this.gameCanvas.getContext('2d');
/**
* 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;
/**
* 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();
// image-rendering: optimizeSpeed;
// image-rendering: pixelated;
/**
* The scale mode currently in use by the Canvas Renderer.
*
* @name Phaser.Renderer.Canvas.CanvasRenderer#currentScaleMode
* @type {number}
* @default 0
* @since 3.0.0
*/
this.currentScaleMode = 0;
/**
* 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.12.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.12.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.12.0
*/
this._tempMatrix3 = new TransformMatrix();
/**
* A temporary Transform Matrix, re-used internally during batching.
*
* @name Phaser.Renderer.Canvas.CanvasRenderer#_tempMatrix4
* @private
* @type {Phaser.GameObjects.Components.TransformMatrix}
* @since 3.12.0
*/
this._tempMatrix4 = new TransformMatrix();
this.init();
},
/**
* Prepares the game canvas for rendering.
*
* @method Phaser.Renderer.Canvas.CanvasRenderer#init
* @since 3.0.0
*/
init: function ()
{
this.game.scale.on('resize', this.onResize, this);
var baseSize = this.game.scale.baseSize;
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.
* @param {Phaser.Structs.Size} displaySize - The display Size object. The size of the canvas style width / height attributes.
* @param {number} [resolution] - The Scale Manager resolution setting.
*/
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
* @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;
// Resizing a canvas will reset imageSmoothingEnabled (and probably other properties)
if (this.scaleMode === ScaleModes.NEAREST)
{
Smoothing.disable(this.gameContext);
}
},
/**
* A NOOP method for handling lost context. Intentionally empty.
*
* @method Phaser.Renderer.Canvas.CanvasRenderer#onContextLost
* @since 3.0.0
*
* @param {function} callback - Ignored parameter.
*/
onContextLost: function ()
{
},
/**
* A NOOP method for handling restored context. Intentionally empty.
*
* @method Phaser.Renderer.Canvas.CanvasRenderer#onContextRestored
* @since 3.0.0
*
* @param {function} callback - Ignored parameter.
*/
onContextRestored: function ()
{
},
/**
* 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
* @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);
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;
},
/**
* Renders the Scene to the given Camera.
*
* @method Phaser.Renderer.Canvas.CanvasRenderer#render
* @since 3.0.0
*
* @param {Phaser.Scene} scene - The Scene to render.
* @param {Phaser.GameObjects.DisplayList} children - The Game Objects within the Scene to be rendered.
* @param {number} interpolationPercentage - The interpolation percentage to apply. Currently unused.
* @param {Phaser.Cameras.Scene2D.Camera} camera - The Scene Camera to render with.
*/
render: function (scene, children, interpolationPercentage, camera)
{
var list = children.list;
var childCount = list.length;
var cx = camera._cx;
var cy = camera._cy;
var cw = camera._cw;
var ch = camera._ch;
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();
}
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 += list.length;
if (camera.renderToTexture)
{
camera.emit(CameraEvents.PRE_RENDER, camera);
}
camera.matrix.copyToContext(ctx);
for (var i = 0; i < childCount; i++)
{
var child = list[i];
if (!child.willRender(camera))
{
continue;
}
if (child.mask)
{
child.mask.preRenderCanvas(this, child, camera);
}
child.renderCanvas(this, child, interpolationPercentage, 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);
scene.sys.context.drawImage(camera.canvas, cx, cy);
}
},
/**
* 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
* @since 3.0.0
*/
postRender: function ()
{
var ctx = this.gameContext;
ctx.restore();
var state = this.snapshotState;
if (state.callback)
{
CanvasSnapshot(this.gameCanvas, state);
state.callback = null;
}
},
/**
* 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 {integer} x - The x coordinate to grab from.
* @param {integer} y - The y coordinate to grab from.
* @param {integer} width - The width of the area to grab.
* @param {integer} 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 {integer} x - The x coordinate of the pixel to get.
* @param {integer} 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 calcMatrix = this._tempMatrix3;
var cd = frame.canvasData;
var frameX = cd.x;
var frameY = cd.y;
var frameWidth = frame.cutWidth;
var frameHeight = frame.cutHeight;
var res = frame.source.resolution;
var x = -sprite.displayOriginX + frame.x;
var y = -sprite.displayOriginY + frame.y;
var fx = (sprite.flipX) ? -1 : 1;
var fy = (sprite.flipY) ? -1 : 1;
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 = -sprite.displayOriginX + crop.x;
y = -sprite.displayOriginY + crop.y;
if (fx === -1)
{
if (x >= 0)
{
x = -(x + frameWidth);
}
else if (x < 0)
{
x = (Math.abs(x) - frameWidth);
}
}
if (fy === -1)
{
if (y >= 0)
{
y = -(y + frameHeight);
}
else if (y < 0)
{
y = (Math.abs(y) - frameHeight);
}
}
}
spriteMatrix.applyITRS(sprite.x, sprite.y, sprite.rotation, sprite.scaleX, sprite.scaleY);
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 = sprite.x;
spriteMatrix.f = sprite.y;
// Multiply by the Sprite matrix, store result in calcMatrix
camMatrix.multiply(spriteMatrix, calcMatrix);
}
else
{
spriteMatrix.e -= camera.scrollX * sprite.scrollFactorX;
spriteMatrix.f -= camera.scrollY * sprite.scrollFactorY;
// Multiply by the Sprite matrix, store result in calcMatrix
camMatrix.multiply(spriteMatrix, calcMatrix);
}
ctx.save();
calcMatrix.setToContext(ctx);
ctx.scale(fx, fy);
ctx.globalCompositeOperation = this.blendModes[sprite.blendMode];
ctx.globalAlpha = alpha;
ctx.drawImage(frame.source.image, frameX, frameY, frameWidth, frameHeight, x, y, frameWidth / res, frameHeight / res);
ctx.restore();
},
/**
* Destroys all object references in the Canvas Renderer.
*
* @method Phaser.Renderer.Canvas.CanvasRenderer#destroy
* @since 3.0.0
*/
destroy: function ()
{
this.gameCanvas = null;
this.gameContext = null;
this.game = null;
}
});
module.exports = CanvasRenderer;