Skip to content

Commit 4e0d2ea

Browse files
obiotclaude
andcommitted
[#1201][#1214] Fix body-renderable misalignment with trimmed TexturePacker atlas frames
- Sprite.setRegion: use sourceSize for width/height on trimmed frames, recover stable anchorPoint from trim offset, and cache trim data per frame - Sprite.draw: apply trim offset after rotation handling, with coordinate transform (-ty, +tx) for -π/2 rotated atlas frames - Entity: auto-inherit renderable's anchorPoint when entity is at default (0,0) - TexturePacker parser: include sourceSize in parsed region data - atlas.createAnimationFromName: use sourceSize for frame dimensions - Add unit tests for trim math (node:test) and sprite/entity integration (vitest) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 402cfb6 commit 4e0d2ea

7 files changed

Lines changed: 841 additions & 17 deletions

File tree

packages/examples/src/examples/texturePacker/ExampleTexturePacker.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
import { DebugPanelPlugin } from "@melonjs/debug-plugin";
12
import {
23
Entity,
34
game,
45
loader,
6+
plugin,
57
type Sprite,
68
Stage,
79
state,
@@ -16,7 +18,7 @@ let texture: TextureAtlas;
1618

1719
class CapGuyEntity extends Entity {
1820
constructor() {
19-
super(0, 200, { width: 100, height: 300 });
21+
super(0, 50, { width: 100, height: 300 });
2022
this.body.setStatic();
2123
this.renderable = texture.createAnimationFromName([
2224
"capguy/walk/0001",
@@ -63,6 +65,9 @@ const createGame = () => {
6365
return;
6466
}
6567

68+
// register the debug plugin
69+
plugin.register(DebugPanelPlugin, "debugPanel");
70+
6671
const resources = [
6772
{ name: "cityscene", type: "json", src: `${base}cityscene.json` },
6873
{ name: "cityscene", type: "image", src: `${base}cityscene.png` },

packages/melonjs/src/renderable/entity/entity.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,10 @@ export default class Entity extends Renderable {
128128
if (value instanceof Renderable) {
129129
this.children[0] = value;
130130
this.children[0].ancestor = this;
131+
// auto-align: inherit renderable's anchor if entity is still at default
132+
if (this.anchorPoint.x === 0 && this.anchorPoint.y === 0) {
133+
this.anchorPoint.setMuted(value.anchorPoint.x, value.anchorPoint.y);
134+
}
131135
this.updateBounds();
132136
} else {
133137
throw new Error(value + "should extend me.Renderable");

packages/melonjs/src/renderable/sprite.js

Lines changed: 47 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,8 @@ export default class Sprite extends Renderable {
125125
angle: 0,
126126
// current frame index
127127
idx: 0,
128+
// trim offset for trimmed sprites
129+
trim: null,
128130
};
129131

130132
// animation frame delta
@@ -544,20 +546,42 @@ export default class Sprite extends Renderable {
544546
this.current.offset.setV(region.offset);
545547
// set angle if defined
546548
this.current.angle = typeof region.angle === "number" ? region.angle : 0;
547-
// update the default "current" size
548-
this.width = this.current.width = region.width;
549-
this.height = this.current.height = region.height;
550-
// set global anchortPoint if defined
551-
if (region.anchorPoint) {
552-
this.anchorPoint.setMuted(
553-
this._flip.x && region.trimmed === true
554-
? 1 - region.anchorPoint.x
555-
: region.anchorPoint.x,
556-
this._flip.y && region.trimmed === true
557-
? 1 - region.anchorPoint.y
558-
: region.anchorPoint.y,
559-
);
549+
// update the current frame size (trimmed dimensions, used for drawing)
550+
this.current.width = region.width;
551+
this.current.height = region.height;
552+
// cache trim offset for drawing
553+
this.current.trim = region.trim || null;
554+
555+
if (region.trimmed && region.sourceSize) {
556+
// use the original untrimmed size for stable bounds across trimmed frames
557+
this.width = region.sourceSize.w;
558+
this.height = region.sourceSize.h;
559+
// recover the original pivot relative to sourceSize for stable anchor
560+
if (region.anchorPoint) {
561+
const pivotX =
562+
(region.trim.x + region.width * region.anchorPoint.x) / this.width;
563+
const pivotY =
564+
(region.trim.y + region.height * region.anchorPoint.y) / this.height;
565+
this.anchorPoint.setMuted(
566+
this._flip.x ? 1 - pivotX : pivotX,
567+
this._flip.y ? 1 - pivotY : pivotY,
568+
);
569+
}
570+
} else {
571+
this.width = region.width;
572+
this.height = region.height;
573+
if (region.anchorPoint) {
574+
this.anchorPoint.setMuted(
575+
this._flip.x && region.trimmed === true
576+
? 1 - region.anchorPoint.x
577+
: region.anchorPoint.x,
578+
this._flip.y && region.trimmed === true
579+
? 1 - region.anchorPoint.y
580+
: region.anchorPoint.y,
581+
);
582+
}
560583
}
584+
561585
// update the sprite bounding box
562586
this.updateBounds();
563587
this.isDirty = true;
@@ -687,7 +711,7 @@ export default class Sprite extends Renderable {
687711

688712
// cache the current position and size
689713
let xpos = this.pos.x;
690-
const ypos = this.pos.y;
714+
let ypos = this.pos.y;
691715

692716
let w = frame.width;
693717
let h = frame.height;
@@ -703,6 +727,15 @@ export default class Sprite extends Renderable {
703727
xpos -= h;
704728
w = frame.height;
705729
h = frame.width;
730+
// apply trim in rotated space: (tx, ty) → (-ty, tx) for -π/2
731+
if (frame.trim) {
732+
xpos -= frame.trim.y;
733+
ypos += frame.trim.x;
734+
}
735+
} else if (frame.trim) {
736+
// apply trim offset for non-rotated trimmed sprites
737+
xpos += frame.trim.x;
738+
ypos += frame.trim.y;
706739
}
707740

708741
renderer.drawImage(

packages/melonjs/src/video/texture/atlas.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -501,9 +501,12 @@ export class TextureAtlas {
501501
tpAtlas.push(region);
502502
// save the corresponding index
503503
indices[name] = tpAtlas.length - 1;
504+
// use sourceSize (original untrimmed size) if available for stable bounds
505+
const frameW = region.sourceSize ? region.sourceSize.w : region.width;
506+
const frameH = region.sourceSize ? region.sourceSize.h : region.height;
504507
// calculate the max size of a frame
505-
width = Math.max(region.width, width);
506-
height = Math.max(region.height, height);
508+
width = Math.max(frameW, width);
509+
height = Math.max(frameH, height);
507510
}
508511

509512
// instantiate a new animation sheet object

packages/melonjs/src/video/texture/parser/texturepacker.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export function parseTexturePacker(data, textureAtlas) {
4949
trim: trim,
5050
width: s.w,
5151
height: s.h,
52+
sourceSize: frame.sourceSize || { w: s.w, h: s.h },
5253
angle: frame.rotated === true ? -ETA : 0,
5354
};
5455
textureAtlas.addUVs(

0 commit comments

Comments
 (0)