Skip to content

[3.9.0] AtlasXML parser swaps trimmed and untrimmed sizes in setTrim(), breaking setOrigin() #7245

@cmnemoi

Description

@cmnemoi

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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions