|
<!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> |
|
|
|
<div class="anime-bg"></div> |
|
|
|
|
|
<div id="sakura-container"></div> |
|
|
|
|
|
<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 class="container mx-auto px-4 py-8"> |
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8"> |
|
|
|
<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> |
|
|
|
|
|
<div class="lg:col-span-2"> |
|
<div class="panel"> |
|
|
|
<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> |
|
|
|
|
|
<div id="itemsGrid" class="p-6"> |
|
|
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</main> |
|
|
|
|
|
<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"> |
|
|
|
<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> |
|
|
|
let scene, camera, renderer, controls; |
|
let character = new THREE.Group(); |
|
let currentParts = { |
|
head: null, |
|
body: null, |
|
hair: null, |
|
accessory: null, |
|
weapon: null |
|
}; |
|
|
|
|
|
let characterItems = { |
|
head: [], |
|
body: [], |
|
hair: [], |
|
accessory: [], |
|
weapon: [] |
|
}; |
|
|
|
|
|
let uploadedFiles = { |
|
models: {}, |
|
previews: {} |
|
}; |
|
|
|
|
|
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'); |
|
|
|
|
|
function initThreeJS() { |
|
|
|
scene = new THREE.Scene(); |
|
scene.background = new THREE.Color(0xf0f0f0); |
|
|
|
|
|
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); |
|
|
|
|
|
camera = new THREE.PerspectiveCamera(45, 1, 0.1, 1000); |
|
camera.position.set(0, 100, 300); |
|
|
|
|
|
const canvas = document.getElementById('renderCanvas'); |
|
renderer = new THREE.WebGLRenderer({ canvas, antialias: true }); |
|
renderer.setPixelRatio(window.devicePixelRatio); |
|
renderer.setSize(canvas.clientWidth, canvas.clientHeight); |
|
|
|
|
|
controls = new THREE.OrbitControls(camera, renderer.domElement); |
|
controls.enableDamping = true; |
|
controls.dampingFactor = 0.05; |
|
controls.minDistance = 150; |
|
controls.maxDistance = 500; |
|
|
|
|
|
scene.add(character); |
|
|
|
|
|
const gridHelper = new THREE.GridHelper(200, 20, 0x888888, 0x888888); |
|
gridHelper.position.y = -10; |
|
scene.add(gridHelper); |
|
|
|
|
|
animate(); |
|
} |
|
|
|
|
|
function animate() { |
|
requestAnimationFrame(animate); |
|
controls.update(); |
|
renderer.render(scene, camera); |
|
} |
|
|
|
|
|
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); |
|
}); |
|
}); |
|
} |
|
|
|
|
|
function addCharacterPart(partType, item) { |
|
|
|
if (currentParts[partType]) { |
|
character.remove(currentParts[partType]); |
|
} |
|
|
|
|
|
if (uploadedFiles.models[item.id]) { |
|
const modelData = uploadedFiles.models[item.id]; |
|
|
|
|
|
const mtlLoader = new THREE.MTLLoader(); |
|
const mtlContent = modelData.mtl; |
|
const materials = mtlLoader.parse(mtlContent); |
|
materials.preload(); |
|
|
|
|
|
const objLoader = new THREE.OBJLoader(); |
|
objLoader.setMaterials(materials); |
|
|
|
|
|
const model = objLoader.parse(modelData.obj); |
|
|
|
|
|
positionModel(partType, model); |
|
|
|
|
|
model.userData = { id: item.id }; |
|
|
|
character.add(model); |
|
currentParts[partType] = model; |
|
} else { |
|
console.error("Model not found in memory:", item.id); |
|
} |
|
} |
|
|
|
|
|
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; |
|
} |
|
} |
|
|
|
|
|
function init() { |
|
initThreeJS(); |
|
|
|
|
|
loadCategoryItems('head'); |
|
|
|
|
|
setupEventListeners(); |
|
|
|
|
|
createSakuraPetals(); |
|
|
|
|
|
loadSavedItems(); |
|
} |
|
|
|
|
|
function setupEventListeners() { |
|
|
|
categoryButtons.forEach(button => { |
|
button.addEventListener('click', () => { |
|
const category = button.dataset.category; |
|
|
|
categoryButtons.forEach(btn => btn.classList.remove('active')); |
|
button.classList.add('active'); |
|
|
|
loadCategoryItems(category); |
|
}); |
|
}); |
|
|
|
|
|
adminBtn.addEventListener('click', () => { |
|
adminModal.style.display = 'flex'; |
|
}); |
|
|
|
|
|
closeAdminModal.addEventListener('click', () => { |
|
adminModal.style.display = 'none'; |
|
}); |
|
|
|
|
|
randomizeBtn.addEventListener('click', randomizeCharacter); |
|
|
|
|
|
downloadBtn.addEventListener('click', downloadCharacter); |
|
|
|
|
|
addItemBtn.addEventListener('click', addNewItem); |
|
|
|
|
|
removeItemBtn.addEventListener('click', removeItem); |
|
|
|
|
|
modelFilesInput.addEventListener('change', handleModelUpload); |
|
|
|
|
|
previewImageInput.addEventListener('change', handlePreviewImageUpload); |
|
|
|
|
|
window.addEventListener('resize', onWindowResize); |
|
|
|
|
|
setupDragAndDrop(); |
|
} |
|
|
|
|
|
function setupDragAndDrop() { |
|
const uploadSection = document.querySelector('.upload-section'); |
|
|
|
|
|
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { |
|
uploadSection.addEventListener(eventName, preventDefaults, false); |
|
document.body.addEventListener(eventName, preventDefaults, false); |
|
}); |
|
|
|
|
|
['dragenter', 'dragover'].forEach(eventName => { |
|
uploadSection.addEventListener(eventName, highlight, false); |
|
}); |
|
|
|
['dragleave', 'drop'].forEach(eventName => { |
|
uploadSection.addEventListener(eventName, unhighlight, false); |
|
}); |
|
|
|
|
|
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; |
|
|
|
|
|
const validFiles = Array.from(files).filter(file => |
|
file.name.endsWith('.obj') || file.name.endsWith('.mtl') |
|
); |
|
|
|
if (validFiles.length > 0) { |
|
|
|
const dataTransfer = new DataTransfer(); |
|
validFiles.forEach(file => dataTransfer.items.add(file)); |
|
|
|
|
|
modelFilesInput.files = dataTransfer.files; |
|
|
|
|
|
const event = new Event('change'); |
|
modelFilesInput.dispatchEvent(event); |
|
} else { |
|
uploadStatus.textContent = 'Please drop OBJ or MTL files only'; |
|
uploadStatus.style.color = 'red'; |
|
} |
|
} |
|
|
|
|
|
function loadCategoryItems(category) { |
|
itemsGrid.innerHTML = ''; |
|
|
|
|
|
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', () => { |
|
|
|
if (currentParts[category]) { |
|
character.remove(currentParts[category]); |
|
currentParts[category] = null; |
|
} |
|
|
|
|
|
document.querySelectorAll(`.item-thumbnail`).forEach(item => { |
|
item.classList.remove('selected'); |
|
}); |
|
noneItem.classList.add('selected'); |
|
}); |
|
itemsGrid.appendChild(noneItem); |
|
|
|
|
|
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' : ''}`; |
|
|
|
|
|
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', () => { |
|
|
|
addCharacterPart(category, item); |
|
|
|
|
|
document.querySelectorAll(`.item-thumbnail`).forEach(item => { |
|
item.classList.remove('selected'); |
|
}); |
|
itemElement.classList.add('selected'); |
|
}); |
|
itemsGrid.appendChild(itemElement); |
|
}); |
|
} |
|
|
|
|
|
function randomizeCharacter() { |
|
Object.keys(characterItems).forEach(category => { |
|
if (characterItems[category].length > 0) { |
|
|
|
const randomIndex = Math.floor(Math.random() * characterItems[category].length); |
|
const randomItem = characterItems[category][randomIndex]; |
|
|
|
|
|
addCharacterPart(category, randomItem); |
|
} |
|
}); |
|
|
|
|
|
const currentCategory = document.querySelector('.category-btn.active').dataset.category; |
|
loadCategoryItems(currentCategory); |
|
} |
|
|
|
|
|
function downloadCharacter() { |
|
|
|
controls.enabled = false; |
|
|
|
|
|
renderer.render(scene, camera); |
|
const canvas = renderer.domElement; |
|
|
|
|
|
const link = document.createElement('a'); |
|
link.download = 'my-anime-character.png'; |
|
link.href = canvas.toDataURL('image/png'); |
|
link.click(); |
|
|
|
|
|
controls.enabled = true; |
|
|
|
|
|
showNotification('Character downloaded!', 'green'); |
|
} |
|
|
|
|
|
function handleModelUpload(e) { |
|
const files = e.target.files; |
|
if (files.length === 0) return; |
|
|
|
|
|
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%'; |
|
|
|
|
|
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'; |
|
|
|
|
|
processModelFiles(files); |
|
} |
|
}, 100); |
|
} |
|
|
|
|
|
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; |
|
} |
|
|
|
|
|
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; |
|
|
|
|
|
const tempId = 'temp_' + Date.now(); |
|
|
|
|
|
uploadedFiles.models[tempId] = { |
|
obj: objContent, |
|
mtl: mtlContent |
|
}; |
|
|
|
|
|
modelFilesInput.dataset.tempId = tempId; |
|
|
|
uploadStatus.textContent = 'Model files processed and ready!'; |
|
uploadStatus.style.color = '#9dff6b'; |
|
}; |
|
|
|
mtlReader.readAsText(mtlFile); |
|
}; |
|
|
|
objReader.readAsText(objFile); |
|
} |
|
|
|
|
|
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; |
|
|
|
|
|
uploadPreview.innerHTML = `<img src="${imageUrl}" alt="Preview">`; |
|
|
|
|
|
const tempId = 'temp_' + Date.now(); |
|
|
|
|
|
uploadedFiles.previews[tempId] = imageUrl; |
|
|
|
|
|
previewImageInput.dataset.tempId = tempId; |
|
|
|
uploadStatus.textContent = 'Preview image uploaded!'; |
|
uploadStatus.style.color = '#9dff6b'; |
|
}; |
|
reader.readAsDataURL(file); |
|
} |
|
|
|
|
|
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; |
|
} |
|
|
|
|
|
const modelTempId = modelFilesInput.dataset.tempId; |
|
if (!modelTempId || !uploadedFiles.models[modelTempId]) { |
|
showNotification('Please upload model files (OBJ + MTL)', 'red'); |
|
return; |
|
} |
|
|
|
|
|
const id = `${category}_${Date.now()}`; |
|
|
|
|
|
const previewTempId = previewImageInput.dataset.tempId; |
|
if (previewTempId && uploadedFiles.previews[previewTempId]) { |
|
|
|
uploadedFiles.previews[id] = uploadedFiles.previews[previewTempId]; |
|
delete uploadedFiles.previews[previewTempId]; |
|
} |
|
|
|
|
|
uploadedFiles.models[id] = uploadedFiles.models[modelTempId]; |
|
delete uploadedFiles.models[modelTempId]; |
|
|
|
|
|
characterItems[category].push({ |
|
id, |
|
name |
|
}); |
|
|
|
|
|
saveItemsToStorage(); |
|
|
|
|
|
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'); |
|
|
|
|
|
const currentCategory = document.querySelector('.category-btn.active').dataset.category; |
|
if (currentCategory === category) { |
|
loadCategoryItems(category); |
|
} |
|
|
|
|
|
showNotification('Item added successfully!', 'blue'); |
|
} |
|
|
|
|
|
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; |
|
} |
|
|
|
|
|
const itemIndex = characterItems[category].findIndex(item => item.name === name); |
|
|
|
if (itemIndex === -1) { |
|
showNotification('Item not found', 'red'); |
|
return; |
|
} |
|
|
|
|
|
const itemId = characterItems[category][itemIndex].id; |
|
|
|
|
|
characterItems[category].splice(itemIndex, 1); |
|
|
|
|
|
delete uploadedFiles.models[itemId]; |
|
delete uploadedFiles.previews[itemId]; |
|
|
|
|
|
if (currentParts[category] && currentParts[category].userData.id === itemId) { |
|
character.remove(currentParts[category]); |
|
currentParts[category] = null; |
|
} |
|
|
|
|
|
saveItemsToStorage(); |
|
|
|
|
|
document.getElementById('adminItemName').value = ''; |
|
|
|
|
|
const currentCategory = document.querySelector('.category-btn.active').dataset.category; |
|
if (currentCategory === category) { |
|
loadCategoryItems(category); |
|
} |
|
|
|
|
|
showNotification('Item removed successfully!', 'red'); |
|
} |
|
|
|
|
|
function saveItemsToStorage() { |
|
localStorage.setItem('animeCharacterItems', JSON.stringify(characterItems)); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
function loadSavedItems() { |
|
const savedItems = localStorage.getItem('animeCharacterItems'); |
|
if (savedItems) { |
|
characterItems = JSON.parse(savedItems); |
|
|
|
|
|
const currentCategory = document.querySelector('.category-btn.active').dataset.category; |
|
loadCategoryItems(currentCategory); |
|
} |
|
} |
|
|
|
|
|
function onWindowResize() { |
|
const canvas = document.getElementById('renderCanvas'); |
|
camera.aspect = canvas.clientWidth / canvas.clientHeight; |
|
camera.updateProjectionMatrix(); |
|
renderer.setSize(canvas.clientWidth, canvas.clientHeight); |
|
} |
|
|
|
|
|
function createSakuraPetals() { |
|
for (let i = 0; i < 30; i++) { |
|
createSakuraPetal(); |
|
} |
|
} |
|
|
|
function createSakuraPetal() { |
|
const petal = document.createElement('div'); |
|
petal.className = 'sakura'; |
|
|
|
|
|
const startX = Math.random() * window.innerWidth; |
|
const startY = -20; |
|
|
|
|
|
const size = Math.random() * 10 + 5; |
|
|
|
|
|
const rotation = Math.random() * 360; |
|
|
|
|
|
const duration = Math.random() * 10 + 10; |
|
|
|
|
|
const delay = Math.random() * 5; |
|
|
|
|
|
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)`; |
|
|
|
|
|
sakuraContainer.appendChild(petal); |
|
|
|
|
|
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' |
|
}); |
|
|
|
|
|
animation.onfinish = () => { |
|
petal.remove(); |
|
createSakuraPetal(); |
|
}; |
|
} |
|
|
|
|
|
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); |
|
} |
|
|
|
|
|
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> |