QUICKUPSCALE

PRACTICAL GUIDE

Sprite sheet tools and workflows

Fix shifted sprite-sheet crops with exact margins and gaps

Start here

Declare the cell size, all four outer margins and both internal gaps. Check complete coverage, then compare the first and last crop with known artwork. A grid can fit the image exactly while its origin is wrong.

Expected result:

The original 79×42 sheet split into eight correct 16×16 frames at margins left 2/top 3/right 4/bottom 5 and gaps 3/2. A shifted origin also passed geometry but changed every intended crop. A separate 65×32 control rejected an incomplete column until an explicit right margin of 1 was declared.

What you need

Extract grid-diagnosis.zip. Use assets/asymmetric-sheet.png and compare its crops with the eight numbered PNGs in assets/frames/. The 64×32 plain sheet and 65×32 partial sheet are separate controls.

Tested with: Chrome 154.0.8037.44/macOS: correct, shifted-origin and partial-grid jobs in Cloudflare preview. Inclusive range 2–5 reuses an earlier actual optimized Next.js production run with byte-identical input. Pillow 12.3.0 compared every exported crop; Node 24.22.0 pure geometry is separate.

1. Separate outer margins from gaps between cells

Original numbered 79 by 42 sheet with asymmetric outside margins and internal gaps
Original input: 79×42 pixels; four columns and two rows of 16×16 artwork.

A margin lies outside the first or last cell. A gap lies between cells. Require leading + count × cell + (count − 1) × gap + trailing = length on each axis.

width:  2 + 4×16 + 3×3 + 4 = 79
height: 3 + 2×16 + 1×2 + 5 = 42

Coordinates begin at the top left. A crop includes its top/left samples and excludes its bottom/right endpoint. The right margin is not another internal gap.

2. Enter the declaration and inspect all eight crops

  1. Open the sprite-sheet splitter and use Choose sprite sheet PNG for asymmetric-sheet.png.
  2. Set both cell dimensions to 16. Set left/top/right/bottom margins to 2/3/4/5.
  3. Set Horizontal gap (px) to 3 and Vertical gap (px) to 2.
  4. Check the first crop (2,3,16,16) and last crop (59,21,16,16), then use Split into PNG frames.
  5. Download the ZIP and manifest. Check each numbered frame, including the middle cells.

The actual eight PNGs all matched independent crops of a separate browser Canvas readback. Their source-global indices remained 0–7. Correct rectangle count alone would not establish correct artwork.

3. Catch a wrong origin that still fits

Keep cell size and gaps unchanged, but enter margins left 0/top 0/right 6/bottom 8. This also describes four columns and two rows. Its first and last origins are now (0,0) and (57,18).

The actual export followed those declared rectangles exactly, but every crop differed from its intended frame. The first wrong crop differed at 118 pixels; its pixel (1,7) was transparent [0,0,0,0] instead of the correct border [28,32,48,255].

Actual first frame exported with the correct margins
Actual correct first crop:16×16.
Actual first frame exported with a shifted origin
Actual shifted first crop:16×16.

This is the important failure: complete coverage cannot infer which margin follows the artwork. Restore the known declaration and compare visible frame identity before accepting the settings.

4. Reject an incomplete column before exporting

Choose partial-sheet.png, 65×32. With 16×16 cells, zero margins and zero gaps, the tool reported Declared grid must cover complete cells exactly between its margins and disabled the split button.

An explicit right margin of 1 makes a different valid declaration: four complete columns followed by one outside column. With that setting, the actual eight exported crops all matched their 16×16 source rectangles. The tool did not infer or silently discard the extra column.

Wrong cell width 17, gapX 2 or zero margins on the asymmetric sheet also fail complete coverage in the source checks. Negative/fractional values and reversed or out-of-range selections reject.

5. Keep global source IDs when selecting a range

On the correct asymmetric declaration, enable Limit frame range (zero-based, inclusive), set Start frame to 2 and End frame to 5.

sourceFrameIndex: 2       3        4       5
source origin:   (40,3)  (59,3)   (2,21)  (21,21)
output frameIndex: 0      1        2       3

An earlier actual run of the byte-identical input verified all four crops and these manifest rectangles. It is retained evidence from optimized Next.js production; the current Cloudflare round verified the complete, shifted and partial cases. The first selected source remains frame 2 even though its output index is 0.

At most 32 frames may be selected. A larger grid needs an explicit range; this workflow does not automatically segment irregular sprites.

6. Distinguish a bad cut from browser alpha normalization

Each original includes hidden RGB at alpha 0 and partial-alpha probes. For each full input here, raw PNG-to-Canvas readback changed 32 pixels/72 RGB channel bytes, with no alpha-channel changes. The exported crops matched that Canvas reference exactly.

Pure source cropping preserves all RGBA samples. Browser output does not promise that same raw-byte fidelity. Compare equivalent decoded references before blaming the cut lines for an alpha/color difference.

To reproduce only the fixed original references, use Node 24 in the extracted directory:

node --test fixture.test.mjs
node generate.mjs
node generate.mjs --check

The source runner and repository geometry tests are separate from the recorded browser exports. A native importer can expose different margin controls; this generic declaration is not proof of its behavior.

Sources and next steps