Pixel Perfect click detection now works even if the Sprite is part of a texture atlas.

This commit is contained in:
photonstorm 2013-10-25 05:40:46 +01:00
parent 1294b3a2b9
commit 2921a6de2e
11 changed files with 135 additions and 66 deletions

View file

@ -151,13 +151,13 @@ Version 1.1
* Added Rectangle.floorAll to floor all values in a Rectangle (x, y, width and height).
* Fixed Sound.resume so it now correctly resumes playback from the point it was paused (fixes issue 51, thanks Yora).
* Sprite.loadTexture now works correctly with static images, RenderTextures and Animations.
* Lots of fixes within Sprite.bounds. The bounds is now correct regardless of rotation, anchor or scale of the Sprite or any of its parent objects.
* On a busy page it's possible for the game to boot with an incorrect stage offset x/y which can cause input events to be calculated wrong. A new property has been added to Stage to combat this issue: Stage.checkOffsetInterval. By default it will check the canvas offset every 2500ms and adjust it accordingly. You can set the value to 'false' to disable the check entirely, or set a higher or lower value. We recommend that you get the value quite low during your games preloader, but once the game has fully loaded hopefully the containing page will have settled down, so it's probably safe to disable the check entirely.
* Pixel Perfect click detection now works even if the Sprite is part of a texture atlas.
Outstanding Tasks
-----------------
* BUG: The pixel perfect click check doesn't work if the sprite is part of a texture atlas yet.
* TODO: look at Sprite.crop (http://www.html5gamedevs.com/topic/1617-error-in-spritecrop/)
* TODO: d-pad example (http://www.html5gamedevs.com/topic/1574-gameinputondown-question/)
* TODO: more touch input examples (http://www.html5gamedevs.com/topic/1556-mobile-touch-event/)
* TODO: Sound.addMarker hh:mm:ss:ms

View file

@ -320,6 +320,10 @@
"file": "pixel+perfect+click+detection.js",
"title": "pixel perfect click detection"
},
{
"file": "pixelpick+-+atlas.js",
"title": "pixelpick - atlas"
},
{
"file": "pixelpick+-+scrolling+effect.js",
"title": "pixelpick - scrolling effect"

View file

@ -43,6 +43,8 @@ function create() {
button3 = game.add.button(100, 300, 'button', changeSky, this, 2, 1, 0);
button3.name = 'sky3';
button3.width = 300;
button3.anchor.setTo(0, 0.5);
// button3.angle = 0.1;
// Scaled button
button4 = game.add.button(300, 450, 'button', changeSky, this, 2, 1, 0);
@ -85,7 +87,8 @@ function render () {
// game.debug.renderWorldTransformInfo(button1, 32, 132);
// game.debug.renderText('sx: ' + button3.scale.x + ' sy: ' + button3.scale.y + ' w: ' + button3.width + ' cw: ' + button3._cache.width, 32, 20);
// game.debug.renderPoint(button2.input._tempPoint);
// game.debug.renderPoint(button6.input._tempPoint);
game.debug.renderText('ox: ' + game.stage.offset.x + ' oy: ' + game.stage.offset.y, 32, 20);
game.debug.renderPoint(button3.input._tempPoint);
game.debug.renderPoint(button6.input._tempPoint);
}

View file

@ -37,11 +37,13 @@ function outSprite() {
}
function update() {
b.angle += 0.1;
b.angle += 0.05;
}
function render() {
game.debug.renderSpriteInputInfo(b, 32, 32);
game.debug.renderSpriteCorners(b);
game.debug.renderPoint(b.input._tempPoint);
}

View file

@ -0,0 +1,52 @@
var game = new Phaser.Game(800, 600, Phaser.CANVAS, 'phaser-example', { preload: preload, create: create });
function preload() {
game.load.atlas('atlas', 'assets/pics/texturepacker_test.png', 'assets/pics/texturepacker_test.json');
}
var chick;
var car;
var mech;
var robot;
var cop;
function create() {
game.stage.backgroundColor = '#404040';
// This demonstrates pixel perfect click detection even if using sprites in a texture atlas.
chick = game.add.sprite(64, 64, 'atlas');
chick.frameName = 'budbrain_chick.png';
chick.inputEnabled = true;
chick.input.pixelPerfect = true;
chick.input.useHandCursor = true;
cop = game.add.sprite(600, 64, 'atlas');
cop.frameName = 'ladycop.png';
cop.inputEnabled = true;
cop.input.pixelPerfect = true;
cop.input.useHandCursor = true;
robot = game.add.sprite(50, 300, 'atlas');
robot.frameName = 'robot.png';
robot.inputEnabled = true;
robot.input.pixelPerfect = true;
robot.input.useHandCursor = true;
car = game.add.sprite(100, 400, 'atlas');
car.frameName = 'supercars_parsec.png';
car.inputEnabled = true;
car.input.pixelPerfect = true;
car.input.useHandCursor = true;
mech = game.add.sprite(250, 100, 'atlas');
mech.frameName = 'titan_mech.png';
mech.inputEnabled = true;
mech.input.pixelPerfect = true;
mech.input.useHandCursor = true;
}

View file

@ -11,7 +11,10 @@ var b;
function create() {
Phaser.Canvas.setSmoothingEnabled(game.context, false);
b = game.add.sprite(game.world.centerX, game.world.centerY, 'mummy');
b.anchor.setTo(0.5, 0.5);
b.scale.setTo(6, 6);
b.animations.add('walk');
@ -42,5 +45,7 @@ function outSprite() {
function render() {
game.debug.renderSpriteInputInfo(b, 32, 32);
game.debug.renderSpriteCorners(b);
game.debug.renderPoint(b.input._tempPoint);
}

View file

@ -400,6 +400,7 @@ Phaser.Game.prototype = {
this.plugins.preUpdate();
this.physics.preUpdate();
this.stage.update();
this.input.update();
this.tweens.update();
this.sound.update();

View file

@ -61,6 +61,18 @@ Phaser.Stage = function (game, width, height) {
*/
this.aspectRatio = width / height;
/**
* @property {number} _nextOffsetCheck - The time to run the next offset check.
* @private
*/
this._nextOffsetCheck = 0;
/**
* @property {number|false} checkOffsetInterval - The time (in ms) between which the stage should check to see if it has moved.
* @default
*/
this.checkOffsetInterval = 2500;
};
Phaser.Stage.prototype = {
@ -93,6 +105,24 @@ Phaser.Stage.prototype = {
window.onblur = this._onChange;
window.onfocus = this._onChange;
},
/**
* Runs Stage processes that need periodic updates, such as the offset checks.
* @method Phaser.Stage#update
*/
update: function () {
if (this.checkOffsetInterval !== false)
{
if (this.game.time.now > this._nextOffsetCheck)
{
Phaser.Canvas.getOffset(this.canvas, this.offset);
this._nextOffsetCheck = this.game.time.now + this.checkOffsetInterval;
}
}
},
/**

View file

@ -392,6 +392,7 @@ Phaser.Sprite.prototype.updateCache = function() {
if (this.worldTransform[1] != this._cache.i01 || this.worldTransform[3] != this._cache.i10)
{
console.log('updateCache wt', this.name);
this._cache.a00 = this.worldTransform[0]; // scaleX a
this._cache.a01 = this.worldTransform[1]; // skewY c
this._cache.a10 = this.worldTransform[3]; // skewX b
@ -430,13 +431,8 @@ Phaser.Sprite.prototype.updateAnimation = function() {
if (this._cache.dirty && this.currentFrame)
{
console.log('ua frame 2 change', this.name);
// this._cache.width = Math.floor(this.currentFrame.sourceSizeW * this._cache.scaleX);
// this._cache.height = Math.floor(this.currentFrame.sourceSizeH * this._cache.scaleY);
this._cache.width = this.currentFrame.width;
this._cache.height = this.currentFrame.height;
// this._cache.width = Math.floor(this.currentFrame.sourceSizeW);
// this._cache.height = Math.floor(this.currentFrame.sourceSizeH);
this._cache.halfWidth = Math.floor(this._cache.width / 2);
this._cache.halfHeight = Math.floor(this._cache.height / 2);
@ -540,14 +536,16 @@ Phaser.Sprite.prototype.getLocalPosition = function(p, x, y, sx, sy) {
* @param {number} y - Description.
* @return {Description} Description.
*/
Phaser.Sprite.prototype.getLocalUnmodifiedPosition = function(p, x, y) {
Phaser.Sprite.prototype.getLocalUnmodifiedPosition = function(p, gx, gy) {
p.x = this._cache.a11 * this._cache.idi * x + -this._cache.i01 * this._cache.idi * y + (this._cache.a12 * this._cache.i01 - this._cache.a02 * this._cache.a11) * this._cache.idi;
p.y = this._cache.a00 * this._cache.idi * y + -this._cache.i10 * this._cache.idi * x + (-this._cache.a12 * this._cache.a00 + this._cache.a02 * this._cache.i10) * this._cache.idi;
var a00 = this.worldTransform[0], a01 = this.worldTransform[1], a02 = this.worldTransform[2],
a10 = this.worldTransform[3], a11 = this.worldTransform[4], a12 = this.worldTransform[5],
id = 1 / (a00 * a11 + a01 * -a10),
x = a11 * id * gx + -a01 * id * gy + (a12 * a01 - a02 * a11) * id,
y = a00 * id * gy + -a10 * id * gx + (-a12 * a00 + a02 * a10) * id;
// apply anchor
p.x += (this.anchor.x * this._cache.width);
p.y += (this.anchor.y * this._cache.height);
p.x = x + (this.anchor.x * this._cache.width);
p.y = y + (this.anchor.y * this._cache.height);
return p;

View file

@ -484,47 +484,19 @@ Phaser.InputHandler.prototype = {
{
this.sprite.getLocalUnmodifiedPosition(this._tempPoint, pointer.x, pointer.y);
// The unmodified position is being offset by the anchor, i.e. into negative space
// var x = this.sprite.anchor.x * this.sprite.width;
// var y = this.sprite.anchor.y * this.sprite.height;
var x = 0;
var y = 0;
// check world transform
if (this.sprite.worldTransform[3] == 0 && this.sprite.worldTransform[1] == 0)
if (this._tempPoint.x >= 0 && this._tempPoint.x <= this.sprite.currentFrame.width && this._tempPoint.y >= 0 && this._tempPoint.y <= this.sprite.currentFrame.height)
{
// Un-rotated (but potentially scaled)
if (this._tempPoint.x >= x && this._tempPoint.x <= this.sprite.width && this._tempPoint.y >= y && this._tempPoint.y <= this.sprite.height)
if (this.pixelPerfect)
{
return true;
return this.checkPixel(this._tempPoint.x, this._tempPoint.y);
}
}
else
{
// Rotated (and could be scaled too)
if (this._tempPoint.x >= x && this._tempPoint.x <= this.sprite.currentFrame.width && this._tempPoint.y >= y && this._tempPoint.y <= this.sprite.currentFrame.height)
else
{
return true;
}
}
}
// if (this.pixelPerfect)
// {
// return this.checkPixel(this._tempPoint.x, this._tempPoint.y);
// }
// else
// {
// return true;
// }
// }
// }
// }
// }
return false;
},
@ -538,16 +510,16 @@ Phaser.InputHandler.prototype = {
*/
checkPixel: function (x, y) {
x += (this.sprite.texture.frame.width * this.sprite.anchor.x);
y += (this.sprite.texture.frame.height * this.sprite.anchor.y);
// Grab a pixel from our image into the hitCanvas and then test it
if (this.sprite.texture.baseTexture.source)
{
this.game.input.hitContext.clearRect(0, 0, 1, 1);
// This will fail if the image is part of a texture atlas - need to modify the x/y values here
x += this.sprite.texture.frame.x;
y += this.sprite.texture.frame.y;
this.game.input.hitContext.drawImage(this.sprite.texture.baseTexture.source, x, y, 1, 1, 0, 0, 1, 1);
var rgb = this.game.input.hitContext.getImageData(0, 0, 1, 1);

View file

@ -206,25 +206,27 @@ Phaser.Utils.Debug.prototype = {
if (showBounds)
{
this.context.strokeStyle = 'rgba(255,0,0,1)';
this.context.beginPath();
this.context.strokeStyle = 'rgba(0, 255, 0, 0.7)';
this.context.strokeRect(sprite.bounds.x, sprite.bounds.y, sprite.bounds.width, sprite.bounds.height);
this.context.closePath();
this.context.stroke();
}
// this.context.beginPath();
// this.context.moveTo(sprite.topLeft.x, sprite.topLeft.y);
// this.context.lineTo(sprite.topRight.x, sprite.topRight.y);
// this.context.lineTo(sprite.bottomRight.x, sprite.bottomRight.y);
// this.context.lineTo(sprite.bottomLeft.x, sprite.bottomLeft.y);
// this.context.closePath();
// this.context.strokeStyle = 'rgba(255,0,0,1)';
// this.context.stroke();
this.context.beginPath();
this.context.moveTo(sprite.topLeft.x, sprite.topLeft.y);
this.context.lineTo(sprite.topRight.x, sprite.topRight.y);
this.context.lineTo(sprite.bottomRight.x, sprite.bottomRight.y);
this.context.lineTo(sprite.bottomLeft.x, sprite.bottomLeft.y);
this.context.closePath();
this.context.strokeStyle = 'rgba(255, 0, 255, 0.7)';
this.context.stroke();
this.renderPoint(sprite.center);
this.renderPoint(sprite.topLeft, 'rgb(255,255,0)');
this.renderPoint(sprite.topRight, 'rgb(255,0,0)');
this.renderPoint(sprite.bottomLeft, 'rgb(0,0,255)');
this.renderPoint(sprite.bottomRight, 'rgb(255,255,255)');
this.renderPoint(sprite.topLeft);
this.renderPoint(sprite.topRight);
this.renderPoint(sprite.bottomLeft);
this.renderPoint(sprite.bottomRight);
if (showText)
{