Stop Sprite Foot Drift After Atlas Trimming

If a character's planted foot jumps when an animation changes frames, compare that foot in the original canvas coordinates before moving the game object. Independent trimming changes a frame's stored rectangle. It should not silently change where the character stands. The fault can sit in missing trim offsets, an anchor based on the cropped rectangle, or the artwork itself.
Key Takeaways
- Keep an original coordinate space for contact points and pivots.
- Use two frames with deliberately different transparent margins to expose a broken importer.
- Validate the visible contact marker as well as metadata; a correct rectangle can contain misplaced art.
Freeze the scene before adjusting the feet
Use a disposable two-frame animation, a stationary camera, an unrotated character and unit scale. Put a bright marker on the planted heel in both frames. Disable code that moves the character while this test runs. Otherwise, camera motion and locomotion can hide an import error.
Record four things for each frame: original canvas size, trimmed rectangle within that canvas, chosen pivot and the marker's position. These are different values. A rectangle's position on the atlas tells the loader where to read pixels, while its trim offset tells the renderer how those pixels fit into the original canvas.
CodeAndWeb's alignment guidance recommends exporting framework metadata with trimmed sprites. The relevant reader check is whether the consumer actually uses that metadata. A PNG-only export cannot communicate the transparent area removed from each frame.
Give the coordinate contract numbers
Use this two-frame coordinate ledger as a worksheet beside the existing pivot and origin guide and trimmed-atlas metadata explanation. The extra deliverable here is a comparison record with an expected result for each frame, rather than another exporter setup recipe.
This synthetic fixture uses top-left origins, X to the right and Y downward. Coordinates identify geometric positions, not integer pixel indices: the bottom boundary of a 64-pixel-high canvas is Y=64. The original canvas is 64×64 and its pivot is (32, 60).
| Frame | Trim offset | Heel in trimmed coordinates | Heel in original coordinates |
|---|---|---|---|
| A | (12, 8) | (20, 52) | (32, 60) |
| B | (8, 4) | (24, 56) | (32, 60) |
For this unrotated fixture, reconstruct the original heel as trim_offset + local_heel. Its position relative to the pivot is then (0, 0) in both frames. If a consumer instead places the local heels directly, B differs from A by (4, 4). That is a useful failure signature, not a reason to paint four pixels off the second frame.
The following Python 3 arithmetic example has no engine dependency. It illustrates the contract; it is not an engine import test.
pivot = (32, 60)
frames = [((12, 8), (20, 52)), ((8, 4), (24, 56))]
contacts = [tuple(t + p - a for t, p, a in zip(trim, foot, pivot))
for trim, foot in frames]
assert contacts == [(0, 0), (0, 0)]
print(contacts)
PixiJS's texture model distinguishes the atlas frame, original dimensions and trim region. Other engines may encode equivalent information differently. Do not paste field names from one schema into another importer without checking its documented format.
Compare the importer with an untrimmed control
Export the same source twice to unused paths: once without trimming and once with trimming plus the correct framework data. Keep frame order, pivot, scale and source art unchanged. Import both through the production loader, not a separate image viewer.
Place their pivots at the same fixed world coordinate and step each frame manually. Overlay the heel markers against a fixed crosshair. If only the trimmed version moves, inspect the metadata path. If both move, compare original heel coordinates and any per-frame transform first.
Check the draw API's anchor convention, too. A normalized anchor of (0.5, 1) is not a promise that every frame uses a common original canvas. A custom cropped-image renderer may interpret it against each crop. TexturePacker's trim modes have distinct positioning contracts, so the mode and consumer must agree.
Accept the correction without erasing animation
For the synthetic fixture, require a zero heel-to-pivot offset in both frames and identical heel positions between trimmed and untrimmed imports. Repeat at a larger integer scale so a one-pixel error is easy to see. Save the two exports and the import settings with the result.
Apply the same method to a real idle frame and a real transition frame. Choose a genuinely planted contact point. A lifting toe, squash or intentional root movement should move; forcing every silhouette to share a bottom edge can damage the animation.
Engine-specific follow-ups cover Bevy ground-contact anchors, HaxeFlixel scale and hitbox changes and Phaser's untrimmed-grid contract. They address different consumers of the same handoff problem.
Evidence and editorial responsibility
This AI-assisted article is maintained by FrameSprite Engineering, which has a commercial interest in FrameSprite. See the team and editorial policy and contact page. Primary documentation was consulted on 2026-10-30. The numeric fixture is illustrative; no game engine or exporter was executed for this article.
About the author
- FrameSprite team publication: technical guides for image processing, sprite workflows and game development. Our AI-generated guides identify their sources, executed synthetic fixtures and testing limits. Original guides: https://www.framesprite.com/guides
Comments
Join the discussionNo comments yet. Be the first to comment.



