-
Notifications
You must be signed in to change notification settings - Fork 7.1k
Description
This bug report was written by Claude Opus, but heavily reviewed by myself.
Version
- Phaser Version: 3.90.0 (present since at least 3.7.0)
- Operating system: All
- Browser: All
Description
TL;DR: The AtlasXML parser passes actualWidth/destWidth arguments to Frame.setTrim() in the wrong order, causing frame.realWidth to return the trimmed size instead of the original untrimmed size. This breaks setOrigin() for any trimmed XML atlas frame.
When loading a trimmed sprite from a Starling/Sparrow XML atlas, frame.realWidth and frame.width are swapped compared to the JSONHash/JSONArray parsers:
| JSON atlas (correct) | XML atlas (buggy) | |
|---|---|---|
frame.realWidth |
169 (original untrimmed) | 57 (trimmed) |
frame.width |
57 (trimmed) | 169 (original untrimmed) |
Since setOrigin() relies on frame.realWidth to compute displayOriginX, the origin pivot ends up wrong for any non-default origin.
The root cause is in AtlasXML.js:61, where the call to setTrim() passes width (trimmed) as actualWidth and frameWidth (untrimmed) as destWidth, when it should be the other way around. The JSONHash parser does it correctly.
Example Test Code
Sandbox : https://phaser.io/sandbox/KbrrRJ1e
Save as index.html and open in a browser (self-contained, no external files needed):
<!DOCTYPE html>
<html>
<head>
<script src="https://cdn.jsdelivr.net/npm/phaser@3.90.0/dist/phaser.min.js"></script>
</head>
<body>
<script>
function createTestAtlasPng() {
const canvas = document.createElement('canvas');
canvas.width = 169; canvas.height = 256;
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#4488ff'; ctx.fillRect(0, 0, 57, 256);
ctx.fillStyle = '#44ff88'; ctx.fillRect(57, 0, 100, 100);
return canvas.toDataURL('image/png');
}
const TEST_XML = `<?xml version="1.0" encoding="UTF-8"?>
<TextureAtlas imagePath="test-atlas.png">
<SubTexture name="trimmedSprite"
x="0" y="0" width="57" height="256"
frameX="-112" frameY="0" frameWidth="169" frameHeight="256"/>
</TextureAtlas>`;
class BugReproScene extends Phaser.Scene {
constructor() { super('BugRepro'); }
preload() {
this.load.image('atlasPng', createTestAtlasPng());
this.load.xml('atlasXml', 'data:text/xml,' + encodeURIComponent(TEST_XML));
}
create() {
const xmlDoc = this.cache.xml.get('atlasXml');
const image = this.textures.get('atlasPng');
this.textures.addAtlasXML('testAtlas', image.source[0].image, xmlDoc);
const sprite = this.add.image(400, 400, 'testAtlas', 'trimmedSprite');
const frame = sprite.frame;
console.log('frame.realWidth:', frame.realWidth, '(expected 169, the untrimmed width)');
console.log('frame.width:', frame.width, '(expected 57, the trimmed width)');
sprite.setOrigin(1, 1);
console.log('displayOriginX:', sprite.displayOriginX, '(expected 169, got', sprite.displayOriginX + ')');
}
}
new Phaser.Game({ type: Phaser.AUTO, width: 800, height: 600, scene: BugReproScene });
</script>
</body>
</html>Additional Information
Suggested fix — in src/textures/parsers/AtlasXML.js, swap the arguments to setTrim():
newFrame.setTrim(
- width,
- height,
+ frameWidth,
+ frameHeight,
frameX,
frameY,
- frameWidth,
- frameHeight
+ width,
+ height
);This makes the XML parser consistent with the JSON parsers: actualWidth/Height = frameWidth/frameHeight (original untrimmed size), destWidth/Height = width/height (trimmed size on atlas).