Skip to content

Fix / double-copy for Safari tilemap bug when rendering with delta scrolling #1498

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jan 5, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/system/Canvas.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Phaser.Canvas = {
* @method Phaser.Canvas.create
* @param {number} [width=256] - The width of the canvas element.
* @param {number} [height=256] - The height of the canvas element..
* @param {string} [id=''] - If given this will be set as the ID of the canvas element, otherwise no ID will be set.
* @param {string} [id=(none)] - If specified, and not the empty string, this will be set as the ID of the canvas element. Otherwise no ID will be set.
* @return {HTMLCanvasElement} The newly created canvas element.
*/
create: function (width, height, id) {
Expand Down
49 changes: 33 additions & 16 deletions src/system/Device.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,18 @@ Phaser.Device = function () {
*/
this.canvas = false;

/**
* @property {?boolean} canvasBitBltShift - True if canvas supports a 'copy' bitblt onto itself when the source and destination regions overlap.
* @default
*/
this.canvasBitBltShift = null;

/**
* @property {boolean} webGL - Is webGL available?
* @default
*/
this.webGL = false;

/**
* @property {boolean} file - Is file available?
* @default
Expand All @@ -161,12 +173,6 @@ Phaser.Device = function () {
*/
this.localStorage = false;

/**
* @property {boolean} webGL - Is webGL available?
* @default
*/
this.webGL = false;

/**
* @property {boolean} worker - Is worker available?
* @default
Expand Down Expand Up @@ -224,7 +230,7 @@ Phaser.Device = function () {
this.mspointer = false;

/**
* @property {string|null} wheelType - The newest type of Wheel/Scroll event supported: 'wheel', 'mousewheel', 'DOMMouseScroll'
* @property {?string} wheelType - The newest type of Wheel/Scroll event supported: 'wheel', 'mousewheel', 'DOMMouseScroll'
* @default
* @protected
*/
Expand Down Expand Up @@ -387,6 +393,8 @@ Phaser.Device = function () {
*/
this.iPad = false;

// Device features

/**
* @property {number} pixelRatio - PixelRatio of the host device?
* @default
Expand Down Expand Up @@ -643,16 +651,9 @@ Phaser.Device._initialize = function () {

device.file = !!window['File'] && !!window['FileReader'] && !!window['FileList'] && !!window['Blob'];
device.fileSystem = !!window['requestFileSystem'];
device.webGL = ( function () { try { var canvas = document.createElement( 'canvas' ); /*Force screencanvas to false*/ canvas.screencanvas = false; return !! window.WebGLRenderingContext && ( canvas.getContext( 'webgl' ) || canvas.getContext( 'experimental-webgl' ) ); } catch( e ) { return false; } } )();

if (device.webGL === null || device.webGL === false)
{
device.webGL = false;
}
else
{
device.webGL = true;
}
device.webGL = ( function () { try { var canvas = document.createElement( 'canvas' ); /*Force screencanvas to false*/ canvas.screencanvas = false; return !! window.WebGLRenderingContext && ( canvas.getContext( 'webgl' ) || canvas.getContext( 'experimental-webgl' ) ); } catch( e ) { return false; } } )();
device.webGL = !!device.webGL;

device.worker = !!window['Worker'];

Expand All @@ -662,6 +663,22 @@ Phaser.Device._initialize = function () {

device.getUserMedia = !!(navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia);

// TODO: replace canvasBitBltShift detection with actual feature check

// Excludes iOS versions as they generally wrap UIWebView (eg. Safari WebKit) and it
// is safer to not try and use the fast copy-over method.
if (!device.iOS &&
(device.ie || device.firefox || device.chrome))
{
device.canvasBitBltShift = true;
}

// Known not to work
if (device.safari || device.mobileSafari)
{
device.canvasBitBltShift = false;
}

}

/**
Expand Down
113 changes: 100 additions & 13 deletions src/tilemap/TilemapLayer.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
* A TilemapLayer is a Phaser.Image/Sprite that renders a specific TileLayer of a Tilemap.
*
* Since a TilemapLayer is a Sprite it can be moved around the display, added to other groups or display objects, etc.
*
* By default TilemapLayers have fixedToCamera set to `true`. Changing this will break Camera follow and scrolling behaviour.
*
* @class Phaser.TilemapLayer
Expand Down Expand Up @@ -61,7 +62,7 @@ Phaser.TilemapLayer = function (game, tilemap, index, width, height) {
* @property {HTMLCanvasElement} canvas
* @protected
*/
this.canvas = Phaser.Canvas.create(width, height, '', true);
this.canvas = Phaser.Canvas.create(width, height);

/**
* The 2d context of the canvas.
Expand Down Expand Up @@ -124,15 +125,24 @@ Phaser.TilemapLayer = function (game, tilemap, index, width, height) {
/**
* Settings that control standard (non-diagnostic) rendering.
*
* @public
* @property {boolean} enableScrollDelta - When enabled, only new newly exposed areas of the layer are redraw after scrolling. This can greatly improve scrolling rendering performance, especially when there are many small tiles.
* @property {boolean} [enableScrollDelta=true] - Delta scroll rendering only draws tiles/edges as them come into view.
* This can greatly improve scrolling rendering performance, especially when there are many small tiles.
* It should only be disabled in rare cases.
*
* @property {?DOMCanvasElement} [copyCanvas=(auto)] - [Internal] If set, force using a separate (shared) copy canvas.
* Using a canvas bitblt/copy when the source and destinations region overlap produces unexpected behavior
* in some browsers, notably Safari.
*
* @property {integer} copySliceCount - [Internal] The number of vertical slices to copy when using a `copyCanvas`.
* This is ratio of the pixel count of the primary canvas to the copy canvas.
*
* @default
*/
this.renderSettings = {

enableScrollDelta: true,
overdrawRatio: 0.20

overdrawRatio: 0.20,
copyCanvas: null,
copySliceCount: 4
};

/**
Expand Down Expand Up @@ -257,6 +267,35 @@ Phaser.TilemapLayer = function (game, tilemap, index, width, height) {
*/
this._results = [];

if (!game.device.canvasBitBltShift)
{
this.renderSettings.copyCanvas = Phaser.TilemapLayer.ensureSharedCopyCanvas();
}

};

/**
* The shared double-copy canvas, created as needed.
*
* @private
* @static
*/
Phaser.TilemapLayer.sharedCopyCanvas = null;

/**
* Create if needed (and return) a shared copy canvas that is shared across all TilemapLayers.
*
* Code that uses the canvas is responsible to ensure the dimensions and save/restore state as appropriate.
*
* @protected
* @static
*/
Phaser.TilemapLayer.ensureSharedCopyCanvas = function () {
if (!this.sharedCopyCanvas)
{
this.sharedCopyCanvas = Phaser.Canvas.create(2, 2);
}
return this.sharedCopyCanvas;
};

Phaser.TilemapLayer.prototype = Object.create(Phaser.Image.prototype);
Expand Down Expand Up @@ -590,7 +629,8 @@ Object.defineProperty(Phaser.TilemapLayer.prototype, "wrap", {
});

/**
* Returns the appropriate tileset for the index, updating the internal cache as required. This should only be called if `tilesets[index]` evaluates to undefined.
* Returns the appropriate tileset for the index, updating the internal cache as required.
* This should only be called if `tilesets[index]` evaluates to undefined.
*
* @method Phaser.TilemapLayer#resolveTileset
* @private
Expand Down Expand Up @@ -624,7 +664,9 @@ Phaser.TilemapLayer.prototype.resolveTileset = function (tileIndex)
};

/**
* The TilemapLayer caches tileset look-ups. Call this method of clear the cache if tilesets have been added or updated after the layer has been rendered.
* The TilemapLayer caches tileset look-ups.
*
* Call this method of clear the cache if tilesets have been added or updated after the layer has been rendered.
*
* @method Phaser.TilemapLayer#resetTilesetCache
* @public
Expand All @@ -640,7 +682,9 @@ Phaser.TilemapLayer.prototype.resetTilesetCache = function ()
};

/**
* Shifts the contents of the canvas - does extra math so that different browsers agree on the result. The specified (x/y) will be shifted to (0,0) after the copy. The newly exposed canvas area will need to be filled in. This method is problematic for transparent tiles.
* Shifts the contents of the canvas - does extra math so that different browsers agree on the result.
*
* The specified (x/y) will be shifted to (0,0) after the copy and the newly exposed canvas area will need to be filled in.
*
* @method Phaser.TilemapLayer#shiftCanvas
* @private
Expand Down Expand Up @@ -673,10 +717,53 @@ Phaser.TilemapLayer.prototype.shiftCanvas = function (context, x, y)
sy = 0;
}

context.save();
context.globalCompositeOperation = 'copy';
context.drawImage(canvas, dx, dy, copyW, copyH, sx, sy, copyW, copyH);
context.restore();
var copyCanvas = this.renderSettings.copyCanvas;
if (copyCanvas)
{
// Copying happens in slices to minimize copy canvas size overhead
var sliceCount = this.renderSettings.copySliceCount;
var sH = Math.ceil(copyH / sliceCount);
// Ensure copy canvas is large enough
if (copyCanvas.width < copyW) { copyCanvas.width = copyW; }
if (copyCanvas.height < sH) { copyCanvas.height = sH; }

var vShift;
if (dy >= sy)
{
// move old region up, or don't change vertically - copy top to bottom
vShift = sH;
}
else
{
// move old region down - copy segments from bottom to top
vShift = -sH;
dy += (sH * (sliceCount - 1));
sy += (sH * (sliceCount - 1));
}

var copyContext = copyCanvas.getContext('2d');
while (sliceCount--)
{
copyContext.clearRect(0, 0, copyW, sH);
copyContext.drawImage(canvas, dx, dy, copyW, sH, 0, 0, copyW, sH);
// clear allows default 'source-over' semantics
context.clearRect(sx, sy, copyW, sH);
context.drawImage(copyCanvas, 0, 0, copyW, sH, sx, sy, copyW, sH);

dy += vShift;
sy += vShift;
}

}
else
{
// Avoids a second copy but flickers in Safari / Safari Mobile
// Ref. https://github.com/photonstorm/phaser/issues/1439
context.save();
context.globalCompositionOperation = 'copy';
context.drawImage(canvas, dx, dy, copyW, copyH, sx, sy, copyW, copyH);
context.restore();
}
};

/**
Expand Down