Skip to content
91 changes: 90 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,20 @@ Three.js powered Minecraft skin viewer.
// Load an elytra (from a cape texture)
skinViewer.loadCape("img/cape.png", { backEquipment: "elytra" });

// Unload(hide) the cape / elytra
// Unload (hide) the cape / elytra
skinViewer.loadCape(null);

// Load armors
skinViewer.loadArmors(
"img/turtle_layer_1.png", // helmet (main)
"img/diamond_layer_1.png", // chestplate (main)
"img/gold_layer_2.png", // leggings (legs)
"img/iron_layer_1.png" // boots (main)
);

// Unload (hide) the armors
skinViewer.loadArmors(null);

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

Expand Down Expand Up @@ -93,6 +104,84 @@ skinViewer.globalLight.intensity = 3;
Setting `globalLight.intensity` to `3.0` and `cameraLight.intensity` to `0.0`
will completely disable shadows.


## Armors

skinview3d supports loading armor textures for the player. Armor textures can be specified as an object with the following optional properties:

- `helmet`, `chestplate`, `leggings`, `boots`: textures for the corresponding pieces.
- `main`: a texture that will be used for helmet, chestplate, and boots if their specific textures are not provided.
- `legs`: a texture that will be used for leggings if `leggings` is not provided.

Each texture can be a `RemoteImage` (URL string), a `TextureSource` (HTML image element or canvas), or `null` to hide that piece.

### Loading Armors

You can load armors using the `loadArmors` method. It accepts an object conforming to the structure above, or `null` to hide all armor.

Examples:

```js
// Hide all armor
skinViewer.loadArmors(null);

// Equip only helmet, chestplate, and boots using the same "main" texture
skinViewer.loadArmors({ main: "img/diamond_layer_1.png" });

// Equip only leggings using a "legs" texture
skinViewer.loadArmors({ legs: "img/diamond_layer_2.png" });

// Equip both main and legs textures
skinViewer.loadArmors({
main: "img/diamond_layer_1.png",
legs: "img/diamond_layer_2.png"
});

// Equip all four pieces individually (helmet, chestplate, leggings, boots)
skinViewer.loadArmors({
helmet: "img/turtle_layer_1.png",
chestplate: "img/diamond_layer_1.png",
leggings: "img/gold_layer_2.png",
boots: "img/iron_layer_1.png"
});

// Mix specific pieces with fallback textures
skinViewer.loadArmors({
helmet: "img/turtle_layer_1.png",
main: "img/diamond_layer_1.png", // used for chestplate and boots
leggings: "img/gold_layer_2.png"
});
```

### Using Armors in the Constructor

You can specify armors directly in the `SkinViewer` options via the `armors` property. It accepts the same object format as `loadArmors`.

```js
new skinview3d.SkinViewer({
skin: "img/skin.png",
armors: {
helmet: "img/turtle_layer_1.png",
chestplate: "img/diamond_layer_1.png",
leggings: "img/gold_layer_2.png",
boots: "img/iron_layer_1.png"
}
});
```

### Loading Armors Together with a Skin

The `loadSkin` method also accepts an `armors` option, which behaves identically to the constructor option.

```js
skinViewer.loadSkin("img/skin.png", {
armors: {
main: "img/diamond_layer_1.png",
legs: "img/diamond_layer_2.png"
}
});
```

## Ears
skinview3d supports two types of ear texture:
* `standalone`: 14x7 image that contains the ear ([example](https://github.com/bs-community/skinview3d/blob/master/examples/public/img/ears.png))
Expand Down
74 changes: 74 additions & 0 deletions examples/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,80 @@ <h1>Cape</h1>
</div>
</div>

<div class="control-section">
<h1>Armors</h1>
<div class="control">
<label
>Helmet: <input id="helmet_url" type="text" placeholder="none" list="default_helmets" size="20"
/></label>
<datalist id="default_helmets">
<option value=""></option>
<option value="img/copper_layer_1.png"></option>
<option value="img/chainmail_layer_1.png"></option>
<option value="img/iron_layer_1.png"></option>
<option value="img/gold_layer_1.png"></option>
<option value="img/diamond_layer_1.png"></option>
<option value="img/netherite_layer_1.png"></option>
<option value="img/turtle_layer_1.png"></option>
</datalist>
<input id="helmet_url_upload" type="file" class="hidden" accept="image/*" />
<button id="helmet_url_unset" type="button" class="control hidden">Unset</button>
<button type="button" class="control" onclick="document.getElementById('helmet_url_upload').click();">
Browse...
</button>
<br />
<label
>Chestplate: <input id="chestplate_url" type="text" placeholder="none" list="default_chestplates" size="20"
/></label>
<datalist id="default_chestplates">
<option value=""></option>
<option value="img/copper_layer_1.png"></option>
<option value="img/chainmail_layer_1.png"></option>
<option value="img/iron_layer_1.png"></option>
<option value="img/gold_layer_1.png"></option>
<option value="img/diamond_layer_1.png"></option>
<option value="img/netherite_layer_1.png"></option>
</datalist>
<input id="chestplate_url_upload" type="file" class="hidden" accept="image/*" />
<button id="chestplate_url_unset" type="button" class="control hidden">Unset</button>
<button type="button" class="control" onclick="document.getElementById('chestplate_url_upload').click();">
Browse...</button
><br />
<label
>Leggings: <input id="leggings_url" type="text" placeholder="none" list="default_leggings" size="20"
/></label>
<datalist id="default_leggings">
<option value=""></option>
<option value="img/copper_layer_2.png"></option>
<option value="img/chainmail_layer_2.png"></option>
<option value="img/iron_layer_2.png"></option>
<option value="img/gold_layer_2.png"></option>
<option value="img/diamond_layer_2.png"></option>
<option value="img/netherite_layer_2.png"></option>
</datalist>
<input id="leggings_url_upload" type="file" class="hidden" accept="image/*" />
<button id="leggings_url_unset" type="button" class="control hidden">Unset</button>
<button type="button" class="control" onclick="document.getElementById('leggings_url_upload').click();">
Browse...</button
><br />
<label>Boots: <input id="boots_url" type="text" placeholder="none" list="default_boots" size="20" /></label>
<datalist id="default_boots">
<option value=""></option>
<option value="img/copper_layer_1.png"></option>
<option value="img/chainmail_layer_1.png"></option>
<option value="img/iron_layer_1.png"></option>
<option value="img/gold_layer_1.png"></option>
<option value="img/diamond_layer_1.png"></option>
<option value="img/netherite_layer_1.png"></option>
</datalist>
<input id="boots_url_upload" type="file" class="hidden" accept="image/*" />
<button id="boots_url_unset" type="button" class="control hidden">Unset</button>
<button type="button" class="control" onclick="document.getElementById('boots_url_upload').click();">
Browse...
</button>
</div>
</div>

<div class="control-section">
<h1>Ears</h1>
<div>
Expand Down
51 changes: 49 additions & 2 deletions examples/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ const availableAnimations = {
};

let skinViewer: skinview3d.SkinViewer;

function obtainTextureUrl(id: string): string {
const urlInput = document.getElementById(id) as HTMLInputElement;
const fileInput = document.getElementById(`${id}_upload`) as HTMLInputElement;
Expand All @@ -42,6 +41,7 @@ function obtainTextureUrl(id: string): string {
}

function reloadSkin(): void {
window.skinViewer = skinViewer; //Set window object to debug in console.
const input = document.getElementById("skin_url") as HTMLInputElement;
const url = obtainTextureUrl("skin_url");
if (url === "") {
Expand All @@ -50,7 +50,6 @@ function reloadSkin(): void {
} else {
const skinModel = document.getElementById("skin_model") as HTMLSelectElement;
const earsSource = document.getElementById("ears_source") as HTMLSelectElement;

skinViewer
.loadSkin(url, {
model: skinModel?.value as ModelType,
Expand Down Expand Up @@ -84,6 +83,41 @@ function reloadCape(): void {
}
}

function reloadArmors(): void {
const input1 = document.getElementById("helmet_url") as HTMLInputElement;
const url1 = obtainTextureUrl("helmet_url");
const input2 = document.getElementById("chestplate_url") as HTMLInputElement;
const url2 = obtainTextureUrl("chestplate_url");
const input3 = document.getElementById("leggings_url") as HTMLInputElement;
const url3 = obtainTextureUrl("leggings_url");
const input4 = document.getElementById("boots_url") as HTMLInputElement;
const url4 = obtainTextureUrl("boots_url");
const textures = {
helmet: url1,
chestplate: url2,
leggings: url3,
boots: url4,
};
const inputs = [input1.input2, input3, input4];
Object.keys(textures).forEach((key, index) => {
if (textures[key] === "") {
inputs[index]?.setCustomValidity("");
textures[key] = null;
}
});
skinViewer
.loadArmors(textures)
?.then(() => {
inputs.forEach(input => {
input?.setCustomValidity("");
});
})
?.catch(e => {
input4?.setCustomValidity("One of the 4 images can't be loaded.");
console.error(e);
});
}

function reloadEars(skipSkinReload = false): void {
const earsSource = document.getElementById("ears_source") as HTMLSelectElement;
const sourceType = earsSource?.value;
Expand Down Expand Up @@ -398,19 +432,31 @@ function initializeControls(): void {

initializeUploadButton("skin_url", reloadSkin);
initializeUploadButton("cape_url", reloadCape);
initializeUploadButton("helmet_url", reloadArmors);
initializeUploadButton("chestplate_url", reloadArmors);
initializeUploadButton("leggings_url", reloadArmors);
initializeUploadButton("boots_url", reloadArmors);
initializeUploadButton("ears_url", reloadEars);
initializeUploadButton("panorama_url", reloadPanorama);

const skinUrl = document.getElementById("skin_url") as HTMLInputElement;
const skinModel = document.getElementById("skin_model") as HTMLSelectElement;
const capeUrl = document.getElementById("cape_url") as HTMLInputElement;
const helmetUrl = document.getElementById("helmet_url") as HTMLInputElement;
const chestplateUrl = document.getElementById("chestplate_url") as HTMLInputElement;
const leggingsUrl = document.getElementById("leggings_url") as HTMLInputElement;
const bootsUrl = document.getElementById("boots_url") as HTMLInputElement;
const earsSource = document.getElementById("ears_source") as HTMLSelectElement;
const earsUrl = document.getElementById("ears_url") as HTMLInputElement;
const panoramaUrl = document.getElementById("panorama_url") as HTMLInputElement;

skinUrl?.addEventListener("change", reloadSkin);
skinModel?.addEventListener("change", reloadSkin);
capeUrl?.addEventListener("change", reloadCape);
helmetUrl?.addEventListener("change", reloadArmors);
chestplateUrl?.addEventListener("change", reloadArmors);
leggingsUrl?.addEventListener("change", reloadArmors);
bootsUrl?.addEventListener("change", reloadArmors);
earsSource?.addEventListener("change", () => reloadEars());
earsUrl?.addEventListener("change", () => reloadEars());
panoramaUrl?.addEventListener("change", reloadPanorama);
Expand Down Expand Up @@ -511,6 +557,7 @@ function initializeViewer(): void {

reloadSkin();
reloadCape();
reloadArmors();
reloadEars(true);
reloadPanorama();
reloadNameTag();
Expand Down
Binary file added examples/public/img/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/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/copper_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/copper_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/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/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/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/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/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/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/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/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/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