Compare commits
No commits in common. "master" and "v2.0.1" have entirely different histories.
|
@ -6,9 +6,6 @@ extends:
|
||||||
- 'eslint:recommended'
|
- 'eslint:recommended'
|
||||||
- 'plugin:@typescript-eslint/eslint-recommended'
|
- 'plugin:@typescript-eslint/eslint-recommended'
|
||||||
- 'plugin:@typescript-eslint/recommended'
|
- 'plugin:@typescript-eslint/recommended'
|
||||||
ignorePatterns:
|
|
||||||
- bundles/**/*
|
|
||||||
- libs/**/*
|
|
||||||
rules:
|
rules:
|
||||||
'@typescript-eslint/no-inferrable-types': off
|
'@typescript-eslint/no-inferrable-types': off
|
||||||
'@typescript-eslint/interface-name-prefix': off
|
'@typescript-eslint/interface-name-prefix': off
|
||||||
|
|
2
LICENSE
2
LICENSE
|
@ -1,7 +1,7 @@
|
||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2014-2018 Kent Rasmussen
|
Copyright (c) 2014-2018 Kent Rasmussen
|
||||||
Copyright (c) 2017-2022 Haowei Wen, Sean Boult and contributors
|
Copyright (c) 2017-2021 Haowei Wen, Sean Boult and contributors
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|
67
README.md
67
README.md
|
@ -12,7 +12,6 @@ Three.js powered Minecraft skin viewer.
|
||||||
* 1.8 Skins
|
* 1.8 Skins
|
||||||
* HD Skins
|
* HD Skins
|
||||||
* Capes
|
* Capes
|
||||||
* Ears
|
|
||||||
* Elytras
|
* Elytras
|
||||||
* Slim Arms
|
* Slim Arms
|
||||||
* Automatic model detection (Slim / Default)
|
* Automatic model detection (Slim / Default)
|
||||||
|
@ -39,27 +38,12 @@ Three.js powered Minecraft skin viewer.
|
||||||
// Load a cape
|
// Load a cape
|
||||||
skinViewer.loadCape("img/cape.png");
|
skinViewer.loadCape("img/cape.png");
|
||||||
|
|
||||||
// Load an elytra (from a cape texture)
|
// Load a elytra (from a cape texture)
|
||||||
skinViewer.loadCape("img/cape.png", { backEquipment: "elytra" });
|
skinViewer.loadCape("img/cape.png", { backEquipment: "elytra" });
|
||||||
|
|
||||||
// Unload(hide) the cape / elytra
|
// Unload(hide) the cape / elytra
|
||||||
skinViewer.loadCape(null);
|
skinViewer.loadCape(null);
|
||||||
|
|
||||||
// Set the background color
|
|
||||||
skinViewer.background = 0x5a76f3;
|
|
||||||
|
|
||||||
// Set the background to a normal image
|
|
||||||
skinViewer.loadBackground("img/background.png");
|
|
||||||
|
|
||||||
// Set the background to a panoramic image
|
|
||||||
skinViewer.loadPanorama("img/panorama1.png");
|
|
||||||
|
|
||||||
// Change camera FOV
|
|
||||||
skinViewer.fov = 70;
|
|
||||||
|
|
||||||
// Zoom out
|
|
||||||
skinViewer.zoom = 0.5;
|
|
||||||
|
|
||||||
// Control objects with your mouse!
|
// Control objects with your mouse!
|
||||||
let control = skinview3d.createOrbitControls(skinViewer);
|
let control = skinview3d.createOrbitControls(skinViewer);
|
||||||
control.enableRotate = true;
|
control.enableRotate = true;
|
||||||
|
@ -90,48 +74,17 @@ Three.js powered Minecraft skin viewer.
|
||||||
skinview3d supports FXAA (fast approximate anti-aliasing).
|
skinview3d supports FXAA (fast approximate anti-aliasing).
|
||||||
To enable it, you need to replace `SkinViewer` with `FXAASkinViewer`.
|
To enable it, you need to replace `SkinViewer` with `FXAASkinViewer`.
|
||||||
|
|
||||||
Note that FXAA is incompatible with transparent backgrounds.
|
It's recommended to use an opaque background when FXAA is enabled,
|
||||||
So when FXAA is enabled, the default background color will be white instead of transparent.
|
as transparent background may look buggy.
|
||||||
|
|
||||||
## Lighting
|
```javascript
|
||||||
By default, there are two lights on the scene. One is an ambient light, and the other is a point light from the camera.
|
let skinViewer = new skinview3d.FXAASkinViewer({
|
||||||
|
// we do not use transparent background, so disable alpha to improve performance
|
||||||
To change the light intensity:
|
alpha: false,
|
||||||
```js
|
...
|
||||||
skinViewer.cameraLight.intensity = 0.9;
|
|
||||||
skinViewer.globalLight.intensity = 0.1;
|
|
||||||
```
|
|
||||||
|
|
||||||
Setting `globalLight.intensity` to `1.0` and `cameraLight.intensity` to `0.0`
|
|
||||||
will completely disable shadows.
|
|
||||||
|
|
||||||
## 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/img/ears.png))
|
|
||||||
* `skin`: Skin texture that contains the ear (e.g. [deadmau5's skin](https://minecraft.fandom.com/wiki/Easter_eggs#Deadmau5.27s_ears))
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
```js
|
|
||||||
// You can specify ears in the constructor:
|
|
||||||
new skinview3d.SkinViewer({
|
|
||||||
skin: "img/deadmau5.png",
|
|
||||||
|
|
||||||
// Use ears drawn on the current skin (img/deadmau5.png)
|
|
||||||
ears: "current-skin",
|
|
||||||
|
|
||||||
// Or use ears from other textures
|
|
||||||
ears: {
|
|
||||||
textureType: "standalone", // "standalone" or "skin"
|
|
||||||
source: "img/ears.png"
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
// set the background color
|
||||||
// Show ears when loading skins:
|
skinViewer.renderer.setClearColor(0x5a76f3);
|
||||||
skinViewer.loadSkin("img/deadmau5.png", { ears: true });
|
|
||||||
|
|
||||||
// Use ears from other textures:
|
|
||||||
skinViewer.loadEars("img/ears.png", { textureType: "standalone" });
|
|
||||||
skinViewer.loadEars("img/deadmau5.png", { textureType: "skin" });
|
|
||||||
```
|
```
|
||||||
|
|
||||||
# Build
|
# Build
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 3.5 KiB |
Binary file not shown.
Before Width: | Height: | Size: 233 B |
Binary file not shown.
Before Width: | Height: | Size: 11 MiB |
|
@ -22,11 +22,17 @@
|
||||||
|
|
||||||
h1,
|
h1,
|
||||||
h2 {
|
h2 {
|
||||||
margin: 5px 0 0 0;
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="number"] {
|
||||||
|
max-width: 60px;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="text"] {
|
input[type="text"] {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
max-width: 250px;
|
||||||
|
width: calc(100% - 100px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.control {
|
.control {
|
||||||
|
@ -95,10 +101,6 @@
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
padding-left: 20px;
|
padding-left: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
@ -111,42 +113,29 @@
|
||||||
<button id="reset_all" type="button" class="control">Reset All</button>
|
<button id="reset_all" type="button" class="control">Reset All</button>
|
||||||
|
|
||||||
<div class="control-section">
|
<div class="control-section">
|
||||||
<h1>Viewport</h1>
|
<h1>Canvas Size</h1>
|
||||||
<div>
|
<label class="control">Width: <input id="canvas_width" type="number" value="300"></label>
|
||||||
<label class="control">Width: <input id="canvas_width" type="number" value="300" size="4"></label>
|
<label class="control">Height: <input id="canvas_height" type="number" value="300"></label>
|
||||||
<label class="control">Height: <input id="canvas_height" type="number" value="300" size="4"></label>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="control">FOV: <input id="fov" type="number" value="70" step="1" min="1" max="179" size="2"></label>
|
|
||||||
<label class="control">Zoom: <input id="zoom" type="number" value="0.90" step="0.01" min="0.01" max="2.00" size="4"></label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="control-section">
|
|
||||||
<h1>Light</h1>
|
|
||||||
<label class="control">Global: <input id="global_light" type="number" value="0.40" step="0.01" min="0.00" max="2.00" size="4"></label>
|
|
||||||
<label class="control">Camera: <input id="camera_light" type="number" value="0.60" step="0.01" min="0.00" max="2.00" size="4"></label>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="control-section">
|
<div class="control-section">
|
||||||
<h1>Animation</h1>
|
<h1>Animation</h1>
|
||||||
<label class="control">Global Speed: <input id="global_animation_speed" type="number" value="1" step="0.1" size="3"></label>
|
<label class="control">Global Speed: <input id="global_animation_speed" type="number" value="1" step="0.1"></label>
|
||||||
<button id="animation_pause_resume" type="button" class="control">Pause / Resume</button>
|
<button id="animation_pause_resume" type="button" class="control">Pause / Resume</button>
|
||||||
<div>
|
<div>
|
||||||
<h2>Rotate</h2>
|
<h2>Rotate</h2>
|
||||||
<label class="control"><input id="rotate_animation" type="checkbox"> Enable</label>
|
<label class="control"><input id="rotate_animation" type="checkbox"> Enable</label>
|
||||||
<label class="control">Speed: <input id="rotate_animation_speed" type="number" value="1" step="0.1" size="3"></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 / Fly</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_idle" name="primary_animation" value="idle"> Idle</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>
|
<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" size="3"></label>
|
<label class="control">Speed: <input id="primary_animation_speed" type="number" value="1" step="0.1"></label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -204,10 +193,10 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="control-section">
|
<div class="control-section">
|
||||||
<h1>Skin</h1>
|
<h1>Textures</h1>
|
||||||
<div>
|
<div>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<label>URL: <input id="skin_url" type="text" value="img/hatsune_miku.png" placeholder="none" list="default_skins" size="20"></label>
|
<label>Skin URL: <input id="skin_url" type="text" value="img/1_8_texturemap_redux.png" placeholder="none" list="default_skins"></label>
|
||||||
<datalist id="default_skins">
|
<datalist id="default_skins">
|
||||||
<option value="img/1_8_texturemap_redux.png">
|
<option value="img/1_8_texturemap_redux.png">
|
||||||
<option value="img/hacksore.png">
|
<option value="img/hacksore.png">
|
||||||
|
@ -215,15 +204,11 @@
|
||||||
<option value="img/hatsune_miku.png">
|
<option value="img/hatsune_miku.png">
|
||||||
<option value="img/ironman_hd.png">
|
<option value="img/ironman_hd.png">
|
||||||
<option value="img/sethbling.png">
|
<option value="img/sethbling.png">
|
||||||
<option value="img/deadmau5.png">
|
|
||||||
</datalist>
|
</datalist>
|
||||||
<input id="skin_url_upload" type="file" class="hidden" accept="image/*">
|
<input id="skin_url_upload" type="file" accept="image/*" style="display: none;">
|
||||||
<button id="skin_url_unset" type="button" class="control hidden">Unset</button>
|
|
||||||
<button type="button" class="control"
|
<button type="button" class="control"
|
||||||
onclick="document.getElementById('skin_url_upload').click();">Browse...</button>
|
onclick="document.getElementById('skin_url_upload').click();">Browse...</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="control">Model:
|
<label class="control">Model:
|
||||||
<select id="skin_model">
|
<select id="skin_model">
|
||||||
<option value="auto-detect" selected>Auto detect</option>
|
<option value="auto-detect" selected>Auto detect</option>
|
||||||
|
@ -232,64 +217,20 @@
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div>
|
||||||
|
|
||||||
<div class="control-section">
|
|
||||||
<h1>Cape</h1>
|
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<label>URL: <input id="cape_url" type="text" value="img/mojang_cape.png" placeholder="none" list="default_capes" size="20"></label>
|
<label>Cape URL: <input id="cape_url" type="text" value="" placeholder="none" list="default_capes"></label>
|
||||||
<datalist id="default_capes">
|
<datalist id="default_capes">
|
||||||
<option value="">
|
<option value="">
|
||||||
<option value="img/mojang_cape.png">
|
<option value="img/mojang_cape.png">
|
||||||
<option value="img/legacy_cape.png">
|
<option value="img/legacy_cape.png">
|
||||||
<option value="img/hd_cape.png">
|
<option value="img/hd_cape.png">
|
||||||
</datalist>
|
</datalist>
|
||||||
<input id="cape_url_upload" type="file" class="hidden" accept="image/*">
|
<input id="cape_url_upload" type="file" accept="image/*" style="display: none;">
|
||||||
<button id="cape_url_unset" type="button" class="control hidden">Unset</button>
|
|
||||||
<button type="button" class="control"
|
<button type="button" class="control"
|
||||||
onclick="document.getElementById('cape_url_upload').click();">Browse...</button>
|
onclick="document.getElementById('cape_url_upload').click();">Browse...</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="control-section">
|
|
||||||
<h1>Ears</h1>
|
|
||||||
<div>
|
|
||||||
<label class="control">Source:
|
|
||||||
<select id="ears_source">
|
|
||||||
<option value="none">None</option>
|
|
||||||
<option value="current_skin">Current skin</option>
|
|
||||||
<option value="skin">Skin texture</option>
|
|
||||||
<option value="standalone">Standalone texture</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div id="ears_texture_input">
|
|
||||||
<label class="control">URL: <input id="ears_url" type="text" value="" placeholder="none" list="default_ears" size="20"></label>
|
|
||||||
<datalist id="default_ears">
|
|
||||||
<option value="">
|
|
||||||
<option value="img/ears.png" data-texture-type="standalone">
|
|
||||||
<option value="img/deadmau5.png" data-texture-type="skin">
|
|
||||||
</datalist>
|
|
||||||
<input id="ears_url_upload" type="file" class="hidden" accept="image/*">
|
|
||||||
<button id="ears_url_unset" type="button" class="control hidden">Unset</button>
|
|
||||||
<button type="button" class="control"
|
|
||||||
onclick="document.getElementById('ears_url_upload').click();">Browse...</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="control-section">
|
|
||||||
<h1>Panorama</h1>
|
|
||||||
<div class="control">
|
|
||||||
<label>URL: <input id="panorama_url" type="text" value="img/panorama.png" placeholder="none" list="default_panorama" size="20"></label>
|
|
||||||
<datalist id="default_panorama">
|
|
||||||
<option value="">
|
|
||||||
<option value="img/panorama.png">
|
|
||||||
</datalist>
|
|
||||||
<input id="panorama_url_upload" type="file" class="hidden" accept="image/*">
|
|
||||||
<button id="panorama_url_unset" type="button" class="control hidden">Unset</button>
|
|
||||||
<button type="button" class="control"
|
|
||||||
onclick="document.getElementById('panorama_url_upload').click();">Browse...</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="control-section">
|
<div class="control-section">
|
||||||
|
@ -319,7 +260,6 @@
|
||||||
const skinParts = ["head", "body", "rightArm", "leftArm", "rightLeg", "leftLeg"];
|
const skinParts = ["head", "body", "rightArm", "leftArm", "rightLeg", "leftLeg"];
|
||||||
const skinLayers = ["innerLayer", "outerLayer"];
|
const skinLayers = ["innerLayer", "outerLayer"];
|
||||||
const availableAnimations = {
|
const availableAnimations = {
|
||||||
idle: skinview3d.IdleAnimation,
|
|
||||||
walk: skinview3d.WalkingAnimation,
|
walk: skinview3d.WalkingAnimation,
|
||||||
run: skinview3d.RunningAnimation,
|
run: skinview3d.RunningAnimation,
|
||||||
fly: skinview3d.FlyingAnimation
|
fly: skinview3d.FlyingAnimation
|
||||||
|
@ -330,35 +270,14 @@
|
||||||
let rotateAnimation;
|
let rotateAnimation;
|
||||||
let primaryAnimation;
|
let primaryAnimation;
|
||||||
|
|
||||||
function obtainTextureUrl(id) {
|
|
||||||
const urlInput = document.getElementById(id);
|
|
||||||
const fileInput = document.getElementById(id + "_upload");
|
|
||||||
const unsetButton = document.getElementById(id + "_unset");
|
|
||||||
const file = fileInput.files[0];
|
|
||||||
if (file === undefined) {
|
|
||||||
if (!unsetButton.classList.contains("hidden")) {
|
|
||||||
unsetButton.classList.add("hidden");
|
|
||||||
}
|
|
||||||
return urlInput.value;
|
|
||||||
} else {
|
|
||||||
unsetButton.classList.remove("hidden");
|
|
||||||
urlInput.value = `Local file: ${file.name}`;
|
|
||||||
urlInput.readOnly = true;
|
|
||||||
return URL.createObjectURL(file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function reloadSkin() {
|
function reloadSkin() {
|
||||||
const input = document.getElementById("skin_url");
|
const input = document.getElementById("skin_url");
|
||||||
const url = obtainTextureUrl("skin_url");
|
const url = input.value;
|
||||||
if (url === "") {
|
if (url === "") {
|
||||||
skinViewer.loadSkin(null);
|
skinViewer.loadSkin(null);
|
||||||
input.setCustomValidity("");
|
input.setCustomValidity("");
|
||||||
} else {
|
} else {
|
||||||
skinViewer.loadSkin(url, {
|
skinViewer.loadSkin(url, document.getElementById("skin_model").value)
|
||||||
model: document.getElementById("skin_model").value,
|
|
||||||
ears: document.getElementById("ears_source").value === "current_skin"
|
|
||||||
})
|
|
||||||
.then(() => input.setCustomValidity(""))
|
.then(() => input.setCustomValidity(""))
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
input.setCustomValidity("Image can't be loaded.");
|
input.setCustomValidity("Image can't be loaded.");
|
||||||
|
@ -369,7 +288,7 @@
|
||||||
|
|
||||||
function reloadCape() {
|
function reloadCape() {
|
||||||
const input = document.getElementById("cape_url");
|
const input = document.getElementById("cape_url");
|
||||||
const url = obtainTextureUrl("cape_url");
|
const url = input.value;
|
||||||
if (url === "") {
|
if (url === "") {
|
||||||
skinViewer.loadCape(null);
|
skinViewer.loadCape(null);
|
||||||
input.setCustomValidity("");
|
input.setCustomValidity("");
|
||||||
|
@ -384,69 +303,9 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function reloadEars(skipSkinReload = false) {
|
|
||||||
const sourceType = document.getElementById("ears_source").value;
|
|
||||||
let hideInput = true;
|
|
||||||
if (sourceType === "none") {
|
|
||||||
skinViewer.loadEars(null);
|
|
||||||
} else if (sourceType === "current_skin") {
|
|
||||||
if (!skipSkinReload){
|
|
||||||
reloadSkin();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
hideInput = false;
|
|
||||||
document.querySelectorAll("#default_ears option[data-texture-type]").forEach(opt => {
|
|
||||||
opt.disabled = opt.dataset.textureType !== sourceType;
|
|
||||||
});
|
|
||||||
|
|
||||||
const input = document.getElementById("ears_url");
|
|
||||||
const url = obtainTextureUrl("ears_url");
|
|
||||||
if (url === "") {
|
|
||||||
skinViewer.loadEars(null);
|
|
||||||
input.setCustomValidity("");
|
|
||||||
} else {
|
|
||||||
skinViewer.loadEars(url, { textureType: sourceType })
|
|
||||||
.then(() => input.setCustomValidity(""))
|
|
||||||
.catch(e => {
|
|
||||||
input.setCustomValidity("Image can't be loaded.");
|
|
||||||
console.error(e);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const el = document.getElementById("ears_texture_input");
|
|
||||||
if (hideInput) {
|
|
||||||
if (!(el.classList.contains("hidden"))){
|
|
||||||
el.classList.add("hidden");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
el.classList.remove("hidden");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function reloadPanorama() {
|
|
||||||
const input = document.getElementById("panorama_url");
|
|
||||||
const url = obtainTextureUrl("panorama_url");
|
|
||||||
if (url === "") {
|
|
||||||
skinViewer.background = "white";
|
|
||||||
input.setCustomValidity("");
|
|
||||||
} else {
|
|
||||||
skinViewer.loadPanorama(url)
|
|
||||||
.then(() => input.setCustomValidity(""))
|
|
||||||
.catch(e => {
|
|
||||||
input.setCustomValidity("Image can't be loaded.");
|
|
||||||
console.error(e);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function initializeControls() {
|
function initializeControls() {
|
||||||
document.getElementById("canvas_width").addEventListener("change", e => skinViewer.width = e.target.value);
|
document.getElementById("canvas_width").addEventListener("change", e => skinViewer.width = e.target.value);
|
||||||
document.getElementById("canvas_height").addEventListener("change", e => skinViewer.height = e.target.value);
|
document.getElementById("canvas_height").addEventListener("change", e => skinViewer.height = e.target.value);
|
||||||
document.getElementById("fov").addEventListener("change", e => skinViewer.fov = e.target.value);
|
|
||||||
document.getElementById("zoom").addEventListener("change", e => skinViewer.zoom = e.target.value);
|
|
||||||
document.getElementById("global_light").addEventListener("change", e => skinViewer.globalLight.intensity = e.target.value);
|
|
||||||
document.getElementById("camera_light").addEventListener("change", e => skinViewer.cameraLight.intensity = e.target.value);
|
|
||||||
document.getElementById("global_animation_speed").addEventListener("change", e => skinViewer.animations.speed = e.target.value);
|
document.getElementById("global_animation_speed").addEventListener("change", e => skinViewer.animations.speed = e.target.value);
|
||||||
document.getElementById("animation_pause_resume").addEventListener("click", () => skinViewer.animations.paused = !skinViewer.animations.paused);
|
document.getElementById("animation_pause_resume").addEventListener("click", () => skinViewer.animations.paused = !skinViewer.animations.paused);
|
||||||
document.getElementById("rotate_animation").addEventListener("change", e => {
|
document.getElementById("rotate_animation").addEventListener("change", e => {
|
||||||
|
@ -489,36 +348,31 @@
|
||||||
.addEventListener("change", e => skinViewer.playerObject.skin[part][layer].visible = e.target.checked);
|
.addEventListener("change", e => skinViewer.playerObject.skin[part][layer].visible = e.target.checked);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const skinReader = new FileReader();
|
||||||
const initializeUploadButton = (id, callback) => {
|
skinReader.addEventListener("load", e => {
|
||||||
const urlInput = document.getElementById(id);
|
document.getElementById("skin_url").value = skinReader.result;
|
||||||
const fileInput = document.getElementById(id + "_upload");
|
reloadSkin();
|
||||||
const unsetButton = document.getElementById(id + "_unset");
|
});
|
||||||
const unsetAction = () => {
|
document.getElementById("skin_url_upload").addEventListener("change", e => {
|
||||||
urlInput.readOnly = false;
|
const file = e.target.files[0];
|
||||||
urlInput.value = "";
|
if (file !== undefined) {
|
||||||
fileInput.value = fileInput.defaultValue;
|
skinReader.readAsDataURL(file);
|
||||||
callback();
|
}
|
||||||
};
|
});
|
||||||
fileInput.addEventListener("change", e => callback());
|
const capeReader = new FileReader();
|
||||||
urlInput.addEventListener("keydown", e => {
|
capeReader.addEventListener("load", e => {
|
||||||
if (e.key === "Backspace" && urlInput.readOnly) {
|
document.getElementById("cape_url").value = capeReader.result;
|
||||||
unsetAction();
|
reloadCape();
|
||||||
|
});
|
||||||
|
document.getElementById("cape_url_upload").addEventListener("change", e => {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
if (file !== undefined) {
|
||||||
|
capeReader.readAsDataURL(file);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
unsetButton.addEventListener("click", e => unsetAction());
|
|
||||||
};
|
|
||||||
initializeUploadButton("skin_url", reloadSkin);
|
|
||||||
initializeUploadButton("cape_url", reloadCape);
|
|
||||||
initializeUploadButton("ears_url", reloadEars);
|
|
||||||
initializeUploadButton("panorama_url", reloadPanorama);
|
|
||||||
|
|
||||||
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());
|
||||||
document.getElementById("ears_source").addEventListener("change", () => reloadEars());
|
|
||||||
document.getElementById("ears_url").addEventListener("change", () => reloadEars());
|
|
||||||
document.getElementById("panorama_url").addEventListener("change", () => reloadPanorama());
|
|
||||||
|
|
||||||
for (const el of document.querySelectorAll('input[type="radio"][name="back_equipment"]')) {
|
for (const el of document.querySelectorAll('input[type="radio"][name="back_equipment"]')) {
|
||||||
el.addEventListener("change", e => {
|
el.addEventListener("change", e => {
|
||||||
|
@ -540,18 +394,16 @@
|
||||||
|
|
||||||
function initializeViewer() {
|
function initializeViewer() {
|
||||||
skinViewer = new skinview3d.FXAASkinViewer({
|
skinViewer = new skinview3d.FXAASkinViewer({
|
||||||
canvas: document.getElementById("skin_container")
|
canvas: document.getElementById("skin_container"),
|
||||||
|
alpha: false
|
||||||
});
|
});
|
||||||
|
skinViewer.renderer.setClearColor(0x5a76f3);
|
||||||
orbitControl = skinview3d.createOrbitControls(skinViewer);
|
orbitControl = skinview3d.createOrbitControls(skinViewer);
|
||||||
rotateAnimation = null;
|
rotateAnimation = null;
|
||||||
primaryAnimation = null;
|
primaryAnimation = null;
|
||||||
|
|
||||||
skinViewer.width = document.getElementById("canvas_width").value;
|
skinViewer.width = document.getElementById("canvas_width").value;
|
||||||
skinViewer.height = document.getElementById("canvas_height").value;
|
skinViewer.height = document.getElementById("canvas_height").value;
|
||||||
skinViewer.fov = document.getElementById("fov").value;
|
|
||||||
skinViewer.zoom = document.getElementById("zoom").value;
|
|
||||||
skinViewer.globalLight.intensity = document.getElementById("global_light").value;
|
|
||||||
skinViewer.cameraLight.intensity = document.getElementById("camera_light").value;
|
|
||||||
skinViewer.animations.speed = document.getElementById("global_animation_speed").value;
|
skinViewer.animations.speed = document.getElementById("global_animation_speed").value;
|
||||||
if (document.getElementById("rotate_animation").checked) {
|
if (document.getElementById("rotate_animation").checked) {
|
||||||
rotateAnimation = skinViewer.animations.add(skinview3d.RotatingAnimation);
|
rotateAnimation = skinViewer.animations.add(skinview3d.RotatingAnimation);
|
||||||
|
@ -573,8 +425,6 @@
|
||||||
}
|
}
|
||||||
reloadSkin();
|
reloadSkin();
|
||||||
reloadCape();
|
reloadCape();
|
||||||
reloadEars(true);
|
|
||||||
reloadPanorama();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
initializeControls();
|
initializeControls();
|
||||||
|
|
|
@ -44,8 +44,10 @@
|
||||||
const skinViewer = new skinview3d.FXAASkinViewer({
|
const skinViewer = new skinview3d.FXAASkinViewer({
|
||||||
width: 200,
|
width: 200,
|
||||||
height: 300,
|
height: 300,
|
||||||
|
alpha: false,
|
||||||
renderPaused: true
|
renderPaused: true
|
||||||
});
|
});
|
||||||
|
skinViewer.renderer.setClearColor(0x5a76f3);
|
||||||
skinViewer.camera.rotation.x = -0.620;
|
skinViewer.camera.rotation.x = -0.620;
|
||||||
skinViewer.camera.rotation.y = 0.534;
|
skinViewer.camera.rotation.y = 0.534;
|
||||||
skinViewer.camera.rotation.z = 0.348;
|
skinViewer.camera.rotation.z = 0.348;
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
26
package.json
26
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "skinview3d",
|
"name": "skinview3d",
|
||||||
"version": "2.2.1",
|
"version": "2.0.1",
|
||||||
"description": "Three.js powered Minecraft skin viewer",
|
"description": "Three.js powered Minecraft skin viewer",
|
||||||
"main": "libs/skinview3d.js",
|
"main": "libs/skinview3d.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
@ -38,22 +38,22 @@
|
||||||
"bundles"
|
"bundles"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/three": "^0.136.1",
|
"@types/three": "^0.128.0",
|
||||||
"skinview-utils": "^0.7.0",
|
"skinview-utils": "^0.6.0",
|
||||||
"three": "^0.136.0"
|
"three": "^0.128.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rollup/plugin-node-resolve": "^13.1.3",
|
"@rollup/plugin-node-resolve": "^13.0.0",
|
||||||
"@rollup/plugin-typescript": "^8.3.0",
|
"@rollup/plugin-typescript": "^8.2.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.9.0",
|
"@typescript-eslint/eslint-plugin": "^4.24.0",
|
||||||
"@typescript-eslint/parser": "^5.9.0",
|
"@typescript-eslint/parser": "^4.24.0",
|
||||||
"@yushijinhun/three-minifier-rollup": "^0.3.1",
|
"@yushijinhun/three-minifier-rollup": "^0.2.0",
|
||||||
"eslint": "^8.6.0",
|
"eslint": "^7.26.0",
|
||||||
"local-web-server": "^5.1.1",
|
"local-web-server": "^4.2.1",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"rollup": "^2.63.0",
|
"rollup": "^2.48.0",
|
||||||
"rollup-plugin-terser": "^7.0.2",
|
"rollup-plugin-terser": "^7.0.2",
|
||||||
"typescript": "^4.5.4"
|
"typescript": "^4.2.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -83,9 +83,9 @@ class AnimationWrapper implements SubAnimationHandle, IAnimation {
|
||||||
|
|
||||||
export class CompositeAnimation implements IAnimation {
|
export class CompositeAnimation implements IAnimation {
|
||||||
|
|
||||||
readonly handles: Set<SubAnimationHandle & IAnimation> = new Set();
|
readonly handles: Set<AnimationHandle & IAnimation> = new Set();
|
||||||
|
|
||||||
add(animation: Animation): SubAnimationHandle {
|
add(animation: Animation): AnimationHandle {
|
||||||
const handle = new AnimationWrapper(animation);
|
const handle = new AnimationWrapper(animation);
|
||||||
handle.remove = (): void => {
|
handle.remove = (): void => {
|
||||||
this.handles.delete(handle);
|
this.handles.delete(handle);
|
||||||
|
@ -133,22 +133,6 @@ export class RootAnimation extends CompositeAnimation implements AnimationHandle
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const IdleAnimation: Animation = (player, time) => {
|
|
||||||
const skin = player.skin;
|
|
||||||
|
|
||||||
// Multiply by animation's natural speed
|
|
||||||
time *= 2;
|
|
||||||
|
|
||||||
// Arm swing
|
|
||||||
const basicArmRotationZ = Math.PI * 0.02;
|
|
||||||
skin.leftArm.rotation.z = Math.cos(time) * 0.03 + basicArmRotationZ;
|
|
||||||
skin.rightArm.rotation.z = Math.cos(time + Math.PI) * 0.03 - basicArmRotationZ;
|
|
||||||
|
|
||||||
// Always add an angle for cape around the x axis
|
|
||||||
const basicCapeRotationX = Math.PI * 0.06;
|
|
||||||
player.cape.rotation.x = Math.sin(time) * 0.01 + basicCapeRotationX;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const WalkingAnimation: Animation = (player, time) => {
|
export const WalkingAnimation: Animation = (player, time) => {
|
||||||
const skin = player.skin;
|
const skin = player.skin;
|
||||||
|
|
||||||
|
|
21
src/fxaa.ts
21
src/fxaa.ts
|
@ -1,5 +1,5 @@
|
||||||
import { EffectComposer } from "three/examples/jsm/postprocessing/EffectComposer.js";
|
import { EffectComposer } from "three/examples/jsm/postprocessing/EffectComposer.js";
|
||||||
import { FullScreenQuad } from "three/examples/jsm/postprocessing/Pass.js";
|
import { Pass } from "three/examples/jsm/postprocessing/Pass.js";
|
||||||
import { RenderPass } from "three/examples/jsm/postprocessing/RenderPass.js";
|
import { RenderPass } from "three/examples/jsm/postprocessing/RenderPass.js";
|
||||||
import { ShaderPass } from "three/examples/jsm/postprocessing/ShaderPass.js";
|
import { ShaderPass } from "three/examples/jsm/postprocessing/ShaderPass.js";
|
||||||
import { FXAAShader } from "three/examples/jsm/shaders/FXAAShader.js";
|
import { FXAAShader } from "three/examples/jsm/shaders/FXAAShader.js";
|
||||||
|
@ -11,17 +11,11 @@ export class FXAASkinViewer extends SkinViewer {
|
||||||
readonly renderPass: RenderPass;
|
readonly renderPass: RenderPass;
|
||||||
readonly fxaaPass: ShaderPass;
|
readonly fxaaPass: ShaderPass;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Note: FXAA doesn't work well with transparent backgrounds.
|
||||||
|
* It's recommended to use an opaque background and set `options.alpha` to false.
|
||||||
|
*/
|
||||||
constructor(options?: SkinViewerOptions) {
|
constructor(options?: SkinViewerOptions) {
|
||||||
// Force options.alpha to false, because FXAA is incompatible with transparent backgrounds
|
|
||||||
if (options === undefined) {
|
|
||||||
options = { alpha: false, background: "white" };
|
|
||||||
} else {
|
|
||||||
options.alpha = false;
|
|
||||||
if (options.background === undefined) {
|
|
||||||
options.background = "white";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
super(options);
|
super(options);
|
||||||
this.composer = new EffectComposer(this.renderer);
|
this.composer = new EffectComposer(this.renderer);
|
||||||
this.renderPass = new RenderPass(this.scene, this.camera);
|
this.renderPass = new RenderPass(this.scene, this.camera);
|
||||||
|
@ -29,9 +23,6 @@ export class FXAASkinViewer extends SkinViewer {
|
||||||
this.composer.addPass(this.renderPass);
|
this.composer.addPass(this.renderPass);
|
||||||
this.composer.addPass(this.fxaaPass);
|
this.composer.addPass(this.fxaaPass);
|
||||||
this.updateComposerSize();
|
this.updateComposerSize();
|
||||||
|
|
||||||
// Default background: white
|
|
||||||
this.renderer.setClearColor("white");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setSize(width: number, height: number): void {
|
setSize(width: number, height: number): void {
|
||||||
|
@ -55,6 +46,6 @@ export class FXAASkinViewer extends SkinViewer {
|
||||||
|
|
||||||
dispose(): void {
|
dispose(): void {
|
||||||
super.dispose();
|
super.dispose();
|
||||||
(this.fxaaPass.fsQuad as FullScreenQuad).dispose();
|
(this.fxaaPass.fsQuad as Pass.FullScreenQuad).dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
53
src/model.ts
53
src/model.ts
|
@ -1,5 +1,5 @@
|
||||||
import { ModelType } from "skinview-utils";
|
import { ModelType } from "skinview-utils";
|
||||||
import { BoxGeometry, BufferAttribute, DoubleSide, FrontSide, Group, Mesh, MeshStandardMaterial, Object3D, Texture, Vector2 } from "three";
|
import { BoxGeometry, BufferAttribute, DoubleSide, FrontSide, Group, Mesh, MeshBasicMaterial, Object3D, Texture, Vector2 } from "three";
|
||||||
|
|
||||||
function setUVs(box: BoxGeometry, u: number, v: number, width: number, height: number, depth: number, textureWidth: number, textureHeight: number): void {
|
function setUVs(box: BoxGeometry, u: number, v: number, width: number, height: number, depth: number, textureWidth: number, textureHeight: number): void {
|
||||||
const toFaceVertices = (x1: number, y1: number, x2: number, y2: number) => [
|
const toFaceVertices = (x1: number, y1: number, x2: number, y2: number) => [
|
||||||
|
@ -66,11 +66,11 @@ export class SkinObject extends Group {
|
||||||
constructor(texture: Texture) {
|
constructor(texture: Texture) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
const layer1Material = new MeshStandardMaterial({
|
const layer1Material = new MeshBasicMaterial({
|
||||||
map: texture,
|
map: texture,
|
||||||
side: FrontSide
|
side: FrontSide
|
||||||
});
|
});
|
||||||
const layer2Material = new MeshStandardMaterial({
|
const layer2Material = new MeshBasicMaterial({
|
||||||
map: texture,
|
map: texture,
|
||||||
side: DoubleSide,
|
side: DoubleSide,
|
||||||
transparent: true,
|
transparent: true,
|
||||||
|
@ -99,8 +99,7 @@ export class SkinObject extends Group {
|
||||||
this.head = new BodyPart(headMesh, head2Mesh);
|
this.head = new BodyPart(headMesh, head2Mesh);
|
||||||
this.head.name = "head";
|
this.head.name = "head";
|
||||||
this.head.add(headMesh, head2Mesh);
|
this.head.add(headMesh, head2Mesh);
|
||||||
headMesh.position.y = 4;
|
this.head.position.y = 4;
|
||||||
head2Mesh.position.y = 4;
|
|
||||||
this.add(this.head);
|
this.add(this.head);
|
||||||
|
|
||||||
// Body
|
// Body
|
||||||
|
@ -258,7 +257,7 @@ export class CapeObject extends Group {
|
||||||
constructor(texture: Texture) {
|
constructor(texture: Texture) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
const capeMaterial = new MeshStandardMaterial({
|
const capeMaterial = new MeshBasicMaterial({
|
||||||
map: texture,
|
map: texture,
|
||||||
side: DoubleSide,
|
side: DoubleSide,
|
||||||
transparent: true,
|
transparent: true,
|
||||||
|
@ -284,7 +283,7 @@ export class ElytraObject extends Group {
|
||||||
constructor(texture: Texture) {
|
constructor(texture: Texture) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
const elytraMaterial = new MeshStandardMaterial({
|
const elytraMaterial = new MeshBasicMaterial({
|
||||||
map: texture,
|
map: texture,
|
||||||
side: DoubleSide,
|
side: DoubleSide,
|
||||||
transparent: true,
|
transparent: true,
|
||||||
|
@ -332,33 +331,6 @@ export class ElytraObject extends Group {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class EarsObject extends Group {
|
|
||||||
|
|
||||||
readonly rightEar: Mesh;
|
|
||||||
readonly leftEar: Mesh;
|
|
||||||
|
|
||||||
constructor(texture: Texture) {
|
|
||||||
super();
|
|
||||||
|
|
||||||
const material = new MeshStandardMaterial({
|
|
||||||
map: texture,
|
|
||||||
side: FrontSide
|
|
||||||
});
|
|
||||||
const earBox = new BoxGeometry(8, 8, 4 / 3);
|
|
||||||
setUVs(earBox, 0, 0, 6, 6, 1, 14, 7);
|
|
||||||
|
|
||||||
this.rightEar = new Mesh(earBox, material);
|
|
||||||
this.rightEar.name = "rightEar";
|
|
||||||
this.rightEar.position.x = -6;
|
|
||||||
this.add(this.rightEar);
|
|
||||||
|
|
||||||
this.leftEar = new Mesh(earBox, material);
|
|
||||||
this.leftEar.name = "leftEar";
|
|
||||||
this.leftEar.position.x = 6;
|
|
||||||
this.add(this.leftEar);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type BackEquipment = "cape" | "elytra";
|
export type BackEquipment = "cape" | "elytra";
|
||||||
|
|
||||||
export class PlayerObject extends Group {
|
export class PlayerObject extends Group {
|
||||||
|
@ -366,19 +338,16 @@ export class PlayerObject extends Group {
|
||||||
readonly skin: SkinObject;
|
readonly skin: SkinObject;
|
||||||
readonly cape: CapeObject;
|
readonly cape: CapeObject;
|
||||||
readonly elytra: ElytraObject;
|
readonly elytra: ElytraObject;
|
||||||
readonly ears: EarsObject;
|
|
||||||
|
|
||||||
constructor(skinTexture: Texture, capeTexture: Texture, earsTexture: Texture) {
|
constructor(skinTexture: Texture, capeTexture: Texture) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.skin = new SkinObject(skinTexture);
|
this.skin = new SkinObject(skinTexture);
|
||||||
this.skin.name = "skin";
|
this.skin.name = "skin";
|
||||||
this.skin.position.y = 8;
|
|
||||||
this.add(this.skin);
|
this.add(this.skin);
|
||||||
|
|
||||||
this.cape = new CapeObject(capeTexture);
|
this.cape = new CapeObject(capeTexture);
|
||||||
this.cape.name = "cape";
|
this.cape.name = "cape";
|
||||||
this.cape.position.y = 8;
|
|
||||||
this.cape.position.z = -2;
|
this.cape.position.z = -2;
|
||||||
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;
|
||||||
|
@ -386,17 +355,9 @@ export class PlayerObject extends Group {
|
||||||
|
|
||||||
this.elytra = new ElytraObject(capeTexture);
|
this.elytra = new ElytraObject(capeTexture);
|
||||||
this.elytra.name = "elytra";
|
this.elytra.name = "elytra";
|
||||||
this.elytra.position.y = 8;
|
|
||||||
this.elytra.position.z = -2;
|
this.elytra.position.z = -2;
|
||||||
this.elytra.visible = false;
|
this.elytra.visible = false;
|
||||||
this.add(this.elytra);
|
this.add(this.elytra);
|
||||||
|
|
||||||
this.ears = new EarsObject(earsTexture);
|
|
||||||
this.ears.name = "ears";
|
|
||||||
this.ears.position.y = 10;
|
|
||||||
this.ears.position.z = 2 / 3;
|
|
||||||
this.ears.visible = false;
|
|
||||||
this.skin.head.add(this.ears);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get backEquipment(): BackEquipment | null {
|
get backEquipment(): BackEquipment | null {
|
||||||
|
|
|
@ -7,7 +7,7 @@ export function createOrbitControls(skinViewer: SkinViewer): OrbitControls {
|
||||||
|
|
||||||
// default configuration
|
// default configuration
|
||||||
control.enablePan = false;
|
control.enablePan = false;
|
||||||
control.target = new Vector3(0, 0, 0);
|
control.target = new Vector3(0, -8, 0);
|
||||||
control.minDistance = 10;
|
control.minDistance = 10;
|
||||||
control.maxDistance = 256;
|
control.maxDistance = 256;
|
||||||
control.update();
|
control.update();
|
||||||
|
|
301
src/viewer.ts
301
src/viewer.ts
|
@ -1,5 +1,5 @@
|
||||||
import { inferModelType, isTextureSource, loadCapeToCanvas, loadEarsToCanvas, loadEarsToCanvasFromSkin, loadImage, loadSkinToCanvas, ModelType, RemoteImage, TextureSource } from "skinview-utils";
|
import { inferModelType, isTextureSource, loadCapeToCanvas, loadImage, loadSkinToCanvas, ModelType, RemoteImage, TextureSource } from "skinview-utils";
|
||||||
import { Color, ColorRepresentation, PointLight, EquirectangularReflectionMapping, Group, NearestFilter, PerspectiveCamera, Scene, Texture, Vector2, WebGLRenderer, AmbientLight, Mapping } from "three";
|
import { NearestFilter, PerspectiveCamera, Scene, Texture, Vector2, WebGLRenderer } from "three";
|
||||||
import { RootAnimation } from "./animation.js";
|
import { RootAnimation } from "./animation.js";
|
||||||
import { BackEquipment, PlayerObject } from "./model.js";
|
import { BackEquipment, PlayerObject } from "./model.js";
|
||||||
|
|
||||||
|
@ -10,21 +10,6 @@ export interface LoadOptions {
|
||||||
makeVisible?: boolean;
|
makeVisible?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SkinLoadOptions extends LoadOptions {
|
|
||||||
/**
|
|
||||||
* The model type of skin. Default is "auto-detect".
|
|
||||||
*/
|
|
||||||
model?: ModelType | "auto-detect";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* true: Loads the ears drawn on the skin texture, and show it.
|
|
||||||
* "load-only": Loads the ears drawn on the skin texture, but do not make it visible.
|
|
||||||
* false: Do not load ears from the skin texture.
|
|
||||||
* Default is false.
|
|
||||||
*/
|
|
||||||
ears?: boolean | "load-only";
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CapeLoadOptions extends LoadOptions {
|
export interface CapeLoadOptions extends LoadOptions {
|
||||||
/**
|
/**
|
||||||
* The equipment (cape or elytra) to show, defaults to "cape".
|
* The equipment (cape or elytra) to show, defaults to "cape".
|
||||||
|
@ -33,15 +18,6 @@ export interface CapeLoadOptions extends LoadOptions {
|
||||||
backEquipment?: BackEquipment;
|
backEquipment?: BackEquipment;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EarsLoadOptions extends LoadOptions {
|
|
||||||
/**
|
|
||||||
* "standalone": The texture is a 14x7 image that only contains the ears;
|
|
||||||
* "skin": The texture is a skin that contains ears, and we only show its ear part.
|
|
||||||
* Default is "standalone".
|
|
||||||
*/
|
|
||||||
textureType?: "standalone" | "skin";
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SkinViewerOptions {
|
export interface SkinViewerOptions {
|
||||||
width?: number;
|
width?: number;
|
||||||
height?: number;
|
height?: number;
|
||||||
|
@ -49,17 +25,6 @@ export interface SkinViewerOptions {
|
||||||
model?: ModelType | "auto-detect";
|
model?: ModelType | "auto-detect";
|
||||||
cape?: RemoteImage | TextureSource;
|
cape?: RemoteImage | TextureSource;
|
||||||
|
|
||||||
/**
|
|
||||||
* If you want to show the ears drawn on the current skin, set this to "current-skin".
|
|
||||||
* To show ears that come from a separate texture, you have to specify 'textureType' ("standalone" or "skin") and 'source'.
|
|
||||||
* "standalone" means the provided texture is a 14x7 image that only contains the ears.
|
|
||||||
* "skin" means the provided texture is a skin that contains ears, and we only show its ear part.
|
|
||||||
*/
|
|
||||||
ears?: "current-skin" | {
|
|
||||||
textureType: "standalone" | "skin",
|
|
||||||
source: RemoteImage | TextureSource
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether the canvas contains an alpha buffer. Default is true.
|
* Whether the canvas contains an alpha buffer. Default is true.
|
||||||
* This option can be turned off if you use an opaque background.
|
* This option can be turned off if you use an opaque background.
|
||||||
|
@ -82,29 +47,6 @@ export interface SkinViewerOptions {
|
||||||
* If this option is true, rendering and animation loops will not start.
|
* If this option is true, rendering and animation loops will not start.
|
||||||
*/
|
*/
|
||||||
renderPaused?: boolean;
|
renderPaused?: boolean;
|
||||||
|
|
||||||
/**
|
|
||||||
* The background of the scene. Default is transparent.
|
|
||||||
*/
|
|
||||||
background?: ColorRepresentation | Texture;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The panorama background to use. This option overrides 'background' option.
|
|
||||||
*/
|
|
||||||
panorama?: RemoteImage | TextureSource;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Camera vertical field of view, in degrees. Default is 50.
|
|
||||||
* The distance between the object and the camera is automatically computed.
|
|
||||||
*/
|
|
||||||
fov?: number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Zoom ratio of the player. Default is 0.9.
|
|
||||||
* This value affects the distance between the object and the camera.
|
|
||||||
* When set to 1.0, the top edge of the player's head coincides with the edge of the view.
|
|
||||||
*/
|
|
||||||
zoom?: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SkinViewer {
|
export class SkinViewer {
|
||||||
|
@ -113,26 +55,15 @@ export class SkinViewer {
|
||||||
readonly camera: PerspectiveCamera;
|
readonly camera: PerspectiveCamera;
|
||||||
readonly renderer: WebGLRenderer;
|
readonly renderer: WebGLRenderer;
|
||||||
readonly playerObject: PlayerObject;
|
readonly playerObject: PlayerObject;
|
||||||
readonly playerWrapper: Group;
|
|
||||||
readonly animations: RootAnimation = new RootAnimation();
|
readonly animations: RootAnimation = new RootAnimation();
|
||||||
readonly globalLight: AmbientLight = new AmbientLight(0xffffff, 0.4);
|
|
||||||
readonly cameraLight: PointLight = new PointLight(0xffffff, 0.6);
|
|
||||||
|
|
||||||
readonly skinCanvas: HTMLCanvasElement;
|
readonly skinCanvas: HTMLCanvasElement;
|
||||||
readonly capeCanvas: HTMLCanvasElement;
|
readonly capeCanvas: HTMLCanvasElement;
|
||||||
readonly earsCanvas: HTMLCanvasElement;
|
|
||||||
private readonly skinTexture: Texture;
|
private readonly skinTexture: Texture;
|
||||||
private readonly capeTexture: Texture;
|
private readonly capeTexture: Texture;
|
||||||
private readonly earsTexture: Texture;
|
|
||||||
private backgroundTexture: Texture | null = null;
|
|
||||||
|
|
||||||
private _disposed: boolean = false;
|
private _disposed: boolean = false;
|
||||||
private _renderPaused: boolean = false;
|
private _renderPaused: boolean = false;
|
||||||
private _zoom: number;
|
|
||||||
|
|
||||||
private animationID: number | null;
|
|
||||||
private onContextLost: (event: Event) => void;
|
|
||||||
private onContextRestored: () => void;
|
|
||||||
|
|
||||||
constructor(options: SkinViewerOptions = {}) {
|
constructor(options: SkinViewerOptions = {}) {
|
||||||
this.canvas = options.canvas === undefined ? document.createElement("canvas") : options.canvas;
|
this.canvas = options.canvas === undefined ? document.createElement("canvas") : options.canvas;
|
||||||
|
@ -148,17 +79,12 @@ export class SkinViewer {
|
||||||
this.capeTexture.magFilter = NearestFilter;
|
this.capeTexture.magFilter = NearestFilter;
|
||||||
this.capeTexture.minFilter = NearestFilter;
|
this.capeTexture.minFilter = NearestFilter;
|
||||||
|
|
||||||
this.earsCanvas = document.createElement("canvas");
|
|
||||||
this.earsTexture = new Texture(this.earsCanvas);
|
|
||||||
this.earsTexture.magFilter = NearestFilter;
|
|
||||||
this.earsTexture.minFilter = NearestFilter;
|
|
||||||
|
|
||||||
this.scene = new Scene();
|
this.scene = new Scene();
|
||||||
|
|
||||||
this.camera = new PerspectiveCamera();
|
// Use smaller fov to avoid distortion
|
||||||
this.camera.add(this.cameraLight);
|
this.camera = new PerspectiveCamera(40);
|
||||||
this.scene.add(this.camera);
|
this.camera.position.y = -8;
|
||||||
this.scene.add(this.globalLight);
|
this.camera.position.z = 60;
|
||||||
|
|
||||||
this.renderer = new WebGLRenderer({
|
this.renderer = new WebGLRenderer({
|
||||||
canvas: this.canvas,
|
canvas: this.canvas,
|
||||||
|
@ -168,106 +94,56 @@ export class SkinViewer {
|
||||||
});
|
});
|
||||||
this.renderer.setPixelRatio(window.devicePixelRatio);
|
this.renderer.setPixelRatio(window.devicePixelRatio);
|
||||||
|
|
||||||
this.playerObject = new PlayerObject(this.skinTexture, this.capeTexture, this.earsTexture);
|
this.playerObject = new PlayerObject(this.skinTexture, this.capeTexture);
|
||||||
this.playerObject.name = "player";
|
this.playerObject.name = "player";
|
||||||
this.playerObject.skin.visible = false;
|
this.playerObject.skin.visible = false;
|
||||||
this.playerObject.cape.visible = false;
|
this.playerObject.cape.visible = false;
|
||||||
this.playerWrapper = new Group();
|
this.scene.add(this.playerObject);
|
||||||
this.playerWrapper.add(this.playerObject);
|
|
||||||
this.scene.add(this.playerWrapper);
|
|
||||||
|
|
||||||
if (options.skin !== undefined) {
|
if (options.skin !== undefined) {
|
||||||
this.loadSkin(options.skin, {
|
this.loadSkin(options.skin, options.model);
|
||||||
model: options.model,
|
|
||||||
ears: options.ears === "current-skin"
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
if (options.cape !== undefined) {
|
if (options.cape !== undefined) {
|
||||||
this.loadCape(options.cape);
|
this.loadCape(options.cape);
|
||||||
}
|
}
|
||||||
if (options.ears !== undefined && options.ears !== "current-skin") {
|
|
||||||
this.loadEars(options.ears.source, {
|
|
||||||
textureType: options.ears.textureType
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (options.width !== undefined) {
|
if (options.width !== undefined) {
|
||||||
this.width = options.width;
|
this.width = options.width;
|
||||||
}
|
}
|
||||||
if (options.height !== undefined) {
|
if (options.height !== undefined) {
|
||||||
this.height = options.height;
|
this.height = options.height;
|
||||||
}
|
}
|
||||||
if (options.background !== undefined) {
|
|
||||||
this.background = options.background;
|
|
||||||
}
|
|
||||||
if (options.panorama !== undefined) {
|
|
||||||
this.loadPanorama(options.panorama);
|
|
||||||
}
|
|
||||||
this.camera.position.z = 1;
|
|
||||||
this._zoom = options.zoom === undefined ? 0.9 : options.zoom;
|
|
||||||
this.fov = options.fov === undefined ? 50 : options.fov;
|
|
||||||
|
|
||||||
if (options.renderPaused === true) {
|
if (options.renderPaused === true) {
|
||||||
this._renderPaused = true;
|
this._renderPaused = true;
|
||||||
this.animationID = null;
|
|
||||||
} else {
|
} else {
|
||||||
this.animationID = window.requestAnimationFrame(() => this.draw());
|
window.requestAnimationFrame(() => this.draw());
|
||||||
}
|
}
|
||||||
|
|
||||||
this.onContextLost = (event: Event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
if (this.animationID !== null) {
|
|
||||||
window.cancelAnimationFrame(this.animationID);
|
|
||||||
this.animationID = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.onContextRestored = () => {
|
|
||||||
if (!this._renderPaused && !this._disposed && this.animationID === null) {
|
|
||||||
this.animationID = window.requestAnimationFrame(() => this.draw());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.canvas.addEventListener("webglcontextlost", this.onContextLost, false);
|
|
||||||
this.canvas.addEventListener("webglcontextrestored", this.onContextRestored, false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
loadSkin(empty: null): void;
|
loadSkin(empty: null): void;
|
||||||
loadSkin<S extends TextureSource | RemoteImage>(
|
loadSkin<S extends TextureSource | RemoteImage>(
|
||||||
source: S,
|
source: S,
|
||||||
options?: SkinLoadOptions
|
model?: ModelType | "auto-detect",
|
||||||
|
options?: LoadOptions
|
||||||
): S extends TextureSource ? void : Promise<void>;
|
): S extends TextureSource ? void : Promise<void>;
|
||||||
|
|
||||||
loadSkin(
|
loadSkin(
|
||||||
source: TextureSource | RemoteImage | null,
|
source: TextureSource | RemoteImage | null,
|
||||||
options: SkinLoadOptions = {}
|
model: ModelType | "auto-detect" = "auto-detect",
|
||||||
|
options: LoadOptions = {}
|
||||||
): void | Promise<void> {
|
): void | Promise<void> {
|
||||||
if (source === null) {
|
if (source === null) {
|
||||||
this.resetSkin();
|
this.resetSkin();
|
||||||
|
|
||||||
} else if (isTextureSource(source)) {
|
} else if (isTextureSource(source)) {
|
||||||
loadSkinToCanvas(this.skinCanvas, source);
|
loadSkinToCanvas(this.skinCanvas, source);
|
||||||
|
const actualModel = model === "auto-detect" ? inferModelType(this.skinCanvas) : model;
|
||||||
this.skinTexture.needsUpdate = true;
|
this.skinTexture.needsUpdate = true;
|
||||||
|
this.playerObject.skin.modelType = actualModel;
|
||||||
if (options.model === undefined || options.model === "auto-detect") {
|
|
||||||
this.playerObject.skin.modelType = inferModelType(this.skinCanvas);
|
|
||||||
} else {
|
|
||||||
this.playerObject.skin.modelType = options.model;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.makeVisible !== false) {
|
if (options.makeVisible !== false) {
|
||||||
this.playerObject.skin.visible = true;
|
this.playerObject.skin.visible = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.ears === true || options.ears == "load-only") {
|
|
||||||
loadEarsToCanvasFromSkin(this.earsCanvas, source);
|
|
||||||
this.earsTexture.needsUpdate = true;
|
|
||||||
if (options.ears === true) {
|
|
||||||
this.playerObject.ears.visible = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
return loadImage(source).then(image => this.loadSkin(image, options));
|
return loadImage(source).then(image => this.loadSkin(image, model, options));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -287,15 +163,12 @@ export class SkinViewer {
|
||||||
): void | Promise<void> {
|
): void | Promise<void> {
|
||||||
if (source === null) {
|
if (source === null) {
|
||||||
this.resetCape();
|
this.resetCape();
|
||||||
|
|
||||||
} else if (isTextureSource(source)) {
|
} else if (isTextureSource(source)) {
|
||||||
loadCapeToCanvas(this.capeCanvas, source);
|
loadCapeToCanvas(this.capeCanvas, source);
|
||||||
this.capeTexture.needsUpdate = true;
|
this.capeTexture.needsUpdate = true;
|
||||||
|
|
||||||
if (options.makeVisible !== false) {
|
if (options.makeVisible !== false) {
|
||||||
this.playerObject.backEquipment = options.backEquipment === undefined ? "cape" : options.backEquipment;
|
this.playerObject.backEquipment = options.backEquipment === undefined ? "cape" : options.backEquipment;
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
return loadImage(source).then(image => this.loadCape(image, options));
|
return loadImage(source).then(image => this.loadCape(image, options));
|
||||||
}
|
}
|
||||||
|
@ -305,75 +178,13 @@ export class SkinViewer {
|
||||||
this.playerObject.backEquipment = null;
|
this.playerObject.backEquipment = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
loadEars(empty: null): void;
|
|
||||||
loadEars<S extends TextureSource | RemoteImage>(
|
|
||||||
source: S,
|
|
||||||
options?: EarsLoadOptions
|
|
||||||
): S extends TextureSource ? void : Promise<void>;
|
|
||||||
|
|
||||||
loadEars(
|
|
||||||
source: TextureSource | RemoteImage | null,
|
|
||||||
options: EarsLoadOptions = {}
|
|
||||||
): void | Promise<void> {
|
|
||||||
if (source === null) {
|
|
||||||
this.resetEars();
|
|
||||||
|
|
||||||
} else if (isTextureSource(source)) {
|
|
||||||
if (options.textureType === "skin") {
|
|
||||||
loadEarsToCanvasFromSkin(this.earsCanvas, source);
|
|
||||||
} else {
|
|
||||||
loadEarsToCanvas(this.earsCanvas, source);
|
|
||||||
}
|
|
||||||
this.earsTexture.needsUpdate = true;
|
|
||||||
|
|
||||||
if (options.makeVisible !== false) {
|
|
||||||
this.playerObject.ears.visible = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
return loadImage(source).then(image => this.loadEars(image, options));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resetEars(): void {
|
|
||||||
this.playerObject.ears.visible = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
loadPanorama<S extends TextureSource | RemoteImage>(
|
|
||||||
source: S
|
|
||||||
): S extends TextureSource ? void : Promise<void> {
|
|
||||||
return this.loadBackground(source, EquirectangularReflectionMapping);
|
|
||||||
}
|
|
||||||
|
|
||||||
loadBackground<S extends TextureSource | RemoteImage>(
|
|
||||||
source: S,
|
|
||||||
mapping?: Mapping
|
|
||||||
): S extends TextureSource ? void : Promise<void>;
|
|
||||||
|
|
||||||
loadBackground<S extends TextureSource | RemoteImage>(
|
|
||||||
source: S,
|
|
||||||
mapping?: Mapping
|
|
||||||
): void | Promise<void> {
|
|
||||||
if (isTextureSource(source)) {
|
|
||||||
if (this.backgroundTexture !== null) {
|
|
||||||
this.backgroundTexture.dispose();
|
|
||||||
}
|
|
||||||
this.backgroundTexture = new Texture();
|
|
||||||
this.backgroundTexture.image = source;
|
|
||||||
if (mapping !== undefined) {
|
|
||||||
this.backgroundTexture.mapping = mapping;
|
|
||||||
}
|
|
||||||
this.backgroundTexture.needsUpdate = true;
|
|
||||||
this.scene.background = this.backgroundTexture;
|
|
||||||
} else {
|
|
||||||
return loadImage(source).then(image => this.loadBackground(image, mapping));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private draw(): void {
|
private draw(): void {
|
||||||
|
if (this.disposed || this._renderPaused) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.animations.runAnimationLoop(this.playerObject);
|
this.animations.runAnimationLoop(this.playerObject);
|
||||||
this.render();
|
this.render();
|
||||||
this.animationID = window.requestAnimationFrame(() => this.draw());
|
window.requestAnimationFrame(() => this.draw());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -392,22 +203,9 @@ export class SkinViewer {
|
||||||
|
|
||||||
dispose(): void {
|
dispose(): void {
|
||||||
this._disposed = true;
|
this._disposed = true;
|
||||||
|
|
||||||
this.canvas.removeEventListener("webglcontextlost", this.onContextLost, false);
|
|
||||||
this.canvas.removeEventListener("webglcontextrestored", this.onContextRestored, false);
|
|
||||||
|
|
||||||
if (this.animationID !== null) {
|
|
||||||
window.cancelAnimationFrame(this.animationID);
|
|
||||||
this.animationID = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.renderer.dispose();
|
this.renderer.dispose();
|
||||||
this.skinTexture.dispose();
|
this.skinTexture.dispose();
|
||||||
this.capeTexture.dispose();
|
this.capeTexture.dispose();
|
||||||
if (this.backgroundTexture !== null) {
|
|
||||||
this.backgroundTexture.dispose();
|
|
||||||
this.backgroundTexture = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get disposed(): boolean {
|
get disposed(): boolean {
|
||||||
|
@ -424,13 +222,10 @@ export class SkinViewer {
|
||||||
}
|
}
|
||||||
|
|
||||||
set renderPaused(value: boolean) {
|
set renderPaused(value: boolean) {
|
||||||
|
const toResume = !this.disposed && !value && this._renderPaused;
|
||||||
this._renderPaused = value;
|
this._renderPaused = value;
|
||||||
|
if (toResume) {
|
||||||
if (this._renderPaused && this.animationID !== null) {
|
window.requestAnimationFrame(() => this.draw());
|
||||||
window.cancelAnimationFrame(this.animationID);
|
|
||||||
this.animationID = null;
|
|
||||||
} else if (!this._renderPaused && !this._disposed && !this.renderer.getContext().isContextLost() && this.animationID == null) {
|
|
||||||
this.animationID = window.requestAnimationFrame(() => this.draw());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -449,52 +244,4 @@ export class SkinViewer {
|
||||||
set height(newHeight: number) {
|
set height(newHeight: number) {
|
||||||
this.setSize(this.width, newHeight);
|
this.setSize(this.width, newHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
get background(): null | Color | Texture {
|
|
||||||
return this.scene.background;
|
|
||||||
}
|
|
||||||
|
|
||||||
set background(value: null | ColorRepresentation | Texture) {
|
|
||||||
if (value === null || value instanceof Color || value instanceof Texture) {
|
|
||||||
this.scene.background = value;
|
|
||||||
} else {
|
|
||||||
this.scene.background = new Color(value);
|
|
||||||
}
|
|
||||||
if (this.backgroundTexture !== null && value !== this.backgroundTexture) {
|
|
||||||
this.backgroundTexture.dispose();
|
|
||||||
this.backgroundTexture = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
adjustCameraDistance(): void {
|
|
||||||
let distance = 4.5 + 16.5 / Math.tan(this.fov / 180 * Math.PI / 2) / this.zoom;
|
|
||||||
|
|
||||||
// limit distance between 10 ~ 256 (default min / max distance of OrbitControls)
|
|
||||||
if (distance < 10) {
|
|
||||||
distance = 10;
|
|
||||||
} else if (distance > 256) {
|
|
||||||
distance = 256;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.camera.position.multiplyScalar(distance / this.camera.position.length());
|
|
||||||
this.camera.updateProjectionMatrix();
|
|
||||||
}
|
|
||||||
|
|
||||||
get fov(): number {
|
|
||||||
return this.camera.fov;
|
|
||||||
}
|
|
||||||
|
|
||||||
set fov(value: number) {
|
|
||||||
this.camera.fov = value;
|
|
||||||
this.adjustCameraDistance();
|
|
||||||
}
|
|
||||||
|
|
||||||
get zoom(): number {
|
|
||||||
return this._zoom;
|
|
||||||
}
|
|
||||||
|
|
||||||
set zoom(value: number) {
|
|
||||||
this._zoom = value;
|
|
||||||
this.adjustCameraDistance();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue