diff --git a/examples/main.ts b/examples/main.ts
index c52cbad4..eea77bd4 100644
--- a/examples/main.ts
+++ b/examples/main.ts
@@ -3,6 +3,12 @@ import type { ModelType } from "skinview-utils";
import type { BackEquipment } from "../src/model";
import "./style.css";
+declare global {
+ interface Window {
+ newArmorBuilder: (name: String, layer1: String, layer2: String) => void;
+ }
+};
+
const skinParts = ["head", "body", "rightArm", "leftArm", "rightLeg", "leftLeg"];
const skinLayers = ["innerLayer", "outerLayer"];
const availableAnimations = {
@@ -16,6 +22,10 @@ const availableAnimations = {
swim: new skinview3d.SwimAnimation(),
};
+var armorTypes: {
+ [key: string]: skinview3d.ArmorType;
+} = {};
+
let skinViewer: skinview3d.SkinViewer;
function obtainTextureUrl(id: string): string {
@@ -128,6 +138,92 @@ function reloadEars(skipSkinReload = false): void {
}
}
+async function reloadArmor(): Promise
{
+ const fallbackInput = document.getElementById("armor-fallback") as HTMLInputElement;
+ await skinViewer.fallbackArmor(armorTypes[fallbackInput.value] || null);
+ const helmet = document.getElementById("armor-helmet") as HTMLInputElement;
+ const chestplate = document.getElementById("armor-chestplate") as HTMLInputElement;
+ const leggings = document.getElementById("armor-leggings") as HTMLInputElement;
+ const boots = document.getElementById("armor-boots") as HTMLInputElement;
+ await skinViewer.loadArmor({
+ helmet: armorTypes[helmet.value] || null,
+ chestplate: armorTypes[chestplate.value] || null,
+ leggings: armorTypes[leggings.value] || null,
+ boots: armorTypes[boots.value] || null,
+ });
+}
+
+function buildArmorTypes(): void {
+ const datalist = document.getElementById("armor-slots") as HTMLDataListElement;
+ datalist.innerHTML = "";
+ armorTypes = {};
+
+ const armorTypesElement = document.getElementById("armor-types") as HTMLDivElement;
+ for(const armorTypeElement of armorTypesElement.querySelectorAll(".armor-type")) {
+ const index = armorTypeElement.dataset.index;
+ const nameInput = armorTypeElement.querySelector(".armor-type-name");
+ const layer1Input = armorTypeElement.querySelector(".armor-type-layer1");
+ const layer2Input = armorTypeElement.querySelector(".armor-type-layer2");
+ const layer1Texture = obtainTextureUrl(`armor${index}-layer1`) || null;
+ const layer2Texture = obtainTextureUrl(`armor${index}-layer2`) || null;
+ if(nameInput?.value && ((layer1Texture && layer1Texture !== "") || (layer2Texture && layer2Texture !== ""))) {
+ armorTypes[nameInput.value] = new skinview3d.ArmorType(layer1Texture, layer2Texture);
+ }
+ const summaryElement = armorTypeElement.querySelector("summary") as HTMLElement;
+ if (summaryElement) {
+ summaryElement.innerText = nameInput?.value || `armor_${index}`;
+ }
+ }
+
+ for(const name in armorTypes) {
+ const optionElement = document.createElement("option");
+ optionElement.value = name || "";
+ datalist.appendChild(optionElement);
+ }
+}
+
+function newArmorBuilder(name: String, layer1: String | null, layer2: String | null): void {
+ const armorTypesElement = document.getElementById("armor-types") as HTMLDivElement;
+ const index = armorTypesElement.querySelectorAll(".armor-type").length + 1;
+ name = name || `armor_${index}`;
+ layer1 = layer1 || "";
+ layer2 = layer2 || "";
+ const armorTypeElement = document.createElement("details");
+ armorTypeElement.dataset.index = index.toString();
+ armorTypeElement.classList.add("controls", "armor-type");
+ armorTypeElement.innerHTML = `
+ ${name}
+
+
+
+
+
+
+
+
+
+
+
+ `;
+ armorTypesElement.appendChild(armorTypeElement);
+
+ initializeUploadButton(`armor${index}-layer1`, () => {
+ buildArmorTypes();
+ reloadArmor();
+ });
+ initializeUploadButton(`armor${index}-layer2`, () => {
+ buildArmorTypes();
+ reloadArmor();
+ });
+ armorTypeElement.addEventListener("change", () => {
+ buildArmorTypes();
+ reloadArmor();
+ });
+}
+window.newArmorBuilder = newArmorBuilder;
+
function reloadPanorama(): void {
const input = document.getElementById("panorama_url") as HTMLInputElement;
const backgroundTypeInput = document.getElementById("background_type") as HTMLSelectElement;
@@ -177,6 +273,31 @@ function reloadNameTag(): void {
}
}
+function initializeUploadButton(id: string, callback: () => void) {
+ const urlInput = document.getElementById(id) as HTMLInputElement;
+ const fileInput = document.getElementById(`${id}_upload`) as HTMLInputElement;
+ const unsetButton = document.getElementById(`${id}_unset`);
+
+ const unsetAction = () => {
+ if (urlInput) {
+ urlInput.readOnly = false;
+ urlInput.value = "";
+ }
+ if (fileInput) {
+ fileInput.value = fileInput.defaultValue;
+ }
+ callback();
+ };
+
+ fileInput?.addEventListener("change", () => callback());
+ urlInput?.addEventListener("keydown", e => {
+ if (e.key === "Backspace" && urlInput?.readOnly) {
+ unsetAction();
+ }
+ });
+ unsetButton?.addEventListener("click", () => unsetAction());
+};
+
function initializeControls(): void {
const canvasWidth = document.getElementById("canvas_width") as HTMLInputElement;
const canvasHeight = document.getElementById("canvas_height") as HTMLInputElement;
@@ -371,31 +492,6 @@ function initializeControls(): void {
}
}
- const initializeUploadButton = (id: string, callback: () => void) => {
- const urlInput = document.getElementById(id) as HTMLInputElement;
- const fileInput = document.getElementById(`${id}_upload`) as HTMLInputElement;
- const unsetButton = document.getElementById(`${id}_unset`);
-
- const unsetAction = () => {
- if (urlInput) {
- urlInput.readOnly = false;
- urlInput.value = "";
- }
- if (fileInput) {
- fileInput.value = fileInput.defaultValue;
- }
- callback();
- };
-
- fileInput?.addEventListener("change", () => callback());
- urlInput?.addEventListener("keydown", e => {
- if (e.key === "Backspace" && urlInput?.readOnly) {
- unsetAction();
- }
- });
- unsetButton?.addEventListener("click", () => unsetAction());
- };
-
initializeUploadButton("skin_url", reloadSkin);
initializeUploadButton("cape_url", reloadCape);
initializeUploadButton("ears_url", reloadEars);
@@ -407,12 +503,14 @@ function initializeControls(): void {
const earsSource = document.getElementById("ears_source") as HTMLSelectElement;
const earsUrl = document.getElementById("ears_url") as HTMLInputElement;
const panoramaUrl = document.getElementById("panorama_url") as HTMLInputElement;
+ const armorTable = document.getElementById("armor-table") as HTMLTableElement;
skinUrl?.addEventListener("change", reloadSkin);
skinModel?.addEventListener("change", reloadSkin);
capeUrl?.addEventListener("change", reloadCape);
earsSource?.addEventListener("change", () => reloadEars());
earsUrl?.addEventListener("change", () => reloadEars());
+ armorTable?.addEventListener("change", reloadArmor);
panoramaUrl?.addEventListener("change", reloadPanorama);
const backEquipmentRadios = document.querySelectorAll('input[type="radio"][name="back_equipment"]');
@@ -509,9 +607,19 @@ function initializeViewer(): void {
}
}
+ newArmorBuilder("netherite_armor", "img/armor/netherite_layer_1.png", "img/armor/netherite_layer_2.png");
+ newArmorBuilder("diamond_armor", "img/armor/diamond_layer_1.png", "img/armor/diamond_layer_2.png");
+ newArmorBuilder("gold_armor", "img/armor/gold_layer_1.png", "img/armor/gold_layer_2.png");
+ newArmorBuilder("iron_armor", "img/armor/iron_layer_1.png", "img/armor/iron_layer_2.png");
+ newArmorBuilder("leather_armor", "img/armor/leather_layer_1.png", "img/armor/leather_layer_2.png");
+ newArmorBuilder("chainmail_armor", "img/armor/chainmail_layer_1.png", "img/armor/chainmail_layer_2.png");
+ newArmorBuilder("turtle_armor", "img/armor/turtle_layer_1.png", null);
+ buildArmorTypes();
+
reloadSkin();
reloadCape();
reloadEars(true);
+ reloadArmor();
reloadPanorama();
reloadNameTag();
}
diff --git a/examples/offscreen-renderer.ts b/examples/offscreen-renderer.ts
index ab0efa98..1075ade5 100644
--- a/examples/offscreen-renderer.ts
+++ b/examples/offscreen-renderer.ts
@@ -1,6 +1,11 @@
import * as skinview3d from "../src/skinview3d";
import "./style.css";
+let netheriteArmor = new skinview3d.ArmorType("./img/armor/netherite_layer_1.png", "./img/armor/netherite_layer_2.png");
+let diamondArmor = new skinview3d.ArmorType("./img/armor/diamond_layer_1.png", "./img/armor/diamond_layer_2.png");
+let turtleHelmet = new skinview3d.ArmorType("./img/armor/turtle_layer_1.png");
+let crusaderArmor = new skinview3d.ArmorType("./img/armor/crusader_layer_1.png", "./img/armor/crusader_layer_2.png");
+
const configurations = [
{
skin: "img/1_8_texturemap_redux.png",
@@ -9,15 +14,27 @@ const configurations = [
{
skin: "img/hacksore.png",
cape: "img/legacy_cape.png",
+ armor: {
+ leggings: crusaderArmor,
+ helmet: crusaderArmor
+ },
},
{
skin: "img/haka.png",
cape: "img/mojang_cape.png",
+ armor: {
+ boots: netheriteArmor,
+ leggings: netheriteArmor,
+ chestplate: netheriteArmor
+ },
},
{
skin: "img/hatsune_miku.png",
cape: "img/mojang_cape.png",
backEquipment: "elytra",
+ armor: {
+ boots: diamondArmor
+ },
},
{
skin: "img/ironman_hd.png",
@@ -26,7 +43,10 @@ const configurations = [
{
skin: "img/sethbling.png",
cape: null,
- },
+ armor: {
+ helmet: turtleHelmet
+ },
+ }
];
(async function () {
@@ -36,7 +56,7 @@ const configurations = [
renderPaused: true,
});
- skinViewer.camera.rotation.x = -0.62;
+ skinViewer.camera.rotation.x = -0.620;
skinViewer.camera.rotation.y = 0.534;
skinViewer.camera.rotation.z = 0.348;
skinViewer.camera.position.x = 30.5;
@@ -47,6 +67,7 @@ const configurations = [
await Promise.all([
skinViewer.loadSkin(config.skin),
skinViewer.loadCape(config.cape, { backEquipment: config.backEquipment }),
+ skinViewer.loadArmor(config.armor),
]);
skinViewer.render();
const image = skinViewer.canvas.toDataURL();
diff --git a/examples/public/img/armor/chainmail_layer_1.png b/examples/public/img/armor/chainmail_layer_1.png
new file mode 100644
index 00000000..c28e0a3b
Binary files /dev/null and b/examples/public/img/armor/chainmail_layer_1.png differ
diff --git a/examples/public/img/armor/chainmail_layer_2.png b/examples/public/img/armor/chainmail_layer_2.png
new file mode 100644
index 00000000..60bf1d77
Binary files /dev/null and b/examples/public/img/armor/chainmail_layer_2.png differ
diff --git a/examples/public/img/armor/crusader_layer_1.png b/examples/public/img/armor/crusader_layer_1.png
new file mode 100644
index 00000000..1952e47d
Binary files /dev/null and b/examples/public/img/armor/crusader_layer_1.png differ
diff --git a/examples/public/img/armor/crusader_layer_2.png b/examples/public/img/armor/crusader_layer_2.png
new file mode 100644
index 00000000..71ee929f
Binary files /dev/null and b/examples/public/img/armor/crusader_layer_2.png differ
diff --git a/examples/public/img/armor/diamond_layer_1.png b/examples/public/img/armor/diamond_layer_1.png
new file mode 100644
index 00000000..1c5e912f
Binary files /dev/null and b/examples/public/img/armor/diamond_layer_1.png differ
diff --git a/examples/public/img/armor/diamond_layer_2.png b/examples/public/img/armor/diamond_layer_2.png
new file mode 100644
index 00000000..fd4d18ad
Binary files /dev/null and b/examples/public/img/armor/diamond_layer_2.png differ
diff --git a/examples/public/img/armor/gold_layer_1.png b/examples/public/img/armor/gold_layer_1.png
new file mode 100644
index 00000000..3c15b4a0
Binary files /dev/null and b/examples/public/img/armor/gold_layer_1.png differ
diff --git a/examples/public/img/armor/gold_layer_2.png b/examples/public/img/armor/gold_layer_2.png
new file mode 100644
index 00000000..5a5e2075
Binary files /dev/null and b/examples/public/img/armor/gold_layer_2.png differ
diff --git a/examples/public/img/armor/iron_layer_1.png b/examples/public/img/armor/iron_layer_1.png
new file mode 100644
index 00000000..1da5d263
Binary files /dev/null and b/examples/public/img/armor/iron_layer_1.png differ
diff --git a/examples/public/img/armor/iron_layer_2.png b/examples/public/img/armor/iron_layer_2.png
new file mode 100644
index 00000000..3170869e
Binary files /dev/null and b/examples/public/img/armor/iron_layer_2.png differ
diff --git a/examples/public/img/armor/leather_layer_1.png b/examples/public/img/armor/leather_layer_1.png
new file mode 100644
index 00000000..cf4aada1
Binary files /dev/null and b/examples/public/img/armor/leather_layer_1.png differ
diff --git a/examples/public/img/armor/leather_layer_1_overlay.png b/examples/public/img/armor/leather_layer_1_overlay.png
new file mode 100644
index 00000000..98dd02af
Binary files /dev/null and b/examples/public/img/armor/leather_layer_1_overlay.png differ
diff --git a/examples/public/img/armor/leather_layer_2.png b/examples/public/img/armor/leather_layer_2.png
new file mode 100644
index 00000000..abd64e6b
Binary files /dev/null and b/examples/public/img/armor/leather_layer_2.png differ
diff --git a/examples/public/img/armor/leather_layer_2_overlay.png b/examples/public/img/armor/leather_layer_2_overlay.png
new file mode 100644
index 00000000..4faea6d6
Binary files /dev/null and b/examples/public/img/armor/leather_layer_2_overlay.png differ
diff --git a/examples/public/img/armor/netherite_layer_1.png b/examples/public/img/armor/netherite_layer_1.png
new file mode 100644
index 00000000..ebcbbaa4
Binary files /dev/null and b/examples/public/img/armor/netherite_layer_1.png differ
diff --git a/examples/public/img/armor/netherite_layer_2.png b/examples/public/img/armor/netherite_layer_2.png
new file mode 100644
index 00000000..963f0826
Binary files /dev/null and b/examples/public/img/armor/netherite_layer_2.png differ
diff --git a/examples/public/img/armor/turtle_layer_1.png b/examples/public/img/armor/turtle_layer_1.png
new file mode 100644
index 00000000..6f392726
Binary files /dev/null and b/examples/public/img/armor/turtle_layer_1.png differ
diff --git a/src/model.ts b/src/model.ts
index ebf99b8b..e7ff39cf 100644
--- a/src/model.ts
+++ b/src/model.ts
@@ -233,9 +233,8 @@ export class SkinObject extends Group {
this.rightLeg = new BodyPart(rightLegMesh, rightLeg2Mesh);
this.rightLeg.name = "rightLeg";
this.rightLeg.add(rightLegPivot);
- this.rightLeg.position.x = -1.9;
+ this.rightLeg.position.x = -2;
this.rightLeg.position.y = -12;
- this.rightLeg.position.z = -0.1;
this.add(this.rightLeg);
// Left Leg
@@ -254,9 +253,8 @@ export class SkinObject extends Group {
this.leftLeg = new BodyPart(leftLegMesh, leftLeg2Mesh);
this.leftLeg.name = "leftLeg";
this.leftLeg.add(leftLegPivot);
- this.leftLeg.position.x = 1.9;
+ this.leftLeg.position.x = 2;
this.leftLeg.position.y = -12;
- this.leftLeg.position.z = -0.1;
this.add(this.leftLeg);
this.modelType = "default";
@@ -467,6 +465,123 @@ export class EarsObject extends Group {
}
}
+export class ArmorObject {
+
+ readonly helmet: Group;
+
+ readonly chestplate: Group;
+ readonly chestplateRight: Group;
+ readonly chestplateLeft: Group;
+
+ readonly leggingsTop: Group;
+ readonly leggingsRight: Group;
+ readonly leggingsLeft: Group;
+
+ readonly bootsRight: Group;
+ readonly bootsLeft: Group;
+
+ helmetMat: MeshStandardMaterial;
+ chestplateMat: MeshStandardMaterial;
+ leggingsMat: MeshStandardMaterial;
+ bootsMat: MeshStandardMaterial;
+
+ constructor() {
+ let defaultMat = {
+ side: DoubleSide,
+ transparent: true,
+ alphaTest: 1e-5
+ };
+
+ this.helmetMat = new MeshStandardMaterial(defaultMat);
+ this.chestplateMat = new MeshStandardMaterial(defaultMat);
+ this.leggingsMat = new MeshStandardMaterial(defaultMat);
+ this.bootsMat = new MeshStandardMaterial(defaultMat);
+
+ // helmet
+ const helmetBox = new BoxGeometry(10, 10, 10);
+ setUVs(helmetBox, 32, 0, 8, 8, 8, 64, 64);
+ const helmetMesh = new Mesh(helmetBox, this.helmetMat);
+ helmetMesh.position.y = 4;
+
+ this.helmet = new Group();
+ this.helmet.name = "helmet";
+ this.helmet.add(helmetMesh);
+
+
+ // torso part of the chestplate
+ const chestplateBox = new BoxGeometry(10, 14, 6);
+ setUVs(chestplateBox, 16, 32, 8, 12, 4, 64, 64);
+ const chestplateMesh = new Mesh(chestplateBox, this.chestplateMat);
+
+ this.chestplate = new Group();
+ this.chestplate.name = "chestplate";
+ this.chestplate.add(chestplateMesh);
+
+
+ // shoulder pads from the chestplate
+ const rightShoulderPadBox = new BoxGeometry(6, 14, 6);
+ setUVs(rightShoulderPadBox, 40, 32, 4, 12, 4, 64, 64);
+ const rightShoulderPadMesh = new Mesh(rightShoulderPadBox, this.chestplateMat);
+
+ const leftShoulderPadBox = new BoxGeometry(6, 14, 6);
+ setUVs(leftShoulderPadBox, 48, 48, 4, 12, 4, 64, 64);
+ const leftShoulderPadMesh = new Mesh(leftShoulderPadBox, this.chestplateMat);
+
+ this.chestplateRight = new Group();
+ this.chestplateRight.name = "chestplateRight";
+ this.chestplateRight.add(rightShoulderPadMesh);
+
+ this.chestplateLeft = new Group();
+ this.chestplateLeft.name = "chestplateLeft";
+ this.chestplateLeft.add(leftShoulderPadMesh);
+
+
+ // the hips part of the leggings
+ const leggingsTopBox = new BoxGeometry(9.2, 13.2, 5.2);
+ setUVs(leggingsTopBox, 16, 16, 8, 12, 4, 64, 64);
+ const leggingsTopMesh = new Mesh(leggingsTopBox, this.leggingsMat);
+
+ this.leggingsTop = new Group();
+ this.leggingsTop.name = "leggingsTop";
+ this.leggingsTop.add(leggingsTopMesh);
+
+ // leg parts of the leggings
+ const rightLeggingBox = new BoxGeometry(5, 13, 5);
+ setUVs(rightLeggingBox, 0, 16, 4, 12, 4, 64, 64);
+ const rightLeggingMesh = new Mesh(rightLeggingBox, this.leggingsMat);
+
+ const leftLeggingBox = new BoxGeometry(5, 13, 5);
+ setUVs(leftLeggingBox, 16, 48, 4, 12, 4, 64, 64);
+ const leftLeggingMesh = new Mesh(leftLeggingBox, this.leggingsMat);
+
+ this.leggingsRight = new Group();
+ this.leggingsRight.name = "leggingsRight";
+ this.leggingsRight.add(rightLeggingMesh);
+
+ this.leggingsLeft = new Group();
+ this.leggingsLeft.name = "leggingsLeft";
+ this.leggingsLeft.add(leftLeggingMesh);
+
+
+ // boots
+ const rightBoot = new BoxGeometry(6, 14, 6);
+ setUVs(rightBoot, 0, 32, 4, 12, 4, 64, 64);
+ const rightBootMesh = new Mesh(rightBoot, this.bootsMat);
+
+ const leftBoot = new BoxGeometry(6, 14, 6);
+ setUVs(leftBoot, 0, 48, 4, 12, 4, 64, 64);
+ const leftBootMesh = new Mesh(leftBoot, this.bootsMat);
+
+ this.bootsRight = new Group();
+ this.bootsRight.name = "bootsRight";
+ this.bootsRight.add(rightBootMesh);
+
+ this.bootsLeft = new Group();
+ this.bootsLeft.name = "bootsLeft";
+ this.bootsLeft.add(leftBootMesh);
+ }
+}
+
export type BackEquipment = "cape" | "elytra";
const CapeDefaultAngle = (10.8 * Math.PI) / 180;
@@ -476,6 +591,7 @@ export class PlayerObject extends Group {
readonly cape: CapeObject;
readonly elytra: ElytraObject;
readonly ears: EarsObject;
+ readonly armor: ArmorObject;
constructor() {
super();
@@ -506,6 +622,31 @@ export class PlayerObject extends Group {
this.ears.position.z = 2 / 3;
this.ears.visible = false;
this.skin.head.add(this.ears);
+
+ this.armor = new ArmorObject();
+ this.armor.helmet.visible = false;
+ this.armor.chestplate.visible = false;
+ this.armor.chestplateRight.visible = false;
+ this.armor.chestplateRight.position.x = -1;
+ this.armor.chestplateRight.position.y = -4;
+ this.armor.chestplateLeft.visible = false;
+ this.armor.chestplateLeft.position.x = 1;
+ this.armor.chestplateLeft.position.y = -4;
+ this.armor.leggingsTop.visible = false;
+ this.armor.leggingsRight.visible = false;
+ this.armor.leggingsRight.position.y = -6;
+ this.armor.leggingsLeft.visible = false;
+ this.armor.leggingsLeft.position.y = -6;
+ this.armor.bootsRight.visible = false;
+ this.armor.bootsRight.position.y = -6;
+ this.armor.bootsLeft.visible = false;
+ this.armor.bootsLeft.position.y = -6;
+ this.skin.head.add(this.armor.helmet);
+ this.skin.body.add(this.armor.chestplate, this.armor.leggingsTop);
+ this.skin.rightArm.add(this.armor.chestplateRight);
+ this.skin.leftArm.add(this.armor.chestplateLeft);
+ this.skin.rightLeg.add(this.armor.leggingsRight, this.armor.bootsRight);
+ this.skin.leftLeg.add(this.armor.leggingsLeft, this.armor.bootsLeft);
}
get backEquipment(): BackEquipment | null {
diff --git a/src/viewer.ts b/src/viewer.ts
index eb3af670..129e623d 100644
--- a/src/viewer.ts
+++ b/src/viewer.ts
@@ -6,6 +6,7 @@ import {
loadEarsToCanvasFromSkin,
loadImage,
loadSkinToCanvas,
+ loadArmorToCanvas,
type ModelType,
type RemoteImage,
type TextureSource,
@@ -249,6 +250,74 @@ export interface SkinViewerOptions {
nameTag?: NameTagObject | string;
}
+export interface ArmorSlots {
+ /**
+ * The types of armor to be used in each slot.
+ *
+ * @defaultValue Slots with a value of null will be invisible.
+ */
+
+ helmet: ArmorType | null;
+
+ chestplate: ArmorType | null;
+
+ leggings: ArmorType | null;
+
+ boots: ArmorType | null;
+}
+
+export class ArmorType {
+ /**
+ * Initializes an armor type.
+ * The textures work the same as in Resource Packs which means that the textures are split into 2 layers,
+ * with Layer 1 being the Helmet, Chestplate and Boots, and Layer 2 being the leggings.
+ *
+ * This *could* be because older skins didn't have outer layers and so there was no image format to support having overlapping textures (leggings overlap with the boots and chestplate) so they were split into 2 different textures. But hey, that's just a theory, a game theory.
+ *
+ * Armor types such as Turtle Shell use only one layer.
+ *
+ * @defaultValue Unspecified layer causes said layer to be invisible.
+ */
+ readonly canvas = document.createElement("canvas");
+
+ texture?: Texture;
+ promise?: Promise;
+
+ constructor(layer1Src?: RemoteImage | TextureSource | null, layer2Src?: RemoteImage | TextureSource | null) {
+ this.promise = new Promise(async (resolve) => {
+ let layer1 = await this.loadLayer(layer1Src);
+ let layer2 = await this.loadLayer(layer2Src);
+ loadArmorToCanvas(this.canvas, layer1, layer2);
+ this.recreateArmorTexture();
+ this.canvas.remove();
+ resolve();
+ });
+ }
+
+ private loadLayer(src: RemoteImage | TextureSource | undefined | null): Promise {
+ return new Promise((resolve) => {
+ if (src === undefined || src === null) {
+ resolve(undefined);
+ } else if (isTextureSource(src)) {
+ resolve(src);
+ } else {
+ loadImage(src).then(image => {
+ resolve(image);
+ });
+ }
+ });
+ }
+
+ private recreateArmorTexture() {
+ if (this.texture) {
+ this.texture.dispose();
+ }
+ this.texture = new CanvasTexture(this.canvas);
+ this.texture.magFilter = NearestFilter;
+ this.texture.minFilter = NearestFilter;
+ }
+}
+
/**
* The SkinViewer renders the player on a canvas.
*/
@@ -296,6 +365,8 @@ export class SkinViewer {
private capeTexture: Texture | null = null;
private earsTexture: Texture | null = null;
private backgroundTexture: Texture | null = null;
+ private armor: ArmorSlots | null = null;
+ private _fallbackArmor: ArmorSlots | null = null;
private _disposed: boolean = false;
private _renderPaused: boolean = false;
@@ -657,6 +728,86 @@ export class SkinViewer {
}
}
+ private toArmorSlots(armor: ArmorType | Partial | null): ArmorSlots {
+ return {
+ helmet: armor instanceof ArmorType ? armor : (armor?.helmet ?? null),
+ chestplate: armor instanceof ArmorType ? armor : (armor?.chestplate ?? null),
+ leggings: armor instanceof ArmorType ? armor : (armor?.leggings ?? null),
+ boots: armor instanceof ArmorType ? armor : (armor?.boots ?? null),
+ };
+ }
+
+ async fallbackArmor(
+ armor: ArmorType | Partial | null
+ ): Promise {
+ this._fallbackArmor = this.toArmorSlots(armor);
+ await this.loadArmor(this.armor);
+ }
+
+ async loadArmor(
+ armor?: ArmorType | Partial | null
+ ): Promise {
+ this.resetArmor();
+ armor = this.toArmorSlots(armor ?? null);
+ let slots: ArmorSlots = {
+ helmet: armor.helmet ?? this._fallbackArmor?.helmet ?? null,
+ chestplate: armor.chestplate ?? this._fallbackArmor?.chestplate ?? null,
+ leggings: armor.leggings ?? this._fallbackArmor?.leggings ?? null,
+ boots: armor.boots ?? this._fallbackArmor?.boots ?? null,
+ };
+ this.armor = slots;
+ for (let [slot, slotArmorType] of Object.entries(slots)) {
+ if (!slotArmorType) continue;
+ await slotArmorType.promise;
+ this.addTextureToSlot(slot, slotArmorType.texture);
+ }
+ }
+
+ private addTextureToSlot(
+ slot: string,
+ texture: Texture
+ ): void {
+ switch (slot) {
+ case "helmet":
+ this.playerObject.armor.helmetMat.map = texture;
+ this.playerObject.armor.helmet.visible = true;
+ break;
+ case "chestplate":
+ this.playerObject.armor.chestplateMat.map = texture;
+ this.playerObject.armor.chestplate.visible = true;
+ this.playerObject.armor.chestplateRight.visible = true;
+ this.playerObject.armor.chestplateLeft.visible = true;
+ break;
+ case "leggings":
+ this.playerObject.armor.leggingsMat.map = texture;
+ this.playerObject.armor.leggingsTop.visible = true;
+ this.playerObject.armor.leggingsRight.visible = true;
+ this.playerObject.armor.leggingsLeft.visible = true;
+ break;
+ case "boots":
+ this.playerObject.armor.bootsMat.map = texture;
+ this.playerObject.armor.bootsRight.visible = true;
+ this.playerObject.armor.bootsLeft.visible = true;
+ break;
+ }
+ }
+
+ resetArmor(): void {
+ this.playerObject.armor.helmetMat.dispose();
+ this.playerObject.armor.chestplateMat.dispose();
+ this.playerObject.armor.leggingsMat.dispose();
+ this.playerObject.armor.bootsMat.dispose();
+ this.playerObject.armor.helmet.visible = false;
+ this.playerObject.armor.chestplate.visible = false;
+ this.playerObject.armor.chestplateRight.visible = false;
+ this.playerObject.armor.chestplateLeft.visible = false;
+ this.playerObject.armor.leggingsTop.visible = false;
+ this.playerObject.armor.leggingsRight.visible = false;
+ this.playerObject.armor.leggingsLeft.visible = false;
+ this.playerObject.armor.bootsRight.visible = false;
+ this.playerObject.armor.bootsLeft.visible = false;
+ }
+
loadPanorama(source: S): S extends TextureSource ? void : Promise {
return this.loadBackground(source, EquirectangularReflectionMapping);
}