Skip to content
Open
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
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Three.js powered Minecraft skin viewer.
* Capes
* Ears
* Elytras
* Armor
* Slim Arms
* Automatic model detection (Slim / Default)
* FXAA (fast approximate anti-aliasing)
Expand Down Expand Up @@ -49,6 +50,31 @@ Three.js powered Minecraft skin viewer.
// Unload(hide) the cape / elytra
skinViewer.loadCape(null);

// Initialize armor types using their layer textures (as found in resource packs)
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");
// Load the armor types on armor slots (in this case, a turtle helmet with diamond armor)
skinViewer.loadArmor({
helmet: turtleHelmet,
chestplate: diamondArmor,
leggings: diamondArmor,
boots: diamondArmor
});

// Short-hand for applying an armor type on all slots
skinViewer.loadArmor(diamondArmor);

// Unload(hide) the armor
skinViewer.loadArmor();

// Specify fallback armor that will always be applied if there is no other armor set
skinViewer.fallbackArmor(diamondArmor);
// Or you can specify slots
skinViewer.fallbackArmor({
helmet: diamondArmor,
chestplate: diamondArmor
});

// Set the background color
skinViewer.background = 0x5a76f3;

Expand Down
60 changes: 60 additions & 0 deletions examples/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,66 @@ <h1>Ears</h1>
</div>
</div>

<div class="control-section">
<h1>Armor</h1>
<table id="armor-table">
<datalist id="armor-slots"></datalist>
<tbody>
<tr>
<th>helmet</th>
<td>
<input id="armor-helmet" type="text" value="" placeholder="none" list="armor-slots" size="20">
</td>
</tr>
<tr>
<th>chestplate</th>
<td>
<input id="armor-chestplate" type="text" value="" placeholder="none" list="armor-slots" size="20">
</td>
</tr>
<tr>
<th>leggings</th>
<td>
<input id="armor-leggings" type="text" value="" placeholder="none" list="armor-slots" size="20">
</td>
</tr>
<tr>
<th>boots</th>
<td>
<input id="armor-boots" type="text" value="" placeholder="none" list="armor-slots" size="20">
</td>
</tr>
<tr>
<th>fallback</th>
<td>
<input id="armor-fallback" type="text" value="" placeholder="none" list="armor-slots" size="20">
</td>
</tr>
</tbody>
</table>

<div id="armor-types">
<datalist id="default_armor_layer1">
<option value="img/armor/netherite_layer_1.png">
<option value="img/armor/diamond_layer_1.png">
<option value="img/armor/gold_layer_1.png">
<option value="img/armor/iron_layer_1.png">
<option value="img/armor/chainmail_layer_1.png">
<option value="img/armor/leather_layer_1.png">
<option value="img/armor/turtle_layer_1.png">
</datalist>
<datalist id="default_armor_layer2">
<option value="img/armor/netherite_layer_2.png">
<option value="img/armor/diamond_layer_2.png">
<option value="img/armor/gold_layer_2.png">
<option value="img/armor/iron_layer_2.png">
<option value="img/armor/chainmail_layer_2.png">
<option value="img/armor/leather_layer_2.png">
</datalist>
</div>
<button type="button" class="control" onclick="newArmorBuilder()">New/Custom Armor Type</button>
</div>

<div class="control-section">
<h1>Background</h1>
<div class="control">
Expand Down
158 changes: 133 additions & 25 deletions examples/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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 {
Expand Down Expand Up @@ -128,6 +138,92 @@ function reloadEars(skipSkinReload = false): void {
}
}

async function reloadArmor(): Promise<void> {
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<HTMLElement>(".armor-type")) {
const index = armorTypeElement.dataset.index;
const nameInput = armorTypeElement.querySelector<HTMLInputElement>(".armor-type-name");
const layer1Input = armorTypeElement.querySelector<HTMLInputElement>(".armor-type-layer1");
const layer2Input = armorTypeElement.querySelector<HTMLInputElement>(".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 = `
<summary>${name}</summary>
<label class="control">Name: <input class="armor-type-name" type="text" value="${name}" placeholder="none" size="20"></label>

<label class="control">Layer 1: <input id="armor${index}-layer1" class="armor-type-layer1" type="text" value="${layer1}" placeholder="none" list="default_armor_layer1" size="30"></label>
<input id="armor${index}-layer1_upload" type="file" class="hidden" accept="image/*">
<button id="armor${index}-layer1_unset" type="button" class="control hidden">Unset</button>
<button type="button" class="control"
onclick="document.getElementById('armor${index}-layer1_upload').click();">Browse...</button>

<label class="control">Layer 2: <input id="armor${index}-layer2" class="armor-type-layer2" type="text" value="${layer2}" placeholder="none" list="default_armor_layer2" size="30"></label>
<input id="armor${index}-layer2_upload" type="file" class="hidden" accept="image/*">
<button id="armor${index}-layer2_unset" type="button" class="control hidden">Unset</button>
<button type="button" class="control"
onclick="document.getElementById('armor${index}-layer2_upload').click();">Browse...</button>
`;
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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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<HTMLInputElement>('input[type="radio"][name="back_equipment"]');
Expand Down Expand Up @@ -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();
}
Expand Down
25 changes: 23 additions & 2 deletions examples/offscreen-renderer.ts
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -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",
Expand All @@ -26,7 +43,10 @@ const configurations = [
{
skin: "img/sethbling.png",
cape: null,
},
armor: {
helmet: turtleHelmet
},
}
];

(async function () {
Expand All @@ -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;
Expand All @@ -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();
Expand Down
Binary file added examples/public/img/armor/chainmail_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 examples/public/img/armor/chainmail_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.
Binary file added examples/public/img/armor/crusader_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 examples/public/img/armor/crusader_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.
Binary file added examples/public/img/armor/diamond_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 examples/public/img/armor/diamond_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.
Binary file added examples/public/img/armor/gold_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 examples/public/img/armor/gold_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.
Binary file added examples/public/img/armor/iron_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 examples/public/img/armor/iron_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.
Binary file added examples/public/img/armor/leather_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.
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 examples/public/img/armor/leather_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.
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 examples/public/img/armor/netherite_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 examples/public/img/armor/netherite_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.
Binary file added examples/public/img/armor/turtle_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.
Loading