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;"> <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.");

3472
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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"
} }

View File

@ -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);
} }

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 { 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 };