Added animated capes and ears

This commit is contained in:
James Harrison 2020-11-19 20:33:11 +00:00
parent 6b752c423a
commit 8eb3f83009
5 changed files with 3451 additions and 179 deletions

View File

@ -235,8 +235,6 @@
<input id="cape_url_upload" type="file" accept="image/*" style="display: none;">
<button type="button" class="control"
onclick="document.getElementById('cape_url_upload').click();">Browse...</button>
<button type="button" class="control"
onclick="skinViewer.toggleElytra()">Toggle Elytra</button>
</div>
</div>
</div>
@ -309,11 +307,11 @@
const input = document.getElementById("cape_url");
const url = input.value;
if (url === "") {
skinViewer.loadCustomCape(null);
skinViewer.loadCape(null);
input.setCustomValidity("");
} else {
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(""))
.catch(e => {
input.setCustomValidity("Image can't be loaded.");

3472
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -38,20 +38,20 @@
"bundles"
],
"dependencies": {
"skinview-utils": "^0.5.9",
"skinview-utils": "file:../skinview-utils",
"three": "^0.122.0"
},
"devDependencies": {
"@rollup/plugin-node-resolve": "^10.0.0",
"@rollup/plugin-typescript": "^6.1.0",
"@typescript-eslint/eslint-plugin": "^4.6.0",
"@typescript-eslint/parser": "^4.6.0",
"@typescript-eslint/eslint-plugin": "^4.8.1",
"@typescript-eslint/parser": "^4.8.1",
"@yushijinhun/three-minifier-rollup": "^0.2.0",
"eslint": "^7.12.1",
"eslint": "^7.13.0",
"local-web-server": "^4.2.1",
"npm-run-all": "^4.1.5",
"rimraf": "^3.0.2",
"rollup": "^2.32.1",
"rollup": "^2.33.3",
"rollup-plugin-terser": "^7.0.2",
"typescript": "^4.0.5"
}

View File

@ -366,15 +366,8 @@ export class EarsObject extends Group {
// front = inside
const earBox = new BoxGeometry(6, 6, 1);
//x1: number, y1: number, x2: number, y2: number
setVertices(earBox,
//from look at back
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
);
setEarUVs(earBox, 0, 0, 6, 6, 1);
//setCapeUVs(leftWingBox, 22, 0, 10, 20, 2);
this.leftEar = new Mesh(earBox, earMaterial);
this.leftEar.position.x = -5.5;
@ -412,6 +405,7 @@ export class PlayerObject extends Group {
this.cape = new CapeObject(capeTexture);
this.cape.name = "cape";
this.cape.position.z = -2;
this.cape.position.y = -2;
this.cape.rotation.x = 10.8 * Math.PI / 180;
this.cape.rotation.y = Math.PI;
this.add(this.cape);
@ -419,12 +413,13 @@ export class PlayerObject extends Group {
this.elytra = new ElytraObject(capeTexture);
this.elytra.name = "elytra";
this.elytra.position.z = -2;
this.elytra.position.y = -2;
this.elytra.visible = false;
this.add(this.elytra);
this.ears = new EarsObject(earTexture);
this.ears.name = "ears";
this.ears.position.y = 3.5;
this.ears.position.y = 7;
this.add(this.ears);
}

View File

@ -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 { RootAnimation } from "./animation.js";
import { BackEquipment, PlayerObject } from "./model.js";
@ -59,19 +59,11 @@ class SkinViewer {
readonly skinCanvas: HTMLCanvasElement;
readonly capeCanvas: HTMLCanvasElement;
readonly earCanvas: HTMLCanvasElement;
readonly earsCanvas: HTMLCanvasElement;
private readonly skinTexture: Texture;
private readonly capeTexture: 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 _renderPaused: boolean = false;
@ -89,26 +81,18 @@ class SkinViewer {
this.capeTexture.magFilter = NearestFilter;
this.capeTexture.minFilter = NearestFilter;
this.earCanvas = document.createElement("canvas");
this.earTexture = new Texture(this.earCanvas);
this.earsCanvas = document.createElement("canvas");
this.earTexture = new Texture(this.earsCanvas);
this.earTexture.magFilter = 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
this.scene = new Scene();
// Use smaller fov to avoid distortion
this.camera = new PerspectiveCamera(40);
this.camera.position.y = -8;
this.camera.position.z = 60;
this.camera.position.z = 63;
this.renderer = new WebGLRenderer({
canvas: this.canvas,
@ -129,7 +113,7 @@ class SkinViewer {
this.loadSkin(options.skin);
}
if (options.cape !== undefined) {
this.loadCustomCape(options.cape);
this.loadCape(options.cape);
}
if (options.ears !== undefined) {
this.loadEars(options.ears);
@ -163,9 +147,9 @@ class SkinViewer {
}
}
protected earsLoaded(options?: LoadOptions): void {
protected earsLoaded(options: LoadOptions = {}): void {
this.earTexture.needsUpdate = true;
if (toMakeVisible(options)) {
if (options.makeVisible !== false) {
this.playerObject.ears.visible = true;
}
}
@ -189,7 +173,8 @@ class SkinViewer {
this.animations.runAnimationLoop(this.playerObject);
this.render();
this.animatedCape();
this.animateCape({ makeVisible: false })
window.requestAnimationFrame(() => this.draw());
}
@ -212,6 +197,7 @@ class SkinViewer {
this.renderer.dispose();
this.skinTexture.dispose();
this.capeTexture.dispose();
this.earTexture.dispose();
}
get disposed(): boolean {
@ -250,94 +236,7 @@ class SkinViewer {
set height(newHeight: number) {
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> { }
applyMixins(SkinViewer, [SkinContainer, CapeContainer]);
interface SkinViewer extends SkinContainer<LoadOptions>, CapeContainer<CapeLoadOptions>, EarsContainer<LoadOptions> { }
applyMixins(SkinViewer, [SkinContainer, CapeContainer, EarsContainer]);
export { SkinViewer };