skydy / index.html
Skyd3d's picture
Add 1 files
3b9552c verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Anime Character Customizer</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/loaders/OBJLoader.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/loaders/MTLLoader.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
body {
background-color: #ffecf5;
font-family: 'Comic Sans MS', cursive, sans-serif;
overflow-x: hidden;
}
.anime-bg {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
opacity: 0.3;
background-image: url('https://images.unsplash.com/photo-1633613286848-e6f43bbafb8d?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1470&q=80');
background-size: cover;
background-position: center;
background-attachment: fixed;
}
.character-container {
position: relative;
width: 100%;
height: 500px;
background-color: rgba(255, 255, 255, 0.5);
border-radius: 15px;
overflow: hidden;
backdrop-filter: blur(5px);
border: 2px solid rgba(255, 255, 255, 0.8);
}
#renderCanvas {
width: 100%;
height: 100%;
}
.category-btn {
transition: all 0.3s ease;
position: relative;
overflow: hidden;
background: rgba(255, 255, 255, 0.3);
backdrop-filter: blur(5px);
}
.category-btn::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.4), transparent);
transition: 0.5s;
}
.category-btn:hover::before {
left: 100%;
}
.category-btn.active {
background-color: #ff6b9d;
color: white;
transform: translateY(-3px);
box-shadow: 0 10px 20px rgba(255, 107, 157, 0.3);
}
.item-thumbnail {
transition: all 0.3s ease;
background: rgba(255, 255, 255, 0.8);
border-radius: 10px;
overflow: hidden;
backdrop-filter: blur(5px);
border: 1px solid rgba(255, 255, 255, 0.5);
}
.item-thumbnail:hover {
transform: scale(1.05) rotate(2deg);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2);
}
.item-thumbnail.selected {
border: 3px solid #ff6b9d;
background: rgba(255, 107, 157, 0.1);
}
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.7);
z-index: 1000;
justify-content: center;
align-items: center;
backdrop-filter: blur(5px);
}
.modal-content {
animation: modalFadeIn 0.3s ease-out;
background: linear-gradient(135deg, #ffcce6, #ff99cc);
padding: 2rem;
border-radius: 15px;
width: 90%;
max-width: 500px;
box-shadow: 0 15px 30px rgba(0, 0, 0, 0.3);
border: 2px solid white;
}
@keyframes modalFadeIn {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
#itemsGrid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 1.5rem;
padding: 1.5rem;
}
.btn-primary {
background: linear-gradient(45deg, #ff6b9d, #ff8fab);
color: white;
border: none;
border-radius: 50px;
padding: 10px 20px;
font-weight: bold;
box-shadow: 0 5px 15px rgba(255, 107, 157, 0.4);
transition: all 0.3s ease;
}
.btn-primary:hover {
transform: translateY(-3px);
box-shadow: 0 8px 20px rgba(255, 107, 157, 0.6);
}
.btn-secondary {
background: linear-gradient(45deg, #6b9dff, #8fabff);
color: white;
border: none;
border-radius: 50px;
padding: 10px 20px;
font-weight: bold;
box-shadow: 0 5px 15px rgba(107, 157, 255, 0.4);
transition: all 0.3s ease;
}
.btn-secondary:hover {
transform: translateY(-3px);
box-shadow: 0 8px 20px rgba(107, 157, 255, 0.6);
}
.header {
background: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(10px);
border-bottom: 1px solid rgba(255, 255, 255, 0.3);
}
.panel {
background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(10px);
border-radius: 20px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
border: 1px solid rgba(255, 255, 255, 0.5);
}
.file-upload {
position: relative;
overflow: hidden;
display: inline-block;
width: 100%;
}
.file-upload-btn {
width: 100%;
padding: 15px;
background: linear-gradient(45deg, #9dff6b, #abff8f);
color: white;
border-radius: 10px;
text-align: center;
cursor: pointer;
font-weight: bold;
box-shadow: 0 5px 15px rgba(157, 255, 107, 0.4);
transition: all 0.3s ease;
}
.file-upload-btn:hover {
background: linear-gradient(45deg, #8fec5d, #9dec7d);
transform: translateY(-3px);
box-shadow: 0 8px 20px rgba(157, 255, 107, 0.6);
}
.file-upload input[type="file"] {
position: absolute;
left: 0;
top: 0;
opacity: 0;
width: 100%;
height: 100%;
cursor: pointer;
}
.progress-bar {
height: 10px;
background: rgba(255, 255, 255, 0.5);
border-radius: 5px;
margin-top: 10px;
overflow: hidden;
}
.progress {
height: 100%;
background: linear-gradient(90deg, #ff6b9d, #ff8fab);
width: 0%;
transition: width 0.3s ease;
}
.sakura {
position: absolute;
width: 10px;
height: 10px;
background-color: #ffb6c1;
border-radius: 50% 0 50% 50%;
opacity: 0.7;
pointer-events: none;
z-index: -1;
}
.upload-preview {
width: 100%;
height: 150px;
background-color: rgba(255, 255, 255, 0.5);
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 10px;
overflow: hidden;
}
.upload-preview img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
.upload-instructions {
background: rgba(255, 255, 255, 0.7);
padding: 15px;
border-radius: 10px;
margin-bottom: 15px;
border-left: 4px solid #ff6b9d;
}
.file-info {
margin-top: 10px;
font-size: 0.9rem;
color: #555;
}
.file-info span {
font-weight: bold;
color: #ff6b9d;
}
.upload-section {
background: rgba(255, 255, 255, 0.8);
border-radius: 15px;
padding: 20px;
margin-bottom: 20px;
border: 2px dashed #ff6b9d;
text-align: center;
}
.upload-icon {
font-size: 3rem;
color: #ff6b9d;
margin-bottom: 10px;
}
.upload-title {
font-size: 1.5rem;
font-weight: bold;
color: #ff6b9d;
margin-bottom: 10px;
}
.upload-subtitle {
color: #666;
margin-bottom: 20px;
}
</style>
</head>
<body>
<!-- Anime Background -->
<div class="anime-bg"></div>
<!-- Sakura Petals Animation -->
<div id="sakura-container"></div>
<!-- Header -->
<header class="header shadow-sm sticky top-0 z-50">
<div class="container mx-auto px-4 py-4">
<div class="flex justify-between items-center">
<h1 class="text-3xl font-bold text-pink-600 flex items-center">
<i class="fas fa-star mr-2"></i> Anime Character Customizer
</h1>
<button id="adminBtn" class="btn-secondary flex items-center">
<i class="fas fa-upload mr-2"></i> Upload Models
</button>
</div>
</div>
</header>
<!-- Main Content -->
<main class="container mx-auto px-4 py-8">
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<!-- Character Preview -->
<div class="lg:col-span-1">
<div class="panel p-6 sticky top-24">
<h2 class="text-2xl font-bold mb-6 text-center text-pink-600">Your Character</h2>
<div class="character-container">
<canvas id="renderCanvas"></canvas>
</div>
<div class="flex justify-between items-center mt-6">
<button id="randomizeBtn" class="btn-primary flex items-center">
<i class="fas fa-random mr-2"></i> Randomize
</button>
<button id="downloadBtn" class="btn-secondary flex items-center">
<i class="fas fa-download mr-2"></i> Download
</button>
</div>
</div>
</div>
<!-- Customization Panel -->
<div class="lg:col-span-2">
<div class="panel">
<!-- Category Tabs -->
<div class="flex border-b border-pink-200">
<button class="category-btn active px-6 py-4 font-medium" data-category="head">
<i class="fas fa-head-side-mask mr-2"></i> Heads
</button>
<button class="category-btn px-6 py-4 font-medium" data-category="body">
<i class="fas fa-tshirt mr-2"></i> Bodies
</button>
<button class="category-btn px-6 py-4 font-medium" data-category="hair">
<i class="fas fa-cut mr-2"></i> Hair
</button>
<button class="category-btn px-6 py-4 font-medium" data-category="accessory">
<i class="fas fa-scarf mr-2"></i> Accessories
</button>
<button class="category-btn px-6 py-4 font-medium" data-category="weapon">
<i class="fas fa-sword mr-2"></i> Weapons
</button>
</div>
<!-- Items Grid -->
<div id="itemsGrid" class="p-6">
<!-- Items will be loaded here dynamically -->
</div>
</div>
</div>
</div>
</main>
<!-- Upload Modal -->
<div id="adminModal" class="modal">
<div class="modal-content">
<div class="flex justify-between items-center mb-6">
<h3 class="text-xl font-bold text-pink-800">Upload 3D Models</h3>
<button id="closeAdminModal" class="text-pink-800 hover:text-pink-600 text-xl">
<i class="fas fa-times"></i>
</button>
</div>
<div class="space-y-6">
<!-- Upload Section -->
<div class="upload-section">
<div class="upload-icon">
<i class="fas fa-cloud-upload-alt"></i>
</div>
<div class="upload-title">Upload Your 3D Models</div>
<div class="upload-subtitle">Drag & drop your OBJ and MTL files here or click to browse</div>
<div class="file-upload">
<div class="file-upload-btn">
<i class="fas fa-file-upload mr-2"></i> Select Files
</div>
<input type="file" id="modelFiles" accept=".obj,.mtl" multiple>
</div>
</div>
<div class="upload-instructions">
<h4 class="font-bold text-pink-800 mb-2">How to upload 3D models:</h4>
<ol class="list-decimal pl-5 space-y-1">
<li>Select the category for your model</li>
<li>Enter a name for your model</li>
<li>Upload both OBJ and MTL files (required)</li>
<li>Optionally upload a preview image</li>
<li>Click "Add Item" to save</li>
</ol>
</div>
<div>
<label class="block text-sm font-medium text-pink-800 mb-2">Category</label>
<select id="adminCategory" class="w-full p-3 border-2 border-pink-300 rounded-lg bg-white">
<option value="head">Head</option>
<option value="body">Body</option>
<option value="hair">Hair</option>
<option value="accessory">Accessory</option>
<option value="weapon">Weapon</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-pink-800 mb-2">Item Name</label>
<input type="text" id="adminItemName" class="w-full p-3 border-2 border-pink-300 rounded-lg" placeholder="Enter item name">
</div>
<div>
<label class="block text-sm font-medium text-pink-800 mb-2">Preview Image (Optional)</label>
<div class="upload-preview">
<span class="text-gray-500">No image selected</span>
</div>
<div class="file-upload">
<div class="file-upload-btn">
<i class="fas fa-image mr-2"></i> Choose Preview Image
</div>
<input type="file" id="previewImage" accept="image/*">
</div>
</div>
<div id="fileUploadInfo">
<div id="fileInfo" class="file-info hidden">
Selected files: <span id="selectedFiles"></span>
</div>
<div class="progress-bar mt-2">
<div id="uploadProgress" class="progress"></div>
</div>
<p id="uploadStatus" class="text-xs text-pink-800 mt-1"></p>
</div>
<div class="flex space-x-4">
<button id="addItemBtn" class="btn-primary flex-1">
<i class="fas fa-plus mr-2"></i> Add Item
</button>
<button id="removeItemBtn" class="btn-secondary flex-1">
<i class="fas fa-trash mr-2"></i> Remove Item
</button>
</div>
</div>
</div>
</div>
<script>
// Three.js variables
let scene, camera, renderer, controls;
let character = new THREE.Group();
let currentParts = {
head: null,
body: null,
hair: null,
accessory: null,
weapon: null
};
// Sample data for character items (initially empty)
let characterItems = {
head: [],
body: [],
hair: [],
accessory: [],
weapon: []
};
// Store uploaded files in memory (in a real app, you'd upload to a server)
let uploadedFiles = {
models: {},
previews: {}
};
// DOM elements
const categoryButtons = document.querySelectorAll('.category-btn');
const itemsGrid = document.getElementById('itemsGrid');
const adminBtn = document.getElementById('adminBtn');
const adminModal = document.getElementById('adminModal');
const closeAdminModal = document.getElementById('closeAdminModal');
const randomizeBtn = document.getElementById('randomizeBtn');
const downloadBtn = document.getElementById('downloadBtn');
const addItemBtn = document.getElementById('addItemBtn');
const removeItemBtn = document.getElementById('removeItemBtn');
const modelFilesInput = document.getElementById('modelFiles');
const previewImageInput = document.getElementById('previewImage');
const uploadProgress = document.getElementById('uploadProgress');
const uploadStatus = document.getElementById('uploadStatus');
const sakuraContainer = document.getElementById('sakura-container');
const uploadPreview = document.querySelector('.upload-preview');
const fileInfo = document.getElementById('fileInfo');
const selectedFiles = document.getElementById('selectedFiles');
const fileUploadInfo = document.getElementById('fileUploadInfo');
// Initialize Three.js scene
function initThreeJS() {
// Create scene
scene = new THREE.Scene();
scene.background = new THREE.Color(0xf0f0f0);
// Add lights
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(100, 100, 50);
scene.add(directionalLight);
// Create camera
camera = new THREE.PerspectiveCamera(45, 1, 0.1, 1000);
camera.position.set(0, 100, 300);
// Create renderer
const canvas = document.getElementById('renderCanvas');
renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(canvas.clientWidth, canvas.clientHeight);
// Add controls
controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.minDistance = 150;
controls.maxDistance = 500;
// Add character container
scene.add(character);
// Add floor grid
const gridHelper = new THREE.GridHelper(200, 20, 0x888888, 0x888888);
gridHelper.position.y = -10;
scene.add(gridHelper);
// Start animation loop
animate();
}
// Animation loop
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}
// Load a 3D model
function loadModel(objUrl, mtlUrl, callback) {
const mtlLoader = new THREE.MTLLoader();
mtlLoader.load(mtlUrl, (materials) => {
materials.preload();
const objLoader = new THREE.OBJLoader();
objLoader.setMaterials(materials);
objLoader.load(objUrl, (object) => {
if (callback) callback(object);
});
});
}
// Add a part to the character
function addCharacterPart(partType, item) {
// Remove existing part if any
if (currentParts[partType]) {
character.remove(currentParts[partType]);
}
// Check if we have the model in memory
if (uploadedFiles.models[item.id]) {
const modelData = uploadedFiles.models[item.id];
// Create a material loader
const mtlLoader = new THREE.MTLLoader();
const mtlContent = modelData.mtl;
const materials = mtlLoader.parse(mtlContent);
materials.preload();
// Create an object loader
const objLoader = new THREE.OBJLoader();
objLoader.setMaterials(materials);
// Parse the OBJ content
const model = objLoader.parse(modelData.obj);
// Position the model based on part type
positionModel(partType, model);
// Store the ID for reference
model.userData = { id: item.id };
character.add(model);
currentParts[partType] = model;
} else {
console.error("Model not found in memory:", item.id);
}
}
// Position model based on part type
function positionModel(partType, model) {
switch(partType) {
case 'head':
model.position.set(0, 80, 0);
model.scale.set(10, 10, 10);
break;
case 'body':
model.position.set(0, 0, 0);
model.scale.set(10, 10, 10);
break;
case 'hair':
model.position.set(0, 90, 0);
model.scale.set(10, 10, 10);
break;
case 'accessory':
model.position.set(0, 100, 0);
model.scale.set(8, 8, 8);
break;
case 'weapon':
model.position.set(20, 50, 0);
model.scale.set(5, 5, 5);
break;
}
}
// Initialize the app
function init() {
initThreeJS();
// Load first category by default
loadCategoryItems('head');
// Set up event listeners
setupEventListeners();
// Create sakura petals
createSakuraPetals();
// Load any saved items from localStorage
loadSavedItems();
}
// Set up all event listeners
function setupEventListeners() {
// Category buttons
categoryButtons.forEach(button => {
button.addEventListener('click', () => {
const category = button.dataset.category;
// Update active button
categoryButtons.forEach(btn => btn.classList.remove('active'));
button.classList.add('active');
// Load items for this category
loadCategoryItems(category);
});
});
// Admin button
adminBtn.addEventListener('click', () => {
adminModal.style.display = 'flex';
});
// Close admin modal
closeAdminModal.addEventListener('click', () => {
adminModal.style.display = 'none';
});
// Randomize button
randomizeBtn.addEventListener('click', randomizeCharacter);
// Download button
downloadBtn.addEventListener('click', downloadCharacter);
// Add item button
addItemBtn.addEventListener('click', addNewItem);
// Remove item button
removeItemBtn.addEventListener('click', removeItem);
// Model files upload
modelFilesInput.addEventListener('change', handleModelUpload);
// Preview image upload
previewImageInput.addEventListener('change', handlePreviewImageUpload);
// Window resize
window.addEventListener('resize', onWindowResize);
// Drag and drop for model files
setupDragAndDrop();
}
// Set up drag and drop functionality
function setupDragAndDrop() {
const uploadSection = document.querySelector('.upload-section');
// Prevent default drag behaviors
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
uploadSection.addEventListener(eventName, preventDefaults, false);
document.body.addEventListener(eventName, preventDefaults, false);
});
// Highlight drop area when item is dragged over it
['dragenter', 'dragover'].forEach(eventName => {
uploadSection.addEventListener(eventName, highlight, false);
});
['dragleave', 'drop'].forEach(eventName => {
uploadSection.addEventListener(eventName, unhighlight, false);
});
// Handle dropped files
uploadSection.addEventListener('drop', handleDrop, false);
}
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
function highlight() {
document.querySelector('.upload-section').style.borderColor = '#ff3d8b';
document.querySelector('.upload-section').style.backgroundColor = 'rgba(255, 255, 255, 0.9)';
}
function unhighlight() {
document.querySelector('.upload-section').style.borderColor = '#ff6b9d';
document.querySelector('.upload-section').style.backgroundColor = 'rgba(255, 255, 255, 0.8)';
}
function handleDrop(e) {
const dt = e.dataTransfer;
const files = dt.files;
// Filter for only OBJ and MTL files
const validFiles = Array.from(files).filter(file =>
file.name.endsWith('.obj') || file.name.endsWith('.mtl')
);
if (validFiles.length > 0) {
// Create a new FileList object
const dataTransfer = new DataTransfer();
validFiles.forEach(file => dataTransfer.items.add(file));
// Assign to our file input
modelFilesInput.files = dataTransfer.files;
// Trigger the change event
const event = new Event('change');
modelFilesInput.dispatchEvent(event);
} else {
uploadStatus.textContent = 'Please drop OBJ or MTL files only';
uploadStatus.style.color = 'red';
}
}
// Load items for a specific category
function loadCategoryItems(category) {
itemsGrid.innerHTML = '';
// Add "None" option
const noneItem = document.createElement('div');
noneItem.className = `item-thumbnail flex flex-col items-center justify-center cursor-pointer ${!currentParts[category] ? 'selected' : ''}`;
noneItem.innerHTML = `
<div class="w-full h-32 bg-gradient-to-br from-pink-100 to-pink-200 rounded-lg mb-2 flex items-center justify-center">
<i class="fas fa-times text-pink-500 text-4xl"></i>
</div>
<span class="text-sm font-medium text-pink-800">None</span>
`;
noneItem.addEventListener('click', () => {
// Remove part from character
if (currentParts[category]) {
character.remove(currentParts[category]);
currentParts[category] = null;
}
// Update selected state
document.querySelectorAll(`.item-thumbnail`).forEach(item => {
item.classList.remove('selected');
});
noneItem.classList.add('selected');
});
itemsGrid.appendChild(noneItem);
// Add items for this category
characterItems[category].forEach(item => {
const itemElement = document.createElement('div');
itemElement.className = `item-thumbnail flex flex-col items-center justify-center cursor-pointer ${currentParts[category] && currentParts[category].userData.id === item.id ? 'selected' : ''}`;
// Check if we have a preview image
let previewContent = '';
if (uploadedFiles.previews[item.id]) {
previewContent = `<img src="${uploadedFiles.previews[item.id]}" alt="${item.name}" class="w-full h-full object-cover">`;
} else {
previewContent = `
<div class="w-full h-full flex items-center justify-center bg-gradient-to-br from-pink-100 to-pink-200">
<i class="fas fa-cube text-pink-500 text-4xl"></i>
</div>
`;
}
itemElement.innerHTML = `
<div class="w-full h-32 rounded-lg mb-2 overflow-hidden">
${previewContent}
</div>
<span class="text-sm font-medium text-pink-800">${item.name}</span>
`;
itemElement.addEventListener('click', () => {
// Add part to character
addCharacterPart(category, item);
// Update selected state
document.querySelectorAll(`.item-thumbnail`).forEach(item => {
item.classList.remove('selected');
});
itemElement.classList.add('selected');
});
itemsGrid.appendChild(itemElement);
});
}
// Randomize all character parts
function randomizeCharacter() {
Object.keys(characterItems).forEach(category => {
if (characterItems[category].length > 0) {
// Get random item from this category
const randomIndex = Math.floor(Math.random() * characterItems[category].length);
const randomItem = characterItems[category][randomIndex];
// Add part to character
addCharacterPart(category, randomItem);
}
});
// Reload current category to update selected states
const currentCategory = document.querySelector('.category-btn.active').dataset.category;
loadCategoryItems(currentCategory);
}
// Download character image
function downloadCharacter() {
// Temporarily disable controls
controls.enabled = false;
// Render to canvas
renderer.render(scene, camera);
const canvas = renderer.domElement;
// Create download link
const link = document.createElement('a');
link.download = 'my-anime-character.png';
link.href = canvas.toDataURL('image/png');
link.click();
// Re-enable controls
controls.enabled = true;
// Show success message
showNotification('Character downloaded!', 'green');
}
// Handle model file upload
function handleModelUpload(e) {
const files = e.target.files;
if (files.length === 0) return;
// Show selected files
const fileNames = Array.from(files).map(file => file.name).join(', ');
selectedFiles.textContent = fileNames;
fileInfo.classList.remove('hidden');
uploadStatus.textContent = 'Uploading model files...';
uploadStatus.style.color = '#ff6b9d';
uploadProgress.style.width = '0%';
// We'll simulate upload progress
let progress = 0;
const interval = setInterval(() => {
progress += 5;
uploadProgress.style.width = `${progress}%`;
if (progress >= 100) {
clearInterval(interval);
uploadStatus.textContent = 'Model files uploaded successfully!';
uploadStatus.style.color = '#9dff6b';
// Process the files
processModelFiles(files);
}
}, 100);
}
// Process uploaded model files
function processModelFiles(files) {
const objFile = Array.from(files).find(file => file.name.endsWith('.obj'));
const mtlFile = Array.from(files).find(file => file.name.endsWith('.mtl'));
if (!objFile || !mtlFile) {
uploadStatus.textContent = 'Error: Need both OBJ and MTL files';
uploadStatus.style.color = 'red';
return;
}
// Read the files
const objReader = new FileReader();
const mtlReader = new FileReader();
objReader.onload = function(e) {
const objContent = e.target.result;
mtlReader.onload = function(e) {
const mtlContent = e.target.result;
// Generate a temporary ID for the model
const tempId = 'temp_' + Date.now();
// Store in memory (in a real app, you'd upload to server)
uploadedFiles.models[tempId] = {
obj: objContent,
mtl: mtlContent
};
// Store the temp ID in the input for reference
modelFilesInput.dataset.tempId = tempId;
uploadStatus.textContent = 'Model files processed and ready!';
uploadStatus.style.color = '#9dff6b';
};
mtlReader.readAsText(mtlFile);
};
objReader.readAsText(objFile);
}
// Handle preview image upload
function handlePreviewImageUpload(e) {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(e) {
const imageUrl = e.target.result;
// Display preview
uploadPreview.innerHTML = `<img src="${imageUrl}" alt="Preview">`;
// Generate a temporary ID for the preview
const tempId = 'temp_' + Date.now();
// Store in memory (in a real app, you'd upload to server)
uploadedFiles.previews[tempId] = imageUrl;
// Store the temp ID in the input for reference
previewImageInput.dataset.tempId = tempId;
uploadStatus.textContent = 'Preview image uploaded!';
uploadStatus.style.color = '#9dff6b';
};
reader.readAsDataURL(file);
}
// Add new item to a category
function addNewItem() {
const category = document.getElementById('adminCategory').value;
const name = document.getElementById('adminItemName').value.trim();
if (!name) {
showNotification('Please enter an item name', 'red');
return;
}
// Check if we have model files
const modelTempId = modelFilesInput.dataset.tempId;
if (!modelTempId || !uploadedFiles.models[modelTempId]) {
showNotification('Please upload model files (OBJ + MTL)', 'red');
return;
}
// Generate ID
const id = `${category}_${Date.now()}`;
// Get preview image if available
const previewTempId = previewImageInput.dataset.tempId;
if (previewTempId && uploadedFiles.previews[previewTempId]) {
// Move from temp storage to permanent
uploadedFiles.previews[id] = uploadedFiles.previews[previewTempId];
delete uploadedFiles.previews[previewTempId];
}
// Move model from temp storage to permanent
uploadedFiles.models[id] = uploadedFiles.models[modelTempId];
delete uploadedFiles.models[modelTempId];
// Add to our data
characterItems[category].push({
id,
name
});
// Save to localStorage
saveItemsToStorage();
// Clear form
document.getElementById('adminItemName').value = '';
modelFilesInput.value = '';
modelFilesInput.dataset.tempId = '';
previewImageInput.value = '';
previewImageInput.dataset.tempId = '';
uploadPreview.innerHTML = '<span class="text-gray-500">No image selected</span>';
uploadProgress.style.width = '0%';
uploadStatus.textContent = '';
fileInfo.classList.add('hidden');
// Reload items if this is the current category
const currentCategory = document.querySelector('.category-btn.active').dataset.category;
if (currentCategory === category) {
loadCategoryItems(category);
}
// Show success notification
showNotification('Item added successfully!', 'blue');
}
// Remove item from a category
function removeItem() {
const category = document.getElementById('adminCategory').value;
const name = document.getElementById('adminItemName').value.trim();
if (!name) {
showNotification('Please enter the name of the item to remove', 'red');
return;
}
// Find item index
const itemIndex = characterItems[category].findIndex(item => item.name === name);
if (itemIndex === -1) {
showNotification('Item not found', 'red');
return;
}
// Get item ID
const itemId = characterItems[category][itemIndex].id;
// Remove item
characterItems[category].splice(itemIndex, 1);
// Remove from memory
delete uploadedFiles.models[itemId];
delete uploadedFiles.previews[itemId];
// Clear selection if it was this item
if (currentParts[category] && currentParts[category].userData.id === itemId) {
character.remove(currentParts[category]);
currentParts[category] = null;
}
// Save to localStorage
saveItemsToStorage();
// Clear form
document.getElementById('adminItemName').value = '';
// Reload items if this is the current category
const currentCategory = document.querySelector('.category-btn.active').dataset.category;
if (currentCategory === category) {
loadCategoryItems(category);
}
// Show success notification
showNotification('Item removed successfully!', 'red');
}
// Save items to localStorage
function saveItemsToStorage() {
localStorage.setItem('animeCharacterItems', JSON.stringify(characterItems));
// We can't save the files to localStorage due to size limits
// In a real app, you'd upload to a server
}
// Load saved items from localStorage
function loadSavedItems() {
const savedItems = localStorage.getItem('animeCharacterItems');
if (savedItems) {
characterItems = JSON.parse(savedItems);
// Reload current category
const currentCategory = document.querySelector('.category-btn.active').dataset.category;
loadCategoryItems(currentCategory);
}
}
// Handle window resize
function onWindowResize() {
const canvas = document.getElementById('renderCanvas');
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
renderer.setSize(canvas.clientWidth, canvas.clientHeight);
}
// Create sakura petals animation
function createSakuraPetals() {
for (let i = 0; i < 30; i++) {
createSakuraPetal();
}
}
function createSakuraPetal() {
const petal = document.createElement('div');
petal.className = 'sakura';
// Random position
const startX = Math.random() * window.innerWidth;
const startY = -20;
// Random size
const size = Math.random() * 10 + 5;
// Random rotation
const rotation = Math.random() * 360;
// Random animation duration
const duration = Math.random() * 10 + 10;
// Random delay
const delay = Math.random() * 5;
// Set initial styles
petal.style.left = `${startX}px`;
petal.style.top = `${startY}px`;
petal.style.width = `${size}px`;
petal.style.height = `${size}px`;
petal.style.transform = `rotate(${rotation}deg)`;
// Add to container
sakuraContainer.appendChild(petal);
// Animate
const animation = petal.animate([
{ top: `${startY}px`, left: `${startX}px`, opacity: 0 },
{ opacity: 0.7 },
{ top: `${window.innerHeight}px`, left: `${startX + (Math.random() * 200 - 100)}px`, opacity: 0 }
], {
duration: duration * 1000,
delay: delay * 1000,
easing: 'linear'
});
// Restart animation when finished
animation.onfinish = () => {
petal.remove();
createSakuraPetal();
};
}
// Show notification
function showNotification(message, color) {
const colors = {
blue: 'bg-blue-500',
green: 'bg-green-500',
red: 'bg-red-500'
};
const notification = document.createElement('div');
notification.className = `fixed bottom-4 right-4 ${colors[color] || 'bg-blue-500'} text-white px-4 py-2 rounded-lg shadow-lg flex items-center`;
notification.innerHTML = `<i class="fas fa-check-circle mr-2"></i> ${message}`;
document.body.appendChild(notification);
setTimeout(() => {
notification.classList.add('opacity-0', 'transition-opacity', 'duration-500');
setTimeout(() => notification.remove(), 500);
}, 3000);
}
// Initialize the app when DOM is loaded
document.addEventListener('DOMContentLoaded', init);
</script>
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=Skyd3d/skydy" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
</html>