Problem
When importing pets from external sources (e.g. Petdex), the bottom of the pet sprite sometimes shows a thin strip of the next row/frame from the spritesheet. It looks like the GIF is "leaking" into the next emotion frame.
Root Cause
The spritesheet rendering in PetSprite.tsx uses background-position to select frames, but the rounding strategy is inconsistent between:
-
Container size — getPetSpriteSize() uses Math.ceil:
width: Math.ceil(PET_ATLAS.cellWidth * renderScale),
height: Math.ceil(PET_ATLAS.cellHeight * renderScale),
-
Background offset — getPetFrameOffset() uses raw floats without rounding:
x: -safeFrame * PET_ATLAS.cellWidth * renderScale,
y: -animation.row * PET_ATLAS.cellHeight * renderScale,
-
Background size — PetSprite.tsx also uses raw floats:
backgroundSize: `${PET_ATLAS.width * renderScale}px ${PET_ATLAS.height * renderScale}px`,
When renderScale produces non-integer pixel values (e.g. cellHeight * renderScale = 155.7), the container is rounded up by Math.ceil to 156 px, but the background offset stays at the fractional position. Browser sub-pixel rendering then causes the bottom/right edge to reveal 1 px (or a fraction) of the adjacent frame in the spritesheet atlas.
Additional Concern: Hard-coded Atlas Dimensions
PET_ATLAS is hard-coded globally:
export const PET_ATLAS = {
width: 1536,
height: 1872,
columns: 8,
rows: 9,
cellWidth: 192,
cellHeight: 208,
} as const;
This means imported pets (e.g. from Petdex or Codex Pets) are forced into the same grid even if their actual spritesheets use different cell dimensions. If a Petdex pack has a cell height smaller than 208, the offset math will cross row boundaries and make the "leak" much worse.
Suggested Fixes
Short-term
Align all spritesheet math to the same rounding strategy, e.g. Math.round:
Long-term
Allow pet.json to declare atlas metadata (cellWidth, cellHeight, columns, rows) so that imported pets from different sources can be rendered with their own correct grid dimensions instead of being forced into the hard-coded 192×208 atlas.
Environment
- OpenPet version: latest master (as of 2026-08-14)
- Platform: observed on imported Petdex packs
Problem
When importing pets from external sources (e.g. Petdex), the bottom of the pet sprite sometimes shows a thin strip of the next row/frame from the spritesheet. It looks like the GIF is "leaking" into the next emotion frame.
Root Cause
The spritesheet rendering in
PetSprite.tsxusesbackground-positionto select frames, but the rounding strategy is inconsistent between:Container size —
getPetSpriteSize()usesMath.ceil:Background offset —
getPetFrameOffset()uses raw floats without rounding:Background size —
PetSprite.tsxalso uses raw floats:When
renderScaleproduces non-integer pixel values (e.g.cellHeight * renderScale = 155.7), the container is rounded up byMath.ceilto 156 px, but the background offset stays at the fractional position. Browser sub-pixel rendering then causes the bottom/right edge to reveal 1 px (or a fraction) of the adjacent frame in the spritesheet atlas.Additional Concern: Hard-coded Atlas Dimensions
PET_ATLASis hard-coded globally:This means imported pets (e.g. from Petdex or Codex Pets) are forced into the same grid even if their actual spritesheets use different cell dimensions. If a Petdex pack has a cell height smaller than 208, the offset math will cross row boundaries and make the "leak" much worse.
Suggested Fixes
Short-term
Align all spritesheet math to the same rounding strategy, e.g.
Math.round:In
getPetFrameOffset():In
PetSprite.tsxbackgroundSize:Long-term
Allow
pet.jsonto declare atlas metadata (cellWidth,cellHeight,columns,rows) so that imported pets from different sources can be rendered with their own correct grid dimensions instead of being forced into the hard-coded192×208atlas.Environment