phaser
Version:
A fast, free and fun HTML5 Game Framework for Desktop and Mobile web browsers from the team at Phaser Studio Inc.
641 lines (531 loc) • 19.1 kB
JavaScript
var WebGLSnapshot = require('../../../src/renderer/snapshot/WebGLSnapshot');
var CanvasPool = require('../../../src/display/canvas/CanvasPool');
describe('Phaser.Renderer.Snapshot.WebGL', function ()
{
var mockGl;
var mockCanvas;
var mockCtx;
var mockImageData;
var capturedImage;
var spyCreateWebGL;
var spyRemove;
beforeEach(function ()
{
mockImageData = { data: new Array(4).fill(0) };
mockCtx = {
getImageData: vi.fn(function (x, y, w, h)
{
mockImageData = { data: new Array(w * h * 4).fill(0) };
return mockImageData;
}),
putImageData: vi.fn()
};
mockCanvas = {
getContext: vi.fn(function ()
{
return mockCtx;
}),
toDataURL: vi.fn(function (type, encoder)
{
return 'data:' + type + ';base64,abc123';
})
};
spyCreateWebGL = vi.spyOn(CanvasPool, 'createWebGL').mockReturnValue(mockCanvas);
spyRemove = vi.spyOn(CanvasPool, 'remove').mockImplementation(function () {});
mockGl = {
drawingBufferWidth: 800,
drawingBufferHeight: 600,
RGBA: 0x1908,
UNSIGNED_BYTE: 0x1401,
readPixels: vi.fn()
};
capturedImage = null;
global.Image = function ()
{
this.onerror = null;
this.onload = null;
this._src = '';
capturedImage = this;
Object.defineProperty(this, 'src', {
set: function (val)
{
this._src = val;
if (this.onload)
{
this.onload();
}
},
get: function ()
{
return this._src;
}
});
};
});
afterEach(function ()
{
spyCreateWebGL.mockRestore();
spyRemove.mockRestore();
delete global.Image;
});
// -------------------------------------------------------------------------
// getPixel path
// -------------------------------------------------------------------------
describe('getPixel mode', function ()
{
it('should call readPixels at the correct coordinates', function ()
{
var config = {
callback: vi.fn(),
getPixel: true,
x: 10,
y: 20
};
WebGLSnapshot(mockGl, config);
expect(mockGl.readPixels).toHaveBeenCalledOnce();
var args = mockGl.readPixels.mock.calls[0];
expect(args[0]).toBe(10);
expect(args[1]).toBe(600 - 20 - 1);
expect(args[2]).toBe(1);
expect(args[3]).toBe(1);
});
it('should call the callback with a Color object containing the pixel values', function ()
{
var callbackArg = null;
var config = {
callback: function (color)
{
callbackArg = color;
},
getPixel: true,
x: 0,
y: 0
};
mockGl.readPixels.mockImplementation(function (x, y, w, h, fmt, type, pixels)
{
pixels[0] = 255;
pixels[1] = 128;
pixels[2] = 64;
pixels[3] = 200;
});
WebGLSnapshot(mockGl, config);
expect(callbackArg).not.toBeNull();
expect(callbackArg.red).toBe(255);
expect(callbackArg.green).toBe(128);
expect(callbackArg.blue).toBe(64);
expect(callbackArg.alpha).toBe(200);
});
it('should invert the Y coordinate to account for WebGL origin', function ()
{
var config = {
callback: vi.fn(),
getPixel: true,
x: 5,
y: 100
};
WebGLSnapshot(mockGl, config);
var destY = mockGl.readPixels.mock.calls[0][1];
expect(destY).toBe(600 - 100 - 1);
});
it('should use absolute value and round for x and y coordinates', function ()
{
var config = {
callback: vi.fn(),
getPixel: true,
x: -7.9,
y: -3.1
};
WebGLSnapshot(mockGl, config);
var args = mockGl.readPixels.mock.calls[0];
expect(args[0]).toBe(8);
expect(args[1]).toBe(600 - 3 - 1);
});
it('should use bufferWidth and bufferHeight from config when isFramebuffer is true', function ()
{
var config = {
callback: vi.fn(),
getPixel: true,
x: 0,
y: 10,
isFramebuffer: true,
bufferWidth: 256,
bufferHeight: 128
};
WebGLSnapshot(mockGl, config);
var destY = mockGl.readPixels.mock.calls[0][1];
expect(destY).toBe(128 - 10 - 1);
});
it('should default bufferHeight to 1 when isFramebuffer is true and not specified', function ()
{
var config = {
callback: vi.fn(),
getPixel: true,
x: 0,
y: 0,
isFramebuffer: true
};
WebGLSnapshot(mockGl, config);
var destY = mockGl.readPixels.mock.calls[0][1];
expect(destY).toBe(1 - 0 - 1);
});
it('should default x and y to 0 when not specified', function ()
{
var config = {
callback: vi.fn(),
getPixel: true
};
WebGLSnapshot(mockGl, config);
var args = mockGl.readPixels.mock.calls[0];
expect(args[0]).toBe(0);
expect(args[1]).toBe(600 - 0 - 1);
});
});
// -------------------------------------------------------------------------
// area snapshot path
// -------------------------------------------------------------------------
describe('area snapshot mode', function ()
{
it('should call readPixels with inverted Y region', function ()
{
var config = {
callback: vi.fn(),
getPixel: false,
x: 10,
y: 20,
width: 100,
height: 50
};
WebGLSnapshot(mockGl, config);
var args = mockGl.readPixels.mock.calls[0];
expect(args[0]).toBe(10);
expect(args[1]).toBe(600 - 20 - 50);
expect(args[2]).toBe(100);
expect(args[3]).toBe(50);
});
it('should default width and height to drawingBuffer dimensions', function ()
{
var config = {
callback: vi.fn(),
getPixel: false
};
WebGLSnapshot(mockGl, config);
expect(spyCreateWebGL).toHaveBeenCalledOnce();
var args = spyCreateWebGL.mock.calls[0];
expect(args[1]).toBe(800);
expect(args[2]).toBe(600);
});
it('should create a canvas of the correct size', function ()
{
var config = {
callback: vi.fn(),
getPixel: false,
width: 200,
height: 100
};
WebGLSnapshot(mockGl, config);
expect(spyCreateWebGL).toHaveBeenCalledOnce();
var args = spyCreateWebGL.mock.calls[0];
expect(args[1]).toBe(200);
expect(args[2]).toBe(100);
});
it('should get a 2d context with willReadFrequently', function ()
{
var config = {
callback: vi.fn(),
getPixel: false,
width: 4,
height: 4
};
WebGLSnapshot(mockGl, config);
expect(mockCanvas.getContext).toHaveBeenCalledWith('2d', { willReadFrequently: true });
});
it('should call putImageData after building image data', function ()
{
var config = {
callback: vi.fn(),
getPixel: false,
width: 4,
height: 4
};
WebGLSnapshot(mockGl, config);
expect(mockCtx.putImageData).toHaveBeenCalledOnce();
});
it('should call toDataURL with the default type and encoder', function ()
{
var config = {
callback: vi.fn(),
getPixel: false,
width: 4,
height: 4
};
WebGLSnapshot(mockGl, config);
expect(mockCanvas.toDataURL).toHaveBeenCalledWith('image/png', 0.92);
});
it('should call toDataURL with custom type and encoder', function ()
{
var config = {
callback: vi.fn(),
getPixel: false,
width: 4,
height: 4,
type: 'image/jpeg',
encoder: 0.75
};
WebGLSnapshot(mockGl, config);
expect(mockCanvas.toDataURL).toHaveBeenCalledWith('image/jpeg', 0.75);
});
it('should set image.src to the data URL from toDataURL', function ()
{
var config = {
callback: vi.fn(),
getPixel: false,
width: 4,
height: 4,
type: 'image/png',
encoder: 0.92
};
WebGLSnapshot(mockGl, config);
expect(capturedImage._src).toBe('data:image/png;base64,abc123');
});
it('should call callback with the image on successful load', function ()
{
var callbackArg = undefined;
var config = {
callback: function (img)
{
callbackArg = img;
},
getPixel: false,
width: 4,
height: 4
};
WebGLSnapshot(mockGl, config);
// onload fires synchronously in our mock when src is set
expect(callbackArg).toBe(capturedImage);
});
it('should remove the canvas from CanvasPool on successful load', function ()
{
var config = {
callback: vi.fn(),
getPixel: false,
width: 4,
height: 4
};
WebGLSnapshot(mockGl, config);
expect(spyRemove).toHaveBeenCalledWith(mockCanvas);
});
it('should call callback with no argument and remove canvas on error', function ()
{
var callbackArg = 'not-called';
global.Image = function ()
{
this.onerror = null;
this.onload = null;
capturedImage = this;
Object.defineProperty(this, 'src', {
set: function (val)
{
this._src = val;
if (this.onerror)
{
this.onerror();
}
},
get: function ()
{
return this._src;
}
});
};
var config = {
callback: function (img)
{
callbackArg = img;
},
getPixel: false,
width: 4,
height: 4
};
WebGLSnapshot(mockGl, config);
expect(callbackArg).toBeUndefined();
expect(spyRemove).toHaveBeenCalledWith(mockCanvas);
});
// -----------------------------------------------------------------------
// Y-axis flip / pixel reordering
// -----------------------------------------------------------------------
it('should flip the Y-axis when copying pixels from WebGL to canvas', function ()
{
var width = 2;
var height = 2;
// WebGL row 0 (bottom) = red, WebGL row 1 (top) = blue
// After Y-flip: canvas row 0 = blue, canvas row 1 = red
var sourcePixels = new Uint8Array([
// row 0 (WebGL bottom) – red
255, 0, 0, 255,
255, 0, 0, 255,
// row 1 (WebGL top) – blue
0, 0, 255, 255,
0, 0, 255, 255
]);
mockGl.readPixels.mockImplementation(function (x, y, w, h, fmt, type, pixels)
{
for (var i = 0; i < sourcePixels.length; i++)
{
pixels[i] = sourcePixels[i];
}
});
var capturedData = null;
mockCtx.getImageData.mockImplementation(function (x, y, w, h)
{
mockImageData = { data: new Array(w * h * 4).fill(0) };
return mockImageData;
});
mockCtx.putImageData.mockImplementation(function (imgData)
{
capturedData = imgData.data.slice();
});
var config = {
callback: vi.fn(),
getPixel: false,
x: 0,
y: 0,
width: width,
height: height
};
WebGLSnapshot(mockGl, config);
// Canvas row 0 should be blue (was WebGL row 1 = top)
expect(capturedData[0]).toBe(0);
expect(capturedData[1]).toBe(0);
expect(capturedData[2]).toBe(255);
expect(capturedData[3]).toBe(255);
// Canvas row 1 should be red (was WebGL row 0 = bottom)
var row1Start = width * 4;
expect(capturedData[row1Start + 0]).toBe(255);
expect(capturedData[row1Start + 1]).toBe(0);
expect(capturedData[row1Start + 2]).toBe(0);
expect(capturedData[row1Start + 3]).toBe(255);
});
// -----------------------------------------------------------------------
// unpremultiplyAlpha
// -----------------------------------------------------------------------
it('should not unpremultiply alpha when unpremultiplyAlpha is false', function ()
{
mockGl.readPixels.mockImplementation(function (x, y, w, h, fmt, type, pixels)
{
pixels[0] = 128;
pixels[1] = 64;
pixels[2] = 32;
pixels[3] = 128;
});
var capturedData = null;
mockCtx.getImageData.mockImplementation(function (x, y, w, h)
{
mockImageData = { data: new Array(w * h * 4).fill(0) };
return mockImageData;
});
mockCtx.putImageData.mockImplementation(function (imgData)
{
capturedData = imgData.data.slice();
});
var config = {
callback: vi.fn(),
getPixel: false,
x: 0,
y: 0,
width: 1,
height: 1,
unpremultiplyAlpha: false
};
WebGLSnapshot(mockGl, config);
expect(capturedData[0]).toBe(128);
expect(capturedData[1]).toBe(64);
expect(capturedData[2]).toBe(32);
expect(capturedData[3]).toBe(128);
});
it('should unpremultiply alpha when unpremultiplyAlpha is true and alpha is non-zero', function ()
{
// Pre-multiplied at ~50% alpha: r=128, g=64, b=32, a=128
mockGl.readPixels.mockImplementation(function (x, y, w, h, fmt, type, pixels)
{
pixels[0] = 128;
pixels[1] = 64;
pixels[2] = 32;
pixels[3] = 128;
});
var capturedData = null;
mockCtx.getImageData.mockImplementation(function (x, y, w, h)
{
mockImageData = { data: new Array(w * h * 4).fill(0) };
return mockImageData;
});
mockCtx.putImageData.mockImplementation(function (imgData)
{
capturedData = imgData.data.slice();
});
var config = {
callback: vi.fn(),
getPixel: false,
x: 0,
y: 0,
width: 1,
height: 1,
unpremultiplyAlpha: true
};
WebGLSnapshot(mockGl, config);
var ratio = 255 / 128;
expect(capturedData[0]).toBe(Math.floor(128 * ratio));
expect(capturedData[1]).toBe(Math.floor(64 * ratio));
expect(capturedData[2]).toBe(Math.floor(32 * ratio));
expect(capturedData[3]).toBe(128);
});
it('should not unpremultiply when alpha is zero even if unpremultiplyAlpha is true', function ()
{
mockGl.readPixels.mockImplementation(function (x, y, w, h, fmt, type, pixels)
{
pixels[0] = 100;
pixels[1] = 100;
pixels[2] = 100;
pixels[3] = 0;
});
var capturedData = null;
mockCtx.getImageData.mockImplementation(function (x, y, w, h)
{
mockImageData = { data: new Array(w * h * 4).fill(0) };
return mockImageData;
});
mockCtx.putImageData.mockImplementation(function (imgData)
{
capturedData = imgData.data.slice();
});
var config = {
callback: vi.fn(),
getPixel: false,
x: 0,
y: 0,
width: 1,
height: 1,
unpremultiplyAlpha: true
};
WebGLSnapshot(mockGl, config);
// RGB unchanged because a === 0
expect(capturedData[0]).toBe(100);
expect(capturedData[1]).toBe(100);
expect(capturedData[2]).toBe(100);
expect(capturedData[3]).toBe(0);
});
it('should floor width and height to integers', function ()
{
var config = {
callback: vi.fn(),
getPixel: false,
width: 10.7,
height: 8.9
};
WebGLSnapshot(mockGl, config);
expect(spyCreateWebGL).toHaveBeenCalledOnce();
var args = spyCreateWebGL.mock.calls[0];
expect(args[1]).toBe(10);
expect(args[2]).toBe(8);
});
});
});