Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 82 additions & 12 deletions examples/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<title>skinview3d</title>
<style>
body {
margin: unset !important;
margin: 0;
padding: 20px;
display: flex;
flex-direction: row;
Expand Down Expand Up @@ -37,20 +37,13 @@
#skin_container {
width: 100%;
height: 400px;
position: relative;
top: 0;
}

.controls {
padding-right: 0;
}
#canvas_warp {
width: 100%;
top: 0px;
background: rgba(255, 255, 255, 0.6);
position: sticky;
backdrop-filter: blur(10px);
padding-bottom: 10px;
}
}

.control-section {
Expand All @@ -69,6 +62,28 @@
margin: 5px 0;
}

#rotation_table {
width: 100%;
border-collapse: collapse;
font-size: 0.85em;
}

#rotation_table th,
#rotation_table td {
padding: 4px 2px;
text-align: center;
}

#rotation_table th {
font-weight: bold;
}

#rotation_table input[type="number"] {
width: 50px;
padding: 2px;
font-size: 0.9em;
}

footer {
position: fixed;
bottom: 0;
Expand All @@ -83,9 +98,8 @@
</head>

<body>
<div id="canvas_warp">
<canvas id="skin_container"></canvas>
</div>
<canvas id="skin_container"></canvas>

<div class="controls">
<button id="reset_all" type="button" class="control">Reset All</button>

Expand Down Expand Up @@ -121,6 +135,62 @@ <h1>Rotation</h1>
<label class="control"
>Speed: <input id="auto_rotate_speed" type="number" value="2" step="0.1" size="3"
/></label>

<h2>Body Part Rotation</h2>
<table id="rotation_table">
<thead>
<tr>
<th></th>
<th>Player</th>
<th>Head</th>
<th>Body</th>
<th>Right Arm</th>
<th>Left Arm</th>
<th>Right Leg</th>
<th>Left Leg</th>
<th>Cape</th>
<th>Elytra</th>
</tr>
</thead>
<tbody>
<tr>
<th>X</th>
<td><input type="number" data-rotation-part="player" data-rotation-axis="x" value="0" step="0.1" size="4" /></td>
<td><input type="number" data-rotation-part="head" data-rotation-axis="x" value="0" step="0.1" size="4" /></td>
<td><input type="number" data-rotation-part="body" data-rotation-axis="x" value="0" step="0.1" size="4" /></td>
<td><input type="number" data-rotation-part="rightArm" data-rotation-axis="x" value="0" step="0.1" size="4" /></td>
<td><input type="number" data-rotation-part="leftArm" data-rotation-axis="x" value="0" step="0.1" size="4" /></td>
<td><input type="number" data-rotation-part="rightLeg" data-rotation-axis="x" value="0" step="0.1" size="4" /></td>
<td><input type="number" data-rotation-part="leftLeg" data-rotation-axis="x" value="0" step="0.1" size="4" /></td>
<td><input type="number" data-rotation-part="cape" data-rotation-axis="x" value="0" step="0.1" size="4" /></td>
<td><input type="number" data-rotation-part="elytra" data-rotation-axis="x" value="0" step="0.1" size="4" /></td>
</tr>
<tr>
<th>Y</th>
<td><input type="number" data-rotation-part="player" data-rotation-axis="y" value="0" step="0.1" size="4" /></td>
<td><input type="number" data-rotation-part="head" data-rotation-axis="y" value="0" step="0.1" size="4" /></td>
<td><input type="number" data-rotation-part="body" data-rotation-axis="y" value="0" step="0.1" size="4" /></td>
<td><input type="number" data-rotation-part="rightArm" data-rotation-axis="y" value="0" step="0.1" size="4" /></td>
<td><input type="number" data-rotation-part="leftArm" data-rotation-axis="y" value="0" step="0.1" size="4" /></td>
<td><input type="number" data-rotation-part="rightLeg" data-rotation-axis="y" value="0" step="0.1" size="4" /></td>
<td><input type="number" data-rotation-part="leftLeg" data-rotation-axis="y" value="0" step="0.1" size="4" /></td>
<td><input type="number" data-rotation-part="cape" data-rotation-axis="y" value="0" step="0.1" size="4" /></td>
<td><input type="number" data-rotation-part="elytra" data-rotation-axis="y" value="0" step="0.1" size="4" /></td>
</tr>
<tr>
<th>Z</th>
<td><input type="number" data-rotation-part="player" data-rotation-axis="z" value="0" step="0.1" size="4" /></td>
<td><input type="number" data-rotation-part="head" data-rotation-axis="z" value="0" step="0.1" size="4" /></td>
<td><input type="number" data-rotation-part="body" data-rotation-axis="z" value="0" step="0.1" size="4" /></td>
<td><input type="number" data-rotation-part="rightArm" data-rotation-axis="z" value="0" step="0.1" size="4" /></td>
<td><input type="number" data-rotation-part="leftArm" data-rotation-axis="z" value="0" step="0.1" size="4" /></td>
<td><input type="number" data-rotation-part="rightLeg" data-rotation-axis="z" value="0" step="0.1" size="4" /></td>
<td><input type="number" data-rotation-part="leftLeg" data-rotation-axis="z" value="0" step="0.1" size="4" /></td>
<td><input type="number" data-rotation-part="cape" data-rotation-axis="z" value="0" step="0.1" size="4" /></td>
<td><input type="number" data-rotation-part="elytra" data-rotation-axis="z" value="0" step="0.1" size="4" /></td>
</tr>
</tbody>
</table>
</div>

<div class="control-section">
Expand Down
16 changes: 16 additions & 0 deletions examples/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,22 @@ function initializeControls(): void {
skinViewer.autoRotateSpeed = Number(target.value);
});

const rotationInputs = document.querySelectorAll<HTMLInputElement>("#rotation_table input[type='number']");
for (const input of rotationInputs) {
input.addEventListener("change", e => {
const target = e.target as HTMLInputElement;
const part = target.dataset.rotationPart;
const axis = target.dataset.rotationAxis as "x" | "y" | "z";
if (part && axis) {
const currentRotation = skinViewer.rotation[part as keyof typeof skinViewer.rotation];
if (currentRotation) {
(currentRotation as any)[axis] = Number(target.value);
skinViewer.rotation = { [part]: currentRotation };
}
}
});
}

const animationRadios = document.querySelectorAll<HTMLInputElement>('input[type="radio"][name="animation"]');
for (const el of animationRadios) {
el.addEventListener("change", e => {
Expand Down
2 changes: 1 addition & 1 deletion src/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -469,7 +469,7 @@ export class EarsObject extends Group {

export type BackEquipment = "cape" | "elytra";

const CapeDefaultAngle = (10.8 * Math.PI) / 180;
export const CapeDefaultAngle = (10.8 * Math.PI) / 180;

export class PlayerObject extends Group {
readonly skin: SkinObject;
Expand Down
177 changes: 176 additions & 1 deletion src/viewer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import { RenderPass } from "three/examples/jsm/postprocessing/RenderPass.js";
import { ShaderPass } from "three/examples/jsm/postprocessing/ShaderPass.js";
import { FXAAShader } from "three/examples/jsm/shaders/FXAAShader.js";
import { PlayerAnimation } from "./animation.js";
import { type BackEquipment, PlayerObject } from "./model.js";
import { type BackEquipment, PlayerObject, CapeDefaultAngle } from "./model.js";
import { NameTagObject } from "./nametag.js";

export interface LoadOptions {
Expand Down Expand Up @@ -246,6 +246,64 @@ export interface SkinViewerOptions {
* @see {@link SkinViewer.nameTag}
*/
nameTag?: NameTagObject | string;

/**
* The initial rotation of body parts.
*
* @defaultValue If unspecified, all rotations are 0.
* @see {@link SkinViewer.rotation}
*/
rotation?: BodyPartRotation;
}

/**
* The rotation of body parts.
*/
export interface BodyPartRotation {
/**
* The rotation of the whole player object.
*/
player?: { x?: number; y?: number; z?: number };

/**
* The rotation of the head.
*/
head?: { x?: number; y?: number; z?: number };

/**
* The rotation of the body.
*/
body?: { x?: number; y?: number; z?: number };

/**
* The rotation of the right arm.
*/
rightArm?: { x?: number; y?: number; z?: number };

/**
* The rotation of the left arm.
*/
leftArm?: { x?: number; y?: number; z?: number };

/**
* The rotation of the right leg.
*/
rightLeg?: { x?: number; y?: number; z?: number };

/**
* The rotation of the left leg.
*/
leftLeg?: { x?: number; y?: number; z?: number };

/**
* The rotation of the cape.
*/
cape?: { x?: number; y?: number; z?: number };

/**
* The rotation of the elytra.
*/
elytra?: { x?: number; y?: number; z?: number };
}

/**
Expand Down Expand Up @@ -430,6 +488,9 @@ export class SkinViewer {
if (options.nameTag !== undefined) {
this.nameTag = options.nameTag;
}
if (options.rotation !== undefined) {
this.rotation = options.rotation;
}
this.camera.position.z = 1;
this._zoom = options.zoom === undefined ? 0.9 : options.zoom;
this.fov = options.fov === undefined ? 50 : options.fov;
Expand Down Expand Up @@ -908,4 +969,118 @@ export class SkinViewer {

this._nameTag = newVal;
}

/**
* The rotation of body parts.
*
* This property allows you to control the rotation of individual body parts.
* The rotations are in radians.
*
* @example
* ```
* // Rotate the head to look up
* skinViewer.rotation.head = { x: -0.5, y: 0, z: 0 };
*
* // Rotate both arms
* skinViewer.rotation.leftArm = { x: 0, y: 0, z: 0.5 };
* skinViewer.rotation.rightArm = { x: 0, y: 0, z: -0.5 };
* ```
*/
get rotation(): BodyPartRotation {
return {
player: {
x: this.playerObject.rotation.x,
y: this.playerObject.rotation.y,
z: this.playerObject.rotation.z,
},
head: {
x: this.playerObject.skin.head.rotation.x,
y: this.playerObject.skin.head.rotation.y,
z: this.playerObject.skin.head.rotation.z,
},
body: {
x: this.playerObject.skin.body.rotation.x,
y: this.playerObject.skin.body.rotation.y,
z: this.playerObject.skin.body.rotation.z,
},
rightArm: {
x: this.playerObject.skin.rightArm.rotation.x,
y: this.playerObject.skin.rightArm.rotation.y,
z: this.playerObject.skin.rightArm.rotation.z,
},
leftArm: {
x: this.playerObject.skin.leftArm.rotation.x,
y: this.playerObject.skin.leftArm.rotation.y,
z: this.playerObject.skin.leftArm.rotation.z,
},
rightLeg: {
x: this.playerObject.skin.rightLeg.rotation.x,
y: this.playerObject.skin.rightLeg.rotation.y,
z: this.playerObject.skin.rightLeg.rotation.z,
},
leftLeg: {
x: this.playerObject.skin.leftLeg.rotation.x,
y: this.playerObject.skin.leftLeg.rotation.y,
z: this.playerObject.skin.leftLeg.rotation.z,
},
cape: {
x: this.playerObject.cape.rotation.x - CapeDefaultAngle,
y: this.playerObject.cape.rotation.y,
z: this.playerObject.cape.rotation.z,
},
elytra: {
x: this.playerObject.elytra.rotation.x,
y: this.playerObject.elytra.rotation.y,
z: this.playerObject.elytra.rotation.z,
},
};
}

set rotation(value: BodyPartRotation) {
if (value.player !== undefined) {
if (value.player.x !== undefined) this.playerObject.rotation.x = value.player.x;
if (value.player.y !== undefined) this.playerObject.rotation.y = value.player.y;
if (value.player.z !== undefined) this.playerObject.rotation.z = value.player.z;
}
if (value.head !== undefined) {
if (value.head.x !== undefined) this.playerObject.skin.head.rotation.x = value.head.x;
if (value.head.y !== undefined) this.playerObject.skin.head.rotation.y = value.head.y;
if (value.head.z !== undefined) this.playerObject.skin.head.rotation.z = value.head.z;
}
if (value.body !== undefined) {
if (value.body.x !== undefined) this.playerObject.skin.body.rotation.x = value.body.x;
if (value.body.y !== undefined) this.playerObject.skin.body.rotation.y = value.body.y;
if (value.body.z !== undefined) this.playerObject.skin.body.rotation.z = value.body.z;
}
if (value.rightArm !== undefined) {
if (value.rightArm.x !== undefined) this.playerObject.skin.rightArm.rotation.x = value.rightArm.x;
if (value.rightArm.y !== undefined) this.playerObject.skin.rightArm.rotation.y = value.rightArm.y;
if (value.rightArm.z !== undefined) this.playerObject.skin.rightArm.rotation.z = value.rightArm.z;
}
if (value.leftArm !== undefined) {
if (value.leftArm.x !== undefined) this.playerObject.skin.leftArm.rotation.x = value.leftArm.x;
if (value.leftArm.y !== undefined) this.playerObject.skin.leftArm.rotation.y = value.leftArm.y;
if (value.leftArm.z !== undefined) this.playerObject.skin.leftArm.rotation.z = value.leftArm.z;
}
if (value.rightLeg !== undefined) {
if (value.rightLeg.x !== undefined) this.playerObject.skin.rightLeg.rotation.x = value.rightLeg.x;
if (value.rightLeg.y !== undefined) this.playerObject.skin.rightLeg.rotation.y = value.rightLeg.y;
if (value.rightLeg.z !== undefined) this.playerObject.skin.rightLeg.rotation.z = value.rightLeg.z;
}
if (value.leftLeg !== undefined) {
if (value.leftLeg.x !== undefined) this.playerObject.skin.leftLeg.rotation.x = value.leftLeg.x;
if (value.leftLeg.y !== undefined) this.playerObject.skin.leftLeg.rotation.y = value.leftLeg.y;
if (value.leftLeg.z !== undefined) this.playerObject.skin.leftLeg.rotation.z = value.leftLeg.z;
}
if (value.cape !== undefined) {
if (value.cape.x !== undefined) this.playerObject.cape.rotation.x = value.cape.x === 0 ? CapeDefaultAngle : value.cape.x;
if (value.cape.y !== undefined) this.playerObject.cape.rotation.y = value.cape.y;
if (value.cape.z !== undefined) this.playerObject.cape.rotation.z = value.cape.z;
}
if (value.elytra !== undefined) {
if (value.elytra.x !== undefined) this.playerObject.elytra.rotation.x = value.elytra.x;
if (value.elytra.y !== undefined) this.playerObject.elytra.rotation.y = value.elytra.y;
if (value.elytra.z !== undefined) this.playerObject.elytra.rotation.z = value.elytra.z;
}
}
}