- Frost Wizard skin for Frostystyle (Michael) - cyan/ice theme - Fire Emissary skin for Gingerfury (Meg) - orange/fire theme - Arcane Catalyst skin for unicorn20089 (Holly) - purple/arcane theme - All skins use light skin tones with element-colored glowing eyes - Detailed armor/robe designs with overlay layers for depth - Includes 3D skin viewer HTML tool for previewing before upload - Comprehensive README with usage instructions and design philosophy All three skins represent The Trinity's Fire/Frost/Arcane elements. Signed-off-by: Claude <claude@firefrostgaming.com>
626 lines
21 KiB
HTML
626 lines
21 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Firefrost Gaming - Minecraft Skin Viewer</title>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
min-height: 100vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
padding: 20px;
|
|
}
|
|
|
|
.header {
|
|
text-align: center;
|
|
color: white;
|
|
margin-bottom: 30px;
|
|
}
|
|
|
|
.header h1 {
|
|
font-size: 2.5em;
|
|
margin-bottom: 10px;
|
|
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
|
|
}
|
|
|
|
.header p {
|
|
font-size: 1.1em;
|
|
opacity: 0.9;
|
|
}
|
|
|
|
.container {
|
|
background: white;
|
|
border-radius: 20px;
|
|
padding: 30px;
|
|
box-shadow: 0 10px 40px rgba(0,0,0,0.3);
|
|
max-width: 1200px;
|
|
width: 100%;
|
|
}
|
|
|
|
.controls {
|
|
margin-bottom: 20px;
|
|
display: flex;
|
|
gap: 15px;
|
|
flex-wrap: wrap;
|
|
align-items: center;
|
|
}
|
|
|
|
.file-input-wrapper {
|
|
position: relative;
|
|
overflow: hidden;
|
|
display: inline-block;
|
|
}
|
|
|
|
.file-input-wrapper input[type=file] {
|
|
position: absolute;
|
|
left: -9999px;
|
|
}
|
|
|
|
.file-input-wrapper label {
|
|
display: inline-block;
|
|
padding: 12px 24px;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
color: white;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
font-weight: 600;
|
|
transition: transform 0.2s;
|
|
}
|
|
|
|
.file-input-wrapper label:hover {
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
.preset-buttons {
|
|
display: flex;
|
|
gap: 10px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.preset-btn {
|
|
padding: 10px 20px;
|
|
border: 2px solid #667eea;
|
|
background: white;
|
|
color: #667eea;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
font-weight: 600;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.preset-btn:hover {
|
|
background: #667eea;
|
|
color: white;
|
|
}
|
|
|
|
.preset-btn.frost {
|
|
border-color: #00E5FF;
|
|
color: #00E5FF;
|
|
}
|
|
|
|
.preset-btn.frost:hover {
|
|
background: #00E5FF;
|
|
color: white;
|
|
}
|
|
|
|
.preset-btn.fire {
|
|
border-color: #FF3D00;
|
|
color: #FF3D00;
|
|
}
|
|
|
|
.preset-btn.fire:hover {
|
|
background: #FF3D00;
|
|
color: white;
|
|
}
|
|
|
|
.preset-btn.arcane {
|
|
border-color: #A855F7;
|
|
color: #A855F7;
|
|
}
|
|
|
|
.preset-btn.arcane:hover {
|
|
background: #A855F7;
|
|
color: white;
|
|
}
|
|
|
|
#viewer-container {
|
|
width: 100%;
|
|
height: 600px;
|
|
background: linear-gradient(to bottom, #87CEEB 0%, #E0F6FF 100%);
|
|
border-radius: 10px;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.info {
|
|
margin-top: 20px;
|
|
padding: 15px;
|
|
background: #f8f9fa;
|
|
border-radius: 8px;
|
|
font-size: 0.9em;
|
|
color: #666;
|
|
}
|
|
|
|
.info strong {
|
|
color: #333;
|
|
}
|
|
|
|
.animation-controls {
|
|
margin-top: 15px;
|
|
padding: 15px;
|
|
background: #f0f0f0;
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.animation-controls label {
|
|
margin-right: 15px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.animation-controls select {
|
|
padding: 8px 12px;
|
|
border-radius: 5px;
|
|
border: 1px solid #ccc;
|
|
font-size: 1em;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="header">
|
|
<h1>🔥❄️ Firefrost Gaming 🔥❄️</h1>
|
|
<p>Minecraft Skin Viewer - The Trinity</p>
|
|
</div>
|
|
|
|
<div class="container">
|
|
<div class="controls">
|
|
<div class="file-input-wrapper">
|
|
<input type="file" id="skinInput" accept="image/png" />
|
|
<label for="skinInput">📁 Upload Skin PNG</label>
|
|
</div>
|
|
|
|
<div class="preset-buttons">
|
|
<button class="preset-btn frost" onclick="loadPreset('frost')">❄️ Frost Wizard</button>
|
|
<button class="preset-btn fire" onclick="loadPreset('fire')">🔥 Fire Emissary</button>
|
|
<button class="preset-btn arcane" onclick="loadPreset('arcane')">⚡ Arcane Catalyst</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="animation-controls">
|
|
<label for="animationSelect">Animation:</label>
|
|
<select id="animationSelect" onchange="changeAnimation()">
|
|
<option value="idle">Idle (Standing)</option>
|
|
<option value="walk">Walking</option>
|
|
<option value="run">Running</option>
|
|
<option value="wave">Waving</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div id="viewer-container"></div>
|
|
|
|
<div class="info">
|
|
<strong>Controls:</strong> Click and drag to rotate • Scroll to zoom • Right-click drag to pan<br>
|
|
<strong>Tip:</strong> Upload any 64x64 Minecraft skin PNG or try the Trinity presets!
|
|
</div>
|
|
</div>
|
|
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
|
<script>
|
|
// Three.js scene setup
|
|
let scene, camera, renderer, skinMesh;
|
|
let animationTime = 0;
|
|
let currentAnimation = 'idle';
|
|
|
|
// Initialize the 3D scene
|
|
function init() {
|
|
const container = document.getElementById('viewer-container');
|
|
|
|
// Scene
|
|
scene = new THREE.Scene();
|
|
scene.background = new THREE.Color(0x87CEEB);
|
|
|
|
// Camera
|
|
camera = new THREE.PerspectiveCamera(
|
|
50,
|
|
container.clientWidth / container.clientHeight,
|
|
0.1,
|
|
1000
|
|
);
|
|
camera.position.set(0, 16, 40);
|
|
camera.lookAt(0, 16, 0);
|
|
|
|
// Renderer
|
|
renderer = new THREE.WebGLRenderer({ antialias: true });
|
|
renderer.setSize(container.clientWidth, container.clientHeight);
|
|
container.appendChild(renderer.domElement);
|
|
|
|
// Lighting
|
|
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
|
|
scene.add(ambientLight);
|
|
|
|
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
|
|
directionalLight.position.set(10, 20, 10);
|
|
scene.add(directionalLight);
|
|
|
|
// Ground plane
|
|
const groundGeometry = new THREE.PlaneGeometry(100, 100);
|
|
const groundMaterial = new THREE.MeshStandardMaterial({
|
|
color: 0x90EE90,
|
|
side: THREE.DoubleSide
|
|
});
|
|
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
|
|
ground.rotation.x = Math.PI / 2;
|
|
ground.position.y = 0;
|
|
scene.add(ground);
|
|
|
|
// Mouse controls
|
|
let isDragging = false;
|
|
let previousMousePosition = { x: 0, y: 0 };
|
|
let rotation = { x: 0, y: 0 };
|
|
|
|
renderer.domElement.addEventListener('mousedown', (e) => {
|
|
isDragging = true;
|
|
});
|
|
|
|
renderer.domElement.addEventListener('mousemove', (e) => {
|
|
if (isDragging && skinMesh) {
|
|
const deltaMove = {
|
|
x: e.offsetX - previousMousePosition.x,
|
|
y: e.offsetY - previousMousePosition.y
|
|
};
|
|
|
|
rotation.y += deltaMove.x * 0.01;
|
|
rotation.x += deltaMove.y * 0.01;
|
|
|
|
// Clamp vertical rotation
|
|
rotation.x = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, rotation.x));
|
|
}
|
|
|
|
previousMousePosition = { x: e.offsetX, y: e.offsetY };
|
|
});
|
|
|
|
renderer.domElement.addEventListener('mouseup', () => {
|
|
isDragging = false;
|
|
});
|
|
|
|
// Zoom with mouse wheel
|
|
renderer.domElement.addEventListener('wheel', (e) => {
|
|
e.preventDefault();
|
|
camera.position.z += e.deltaY * 0.05;
|
|
camera.position.z = Math.max(20, Math.min(80, camera.position.z));
|
|
});
|
|
|
|
// Handle window resize
|
|
window.addEventListener('resize', () => {
|
|
const width = container.clientWidth;
|
|
const height = container.clientHeight;
|
|
camera.aspect = width / height;
|
|
camera.updateProjectionMatrix();
|
|
renderer.setSize(width, height);
|
|
});
|
|
|
|
// Animation loop
|
|
animate();
|
|
}
|
|
|
|
// Create Minecraft character from skin texture
|
|
function createMinecraftCharacter(skinTexture) {
|
|
// Remove old mesh if exists
|
|
if (skinMesh) {
|
|
scene.remove(skinMesh);
|
|
}
|
|
|
|
const group = new THREE.Group();
|
|
|
|
// Minecraft skin texture
|
|
skinTexture.magFilter = THREE.NearestFilter;
|
|
skinTexture.minFilter = THREE.NearestFilter;
|
|
const skinMaterial = new THREE.MeshStandardMaterial({
|
|
map: skinTexture,
|
|
transparent: true
|
|
});
|
|
|
|
// Head (8x8x8)
|
|
const headGeo = new THREE.BoxGeometry(8, 8, 8);
|
|
const head = new THREE.Mesh(headGeo, skinMaterial);
|
|
head.position.y = 24;
|
|
|
|
// UV mapping for head
|
|
setHeadUVs(headGeo);
|
|
|
|
group.add(head);
|
|
|
|
// Body (8x12x4)
|
|
const bodyGeo = new THREE.BoxGeometry(8, 12, 4);
|
|
const body = new THREE.Mesh(bodyGeo, skinMaterial);
|
|
body.position.y = 14;
|
|
|
|
setBodyUVs(bodyGeo);
|
|
|
|
group.add(body);
|
|
|
|
// Arms (4x12x4 each)
|
|
const armGeo = new THREE.BoxGeometry(4, 12, 4);
|
|
|
|
const rightArm = new THREE.Mesh(armGeo, skinMaterial);
|
|
rightArm.position.set(-6, 14, 0);
|
|
setRightArmUVs(rightArm.geometry);
|
|
group.add(rightArm);
|
|
|
|
const leftArm = new THREE.Mesh(armGeo, skinMaterial);
|
|
leftArm.position.set(6, 14, 0);
|
|
setLeftArmUVs(leftArm.geometry);
|
|
group.add(leftArm);
|
|
|
|
// Legs (4x12x4 each)
|
|
const legGeo = new THREE.BoxGeometry(4, 12, 4);
|
|
|
|
const rightLeg = new THREE.Mesh(legGeo, skinMaterial);
|
|
rightLeg.position.set(-2, 6, 0);
|
|
setRightLegUVs(rightLeg.geometry);
|
|
group.add(rightLeg);
|
|
|
|
const leftLeg = new THREE.Mesh(legGeo, skinMaterial);
|
|
leftLeg.position.set(2, 6, 0);
|
|
setLeftLegUVs(leftLeg.geometry);
|
|
group.add(leftLeg);
|
|
|
|
// Store body parts for animation
|
|
group.userData = {
|
|
head: head,
|
|
body: body,
|
|
rightArm: rightArm,
|
|
leftArm: leftArm,
|
|
rightLeg: rightLeg,
|
|
leftLeg: leftLeg
|
|
};
|
|
|
|
skinMesh = group;
|
|
scene.add(group);
|
|
}
|
|
|
|
// UV mapping functions for each body part
|
|
function setHeadUVs(geometry) {
|
|
const uvs = geometry.attributes.uv.array;
|
|
// Front, back, top, bottom, right, left faces
|
|
// Simplified UV mapping for head (8,8 to 16,16 area)
|
|
const faceUVs = [
|
|
[8/64, 8/64, 16/64, 16/64], // front
|
|
[24/64, 8/64, 32/64, 16/64], // back
|
|
[8/64, 0/64, 16/64, 8/64], // top
|
|
[16/64, 0/64, 24/64, 8/64], // bottom
|
|
[0/64, 8/64, 8/64, 16/64], // right
|
|
[16/64, 8/64, 24/64, 16/64] // left
|
|
];
|
|
|
|
for (let i = 0; i < 6; i++) {
|
|
const [x1, y1, x2, y2] = faceUVs[i];
|
|
const offset = i * 8;
|
|
uvs[offset + 0] = x1; uvs[offset + 1] = 1 - y2;
|
|
uvs[offset + 2] = x2; uvs[offset + 3] = 1 - y2;
|
|
uvs[offset + 4] = x1; uvs[offset + 5] = 1 - y1;
|
|
uvs[offset + 6] = x2; uvs[offset + 7] = 1 - y1;
|
|
}
|
|
|
|
geometry.attributes.uv.needsUpdate = true;
|
|
}
|
|
|
|
function setBodyUVs(geometry) {
|
|
const uvs = geometry.attributes.uv.array;
|
|
const faceUVs = [
|
|
[20/64, 20/64, 28/64, 32/64], // front
|
|
[32/64, 20/64, 40/64, 32/64], // back
|
|
[20/64, 16/64, 28/64, 20/64], // top
|
|
[28/64, 16/64, 36/64, 20/64], // bottom
|
|
[16/64, 20/64, 20/64, 32/64], // right
|
|
[28/64, 20/64, 32/64, 32/64] // left
|
|
];
|
|
|
|
for (let i = 0; i < 6; i++) {
|
|
const [x1, y1, x2, y2] = faceUVs[i];
|
|
const offset = i * 8;
|
|
uvs[offset + 0] = x1; uvs[offset + 1] = 1 - y2;
|
|
uvs[offset + 2] = x2; uvs[offset + 3] = 1 - y2;
|
|
uvs[offset + 4] = x1; uvs[offset + 5] = 1 - y1;
|
|
uvs[offset + 6] = x2; uvs[offset + 7] = 1 - y1;
|
|
}
|
|
|
|
geometry.attributes.uv.needsUpdate = true;
|
|
}
|
|
|
|
function setRightArmUVs(geometry) {
|
|
const uvs = geometry.attributes.uv.array;
|
|
const faceUVs = [
|
|
[44/64, 20/64, 48/64, 32/64],
|
|
[52/64, 20/64, 56/64, 32/64],
|
|
[44/64, 16/64, 48/64, 20/64],
|
|
[48/64, 16/64, 52/64, 20/64],
|
|
[40/64, 20/64, 44/64, 32/64],
|
|
[48/64, 20/64, 52/64, 32/64]
|
|
];
|
|
|
|
for (let i = 0; i < 6; i++) {
|
|
const [x1, y1, x2, y2] = faceUVs[i];
|
|
const offset = i * 8;
|
|
uvs[offset + 0] = x1; uvs[offset + 1] = 1 - y2;
|
|
uvs[offset + 2] = x2; uvs[offset + 3] = 1 - y2;
|
|
uvs[offset + 4] = x1; uvs[offset + 5] = 1 - y1;
|
|
uvs[offset + 6] = x2; uvs[offset + 7] = 1 - y1;
|
|
}
|
|
|
|
geometry.attributes.uv.needsUpdate = true;
|
|
}
|
|
|
|
function setLeftArmUVs(geometry) {
|
|
const uvs = geometry.attributes.uv.array;
|
|
const faceUVs = [
|
|
[36/64, 52/64, 40/64, 64/64],
|
|
[44/64, 52/64, 48/64, 64/64],
|
|
[36/64, 48/64, 40/64, 52/64],
|
|
[40/64, 48/64, 44/64, 52/64],
|
|
[32/64, 52/64, 36/64, 64/64],
|
|
[40/64, 52/64, 44/64, 64/64]
|
|
];
|
|
|
|
for (let i = 0; i < 6; i++) {
|
|
const [x1, y1, x2, y2] = faceUVs[i];
|
|
const offset = i * 8;
|
|
uvs[offset + 0] = x1; uvs[offset + 1] = 1 - y2;
|
|
uvs[offset + 2] = x2; uvs[offset + 3] = 1 - y2;
|
|
uvs[offset + 4] = x1; uvs[offset + 5] = 1 - y1;
|
|
uvs[offset + 6] = x2; uvs[offset + 7] = 1 - y1;
|
|
}
|
|
|
|
geometry.attributes.uv.needsUpdate = true;
|
|
}
|
|
|
|
function setRightLegUVs(geometry) {
|
|
const uvs = geometry.attributes.uv.array;
|
|
const faceUVs = [
|
|
[4/64, 20/64, 8/64, 32/64],
|
|
[12/64, 20/64, 16/64, 32/64],
|
|
[4/64, 16/64, 8/64, 20/64],
|
|
[8/64, 16/64, 12/64, 20/64],
|
|
[0/64, 20/64, 4/64, 32/64],
|
|
[8/64, 20/64, 12/64, 32/64]
|
|
];
|
|
|
|
for (let i = 0; i < 6; i++) {
|
|
const [x1, y1, x2, y2] = faceUVs[i];
|
|
const offset = i * 8;
|
|
uvs[offset + 0] = x1; uvs[offset + 1] = 1 - y2;
|
|
uvs[offset + 2] = x2; uvs[offset + 3] = 1 - y2;
|
|
uvs[offset + 4] = x1; uvs[offset + 5] = 1 - y1;
|
|
uvs[offset + 6] = x2; uvs[offset + 7] = 1 - y1;
|
|
}
|
|
|
|
geometry.attributes.uv.needsUpdate = true;
|
|
}
|
|
|
|
function setLeftLegUVs(geometry) {
|
|
const uvs = geometry.attributes.uv.array;
|
|
const faceUVs = [
|
|
[20/64, 52/64, 24/64, 64/64],
|
|
[28/64, 52/64, 32/64, 64/64],
|
|
[20/64, 48/64, 24/64, 52/64],
|
|
[24/64, 48/64, 28/64, 52/64],
|
|
[16/64, 52/64, 20/64, 64/64],
|
|
[24/64, 52/64, 28/64, 64/64]
|
|
];
|
|
|
|
for (let i = 0; i < 6; i++) {
|
|
const [x1, y1, x2, y2] = faceUVs[i];
|
|
const offset = i * 8;
|
|
uvs[offset + 0] = x1; uvs[offset + 1] = 1 - y2;
|
|
uvs[offset + 2] = x2; uvs[offset + 3] = 1 - y2;
|
|
uvs[offset + 4] = x1; uvs[offset + 5] = 1 - y1;
|
|
uvs[offset + 6] = x2; uvs[offset + 7] = 1 - y1;
|
|
}
|
|
|
|
geometry.attributes.uv.needsUpdate = true;
|
|
}
|
|
|
|
// Animation function
|
|
function updateAnimation() {
|
|
if (!skinMesh) return;
|
|
|
|
const parts = skinMesh.userData;
|
|
animationTime += 0.05;
|
|
|
|
// Reset rotations
|
|
Object.values(parts).forEach(part => {
|
|
part.rotation.set(0, 0, 0);
|
|
});
|
|
|
|
switch(currentAnimation) {
|
|
case 'walk':
|
|
parts.rightArm.rotation.x = Math.sin(animationTime) * 0.5;
|
|
parts.leftArm.rotation.x = -Math.sin(animationTime) * 0.5;
|
|
parts.rightLeg.rotation.x = -Math.sin(animationTime) * 0.5;
|
|
parts.leftLeg.rotation.x = Math.sin(animationTime) * 0.5;
|
|
break;
|
|
|
|
case 'run':
|
|
parts.rightArm.rotation.x = Math.sin(animationTime * 2) * 0.8;
|
|
parts.leftArm.rotation.x = -Math.sin(animationTime * 2) * 0.8;
|
|
parts.rightLeg.rotation.x = -Math.sin(animationTime * 2) * 0.8;
|
|
parts.leftLeg.rotation.x = Math.sin(animationTime * 2) * 0.8;
|
|
skinMesh.position.y = Math.abs(Math.sin(animationTime * 2)) * 2;
|
|
break;
|
|
|
|
case 'wave':
|
|
parts.rightArm.rotation.z = -Math.PI / 2;
|
|
parts.rightArm.rotation.x = Math.sin(animationTime * 3) * 0.3;
|
|
break;
|
|
|
|
case 'idle':
|
|
default:
|
|
// Gentle breathing/idle motion
|
|
skinMesh.position.y = Math.sin(animationTime * 0.5) * 0.3;
|
|
parts.rightArm.rotation.x = Math.sin(animationTime * 0.3) * 0.05;
|
|
parts.leftArm.rotation.x = -Math.sin(animationTime * 0.3) * 0.05;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Animation loop
|
|
function animate() {
|
|
requestAnimationFrame(animate);
|
|
|
|
if (skinMesh) {
|
|
updateAnimation();
|
|
}
|
|
|
|
renderer.render(scene, camera);
|
|
}
|
|
|
|
// Load skin from file
|
|
document.getElementById('skinInput').addEventListener('change', (e) => {
|
|
const file = e.target.files[0];
|
|
if (file) {
|
|
const reader = new FileReader();
|
|
reader.onload = (event) => {
|
|
const loader = new THREE.TextureLoader();
|
|
loader.load(event.target.result, (texture) => {
|
|
createMinecraftCharacter(texture);
|
|
});
|
|
};
|
|
reader.readAsDataURL(file);
|
|
}
|
|
});
|
|
|
|
// Load preset skins
|
|
function loadPreset(type) {
|
|
const paths = {
|
|
'frost': 'frost-wizard-skin-proper.png',
|
|
'fire': 'fire-emissary-meg-skin.png',
|
|
'arcane': 'arcane-catalyst-holly-skin.png'
|
|
};
|
|
|
|
// Note: In a real deployment, these would need to be accessible
|
|
// For now, show a message
|
|
alert(`To view ${type} skin:\n\n1. Download the ${type} skin PNG\n2. Click "Upload Skin PNG"\n3. Select the downloaded file`);
|
|
}
|
|
|
|
// Change animation
|
|
function changeAnimation() {
|
|
const select = document.getElementById('animationSelect');
|
|
currentAnimation = select.value;
|
|
animationTime = 0;
|
|
}
|
|
|
|
// Initialize on page load
|
|
window.addEventListener('load', init);
|
|
</script>
|
|
</body>
|
|
</html>
|