diff --git a/src/process.ts b/src/process.ts index 165525c..110f02a 100644 --- a/src/process.ts +++ b/src/process.ts @@ -322,3 +322,58 @@ export function loadEarsToCanvasFromSkin(canvas: TextureCanvas, image: TextureSo context.clearRect(0, 0, w, h); context.drawImage(image, 24 * scale, 0, w, h, 0, 0, w, h); } + +export function loadArmorToCanvas( + canvas: TextureCanvas, + layer1Image: TextureSource | null | undefined, + layer2Image: TextureSource | null | undefined, +): void { + if (!layer1Image && !layer2Image) return; + + const context = canvas.getContext("2d", { willReadFrequently: true }) as CanvasContext; + const width = layer1Image ? layer1Image.width : layer2Image!.width; + const height = layer1Image ? layer1Image.height : layer2Image!.height; + if (!width || !height || width !== height * 2) { + throw new Error(`Bad armor size: ${width}x${height}`); + } + const scale = computeSkinScale(width); + const copyRegion = (sX: number, sY: number, w: number, h: number, dX: number, dY: number): void => + context.drawImage( + canvas, + sX * scale, + sY * scale, + w * scale, + h * scale, + dX * scale, + dY * scale, + w * scale, + h * scale, + ); + const clearArea = (x: number, y: number, w: number, h: number): void => + context.clearRect(x * scale, y * scale, w * scale, h * scale); + + canvas.width = width; + canvas.height = width; + context.clearRect(0, 0, width, width); + if (layer1Image) { + context.drawImage(layer1Image, 0, 0, width, height); + convertSkinTo1_8(context, width); + copyRegion(0, 0, 32, 16, 32, 0); // Move head to overlay + clearArea(0, 0, 32, 16); // Clear head + copyRegion(16, 16, 24, 16, 16, 32); // Move body to overlay + clearArea(16, 16, 24, 16); // Clear body + copyRegion(40, 16, 16, 16, 40, 32); // Move right arm to overlay + clearArea(40, 16, 16, 16); // Clear right arm + copyRegion(32, 48, 16, 16, 48, 48); // Move left arm to overlay + clearArea(32, 48, 16, 16); // Clear left arm + copyRegion(0, 16, 16, 16, 0, 32); // Move right leg to overlay + clearArea(0, 16, 16, 16); // Clear right leg + copyRegion(16, 48, 16, 16, 0, 48); // Move left leg to overlay + clearArea(16, 48, 16, 16); // Clear left leg + } + if (layer2Image) { + context.drawImage(layer2Image, 0, 0, width, height); + convertSkinTo1_8(context, width); + } + fixOpaqueSkin(context, canvas.width, true); +} diff --git a/test/test.ts b/test/test.ts index 8bdf09b..ddc1c04 100644 --- a/test/test.ts +++ b/test/test.ts @@ -2,7 +2,13 @@ import { describe, it, expect } from "vitest"; -import { inferModelType, loadSkinToCanvas, loadImage, isTextureSource } from "../src/index.js"; +import { + inferModelType, + loadSkinToCanvas, + loadImage, + isTextureSource, + loadArmorToCanvas, +} from "../src/index.js"; import skin1_8Default from "./textures/skin-1.8-default-no_hd.png"; import skin1_8Slim from "./textures/skin-1.8-slim-no_hd.png"; @@ -11,6 +17,8 @@ import skinLegacyHatDefault from "./textures/skin-legacyhat-default-no_hd.png"; import skin1_8SlimBlackedge from "./textures/skin-1.8-slim-no_hd-blackedge.png"; import skin1_8SlimWhiteedge from "./textures/skin-1.8-slim-no_hd-whiteedge.png"; import skin1_8Opaque from "./textures/skin-1.8_opaque-default-no_hd.png"; +import armorLayer1 from "./textures/armor-layer-1.png"; +import armorLayer2 from "./textures/armor-layer-2.png"; async function loadSkin(src: string) { const texture = await loadImage(src); @@ -19,6 +27,14 @@ async function loadSkin(src: string) { return canvas; } +async function loadArmor(layer1Src: string, layer2Src: string) { + const layer1 = await loadImage(layer1Src); + const layer2 = await loadImage(layer2Src); + const canvas = document.createElement("canvas"); + loadArmorToCanvas(canvas, layer1, layer2); + return canvas; +} + describe("detect model of texture", () => { it("1.8 default", async () => expect(inferModelType(await loadSkin(skin1_8Default))).toBe("default")); @@ -106,3 +122,75 @@ describe("isTextureSource", () => { ).toBe(false); }); }); + +describe("process armor textures", () => { + const expectColors = ( + canvas: HTMLCanvasElement, + groups: { + name: string; + x0: number; + y0: number; + w: number; + h: number; + color: { r: number; g: number; b: number }; + }[], + ) => { + const ctx = canvas.getContext("2d", { willReadFrequently: true }); + + const data = ctx!.getImageData(0, 0, canvas.width, canvas.height).data; + for (let x = 0; x < canvas.width; x++) { + for (let y = 0; y < canvas.height; y++) { + const group = groups.find( + (g) => x >= g.x0 && x < g.x0 + g.w && y >= g.y0 && y < g.y0 + g.h, + ); + const i = (y * canvas.width + x) * 4; + const pixel = { r: data[i], g: data[i + 1], b: data[i + 2], a: data[i + 3] }; + if (group) { + expect(pixel, `pixel (${x}, ${y}) in ${group.name}`).toEqual({ ...group.color, a: 255 }); + } else { + expect(pixel, `pixel (${x}, ${y})`).toEqual({ r: 0, g: 0, b: 0, a: 0 }); + } + } + } + }; + + it("all armor parts are moved correctly", async () => { + const canvas = await loadArmor(armorLayer1, armorLayer2); + expect(canvas.width, "canvas width").toBe(64); + expect(canvas.height, "canvas height").toBe(64); + expectColors(canvas, [ + { name: "helmet", x0: 40, y0: 0, w: 16, h: 8, color: { r: 255, g: 0, b: 0 } }, // top/bottom + { name: "helmet", x0: 32, y0: 8, w: 32, h: 8, color: { r: 255, g: 0, b: 0 } }, // right/front/left/back + { name: "chestplate-body", x0: 20, y0: 32, w: 16, h: 4, color: { r: 0, g: 0, b: 255 } }, // top/bottom + { name: "chestplate-body", x0: 16, y0: 36, w: 24, h: 12, color: { r: 0, g: 0, b: 255 } }, // right/front/left/back + { name: "chestplate-left-arm", x0: 52, y0: 48, w: 8, h: 4, color: { r: 255, g: 255, b: 0 } }, // top/bottom + { + name: "chestplate-left-arm", + x0: 48, + y0: 52, + w: 16, + h: 12, + color: { r: 255, g: 255, b: 0 }, + }, // right/front/left/back + { name: "chestplate-right-arm", x0: 44, y0: 32, w: 8, h: 4, color: { r: 255, g: 255, b: 0 } }, // top/bottom + { + name: "chestplate-right-arm", + x0: 40, + y0: 36, + w: 16, + h: 12, + color: { r: 255, g: 255, b: 0 }, + }, // right/front/left/back + { name: "leggings-body", x0: 20, y0: 16, w: 16, h: 4, color: { r: 255, g: 0, b: 255 } }, // top/bottom + { name: "leggings-body", x0: 16, y0: 20, w: 24, h: 12, color: { r: 255, g: 0, b: 255 } }, // right/front/left/back + { name: "leggings-left-leg", x0: 4, y0: 16, w: 8, h: 4, color: { r: 0, g: 255, b: 255 } }, // top/bottom + { name: "leggings-left-leg", x0: 0, y0: 20, w: 16, h: 12, color: { r: 0, g: 255, b: 255 } }, // right/front/left/back + { name: "leggings-right-leg", x0: 20, y0: 48, w: 8, h: 4, color: { r: 0, g: 255, b: 255 } }, // top/bottom + { name: "leggings-right-leg", x0: 16, y0: 52, w: 16, h: 12, color: { r: 0, g: 255, b: 255 } }, // right/front/left/back + { name: "boots-left-leg", x0: 4, y0: 48, w: 8, h: 4, color: { r: 0, g: 255, b: 0 } }, // top/bottom + { name: "boots-left-leg", x0: 0, y0: 52, w: 16, h: 12, color: { r: 0, g: 255, b: 0 } }, // right/front/left/back + { name: "boots-right-leg", x0: 4, y0: 32, w: 8, h: 4, color: { r: 0, g: 255, b: 0 } }, // top/bottom + { name: "boots-right-leg", x0: 0, y0: 36, w: 16, h: 12, color: { r: 0, g: 255, b: 0 } }, // right/front/left/back + ]); + }); +}); diff --git a/test/textures/armor-layer-1.png b/test/textures/armor-layer-1.png new file mode 100644 index 0000000..76c3f45 Binary files /dev/null and b/test/textures/armor-layer-1.png differ diff --git a/test/textures/armor-layer-2.png b/test/textures/armor-layer-2.png new file mode 100644 index 0000000..fc6e1b6 Binary files /dev/null and b/test/textures/armor-layer-2.png differ