style: format code with prettier
Local Build and Deploy / deploy (push) Successful in 49s

This commit is contained in:
openhands committed 2026-07-12 21:07:34 +02:00
1 parent e85e4d74ea
commit 2e4ed76121
378 files changed
+22404 -20686

No files matched your search

+143 -143
View File
@@ -1,91 +1,91 @@
import { deflateSync, inflateSync } from 'node:zlib'
import type { SpriteImage } from './image-decoder'
import { deflateSync, inflateSync } from "node:zlib";
import type { SpriteImage } from "./image-decoder";
// ── Minimal PNG Encoder ────────────────────────────────────────────
const CRC_TABLE = (() => {
const table = new Uint32Array(256)
const table = new Uint32Array(256);
for (let n = 0; n < 256; n++) {
let c = n
let c = n;
for (let k = 0; k < 8; k++) {
c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1
c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
}
table[n] = c
table[n] = c;
}
return table
})()
return table;
})();
function crc32(data: Buffer): number {
let crc = 0xffffffff
let crc = 0xffffffff;
for (let i = 0; i < data.length; i++) {
crc = CRC_TABLE[(crc ^ data[i]) & 0xff] ^ (crc >>> 8)
crc = CRC_TABLE[(crc ^ data[i]) & 0xff] ^ (crc >>> 8);
}
return (crc ^ 0xffffffff) >>> 0
return (crc ^ 0xffffffff) >>> 0;
}
function pngChunk(type: string, data: Buffer): Buffer {
const buf = Buffer.alloc(4 + 4 + data.length + 4)
buf.writeUInt32BE(data.length, 0)
buf.write(type, 4, 4, 'ascii')
data.copy(buf, 8)
const crc = crc32(buf.subarray(4, 8 + data.length))
buf.writeUInt32BE(crc >>> 0, 8 + data.length)
return buf
const buf = Buffer.alloc(4 + 4 + data.length + 4);
buf.writeUInt32BE(data.length, 0);
buf.write(type, 4, 4, "ascii");
data.copy(buf, 8);
const crc = crc32(buf.subarray(4, 8 + data.length));
buf.writeUInt32BE(crc >>> 0, 8 + data.length);
return buf;
}
export function encodePng(width: number, height: number, rgba: Buffer): Buffer {
const rawData = Buffer.alloc(height * (1 + width * 4))
const rawData = Buffer.alloc(height * (1 + width * 4));
for (let y = 0; y < height; y++) {
const rowStart = y * (1 + width * 4)
rawData[rowStart] = 0 // filter: None
rgba.copy(rawData, rowStart + 1, y * width * 4, (y + 1) * width * 4)
const rowStart = y * (1 + width * 4);
rawData[rowStart] = 0; // filter: None
rgba.copy(rawData, rowStart + 1, y * width * 4, (y + 1) * width * 4);
}
const compressed = deflateSync(rawData)
const compressed = deflateSync(rawData);
const chunks: Buffer[] = []
const chunks: Buffer[] = [];
// Signature
chunks.push(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]))
chunks.push(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]));
// IHDR
const ihdr = Buffer.alloc(13)
ihdr.writeUInt32BE(width, 0)
ihdr.writeUInt32BE(height, 4)
ihdr[8] = 8 // bit depth
ihdr[9] = 6 // color type: RGBA
ihdr[10] = 0 // compression
ihdr[11] = 0 // filter
ihdr[12] = 0 // interlace
chunks.push(pngChunk('IHDR', ihdr))
const ihdr = Buffer.alloc(13);
ihdr.writeUInt32BE(width, 0);
ihdr.writeUInt32BE(height, 4);
ihdr[8] = 8; // bit depth
ihdr[9] = 6; // color type: RGBA
ihdr[10] = 0; // compression
ihdr[11] = 0; // filter
ihdr[12] = 0; // interlace
chunks.push(pngChunk("IHDR", ihdr));
// IDAT
chunks.push(pngChunk('IDAT', compressed))
chunks.push(pngChunk("IDAT", compressed));
// IEND
chunks.push(pngChunk('IEND', Buffer.alloc(0)))
chunks.push(pngChunk("IEND", Buffer.alloc(0)));
return Buffer.concat(chunks)
return Buffer.concat(chunks);
}
// ── Bin-Packing Spritesheet Builder ────────────────────────────────
interface PackedRect {
x: number
y: number
w: number
h: number
name: string
x: number;
y: number;
w: number;
h: number;
name: string;
}
function nextPow2(v: number): number {
v--
v |= v >> 1
v |= v >> 2
v |= v >> 4
v |= v >> 8
v |= v >> 16
return v + 1
v--;
v |= v >> 1;
v |= v >> 2;
v |= v >> 4;
v |= v >> 8;
v |= v >> 16;
return v + 1;
}
/**
@@ -95,166 +95,166 @@ function nextPow2(v: number): number {
*/
function binPack(images: SpriteImage[]): { rects: PackedRect[]; width: number; height: number } {
if (images.length === 0) {
return { rects: [], width: 1, height: 1 }
return { rects: [], width: 1, height: 1 };
}
// Sort by height descending for better shelf packing
const sorted = [...images].sort((a, b) => b.height - a.height || b.width - a.width)
const sorted = [...images].sort((a, b) => b.height - a.height || b.width - a.width);
// Estimate initial atlas width based on total area
const totalArea = sorted.reduce((sum, img) => sum + img.width * img.height, 0)
const maxSingleWidth = Math.max(...sorted.map((i) => i.width))
let atlasWidth = Math.max(maxSingleWidth, Math.ceil(Math.sqrt(totalArea)))
const totalArea = sorted.reduce((sum, img) => sum + img.width * img.height, 0);
const maxSingleWidth = Math.max(...sorted.map((i) => i.width));
let atlasWidth = Math.max(maxSingleWidth, Math.ceil(Math.sqrt(totalArea)));
// Round up to power of 2 for GPU friendliness (max 2048)
atlasWidth = Math.min(2048, nextPow2(atlasWidth))
atlasWidth = Math.min(2048, nextPow2(atlasWidth));
const rects: PackedRect[] = []
let shelfY = 0
let shelfHeight = 0
let shelfX = 0
const rects: PackedRect[] = [];
let shelfY = 0;
let shelfHeight = 0;
let shelfX = 0;
for (const img of sorted) {
// If sprite doesn't fit on current shelf, start new shelf
if (shelfX + img.width > atlasWidth) {
shelfY += shelfHeight
shelfHeight = 0
shelfX = 0
shelfY += shelfHeight;
shelfHeight = 0;
shelfX = 0;
}
rects.push({ x: shelfX, y: shelfY, w: img.width, h: img.height, name: img.name })
shelfX += img.width
shelfHeight = Math.max(shelfHeight, img.height)
rects.push({ x: shelfX, y: shelfY, w: img.width, h: img.height, name: img.name });
shelfX += img.width;
shelfHeight = Math.max(shelfHeight, img.height);
}
const finalHeight = shelfY + shelfHeight
return { rects, width: atlasWidth, height: finalHeight || 1 }
const finalHeight = shelfY + shelfHeight;
return { rects, width: atlasWidth, height: finalHeight || 1 };
}
// ── Spritesheet Builder ────────────────────────────────────────────
interface FrameInfo {
x: number
y: number
w: number
h: number
x: number;
y: number;
w: number;
h: number;
}
export function buildSpritesheet(images: SpriteImage[]): {
png: Buffer
frames: Map<string, FrameInfo>
width: number
height: number
png: Buffer;
frames: Map<string, FrameInfo>;
width: number;
height: number;
} {
if (images.length === 0) {
const emptyPng = encodePng(1, 1, Buffer.alloc(4))
return { png: emptyPng, frames: new Map(), width: 1, height: 1 }
const emptyPng = encodePng(1, 1, Buffer.alloc(4));
return { png: emptyPng, frames: new Map(), width: 1, height: 1 };
}
const { rects, width: sheetWidth, height: sheetHeight } = binPack(images)
const { rects, width: sheetWidth, height: sheetHeight } = binPack(images);
// Build lookup by name for pixel data
const imageByName = new Map(images.map((img) => [img.name, img]))
const imageByName = new Map(images.map((img) => [img.name, img]));
const combined = Buffer.alloc(sheetWidth * sheetHeight * 4)
const frames = new Map<string, FrameInfo>()
const combined = Buffer.alloc(sheetWidth * sheetHeight * 4);
const frames = new Map<string, FrameInfo>();
for (const rect of rects) {
const img = imageByName.get(rect.name)
if (!img) continue
const img = imageByName.get(rect.name);
if (!img) continue;
// Copy pixels row by row
for (let y = 0; y < img.height; y++) {
const srcStart = y * img.width * 4
const dstStart = ((rect.y + y) * sheetWidth + rect.x) * 4
img.pixels.copy(combined, dstStart, srcStart, srcStart + img.width * 4)
const srcStart = y * img.width * 4;
const dstStart = ((rect.y + y) * sheetWidth + rect.x) * 4;
img.pixels.copy(combined, dstStart, srcStart, srcStart + img.width * 4);
}
frames.set(rect.name, { x: rect.x, y: rect.y, w: rect.w, h: rect.h })
frames.set(rect.name, { x: rect.x, y: rect.y, w: rect.w, h: rect.h });
}
const png = encodePng(sheetWidth, sheetHeight, combined)
return { png, frames, width: sheetWidth, height: sheetHeight }
const png = encodePng(sheetWidth, sheetHeight, combined);
return { png, frames, width: sheetWidth, height: sheetHeight };
}
// ── .nitro Bundle Creator ──────────────────────────────────────────
export function createNitroBundle(jsonData: object, pngData: Buffer, name: string): Buffer {
const jsonStr = JSON.stringify(jsonData)
const jsonCompressed = deflateSync(Buffer.from(jsonStr, 'utf-8'))
const pngCompressed = deflateSync(pngData)
const jsonStr = JSON.stringify(jsonData);
const jsonCompressed = deflateSync(Buffer.from(jsonStr, "utf-8"));
const pngCompressed = deflateSync(pngData);
const jsonFileName = `${name}.json`
const pngFileName = `${name}.png`
const jsonFileName = `${name}.json`;
const pngFileName = `${name}.png`;
const totalSize =
2 +
(2 + jsonFileName.length + 4 + jsonCompressed.length) +
(2 + pngFileName.length + 4 + pngCompressed.length)
(2 + pngFileName.length + 4 + pngCompressed.length);
const bundle = Buffer.alloc(totalSize)
let offset = 0
const bundle = Buffer.alloc(totalSize);
let offset = 0;
bundle.writeInt16BE(2, offset)
offset += 2
bundle.writeInt16BE(2, offset);
offset += 2;
bundle.writeInt16BE(jsonFileName.length, offset)
offset += 2
bundle.write(jsonFileName, offset, jsonFileName.length, 'utf-8')
offset += jsonFileName.length
bundle.writeInt32BE(jsonCompressed.length, offset)
offset += 4
jsonCompressed.copy(bundle, offset)
offset += jsonCompressed.length
bundle.writeInt16BE(jsonFileName.length, offset);
offset += 2;
bundle.write(jsonFileName, offset, jsonFileName.length, "utf-8");
offset += jsonFileName.length;
bundle.writeInt32BE(jsonCompressed.length, offset);
offset += 4;
jsonCompressed.copy(bundle, offset);
offset += jsonCompressed.length;
bundle.writeInt16BE(pngFileName.length, offset)
offset += 2
bundle.write(pngFileName, offset, pngFileName.length, 'utf-8')
offset += pngFileName.length
bundle.writeInt32BE(pngCompressed.length, offset)
offset += 4
pngCompressed.copy(bundle, offset)
bundle.writeInt16BE(pngFileName.length, offset);
offset += 2;
bundle.write(pngFileName, offset, pngFileName.length, "utf-8");
offset += pngFileName.length;
bundle.writeInt32BE(pngCompressed.length, offset);
offset += 4;
pngCompressed.copy(bundle, offset);
return bundle
return bundle;
}
// ── .nitro Bundle Parser ──────────────────────────────────────────
export function parseNitroBundle(buffer: Buffer): {
json: Record<string, unknown>
jsonFileName: string
png: Buffer
pngFileName: string
json: Record<string, unknown>;
jsonFileName: string;
png: Buffer;
pngFileName: string;
} {
let offset = 0
let offset = 0;
const fileCount = buffer.readInt16BE(offset)
offset += 2
const fileCount = buffer.readInt16BE(offset);
offset += 2;
if (fileCount < 2) {
throw new Error(`Expected at least 2 files in .nitro bundle, got ${fileCount}`)
throw new Error(`Expected at least 2 files in .nitro bundle, got ${fileCount}`);
}
// File 1: JSON
const jsonFileNameLen = buffer.readInt16BE(offset)
offset += 2
const jsonFileName = buffer.toString('utf-8', offset, offset + jsonFileNameLen)
offset += jsonFileNameLen
const jsonCompressedLen = buffer.readInt32BE(offset)
offset += 4
const jsonCompressed = buffer.subarray(offset, offset + jsonCompressedLen)
offset += jsonCompressedLen
const jsonStr = inflateSync(jsonCompressed).toString('utf-8')
const json = JSON.parse(jsonStr)
const jsonFileNameLen = buffer.readInt16BE(offset);
offset += 2;
const jsonFileName = buffer.toString("utf-8", offset, offset + jsonFileNameLen);
offset += jsonFileNameLen;
const jsonCompressedLen = buffer.readInt32BE(offset);
offset += 4;
const jsonCompressed = buffer.subarray(offset, offset + jsonCompressedLen);
offset += jsonCompressedLen;
const jsonStr = inflateSync(jsonCompressed).toString("utf-8");
const json = JSON.parse(jsonStr);
// File 2: PNG
const pngFileNameLen = buffer.readInt16BE(offset)
offset += 2
const pngFileName = buffer.toString('utf-8', offset, offset + pngFileNameLen)
offset += pngFileNameLen
const pngCompressedLen = buffer.readInt32BE(offset)
offset += 4
const pngCompressed = buffer.subarray(offset, offset + pngCompressedLen)
offset += pngCompressedLen
const png = inflateSync(pngCompressed)
const pngFileNameLen = buffer.readInt16BE(offset);
offset += 2;
const pngFileName = buffer.toString("utf-8", offset, offset + pngFileNameLen);
offset += pngFileNameLen;
const pngCompressedLen = buffer.readInt32BE(offset);
offset += 4;
const pngCompressed = buffer.subarray(offset, offset + pngCompressedLen);
offset += pngCompressedLen;
const png = inflateSync(pngCompressed);
return { json, jsonFileName, png, pngFileName }
return { json, jsonFileName, png, pngFileName };
}