commit
6c0bc016ed
|
@ -12,6 +12,7 @@ Three.js powered Minecraft skin viewer.
|
||||||
* 1.8 Skins
|
* 1.8 Skins
|
||||||
* HD Skins
|
* HD Skins
|
||||||
* Capes
|
* Capes
|
||||||
|
* Elytras
|
||||||
* Slim Arms
|
* Slim Arms
|
||||||
* Automatic model detection (Slim / Default)
|
* Automatic model detection (Slim / Default)
|
||||||
|
|
||||||
|
@ -37,7 +38,10 @@ Three.js powered Minecraft skin viewer.
|
||||||
// Load a cape
|
// Load a cape
|
||||||
skinViewer.loadCape("img/cape.png");
|
skinViewer.loadCape("img/cape.png");
|
||||||
|
|
||||||
// Unload(hide) the cape
|
// Load a elytra (from a cape texture)
|
||||||
|
skinViewer.loadCape("img/cape.png", { backEquipment: "elytra" });
|
||||||
|
|
||||||
|
// Unload(hide) the cape / elytra
|
||||||
skinViewer.loadCape(null);
|
skinViewer.loadCape(null);
|
||||||
|
|
||||||
// Control objects with your mouse!
|
// Control objects with your mouse!
|
||||||
|
|
|
@ -128,11 +128,12 @@
|
||||||
<label class="control">Speed: <input id="rotate_animation_speed" type="number" value="1" step="0.1"></label>
|
<label class="control">Speed: <input id="rotate_animation_speed" type="number" value="1" step="0.1"></label>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h2>Walk / Run</h2>
|
<h2>Walk / Run / Fly</h2>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<label><input type="radio" id="primary_animation_none" name="primary_animation" value="" checked> None</label>
|
<label><input type="radio" id="primary_animation_none" name="primary_animation" value="" checked> None</label>
|
||||||
<label><input type="radio" id="primary_animation_walk" name="primary_animation" value="walk"> Walk</label>
|
<label><input type="radio" id="primary_animation_walk" name="primary_animation" value="walk"> Walk</label>
|
||||||
<label><input type="radio" id="primary_animation_run" name="primary_animation" value="run"> Run</label>
|
<label><input type="radio" id="primary_animation_run" name="primary_animation" value="run"> Run</label>
|
||||||
|
<label><input type="radio" id="primary_animation_fly" name="primary_animation" value="fly"> Fly</label>
|
||||||
</div>
|
</div>
|
||||||
<label class="control">Speed: <input id="primary_animation_speed" type="number" value="1" step="0.1"></label>
|
<label class="control">Speed: <input id="primary_animation_speed" type="number" value="1" step="0.1"></label>
|
||||||
</div>
|
</div>
|
||||||
|
@ -182,6 +183,13 @@
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
<div>
|
||||||
|
<h2>Back Equipment</h2>
|
||||||
|
<div class="control">
|
||||||
|
<label><input type="radio" id="back_equipment_cape" name="back_equipment" value="cape" checked> Cape</label>
|
||||||
|
<label><input type="radio" id="back_equipment_elytra" name="back_equipment" value="elytra"> Elytra</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="control-section">
|
<div class="control-section">
|
||||||
|
@ -253,7 +261,8 @@
|
||||||
const skinLayers = ["innerLayer", "outerLayer"];
|
const skinLayers = ["innerLayer", "outerLayer"];
|
||||||
const availableAnimations = {
|
const availableAnimations = {
|
||||||
walk: skinview3d.WalkingAnimation,
|
walk: skinview3d.WalkingAnimation,
|
||||||
run: skinview3d.RunningAnimation
|
run: skinview3d.RunningAnimation,
|
||||||
|
fly: skinview3d.FlyingAnimation
|
||||||
};
|
};
|
||||||
|
|
||||||
let skinViewer;
|
let skinViewer;
|
||||||
|
@ -284,7 +293,8 @@
|
||||||
skinViewer.loadCape(null);
|
skinViewer.loadCape(null);
|
||||||
input.setCustomValidity("");
|
input.setCustomValidity("");
|
||||||
} else {
|
} else {
|
||||||
skinViewer.loadCape(url)
|
const selectedBackEquipment = document.querySelector('input[type="radio"][name="back_equipment"]:checked');
|
||||||
|
skinViewer.loadCape(url, { backEquipment: selectedBackEquipment.value })
|
||||||
.then(() => input.setCustomValidity(""))
|
.then(() => input.setCustomValidity(""))
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
input.setCustomValidity("Image can't be loaded.");
|
input.setCustomValidity("Image can't be loaded.");
|
||||||
|
@ -363,6 +373,18 @@
|
||||||
document.getElementById("skin_url").addEventListener("change", () => reloadSkin());
|
document.getElementById("skin_url").addEventListener("change", () => reloadSkin());
|
||||||
document.getElementById("skin_model").addEventListener("change", () => reloadSkin());
|
document.getElementById("skin_model").addEventListener("change", () => reloadSkin());
|
||||||
document.getElementById("cape_url").addEventListener("change", () => reloadCape());
|
document.getElementById("cape_url").addEventListener("change", () => reloadCape());
|
||||||
|
|
||||||
|
for (const el of document.querySelectorAll('input[type="radio"][name="back_equipment"]')) {
|
||||||
|
el.addEventListener("change", e => {
|
||||||
|
if (skinViewer.playerObject.backEquipment === null) {
|
||||||
|
// cape texture hasn't been loaded yet
|
||||||
|
// this option will be processed on texture loading
|
||||||
|
} else {
|
||||||
|
skinViewer.playerObject.backEquipment = e.target.value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
document.getElementById("reset_all").addEventListener("click", () => {
|
document.getElementById("reset_all").addEventListener("click", () => {
|
||||||
skinViewer.dispose();
|
skinViewer.dispose();
|
||||||
orbitControl.dispose();
|
orbitControl.dispose();
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
<div id="rendered_imgs"></div>
|
<div id="rendered_imgs"></div>
|
||||||
<script src="../bundles/skinview3d.bundle.js"></script>
|
<script src="../bundles/skinview3d.bundle.js"></script>
|
||||||
<script>
|
<script>
|
||||||
const textures = [
|
const configurations = [
|
||||||
{
|
{
|
||||||
skin: "img/1_8_texturemap_redux.png",
|
skin: "img/1_8_texturemap_redux.png",
|
||||||
cape: null
|
cape: null
|
||||||
|
@ -23,11 +23,12 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
skin: "img/haka.png",
|
skin: "img/haka.png",
|
||||||
cape: null
|
cape: "img/mojang_cape.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
skin: "img/hatsune_miku.png",
|
skin: "img/hatsune_miku.png",
|
||||||
cape: "img/mojang_cape.png"
|
cape: "img/mojang_cape.png",
|
||||||
|
backEquipment: "elytra"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
skin: "img/ironman_hd.png",
|
skin: "img/ironman_hd.png",
|
||||||
|
@ -54,8 +55,11 @@
|
||||||
skinViewer.camera.position.y = 22.0;
|
skinViewer.camera.position.y = 22.0;
|
||||||
skinViewer.camera.position.z = 42.0;
|
skinViewer.camera.position.z = 42.0;
|
||||||
|
|
||||||
for (const { skin, cape } of textures) {
|
for (const config of configurations) {
|
||||||
await Promise.all([skinViewer.loadSkin(skin), skinViewer.loadCape(cape)]);
|
await Promise.all([
|
||||||
|
skinViewer.loadSkin(config.skin),
|
||||||
|
skinViewer.loadCape(config.cape, { backEquipment: config.backEquipment })
|
||||||
|
]);
|
||||||
skinViewer.render();
|
skinViewer.render();
|
||||||
const image = skinViewer.canvas.toDataURL();
|
const image = skinViewer.canvas.toDataURL();
|
||||||
|
|
||||||
|
|
|
@ -195,3 +195,30 @@ export const RunningAnimation: Animation = (player, time) => {
|
||||||
export const RotatingAnimation: Animation = (player, time) => {
|
export const RotatingAnimation: Animation = (player, time) => {
|
||||||
player.rotation.y = time;
|
player.rotation.y = time;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function clamp(num: number, min: number, max: number): number {
|
||||||
|
return num <= min ? min : num >= max ? max : num;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FlyingAnimation: Animation = (player, time) => {
|
||||||
|
// body rotation finishes in 0.5s
|
||||||
|
// elytra expansion finishes in 3.3s
|
||||||
|
|
||||||
|
if (time < 0) time = 0;
|
||||||
|
time *= 20;
|
||||||
|
const startProgress = clamp((time * time) / 100, 0, 1);
|
||||||
|
|
||||||
|
player.rotation.x = startProgress * Math.PI / 2;
|
||||||
|
player.skin.head.rotation.x = startProgress > .5 ? Math.PI / 4 - player.rotation.x : 0;
|
||||||
|
|
||||||
|
const basicArmRotationZ = Math.PI * .25 * startProgress;
|
||||||
|
player.skin.leftArm.rotation.z = basicArmRotationZ;
|
||||||
|
player.skin.rightArm.rotation.z = -basicArmRotationZ;
|
||||||
|
|
||||||
|
const elytraRotationX = .34906584;
|
||||||
|
const elytraRotationZ = Math.PI / 2;
|
||||||
|
const interpolation = Math.pow(.9, time);
|
||||||
|
player.elytra.leftWing.rotation.x = elytraRotationX + interpolation * (.2617994 - elytraRotationX);
|
||||||
|
player.elytra.leftWing.rotation.z = elytraRotationZ + interpolation * (.2617994 - elytraRotationZ);
|
||||||
|
player.elytra.updateRightWing();
|
||||||
|
};
|
||||||
|
|
84
src/model.ts
84
src/model.ts
|
@ -123,7 +123,7 @@ export class SkinObject extends Group {
|
||||||
|
|
||||||
// Right Arm
|
// Right Arm
|
||||||
const rightArmBox = new BoxGeometry();
|
const rightArmBox = new BoxGeometry();
|
||||||
const rightArmMesh = new Mesh(rightArmBox, layer1Material);
|
const rightArmMesh = new Mesh(rightArmBox, layer1MaterialBiased);
|
||||||
this.modelListeners.push(() => {
|
this.modelListeners.push(() => {
|
||||||
rightArmMesh.scale.x = this.slim ? 3 : 4;
|
rightArmMesh.scale.x = this.slim ? 3 : 4;
|
||||||
rightArmMesh.scale.y = 12;
|
rightArmMesh.scale.y = 12;
|
||||||
|
@ -160,7 +160,7 @@ export class SkinObject extends Group {
|
||||||
|
|
||||||
// Left Arm
|
// Left Arm
|
||||||
const leftArmBox = new BoxGeometry();
|
const leftArmBox = new BoxGeometry();
|
||||||
const leftArmMesh = new Mesh(leftArmBox, layer1Material);
|
const leftArmMesh = new Mesh(leftArmBox, layer1MaterialBiased);
|
||||||
this.modelListeners.push(() => {
|
this.modelListeners.push(() => {
|
||||||
leftArmMesh.scale.x = this.slim ? 3 : 4;
|
leftArmMesh.scale.x = this.slim ? 3 : 4;
|
||||||
leftArmMesh.scale.y = 12;
|
leftArmMesh.scale.y = 12;
|
||||||
|
@ -287,10 +287,69 @@ export class CapeObject extends Group {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class ElytraObject extends Group {
|
||||||
|
|
||||||
|
readonly leftWing: Group;
|
||||||
|
readonly rightWing: Group;
|
||||||
|
|
||||||
|
constructor(texture: Texture) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
const elytraMaterial = new MeshBasicMaterial({
|
||||||
|
map: texture,
|
||||||
|
side: DoubleSide,
|
||||||
|
transparent: true,
|
||||||
|
alphaTest: 1e-5
|
||||||
|
});
|
||||||
|
|
||||||
|
const leftWingBox = new BoxGeometry(12, 22, 4);
|
||||||
|
setCapeUVs(leftWingBox, 22, 0, 10, 20, 2);
|
||||||
|
const leftWingMesh = new Mesh(leftWingBox, elytraMaterial);
|
||||||
|
leftWingMesh.position.x = -5;
|
||||||
|
leftWingMesh.position.y = -10;
|
||||||
|
leftWingMesh.position.z = -1;
|
||||||
|
this.leftWing = new Group();
|
||||||
|
this.leftWing.add(leftWingMesh);
|
||||||
|
this.add(this.leftWing);
|
||||||
|
|
||||||
|
const rightWingBox = new BoxGeometry(12, 22, 4);
|
||||||
|
setCapeUVs(rightWingBox, 22, 0, 10, 20, 2);
|
||||||
|
const rightWingMesh = new Mesh(rightWingBox, elytraMaterial);
|
||||||
|
rightWingMesh.scale.x = -1;
|
||||||
|
rightWingMesh.position.x = 5;
|
||||||
|
rightWingMesh.position.y = -10;
|
||||||
|
rightWingMesh.position.z = -1;
|
||||||
|
this.rightWing = new Group();
|
||||||
|
this.rightWing.add(rightWingMesh);
|
||||||
|
this.add(this.rightWing);
|
||||||
|
|
||||||
|
this.leftWing.position.x = 5;
|
||||||
|
this.leftWing.rotation.x = .2617994;
|
||||||
|
this.leftWing.rotation.y = .01; // to avoid z-fighting
|
||||||
|
this.leftWing.rotation.z = .2617994;
|
||||||
|
this.updateRightWing();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mirrors the position & rotation of left wing,
|
||||||
|
* and apply them to the right wing.
|
||||||
|
*/
|
||||||
|
updateRightWing(): void {
|
||||||
|
this.rightWing.position.x = -this.leftWing.position.x;
|
||||||
|
this.rightWing.position.y = this.leftWing.position.y;
|
||||||
|
this.rightWing.rotation.x = this.leftWing.rotation.x;
|
||||||
|
this.rightWing.rotation.y = -this.leftWing.rotation.y;
|
||||||
|
this.rightWing.rotation.z = -this.leftWing.rotation.z;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BackEquipment = "cape" | "elytra";
|
||||||
|
|
||||||
export class PlayerObject extends Group {
|
export class PlayerObject extends Group {
|
||||||
|
|
||||||
readonly skin: SkinObject;
|
readonly skin: SkinObject;
|
||||||
readonly cape: CapeObject;
|
readonly cape: CapeObject;
|
||||||
|
readonly elytra: ElytraObject;
|
||||||
|
|
||||||
constructor(skinTexture: Texture, capeTexture: Texture) {
|
constructor(skinTexture: Texture, capeTexture: Texture) {
|
||||||
super();
|
super();
|
||||||
|
@ -305,5 +364,26 @@ export class PlayerObject extends Group {
|
||||||
this.cape.rotation.x = 10.8 * Math.PI / 180;
|
this.cape.rotation.x = 10.8 * Math.PI / 180;
|
||||||
this.cape.rotation.y = Math.PI;
|
this.cape.rotation.y = Math.PI;
|
||||||
this.add(this.cape);
|
this.add(this.cape);
|
||||||
|
|
||||||
|
this.elytra = new ElytraObject(capeTexture);
|
||||||
|
this.elytra.name = "elytra";
|
||||||
|
this.elytra.position.z = -2;
|
||||||
|
this.elytra.visible = false;
|
||||||
|
this.add(this.elytra);
|
||||||
|
}
|
||||||
|
|
||||||
|
get backEquipment(): BackEquipment | null {
|
||||||
|
if (this.cape.visible) {
|
||||||
|
return "cape";
|
||||||
|
} else if (this.elytra.visible) {
|
||||||
|
return "elytra";
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set backEquipment(value: BackEquipment | null) {
|
||||||
|
this.cape.visible = value === "cape";
|
||||||
|
this.elytra.visible = value === "elytra";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { applyMixins, CapeContainer, ModelType, SkinContainer, RemoteImage, TextureSource } from "skinview-utils";
|
import { applyMixins, CapeContainer, ModelType, SkinContainer, RemoteImage, TextureSource } from "skinview-utils";
|
||||||
import { NearestFilter, PerspectiveCamera, Scene, Texture, Vector2, WebGLRenderer } from "three";
|
import { NearestFilter, PerspectiveCamera, Scene, Texture, Vector2, WebGLRenderer } from "three";
|
||||||
import { RootAnimation } from "./animation.js";
|
import { RootAnimation } from "./animation.js";
|
||||||
import { PlayerObject } from "./model.js";
|
import { BackEquipment, PlayerObject } from "./model.js";
|
||||||
|
|
||||||
export interface LoadOptions {
|
export interface LoadOptions {
|
||||||
/**
|
/**
|
||||||
|
@ -10,6 +10,14 @@ export interface LoadOptions {
|
||||||
makeVisible?: boolean;
|
makeVisible?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CapeLoadOptions extends LoadOptions {
|
||||||
|
/**
|
||||||
|
* The equipment (cape or elytra) to show, defaults to "cape".
|
||||||
|
* If makeVisible is set to false, this option will have no effect.
|
||||||
|
*/
|
||||||
|
backEquipment?: BackEquipment;
|
||||||
|
}
|
||||||
|
|
||||||
export interface SkinViewerOptions {
|
export interface SkinViewerOptions {
|
||||||
width?: number;
|
width?: number;
|
||||||
height?: number;
|
height?: number;
|
||||||
|
@ -40,13 +48,6 @@ export interface SkinViewerOptions {
|
||||||
renderPaused?: boolean;
|
renderPaused?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function toMakeVisible(options?: LoadOptions): boolean {
|
|
||||||
if (options && options.makeVisible === false) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
class SkinViewer {
|
class SkinViewer {
|
||||||
readonly canvas: HTMLCanvasElement;
|
readonly canvas: HTMLCanvasElement;
|
||||||
readonly scene: Scene;
|
readonly scene: Scene;
|
||||||
|
@ -118,18 +119,18 @@ class SkinViewer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected skinLoaded(model: ModelType, options?: LoadOptions): void {
|
protected skinLoaded(model: ModelType, options: LoadOptions = {}): void {
|
||||||
this.skinTexture.needsUpdate = true;
|
this.skinTexture.needsUpdate = true;
|
||||||
this.playerObject.skin.modelType = model;
|
this.playerObject.skin.modelType = model;
|
||||||
if (toMakeVisible(options)) {
|
if (options.makeVisible !== false) {
|
||||||
this.playerObject.skin.visible = true;
|
this.playerObject.skin.visible = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected capeLoaded(options?: LoadOptions): void {
|
protected capeLoaded(options: CapeLoadOptions = {}): void {
|
||||||
this.capeTexture.needsUpdate = true;
|
this.capeTexture.needsUpdate = true;
|
||||||
if (toMakeVisible(options)) {
|
if (options.makeVisible !== false) {
|
||||||
this.playerObject.cape.visible = true;
|
this.playerObject.backEquipment = options.backEquipment === undefined ? "cape" : options.backEquipment;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -138,7 +139,7 @@ class SkinViewer {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected resetCape(): void {
|
protected resetCape(): void {
|
||||||
this.playerObject.cape.visible = false;
|
this.playerObject.backEquipment = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private draw(): void {
|
private draw(): void {
|
||||||
|
@ -208,6 +209,6 @@ class SkinViewer {
|
||||||
this.setSize(this.width, newHeight);
|
this.setSize(this.width, newHeight);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
interface SkinViewer extends SkinContainer<LoadOptions>, CapeContainer<LoadOptions> { }
|
interface SkinViewer extends SkinContainer<LoadOptions>, CapeContainer<CapeLoadOptions> { }
|
||||||
applyMixins(SkinViewer, [SkinContainer, CapeContainer]);
|
applyMixins(SkinViewer, [SkinContainer, CapeContainer]);
|
||||||
export { SkinViewer };
|
export { SkinViewer };
|
||||||
|
|
Loading…
Reference in New Issue