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;">
|
||||
<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.");
|
||||
|
|
File diff suppressed because it is too large
Load Diff
10
package.json
10
package.json
|
@ -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"
|
||||
}
|
||||
|
|
15
src/model.ts
15
src/model.ts
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
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 { 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 };
|
Loading…
Reference in New Issue