Start here
Load one image, register each known rectangle with texture.add(name, 0, x, y, width, height), and select only names confirmed by texture.has. A regular spritesheet grid does not describe unequal regions.
The recorded Phaser consumer renders all three declared regions. Reusing a name returns null without replacing the original frame; selecting an absent name keeps the previously selected badge. The complete rendered checks matched 15 independent native Canvas reference crops.
What you need
The original CC0 48×32 PNG has transparent gutters and three explicitly declared controls. It includes alpha-127/128 edges and a zero-alpha hidden-RGB pixel for source/render comparison. The declaration is first-party data, not detected segmentation or Phaser atlas JSON.
Tested with: Phaser 3.90.0 Canvas, eventemitter3 5.0.1, Chrome 154.0.8037.44 on macOS. Retained actual source-project consumer and six bundled Node tests; no WebGL, mobile, Safari or FPS claim.
1. Declare names and exact source rectangles
Extract the project and inspect assets/named-controls.png beside regions.mjs. Coordinates are top-left, half-open:
| Name | x, y | Width × height |
|---|---|---|
| normal | 2, 2 | 12×10 |
| pressed | 21, 3 | 16×8 |
| badge | 3, 18 | 9×11 |
The included validateRegions accepts one to three unique lowercase ASCII names, safe integer rectangles and these bounded source dimensions. It rejects duplicate/reserved names and out-of-bounds rectangles before registration. Its inspection manifest is not an atlas-loader format.
2. Load one image and register its named regions
// preload()
this.load.image("controls", "assets/named-controls.png");
// create(), after validating the original declaration
const texture = this.textures.get("controls");
const frame = texture.add("normal", 0, 2, 2, 12, 10);
if (frame === null || !texture.has("normal")) {
throw new Error("Registration failed");
}
this.add.image(64, 164, "controls", "normal").setOrigin(0).setScale(8);Source index 0 refers to the loaded image. The scene checks each returned frame’s cut rectangle and keeps all three originals visible. It uses integer positions, 8× scale, an opaque canvas and explicit antialias: false, pixelArt: false and roundPixels: false. The recorded consumer used these settings with its original 960×420 canvas.
3. Keep an existing name and guard unknown selection
The pinned Texture source returns null when add receives an existing name. The example attempts a second, different normal rectangle and checks the return value, original frame object, cut coordinates and frame count. A rejected duplicate must not replace the original region.
const before = texture.get("normal");
const duplicate = texture.add("normal", 0, 21, 3, 16, 8);
if (duplicate !== null || texture.get("normal") !== before) {
throw new Error("Duplicate changed the original");
}
if (texture.has(requestedName)) sprite.setFrame(requestedName);Do not use get as a missing-name test: pinned Phaser falls back for an unknown frame. The prepared absent control checks has first and keeps the previous image. In the retained run, the duplicate returned null and preserved the original object, cut rectangle and frame count. After Badge, the absent control retained Badge and left the entire canvas unchanged.
4. Run the local consumer and inspect each region
With Node 24+ inside the extracted phaser-named-regions directory:
npm ci --ignore-scripts
npm test
npm startOpen http://127.0.0.1:8182; set PHASER_NAMED_REGIONS_PORT if that port is occupied. The explicit local server serves the pinned npm runtime, source and allowed assets. No CDN is used. Opening the HTML directly as a file is not this loader workflow.
Use the normal, pressed, badge and absent controls. The recorded real controls ran Pressed → Badge → absent → Normal. The exposed diagnostic state retains at most 32 selection records.


Across five recorded states, all 15 allocated 128×88 display crops, including blank surrounding pixels, matched independently prepared native Canvas references with zero differing channels. These references include the browser’s actual PNG decoding and compositing.
A direct original-byte/Pillow comparison differs by at most one channel value at partial-alpha pixels. Alpha was preserved in the inspected decode samples, but hidden RGB at zero alpha became zero; a source [80,160,240,128] sample decoded as [80,159,239,128]. Exact source bytes and displayed composition are different checks. This example does not repack, trim, detect artwork, animate tags or establish WebGL/mobile/FPS behavior.
The package’s README and original fixture manifest retain their historical UNEXECUTED preparation status. The later consumer results above used that unchanged 27,902-byte, 14-member project, freshly extracted from an unpublished local artifact download. That run establishes this source project’s behavior.
First-party source and art are CC0. Phaser and eventemitter3 are separately MIT-licensed; retain their notices. Runtime downloads remain local dependencies and are not bundled as original artwork.