Added animated capes and ears
This commit is contained in:
parent
6b752c423a
commit
8eb3f83009
|
|
@ -235,8 +235,6 @@
|
||||||
<input id="cape_url_upload" type="file" accept="image/*" style="display: none;">
|
<input id="cape_url_upload" type="file" accept="image/*" style="display: none;">
|
||||||
<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>
|
||||||
<button type="button" class="control"
|
|
||||||
onclick="skinViewer.toggleElytra()">Toggle Elytra</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -309,11 +307,11 @@
|
||||||
const input = document.getElementById("cape_url");
|
const input = document.getElementById("cape_url");
|
||||||
const url = input.value;
|
const url = input.value;
|
||||||
if (url === "") {
|
if (url === "") {
|
||||||
skinViewer.loadCustomCape(null);
|
skinViewer.loadCape(null);
|
||||||
input.setCustomValidity("");
|
input.setCustomValidity("");
|
||||||
} else {
|
} else {
|
||||||
const selectedBackEquipment = document.querySelector('input[type="radio"][name="back_equipment"]:checked');
|
const selectedBackEquipment = document.querySelector('input[type="radio"][name="back_equipment"]:checked');
|
||||||
skinViewer.loadCustomCape(url, { backEquipment: selectedBackEquipment.value })
|
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.");
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
10
package.json
10
package.json
|
|
@ -38,20 +38,20 @@
|
||||||
"bundles"
|
"bundles"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"skinview-utils": "^0.5.9",
|
"skinview-utils": "file:../skinview-utils",
|
||||||
"three": "^0.122.0"
|
"three": "^0.122.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rollup/plugin-node-resolve": "^10.0.0",
|
"@rollup/plugin-node-resolve": "^10.0.0",
|
||||||
"@rollup/plugin-typescript": "^6.1.0",
|
"@rollup/plugin-typescript": "^6.1.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.6.0",
|
"@typescript-eslint/eslint-plugin": "^4.8.1",
|
||||||
"@typescript-eslint/parser": "^4.6.0",
|
"@typescript-eslint/parser": "^4.8.1",
|
||||||
"@yushijinhun/three-minifier-rollup": "^0.2.0",
|
"@yushijinhun/three-minifier-rollup": "^0.2.0",
|
||||||
"eslint": "^7.12.1",
|
"eslint": "^7.13.0",
|
||||||
"local-web-server": "^4.2.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.32.1",
|
"rollup": "^2.33.3",
|
||||||
"rollup-plugin-terser": "^7.0.2",
|
"rollup-plugin-terser": "^7.0.2",
|
||||||
"typescript": "^4.0.5"
|
"typescript": "^4.0.5"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
15
src/model.ts
15
src/model.ts
|
|
@ -366,15 +366,8 @@ export class EarsObject extends Group {
|
||||||
// front = inside
|
// front = inside
|
||||||
const earBox = new BoxGeometry(6, 6, 1);
|
const earBox = new BoxGeometry(6, 6, 1);
|
||||||
//x1: number, y1: number, x2: number, y2: number
|
//x1: number, y1: number, x2: number, y2: number
|
||||||
setVertices(earBox,
|
setEarUVs(earBox, 0, 0, 6, 6, 1);
|
||||||
//from look at back
|
//setCapeUVs(leftWingBox, 22, 0, 10, 20, 2);
|
||||||
toEarVertices(1, 0, 7, 1), //top
|
|
||||||
toEarVertices(7, 0, 13, 1), //bottom
|
|
||||||
toEarVertices(0, 1, 1, 7), //right
|
|
||||||
toEarVertices(1, 1, 7, 7), //front
|
|
||||||
toEarVertices(0, 1, 1, 7), //left
|
|
||||||
toEarVertices(8, 1, 14, 7) //back
|
|
||||||
);
|
|
||||||
|
|
||||||
this.leftEar = new Mesh(earBox, earMaterial);
|
this.leftEar = new Mesh(earBox, earMaterial);
|
||||||
this.leftEar.position.x = -5.5;
|
this.leftEar.position.x = -5.5;
|
||||||
|
|
@ -412,6 +405,7 @@ export class PlayerObject extends Group {
|
||||||
this.cape = new CapeObject(capeTexture);
|
this.cape = new CapeObject(capeTexture);
|
||||||
this.cape.name = "cape";
|
this.cape.name = "cape";
|
||||||
this.cape.position.z = -2;
|
this.cape.position.z = -2;
|
||||||
|
this.cape.position.y = -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;
|
||||||
this.add(this.cape);
|
this.add(this.cape);
|
||||||
|
|
@ -419,12 +413,13 @@ 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.z = -2;
|
this.elytra.position.z = -2;
|
||||||
|
this.elytra.position.y = -2;
|
||||||
this.elytra.visible = false;
|
this.elytra.visible = false;
|
||||||
this.add(this.elytra);
|
this.add(this.elytra);
|
||||||
|
|
||||||
this.ears = new EarsObject(earTexture);
|
this.ears = new EarsObject(earTexture);
|
||||||
this.ears.name = "ears";
|
this.ears.name = "ears";
|
||||||
this.ears.position.y = 3.5;
|
this.ears.position.y = 7;
|
||||||
this.add(this.ears);
|
this.add(this.ears);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
127
src/viewer.ts
127
src/viewer.ts
|
|
@ -1,4 +1,4 @@
|
||||||
import { applyMixins, CapeContainer, ModelType, SkinContainer, RemoteImage, TextureSource, TextureCanvas, loadImage, isTextureSource } from "skinview-utils";
|
import { applyMixins, CapeContainer, EarsContainer, 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 { BackEquipment, PlayerObject } from "./model.js";
|
import { BackEquipment, PlayerObject } from "./model.js";
|
||||||
|
|
@ -59,19 +59,11 @@ class SkinViewer {
|
||||||
|
|
||||||
readonly skinCanvas: HTMLCanvasElement;
|
readonly skinCanvas: HTMLCanvasElement;
|
||||||
readonly capeCanvas: HTMLCanvasElement;
|
readonly capeCanvas: HTMLCanvasElement;
|
||||||
readonly earCanvas: HTMLCanvasElement;
|
readonly earsCanvas: HTMLCanvasElement;
|
||||||
private readonly skinTexture: Texture;
|
private readonly skinTexture: Texture;
|
||||||
private readonly capeTexture: Texture;
|
private readonly capeTexture: Texture;
|
||||||
private readonly earTexture: Texture;
|
private readonly earTexture: Texture;
|
||||||
|
|
||||||
// Animated Capes (MinecraftCapes)
|
|
||||||
private isCapeAnimated: boolean
|
|
||||||
private customCapeImage: TextureSource
|
|
||||||
private lastFrame: number;
|
|
||||||
private maxFrames: number;
|
|
||||||
private lastFrameTime: number;
|
|
||||||
private capeInterval: number;
|
|
||||||
|
|
||||||
private _disposed: boolean = false;
|
private _disposed: boolean = false;
|
||||||
private _renderPaused: boolean = false;
|
private _renderPaused: boolean = false;
|
||||||
|
|
||||||
|
|
@ -89,26 +81,18 @@ class SkinViewer {
|
||||||
this.capeTexture.magFilter = NearestFilter;
|
this.capeTexture.magFilter = NearestFilter;
|
||||||
this.capeTexture.minFilter = NearestFilter;
|
this.capeTexture.minFilter = NearestFilter;
|
||||||
|
|
||||||
this.earCanvas = document.createElement("canvas");
|
this.earsCanvas = document.createElement("canvas");
|
||||||
this.earTexture = new Texture(this.earCanvas);
|
this.earTexture = new Texture(this.earsCanvas);
|
||||||
this.earTexture.magFilter = NearestFilter;
|
this.earTexture.magFilter = NearestFilter;
|
||||||
this.earTexture.minFilter = NearestFilter;
|
this.earTexture.minFilter = NearestFilter;
|
||||||
|
|
||||||
// Animated Capes (MinecraftCapes)
|
|
||||||
this.isCapeAnimated = false;
|
|
||||||
this.customCapeImage = new Image()
|
|
||||||
this.lastFrame = 0,
|
|
||||||
this.maxFrames = 1,
|
|
||||||
this.lastFrameTime = 0,
|
|
||||||
this.capeInterval = 100,
|
|
||||||
|
|
||||||
// scene
|
// scene
|
||||||
this.scene = new Scene();
|
this.scene = new Scene();
|
||||||
|
|
||||||
// Use smaller fov to avoid distortion
|
// Use smaller fov to avoid distortion
|
||||||
this.camera = new PerspectiveCamera(40);
|
this.camera = new PerspectiveCamera(40);
|
||||||
this.camera.position.y = -8;
|
this.camera.position.y = -8;
|
||||||
this.camera.position.z = 60;
|
this.camera.position.z = 63;
|
||||||
|
|
||||||
this.renderer = new WebGLRenderer({
|
this.renderer = new WebGLRenderer({
|
||||||
canvas: this.canvas,
|
canvas: this.canvas,
|
||||||
|
|
@ -129,7 +113,7 @@ class SkinViewer {
|
||||||
this.loadSkin(options.skin);
|
this.loadSkin(options.skin);
|
||||||
}
|
}
|
||||||
if (options.cape !== undefined) {
|
if (options.cape !== undefined) {
|
||||||
this.loadCustomCape(options.cape);
|
this.loadCape(options.cape);
|
||||||
}
|
}
|
||||||
if (options.ears !== undefined) {
|
if (options.ears !== undefined) {
|
||||||
this.loadEars(options.ears);
|
this.loadEars(options.ears);
|
||||||
|
|
@ -163,9 +147,9 @@ class SkinViewer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected earsLoaded(options?: LoadOptions): void {
|
protected earsLoaded(options: LoadOptions = {}): void {
|
||||||
this.earTexture.needsUpdate = true;
|
this.earTexture.needsUpdate = true;
|
||||||
if (toMakeVisible(options)) {
|
if (options.makeVisible !== false) {
|
||||||
this.playerObject.ears.visible = true;
|
this.playerObject.ears.visible = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -189,7 +173,8 @@ class SkinViewer {
|
||||||
this.animations.runAnimationLoop(this.playerObject);
|
this.animations.runAnimationLoop(this.playerObject);
|
||||||
this.render();
|
this.render();
|
||||||
|
|
||||||
this.animatedCape();
|
this.animateCape({ makeVisible: false })
|
||||||
|
|
||||||
window.requestAnimationFrame(() => this.draw());
|
window.requestAnimationFrame(() => this.draw());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -212,6 +197,7 @@ class SkinViewer {
|
||||||
this.renderer.dispose();
|
this.renderer.dispose();
|
||||||
this.skinTexture.dispose();
|
this.skinTexture.dispose();
|
||||||
this.capeTexture.dispose();
|
this.capeTexture.dispose();
|
||||||
|
this.earTexture.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
get disposed(): boolean {
|
get disposed(): boolean {
|
||||||
|
|
@ -250,94 +236,7 @@ class SkinViewer {
|
||||||
set height(newHeight: number) {
|
set height(newHeight: number) {
|
||||||
this.setSize(this.width, newHeight);
|
this.setSize(this.width, newHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Code for MinecraftCapes
|
|
||||||
*/
|
|
||||||
public loadCustomCape(source: TextureSource | RemoteImage | null): void | Promise<void> {
|
|
||||||
if(source === null) {
|
|
||||||
this.resetCape();
|
|
||||||
} else if(isTextureSource(source)) {
|
|
||||||
this.customCapeImage = source;
|
|
||||||
this.loadCapeToCanvas(this.capeCanvas, source, 0);
|
|
||||||
} else {
|
|
||||||
loadImage(source).then(image => this.loadCustomCape(image));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected loadCapeToCanvas(canvas: TextureCanvas, image: TextureSource, offset: number): void {
|
|
||||||
let canvasWidth = 64;
|
|
||||||
let canvasHeight = 32;
|
|
||||||
|
|
||||||
if((image.height > image.width / 2) && (image.height >= image.width)) {
|
|
||||||
this.isCapeAnimated = true;
|
|
||||||
canvasWidth = image.width
|
|
||||||
canvasHeight = image.width / 2
|
|
||||||
} else {
|
|
||||||
while(image.width > canvasWidth) {
|
|
||||||
canvasWidth *= 2
|
|
||||||
canvasHeight *= 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
canvas.width = canvasWidth,
|
|
||||||
canvas.height = canvasHeight;
|
|
||||||
|
|
||||||
const frame = canvas.getContext("2d");
|
|
||||||
if(frame != null) {
|
|
||||||
frame.clearRect(0, 0, canvas.width, canvas.height);
|
|
||||||
if(this.isCapeAnimated) {
|
|
||||||
frame.drawImage(image, 0, offset, canvas.width, canvas.height, 0, 0, canvas.width, canvas.height);
|
|
||||||
} else {
|
|
||||||
frame.drawImage(image, 0, 0);
|
|
||||||
}
|
|
||||||
this.capeLoaded();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected animatedCape(): void {
|
|
||||||
if(!this.isCapeAnimated) return;
|
|
||||||
|
|
||||||
if (this.customCapeImage.height !== this.customCapeImage.width / 2) {
|
|
||||||
const currentTime = Date.now();
|
|
||||||
if (currentTime > this.lastFrameTime + this.capeInterval) {
|
|
||||||
this.maxFrames = this.customCapeImage.height / (this.customCapeImage.width / 2);
|
|
||||||
const currentFrame = this.lastFrame + 1 > this.maxFrames - 1 ? 0 : this.lastFrame + 1;
|
|
||||||
this.lastFrame = currentFrame,
|
|
||||||
this.lastFrameTime = currentTime;
|
|
||||||
const offset = currentFrame * (this.customCapeImage.width / 2);
|
|
||||||
this.loadCapeToCanvas(this.capeCanvas, this.customCapeImage, offset),
|
|
||||||
this.capeTexture.needsUpdate = true
|
|
||||||
this.playerObject.cape.visible = !this.playerObject.elytra.visible;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public loadEars(source: TextureSource | RemoteImage | null): void | Promise<void> {
|
|
||||||
if(source === null) {
|
|
||||||
this.resetEars();
|
|
||||||
} else if(isTextureSource(source)) {
|
|
||||||
this.loadEarsToCanvas(this.earCanvas, source);
|
|
||||||
this.earsLoaded();
|
|
||||||
} else {
|
|
||||||
loadImage(source).then(image => this.loadEars(image));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected loadEarsToCanvas(canvas: TextureCanvas, image: TextureSource): void {
|
|
||||||
canvas.width = 14;
|
|
||||||
canvas.height = 7;
|
|
||||||
|
|
||||||
const context = canvas.getContext("2d");
|
|
||||||
context?.clearRect(0, 0, canvas.width, canvas.height);
|
|
||||||
context?.drawImage(image, 0, 0, image.width, image.height);
|
|
||||||
}
|
|
||||||
|
|
||||||
public toggleElytra(): void {
|
|
||||||
this.playerObject.cape.visible = !this.playerObject.cape.visible;
|
|
||||||
this.playerObject.elytra.visible = !this.playerObject.cape.visible;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
interface SkinViewer extends SkinContainer<LoadOptions>, CapeContainer<CapeLoadOptions> { }
|
interface SkinViewer extends SkinContainer<LoadOptions>, CapeContainer<CapeLoadOptions>, EarsContainer<LoadOptions> { }
|
||||||
applyMixins(SkinViewer, [SkinContainer, CapeContainer]);
|
applyMixins(SkinViewer, [SkinContainer, CapeContainer, EarsContainer]);
|
||||||
export { SkinViewer };
|
export { SkinViewer };
|
||||||
Loading…
Reference in New Issue