Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions src/process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
90 changes: 89 additions & 1 deletion test/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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);
Expand All @@ -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"));
Expand Down Expand Up @@ -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
]);
});
});
Binary file added test/textures/armor-layer-1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/textures/armor-layer-2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.