Skip to content

Commit 872bd57

Browse files
perf: build each distinct sprite image once
The game encoded 2,291 sprite PNGs as its modules loaded, and 845 of them (37%) were an image already built: flash frames of sprites whose flash palette matches, wear stages with no wear yet, and glyph tones with the same colors. Each encode is a base64 PNG data URL on the main thread behind the splash. The data URL is now kept by the image's content, its size and every run of pixels in paint order, so an identical image reuses the first one's data URL. The 1,446 distinct images come out byte for byte the same (one hash over every built source before and after), and sprite building at load drops from about 107 ms to 87 ms in Bun, with the encoder's own time from 56 ms to 40 ms.
1 parent eae384c commit 872bd57

1 file changed

Lines changed: 32 additions & 3 deletions

File tree

‎src/sprite.ts‎

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -146,17 +146,46 @@ const fillsOf = (
146146
),
147147
)
148148

149+
// NOTE: the size and every run in paint order, which is everything the PNG is
150+
// built from, so two equal keys always make the same image.
151+
const contentKeyOf = (
152+
width: number,
153+
height: number,
154+
runs: ReadonlyArray<PixelRun>,
155+
): string => {
156+
let key = `${width}x${height}`
157+
for (const run of runs) {
158+
key += `;${run.x},${run.y},${run.width},${run.height},${run.color}`
159+
}
160+
return key
161+
}
162+
163+
const sourcesByContent = new Map<string, Option.Option<string>>()
164+
149165
// NOTE: a sprite image is a PNG data URL built here from its pixel runs, not
150166
// an SVG. The browser makes a whole SVG document for every SVG image and
151167
// redraws its shapes each time a canvas draws it, which cost seconds at load
152168
// and nothing saved per frame over the Rect shapes. A PNG decodes once into a
153-
// bitmap that drawImage copies.
169+
// bitmap that drawImage copies. About a third of the sprites the game builds
170+
// come out the same as one built before (a flash frame of an all white
171+
// sprite, a wear stage with no wear yet, a glyph in two tones that match), so
172+
// each distinct image is encoded once and its data URL reused.
154173
const runsDataUrl = (
155174
width: number,
156175
height: number,
157176
runs: ReadonlyArray<PixelRun>,
158-
): Option.Option<string> =>
159-
Option.map(fillsOf(runs), fills => pngDataUrl(width, height, fills))
177+
): Option.Option<string> => {
178+
const key = contentKeyOf(width, height, runs)
179+
const cached = sourcesByContent.get(key)
180+
if (cached !== undefined) {
181+
return cached
182+
}
183+
const built = Option.map(fillsOf(runs), fills =>
184+
pngDataUrl(width, height, fills),
185+
)
186+
sourcesByContent.set(key, built)
187+
return built
188+
}
160189

161190
export const spriteDataUrl = (
162191
pixels: ReadonlyArray<string>,

0 commit comments

Comments
 (0)