Add shared-scene 2D authoring, sprite animation and Rapier2D physics
CI / validate (push) Canceled after 0s

This commit is contained in:
emil28092005
2026-09-12 04:58:59 +03:00
parent 6f497d1977
commit 3afac743a4
32 changed files with 4999 additions and 270 deletions
+84
View File
@@ -0,0 +1,84 @@
import { base64 } from "./archive.ts";
import { uid, type Asset } from "./schema.ts";
import { sliceImage } from "./two-d.ts";
/** Read image dimensions without DOM, so browser and MCP import share validation. */
export function imageInfo(bytes: Uint8Array) {
if (bytes.length > 25 * 1024 * 1024)
throw Error("Изображение превышает 25 МБ");
const d = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
let width = 0,
height = 0,
mime = "",
ext = "";
if (
bytes.length >= 24 &&
d.getUint32(0) === 0x89504e47 &&
d.getUint32(4) === 0x0d0a1a0a
) {
width = d.getUint32(16);
height = d.getUint32(20);
mime = "image/png";
ext = "png";
} else if (
bytes.length >= 30 &&
d.getUint32(0) === 0x52494646 &&
d.getUint32(8) === 0x57454250
) {
const kind = d.getUint32(12);
if (kind === 0x56503858) {
width = 1 + bytes[24] + bytes[25] * 256 + bytes[26] * 65536;
height = 1 + bytes[27] + bytes[28] * 256 + bytes[29] * 65536;
} else if (kind === 0x5650384c && bytes[20] === 0x2f) {
const bits = d.getUint32(21, true);
width = (bits & 0x3fff) + 1;
height = ((bits >>> 14) & 0x3fff) + 1;
} else if (
kind === 0x56503820 &&
bytes[23] === 0x9d &&
bytes[24] === 1 &&
bytes[25] === 0x2a
) {
width = d.getUint16(26, true) & 0x3fff;
height = d.getUint16(28, true) & 0x3fff;
}
mime = "image/webp";
ext = "webp";
} else if (bytes.length > 4 && d.getUint16(0) === 0xffd8) {
let offset = 2;
while (offset + 4 <= bytes.length) {
if (bytes[offset++] !== 255) break;
while (bytes[offset] === 255) offset++;
const marker = bytes[offset++];
if (marker === 0xda || marker === 0xd9) break;
const length = d.getUint16(offset);
if (length < 2 || offset + length > bytes.length) break;
if ([0xc0, 0xc1, 0xc2].includes(marker) && length >= 7) {
height = d.getUint16(offset + 3);
width = d.getUint16(offset + 5);
break;
}
offset += length;
}
mime = "image/jpeg";
ext = "jpg";
}
if (!width || !height || width > 16384 || height > 16384)
throw Error("Ожидалось PNG, JPEG или WebP размером до 16384×16384");
return { width, height, mime, ext };
}
export function imageAsset(bytes: Uint8Array, name: string): Asset {
const info = imageInfo(bytes);
return {
id: uid("image"),
name: name.replace(/\.[^.]+$/, "") + "." + info.ext,
kind: "image",
uri: `data:${info.mime};base64,${base64(bytes)}`,
image: {
width: info.width,
height: info.height,
pixelsPerUnit: 100,
filter: "nearest",
frames: sliceImage(info.width, info.height, info.width, info.height),
},
};
}