// the texture atlas: the engine's textures and every mod's in one image, built by the client when it joins a server // from the textures in the server's .bmod files import { TEXTURE_SIZE } from "$/common/constants.ts"; import type { SpriteRegion } from "$/common/constants.ts"; export interface AtlasLayout { // in pixels, a power of two size: number; // in sprites, by texture id. engine:missing is always at 0, 0 regions: Record; } // engine:missing first, then every other texture sorted by id, row by row export function atlas_layout(ids: Iterable): AtlasLayout { const sorted = [...new Set(ids)].filter((id) => id !== "engine:missing").sort((a, b) => a.localeCompare(b)); const count = sorted.length + 1; const size = 2 ** Math.ceil(Math.log2(Math.ceil(Math.sqrt(count)) * TEXTURE_SIZE)); const per_row = size / TEXTURE_SIZE; const regions: Record = { "engine:missing": { x: 0, y: 0 } }; sorted.forEach((id, i) => { regions[id] = { x: (i + 1) % per_row, y: Math.floor((i + 1) / per_row) }; }); return { size, regions }; } // textures are png bytes by id. ones that don't decode show as missing export async function build_atlas(textures: Map) { const { size, regions } = atlas_layout(textures.keys()); const canvas = new OffscreenCanvas(size, size); const ctx = canvas.getContext("2d")!; // magenta and black checker ctx.fillStyle = "magenta"; ctx.fillRect(0, 0, 8, 8); ctx.fillRect(8, 8, 8, 8); ctx.fillStyle = "black"; ctx.fillRect(8, 0, 8, 8); ctx.fillRect(0, 8, 8, 8); await Promise.all([...textures].map(async ([id, png]) => { const region = regions[id]; try { const image = await createImageBitmap(new Blob([png as Uint8Array], { type: "image/png" })); ctx.drawImage(image, region.x * TEXTURE_SIZE, region.y * TEXTURE_SIZE); image.close(); } catch { console.warn(`Texture ${id} isn't a png that can be shown, it will show as missing`); regions[id] = regions["engine:missing"]; } })); return { image: canvas.transferToImageBitmap(), regions }; } // the engine's own textures, which come with the client instead of a mod. see build_engine_textures in build.ts export async function engine_textures(): Promise> { const names: string[] = await (await fetch("/assets/textures/index.json")).json(); const textures = new Map(); await Promise.all(names.map(async (name) => { const response = await fetch(`/assets/textures/${name}.png`); textures.set(`engine:${name}`, new Uint8Array(await response.arrayBuffer())); })); return textures; }