Skip to content

Commit 2921a6d

Browse files
committed
Pixel Perfect click detection now works even if the Sprite is part of a texture atlas.
1 parent 1294b3a commit 2921a6d

11 files changed

Lines changed: 135 additions & 66 deletions

File tree

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -151,13 +151,13 @@ Version 1.1
151151
* Added Rectangle.floorAll to floor all values in a Rectangle (x, y, width and height).
152152
* Fixed Sound.resume so it now correctly resumes playback from the point it was paused (fixes issue 51, thanks Yora).
153153
* Sprite.loadTexture now works correctly with static images, RenderTextures and Animations.
154-
154+
* 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.
155+
* 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.
156+
* Pixel Perfect click detection now works even if the Sprite is part of a texture atlas.
155157

156158
Outstanding Tasks
157159
-----------------
158160

159-
* BUG: The pixel perfect click check doesn't work if the sprite is part of a texture atlas yet.
160-
* TODO: look at Sprite.crop (http://www.html5gamedevs.com/topic/1617-error-in-spritecrop/)
161161
* TODO: d-pad example (http://www.html5gamedevs.com/topic/1574-gameinputondown-question/)
162162
* TODO: more touch input examples (http://www.html5gamedevs.com/topic/1556-mobile-touch-event/)
163163
* TODO: Sound.addMarker hh:mm:ss:ms

examples/_site/examples.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,10 @@
320320
"file": "pixel+perfect+click+detection.js",
321321
"title": "pixel perfect click detection"
322322
},
323+
{
324+
"file": "pixelpick+-+atlas.js",
325+
"title": "pixelpick - atlas"
326+
},
323327
{
324328
"file": "pixelpick+-+scrolling+effect.js",
325329
"title": "pixelpick - scrolling effect"

examples/buttons/button scale.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ function create() {
4343
button3 = game.add.button(100, 300, 'button', changeSky, this, 2, 1, 0);
4444
button3.name = 'sky3';
4545
button3.width = 300;
46+
button3.anchor.setTo(0, 0.5);
47+
// button3.angle = 0.1;
4648

4749
// Scaled button
4850
button4 = game.add.button(300, 450, 'button', changeSky, this, 2, 1, 0);
@@ -85,7 +87,8 @@ function render () {
8587

8688
// game.debug.renderWorldTransformInfo(button1, 32, 132);
8789
// game.debug.renderText('sx: ' + button3.scale.x + ' sy: ' + button3.scale.y + ' w: ' + button3.width + ' cw: ' + button3._cache.width, 32, 20);
88-
// game.debug.renderPoint(button2.input._tempPoint);
89-
// game.debug.renderPoint(button6.input._tempPoint);
90+
game.debug.renderText('ox: ' + game.stage.offset.x + ' oy: ' + game.stage.offset.y, 32, 20);
91+
game.debug.renderPoint(button3.input._tempPoint);
92+
game.debug.renderPoint(button6.input._tempPoint);
9093

9194
}

examples/input/pixel perfect click detection.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,13 @@ function outSprite() {
3737
}
3838

3939
function update() {
40-
b.angle += 0.1;
40+
b.angle += 0.05;
4141
}
4242

4343
function render() {
4444

4545
game.debug.renderSpriteInputInfo(b, 32, 32);
46+
game.debug.renderSpriteCorners(b);
47+
game.debug.renderPoint(b.input._tempPoint);
4648

4749
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
2+
var game = new Phaser.Game(800, 600, Phaser.CANVAS, 'phaser-example', { preload: preload, create: create });
3+
4+
function preload() {
5+
6+
game.load.atlas('atlas', 'assets/pics/texturepacker_test.png', 'assets/pics/texturepacker_test.json');
7+
8+
}
9+
10+
var chick;
11+
var car;
12+
var mech;
13+
var robot;
14+
var cop;
15+
16+
function create() {
17+
18+
game.stage.backgroundColor = '#404040';
19+
20+
// This demonstrates pixel perfect click detection even if using sprites in a texture atlas.
21+
22+
chick = game.add.sprite(64, 64, 'atlas');
23+
chick.frameName = 'budbrain_chick.png';
24+
chick.inputEnabled = true;
25+
chick.input.pixelPerfect = true;
26+
chick.input.useHandCursor = true;
27+
28+
cop = game.add.sprite(600, 64, 'atlas');
29+
cop.frameName = 'ladycop.png';
30+
cop.inputEnabled = true;
31+
cop.input.pixelPerfect = true;
32+
cop.input.useHandCursor = true;
33+
34+
robot = game.add.sprite(50, 300, 'atlas');
35+
robot.frameName = 'robot.png';
36+
robot.inputEnabled = true;
37+
robot.input.pixelPerfect = true;
38+
robot.input.useHandCursor = true;
39+
40+
car = game.add.sprite(100, 400, 'atlas');
41+
car.frameName = 'supercars_parsec.png';
42+
car.inputEnabled = true;
43+
car.input.pixelPerfect = true;
44+
car.input.useHandCursor = true;
45+
46+
mech = game.add.sprite(250, 100, 'atlas');
47+
mech.frameName = 'titan_mech.png';
48+
mech.inputEnabled = true;
49+
mech.input.pixelPerfect = true;
50+
mech.input.useHandCursor = true;
51+
52+
}

examples/input/pixelpick - spritesheet.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ var b;
1111

1212
function create() {
1313

14+
Phaser.Canvas.setSmoothingEnabled(game.context, false);
15+
1416
b = game.add.sprite(game.world.centerX, game.world.centerY, 'mummy');
17+
1518
b.anchor.setTo(0.5, 0.5);
1619
b.scale.setTo(6, 6);
1720
b.animations.add('walk');
@@ -42,5 +45,7 @@ function outSprite() {
4245
function render() {
4346

4447
game.debug.renderSpriteInputInfo(b, 32, 32);
48+
game.debug.renderSpriteCorners(b);
49+
game.debug.renderPoint(b.input._tempPoint);
4550

4651
}

src/core/Game.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,7 @@ Phaser.Game.prototype = {
400400
this.plugins.preUpdate();
401401
this.physics.preUpdate();
402402

403+
this.stage.update();
403404
this.input.update();
404405
this.tweens.update();
405406
this.sound.update();

src/core/Stage.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,18 @@ Phaser.Stage = function (game, width, height) {
6161
*/
6262
this.aspectRatio = width / height;
6363

64+
/**
65+
* @property {number} _nextOffsetCheck - The time to run the next offset check.
66+
* @private
67+
*/
68+
this._nextOffsetCheck = 0;
69+
70+
/**
71+
* @property {number|false} checkOffsetInterval - The time (in ms) between which the stage should check to see if it has moved.
72+
* @default
73+
*/
74+
this.checkOffsetInterval = 2500;
75+
6476
};
6577

6678
Phaser.Stage.prototype = {
@@ -93,6 +105,24 @@ Phaser.Stage.prototype = {
93105
window.onblur = this._onChange;
94106
window.onfocus = this._onChange;
95107

108+
},
109+
110+
/**
111+
* Runs Stage processes that need periodic updates, such as the offset checks.
112+
* @method Phaser.Stage#update
113+
*/
114+
update: function () {
115+
116+
if (this.checkOffsetInterval !== false)
117+
{
118+
if (this.game.time.now > this._nextOffsetCheck)
119+
{
120+
Phaser.Canvas.getOffset(this.canvas, this.offset);
121+
this._nextOffsetCheck = this.game.time.now + this.checkOffsetInterval;
122+
}
123+
124+
}
125+
96126
},
97127

98128
/**

src/gameobjects/Sprite.js

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,7 @@ Phaser.Sprite.prototype.updateCache = function() {
392392

393393
if (this.worldTransform[1] != this._cache.i01 || this.worldTransform[3] != this._cache.i10)
394394
{
395+
console.log('updateCache wt', this.name);
395396
this._cache.a00 = this.worldTransform[0]; // scaleX a
396397
this._cache.a01 = this.worldTransform[1]; // skewY c
397398
this._cache.a10 = this.worldTransform[3]; // skewX b
@@ -430,13 +431,8 @@ Phaser.Sprite.prototype.updateAnimation = function() {
430431

431432
if (this._cache.dirty && this.currentFrame)
432433
{
433-
console.log('ua frame 2 change', this.name);
434-
// this._cache.width = Math.floor(this.currentFrame.sourceSizeW * this._cache.scaleX);
435-
// this._cache.height = Math.floor(this.currentFrame.sourceSizeH * this._cache.scaleY);
436434
this._cache.width = this.currentFrame.width;
437435
this._cache.height = this.currentFrame.height;
438-
// this._cache.width = Math.floor(this.currentFrame.sourceSizeW);
439-
// this._cache.height = Math.floor(this.currentFrame.sourceSizeH);
440436
this._cache.halfWidth = Math.floor(this._cache.width / 2);
441437
this._cache.halfHeight = Math.floor(this._cache.height / 2);
442438

@@ -540,14 +536,16 @@ Phaser.Sprite.prototype.getLocalPosition = function(p, x, y, sx, sy) {
540536
* @param {number} y - Description.
541537
* @return {Description} Description.
542538
*/
543-
Phaser.Sprite.prototype.getLocalUnmodifiedPosition = function(p, x, y) {
539+
Phaser.Sprite.prototype.getLocalUnmodifiedPosition = function(p, gx, gy) {
544540

545-
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;
546-
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;
541+
var a00 = this.worldTransform[0], a01 = this.worldTransform[1], a02 = this.worldTransform[2],
542+
a10 = this.worldTransform[3], a11 = this.worldTransform[4], a12 = this.worldTransform[5],
543+
id = 1 / (a00 * a11 + a01 * -a10),
544+
x = a11 * id * gx + -a01 * id * gy + (a12 * a01 - a02 * a11) * id,
545+
y = a00 * id * gy + -a10 * id * gx + (-a12 * a00 + a02 * a10) * id;
547546

548-
// apply anchor
549-
p.x += (this.anchor.x * this._cache.width);
550-
p.y += (this.anchor.y * this._cache.height);
547+
p.x = x + (this.anchor.x * this._cache.width);
548+
p.y = y + (this.anchor.y * this._cache.height);
551549

552550
return p;
553551

src/input/InputHandler.js

Lines changed: 8 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -484,47 +484,19 @@ Phaser.InputHandler.prototype = {
484484
{
485485
this.sprite.getLocalUnmodifiedPosition(this._tempPoint, pointer.x, pointer.y);
486486

487-
// The unmodified position is being offset by the anchor, i.e. into negative space
488-
489-
// var x = this.sprite.anchor.x * this.sprite.width;
490-
// var y = this.sprite.anchor.y * this.sprite.height;
491-
var x = 0;
492-
var y = 0;
493-
494-
// check world transform
495-
if (this.sprite.worldTransform[3] == 0 && this.sprite.worldTransform[1] == 0)
487+
if (this._tempPoint.x >= 0 && this._tempPoint.x <= this.sprite.currentFrame.width && this._tempPoint.y >= 0 && this._tempPoint.y <= this.sprite.currentFrame.height)
496488
{
497-
// Un-rotated (but potentially scaled)
498-
if (this._tempPoint.x >= x && this._tempPoint.x <= this.sprite.width && this._tempPoint.y >= y && this._tempPoint.y <= this.sprite.height)
489+
if (this.pixelPerfect)
499490
{
500-
return true;
491+
return this.checkPixel(this._tempPoint.x, this._tempPoint.y);
501492
}
502-
}
503-
else
504-
{
505-
// Rotated (and could be scaled too)
506-
if (this._tempPoint.x >= x && this._tempPoint.x <= this.sprite.currentFrame.width && this._tempPoint.y >= y && this._tempPoint.y <= this.sprite.currentFrame.height)
493+
else
507494
{
508495
return true;
509496
}
510497
}
511498
}
512499

513-
// if (this.pixelPerfect)
514-
// {
515-
// return this.checkPixel(this._tempPoint.x, this._tempPoint.y);
516-
// }
517-
// else
518-
// {
519-
// return true;
520-
// }
521-
// }
522-
// }
523-
524-
// }
525-
526-
// }
527-
528500
return false;
529501

530502
},
@@ -538,16 +510,16 @@ Phaser.InputHandler.prototype = {
538510
*/
539511
checkPixel: function (x, y) {
540512

541-
x += (this.sprite.texture.frame.width * this.sprite.anchor.x);
542-
y += (this.sprite.texture.frame.height * this.sprite.anchor.y);
543-
544513
// Grab a pixel from our image into the hitCanvas and then test it
545-
546514
if (this.sprite.texture.baseTexture.source)
547515
{
548516
this.game.input.hitContext.clearRect(0, 0, 1, 1);
549517

550518
// This will fail if the image is part of a texture atlas - need to modify the x/y values here
519+
520+
x += this.sprite.texture.frame.x;
521+
y += this.sprite.texture.frame.y;
522+
551523
this.game.input.hitContext.drawImage(this.sprite.texture.baseTexture.source, x, y, 1, 1, 0, 0, 1, 1);
552524

553525
var rgb = this.game.input.hitContext.getImageData(0, 0, 1, 1);

0 commit comments

Comments
 (0)