Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Realistic Gun Customizer</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/three@0.132.2/build/three.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/controls/OrbitControls.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/loaders/GLTFLoader.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/loaders/DRACOLoader.min.js"></script> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css"> | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Ubuntu+Mono:wght@400;700&display=swap'); | |
| body { | |
| font-family: 'Ubuntu Mono', monospace; | |
| background: #1a1a1a; | |
| color: #e0e0e0; | |
| overflow-x: hidden; | |
| } | |
| #gun-container { | |
| width: 100%; | |
| height: 60vh; | |
| background: #222; | |
| border-radius: 8px; | |
| position: relative; | |
| } | |
| #gun-canvas { | |
| width: 100%; | |
| height: 100%; | |
| display: block; | |
| } | |
| .tab-button.active { | |
| border-bottom: 2px solid #dc2626; | |
| color: white; | |
| } | |
| .part-item { | |
| transition: all 0.2s ease; | |
| } | |
| .part-item:hover { | |
| transform: translateY(-2px); | |
| background: #333; | |
| } | |
| .part-item.selected { | |
| border: 1px solid #dc2626; | |
| background: #333; | |
| } | |
| ::-webkit-scrollbar { | |
| width: 8px; | |
| } | |
| ::-webkit-scrollbar-track { | |
| background: #1a1a1a; | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background: #444; | |
| border-radius: 4px; | |
| } | |
| .color-swatch { | |
| width: 24px; | |
| height: 24px; | |
| border-radius: 50%; | |
| cursor: pointer; | |
| border: 2px solid transparent; | |
| } | |
| .color-swatch.selected { | |
| border-color: white; | |
| transform: scale(1.1); | |
| } | |
| #music-control { | |
| position: fixed; | |
| bottom: 20px; | |
| right: 20px; | |
| z-index: 100; | |
| background: rgba(0,0,0,0.7); | |
| border-radius: 50%; | |
| width: 50px; | |
| height: 50px; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| } | |
| #music-control:hover { | |
| transform: scale(1.1); | |
| background: rgba(220, 38, 38, 0.7); | |
| } | |
| </style> | |
| </head> | |
| <body class="min-h-screen"> | |
| <!-- Audio Elements --> | |
| <audio id="clickSound" src="https://assets.mixkit.co/sfx/preview/mixkit-gun-reload-1668.mp3" preload="auto"></audio> | |
| <audio id="lofiMusic" loop> | |
| <source src="https://assets.mixkit.co/music/preview/mixkit-chilled-urban-jazz-522.mp3" type="audio/mpeg"> | |
| </audio> | |
| <!-- Music Control --> | |
| <div id="music-control" title="Toggle Music"> | |
| <i class="fas fa-music text-white text-xl"></i> | |
| </div> | |
| <div class="container mx-auto px-4 py-8"> | |
| <div class="flex flex-col lg:flex-row gap-8"> | |
| <!-- Left Panel - 3D Gun Viewer --> | |
| <div class="w-full lg:w-2/3"> | |
| <div class="flex justify-between items-center mb-4"> | |
| <h1 class="text-2xl font-bold text-red-600">GUN CUSTOMIZER</h1> | |
| <button id="save-design" class="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-md"> | |
| <i class="fas fa-save mr-2"></i>Save Design | |
| </button> | |
| </div> | |
| <div id="gun-container" class="shadow-lg"> | |
| <canvas id="gun-canvas"></canvas> | |
| </div> | |
| <div class="mt-6 bg-gray-800 p-4 rounded-md"> | |
| <h2 class="text-lg font-semibold mb-2">Current Model: <span id="current-model" class="text-red-400">Glock 17</span></h2> | |
| <div class="grid grid-cols-2 md:grid-cols-4 gap-4"> | |
| <div class="bg-gray-700 p-3 rounded-md"> | |
| <h3 class="text-sm text-gray-400">Model</h3> | |
| <p class="font-mono">3D Demo</p> | |
| </div> | |
| <div class="bg-gray-700 p-3 rounded-md"> | |
| <h3 class="text-sm text-gray-400">Format</h3> | |
| <p class="font-mono">GLTF</p> | |
| </div> | |
| <div class="bg-gray-700 p-3 rounded-md"> | |
| <h3 class="text-sm text-gray-400">Type</h3> | |
| <p class="font-mono">3D Model</p> | |
| </div> | |
| <div class="bg-gray-700 p-3 rounded-md"> | |
| <h3 class="text-sm text-gray-400">Source</h3> | |
| <p class="font-mono">Three.js</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Right Panel - Customization Options --> | |
| <div class="w-full lg:w-1/3 bg-gray-800 rounded-lg shadow-lg p-4 flex flex-col"> | |
| <!-- Tabs --> | |
| <div class="flex border-b border-gray-700 mb-4"> | |
| <button class="tab-button active px-4 py-2" data-tab="model">MODEL</button> | |
| <button class="tab-button px-4 py-2" data-tab="parts">PARTS</button> | |
| <button class="tab-button px-4 py-2" data-tab="colors">COLORS</button> | |
| <button class="tab-button px-4 py-2" data-tab="attachments">ATTACHMENTS</button> | |
| </div> | |
| <!-- Tab Content --> | |
| <div class="flex-grow overflow-y-auto pb-4"> | |
| <!-- Model Selection Tab --> | |
| <div id="model-tab" class="tab-content active"> | |
| <h3 class="font-semibold mb-3 text-gray-300">SELECT WEAPON TYPE</h3> | |
| <div class="space-y-2"> | |
| <button class="model-select-btn w-full text-left px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded-md" data-model="glock17"> | |
| <div class="flex items-center"> | |
| <img src="https://via.placeholder.com/80x40/333/666?text=Glock17" class="w-20 h-10 object-contain mr-3"> | |
| <span>Glock 17 (Pistol)</span> | |
| </div> | |
| </button> | |
| <button class="model-select-btn w-full text-left px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded-md" data-model="m4a1"> | |
| <div class="flex items-center"> | |
| <img src="https://via.placeholder.com/80x40/333/666?text=M4A1" class="w-20 h-10 object-contain mr-3"> | |
| <span>M4A1 (Assault Rifle)</span> | |
| </div> | |
| </button> | |
| <button class="model-select-btn w-full text-left px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded-md" data-model="awp"> | |
| <div class="flex items-center"> | |
| <img src="https://via.placeholder.com/80x40/333/666?text=AWP" class="w-20 h-10 object-contain mr-3"> | |
| <span>AWP (Sniper Rifle)</span> | |
| </div> | |
| </button> | |
| <button class="model-select-btn w-full text-left px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded-md" data-model="mp5"> | |
| <div class="flex items-center"> | |
| <img src="https://via.placeholder.com/80x40/333/666?text=MP5" class="w-20 h-10 object-contain mr-3"> | |
| <span>MP5 (SMG)</span> | |
| </div> | |
| </button> | |
| <button class="model-select-btn w-full text-left px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded-md" data-model="870"> | |
| <div class="flex items-center"> | |
| <img src="https://via.placeholder.com/80x40/333/666?text=Rem870" class="w-20 h-10 object-contain mr-3"> | |
| <span>Remington 870 (Shotgun)</span> | |
| </div> | |
| </button> | |
| <button class="model-select-btn w-full text-left px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded-md" data-model="x26"> | |
| <div class="flex items-center"> | |
| <img src="https://via.placeholder.com/80x40/333/666?text=Taser" class="w-20 h-10 object-contain mr-3"> | |
| <span>Taser X26 (Non-Lethal)</span> | |
| </div> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Parts Customization Tab --> | |
| <div id="parts-tab" class="tab-content hidden"> | |
| <h3 class="font-semibold mb-3 text-gray-300">SELECT PART TO MODIFY</h3> | |
| <div class="space-y-2"> | |
| <button class="part-btn part-item px-4 py-2 bg-gray-700 rounded-md w-full text-left" data-part="barrel"> | |
| <i class="fas fa-bolt mr-2 text-red-400"></i> Barrel | |
| </button> | |
| <button class="part-btn part-item px-4 py-2 bg-gray-700 rounded-md w-full text-left" data-part="slide"> | |
| <i class="fas fa-arrows-alt-h mr-2 text-red-400"></i> Slide | |
| </button> | |
| <button class="part-btn part-item px-4 py-2 bg-gray-700 rounded-md w-full text-left" data-part="grip"> | |
| <i class="fas fa-hand-holding mr-2 text-red-400"></i> Grip | |
| </button> | |
| <button class="part-btn part-item px-4 py-2 bg-gray-700 rounded-md w-full text-left" data-part="trigger"> | |
| <i class="fas fa-arrow-circle-left mr-2 text-red-400"></i> Trigger | |
| </button> | |
| <button class="part-btn part-item px-4 py-2 bg-gray-700 rounded-md w-full text-left" data-part="magazine"> | |
| <i class="fas fa-clone mr-2 text-red-400"></i> Magazine | |
| </button> | |
| </div> | |
| <div class="mt-6" id="part-options"> | |
| <h3 class="font-semibold mb-3 text-gray-300">SELECT BARREL TYPE</h3> | |
| <div class="grid grid-cols-2 gap-3"> | |
| <div class="part-option part-item p-3 bg-gray-700 rounded-md cursor-pointer text-center" data-option="standard"> | |
| <img src="https://via.placeholder.com/80x40/333/666?text=Standard" class="w-full h-12 object-contain mb-2"> | |
| <p class="text-sm">Standard</p> | |
| </div> | |
| <div class="part-option part-item p-3 bg-gray-700 rounded-md cursor-pointer text-center" data-option="threaded"> | |
| <img src="https://via.placeholder.com/80x40/333/666?text=Threaded" class="w-full h-12 object-contain mb-2"> | |
| <p class="text-sm">Threaded</p> | |
| </div> | |
| <div class="part-option part-item p-3 bg-gray-700 rounded-md cursor-pointer text-center" data-option="extended"> | |
| <img src="https://via.placeholder.com/80x40/333/666?text=Extended" class="w-full h-12 object-contain mb-2"> | |
| <p class="text-sm">Extended</p> | |
| </div> | |
| <div class="part-option part-item p-3 bg-gray-700 rounded-md cursor-pointer text-center" data-option="suppressed"> | |
| <img src="https://via.placeholder.com/80x40/333/666?text=Suppressed" class="w-full h-12 object-contain mb-2"> | |
| <p class="text-sm">Suppressed</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Colors Customization Tab --> | |
| <div id="colors-tab" class="tab-content hidden"> | |
| <h3 class="font-semibold mb-3 text-gray-300">SELECT PART TO RECOLOR</h3> | |
| <div class="space-y-2"> | |
| <button class="color-part-btn part-item px-4 py-2 bg-gray-700 rounded-md w-full text-left" data-part="frame"> | |
| <i class="fas fa-square mr-2" style="color: #555;"></i> Frame | |
| </button> | |
| <button class="color-part-btn part-item px-4 py-2 bg-gray-700 rounded-md w-full text-left" data-part="slide"> | |
| <i class="fas fa-square mr-2" style="color: #777;"></i> Slide | |
| </button> | |
| <button class="color-part-btn part-item px-4 py-2 bg-gray-700 rounded-md w-full text-left" data-part="grip"> | |
| <i class="fas fa-square mr-2" style="color: #333;"></i> Grip | |
| </button> | |
| <button class="color-part-btn part-item px-4 py-2 bg-gray-700 rounded-md w-full text-left" data-part="barrel"> | |
| <i class="fas fa-square mr-2" style="color: #444;"></i> Barrel | |
| </button> | |
| <button class="color-part-btn part-item px-4 py-2 bg-gray-700 rounded-md w-full text-left" data-part="trigger"> | |
| <i class="fas fa-square mr-2" style="color: #888;"></i> Trigger | |
| </button> | |
| </div> | |
| <div class="mt-6"> | |
| <h3 class="font-semibold mb-3 text-gray-300">SELECT COLOR</h3> | |
| <div class="grid grid-cols-6 gap-3"> | |
| <div class="color-swatch selected" style="background-color: #222;" data-color="#222"></div> | |
| <div class="color-swatch" style="background-color: #555;" data-color="#555"></div> | |
| <div class="color-swatch" style="background-color: #777;" data-color="#777"></div> | |
| <div class="color-swatch" style="background-color: #454545;" data-color="#454545"></div> | |
| <div class="color-swatch" style="background-color: #dc2626;" data-color="#dc2626"></div> | |
| <div class="color-swatch" style="background-color: #166534;" data-color="#166534"></div> | |
| <div class="color-swatch" style="background-color: #1e40af;" data-color="#1e40af"></div> | |
| <div class="color-swatch" style="background-color: #713f12;" data-color="#713f12"></div> | |
| <div class="color-swatch" style="background-color: #111;" data-color="#111"></div> | |
| <div class="color-swatch" style="background-color: #444;" data-color="#444"></div> | |
| <div class="color-swatch" style="background-color: #666;" data-color="#666"></div> | |
| <div class="color-swatch" style="background-color: #999;" data-color="#999"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Attachments Tab --> | |
| <div id="attachments-tab" class="tab-content hidden"> | |
| <h3 class="font-semibold mb-3 text-gray-300">ATTACHMENT POINTS</h3> | |
| <div class="space-y-2"> | |
| <button class="attachment-btn part-item px-4 py-2 bg-gray-700 rounded-md w-full text-left" data-slot="top"> | |
| <i class="fas fa-arrow-up mr-2 text-red-400"></i> Top Rail (Optics) | |
| </button> | |
| <button class="attachment-btn part-item px-4 py-2 bg-gray-700 rounded-md w-full text-left" data-slot="front"> | |
| <i class="fas fa-arrow-right mr-2 text-red-400"></i> Front Rail (Flashlight) | |
| </button> | |
| <button class="attachment-btn part-item px-4 py-2 bg-gray-700 rounded-md w-full text-left" data-slot="side"> | |
| <i class="fas fa-arrow-left mr-2 text-red-400"></i> Side Rail (Laser) | |
| </button> | |
| <button class="attachment-btn part-item px-4 py-2 bg-gray-700 rounded-md w-full text-left" data-slot="under"> | |
| <i class="fas fa-arrow-down mr-2 text-red-400"></i> Under Rail (Grip) | |
| </button> | |
| <button class="attachment-btn part-item px-4 py-2 bg-gray-700 rounded-md w-full text-left" data-slot="muzzle"> | |
| <i class="fas fa-bullseye mr-2 text-red-400"></i> Muzzle (Suppressor) | |
| </button> | |
| </div> | |
| <div class="mt-6"> | |
| <h3 class="font-semibold mb-3 text-gray-300">AVAILABLE ATTACHMENTS</h3> | |
| <div id="attachment-options" class="grid grid-cols-2 gap-3"> | |
| <div class="attachment-option part-item p-3 bg-gray-700 rounded-md cursor-pointer text-center" data-attachment="none"> | |
| <img src="https://via.placeholder.com/80x40/333/666?text=Empty" class="w-full h-12 object-contain mb-2"> | |
| <p class="text-sm">None</p> | |
| </div> | |
| <div class="attachment-option part-item p-3 bg-gray-700 rounded-md cursor-pointer text-center" data-attachment="holo"> | |
| <img src="https://via.placeholder.com/80x40/333/666?text=Holo" class="w-full h-12 object-contain mb-2"> | |
| <p class="text-sm">Holo Sight</p> | |
| </div> | |
| <div class="attachment-option part-item p-3 bg-gray-700 rounded-md cursor-pointer text-center" data-attachment="reddot"> | |
| <img src="https://via.placeholder.com/80x40/333/666?text=RedDot" class="w-full h-12 object-contain mb-2"> | |
| <p class="text-sm">Red Dot</p> | |
| </div> | |
| <div class="attachment-option part-item p-3 bg-gray-700 rounded-md cursor-pointer text-center" data-attachment="acog"> | |
| <img src="https://via.placeholder.com/80x40/333/666?text=ACOG" class="w-full h-12 object-contain mb-2"> | |
| <p class="text-sm">ACOG Scope</p> | |
| </div> | |
| <div class="attachment-option part-item p-3 bg-gray-700 rounded-md cursor-pointer text-center" data-attachment="suppressor"> | |
| <img src="https://via.placeholder.com/80x40/333/666?text=Suppressor" class="w-full h-12 object-contain mb-2"> | |
| <p class="text-sm">Suppressor</p> | |
| </div> | |
| <div class="attachment-option part-item p-3 bg-gray-700 rounded-md cursor-pointer text-center" data-attachment="flashlight"> | |
| <img src="https://via.placeholder.com/80x40/333/666?text=Flashlight" class="w-full h-12 object-contain mb-2"> | |
| <p class="text-sm">Flashlight</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Preview & Reset --> | |
| <div class="mt-auto pt-4 border-t border-gray-700"> | |
| <div class="flex gap-3"> | |
| <button id="reset-colors" class="flex-1 bg-gray-600 hover:bg-gray-500 text-white px-4 py-2 rounded-md"> | |
| <i class="fas fa-palette mr-2"></i>Reset Colors | |
| </button> | |
| <button id="reset-parts" class="flex-1 bg-gray-600 hover:bg-gray-500 text-white px-4 py-2 rounded-md"> | |
| <i class="fas fa-undo mr-2"></i>Reset Parts | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // Initialize Three.js scene, camera, and renderer | |
| let scene, camera, renderer, gunModel, controls; | |
| // Current gun configuration | |
| const gunConfig = { | |
| model: 'glock17', | |
| parts: { | |
| barrel: 'standard', | |
| slide: 'standard', | |
| grip: 'standard', | |
| trigger: 'standard', | |
| magazine: 'standard' | |
| }, | |
| colors: { | |
| frame: '#222', | |
| slide: '#777', | |
| grip: '#333', | |
| barrel: '#444', | |
| trigger: '#888' | |
| }, | |
| attachments: { | |
| top: 'none', | |
| front: 'none', | |
| side: 'none', | |
| under: 'none', | |
| muzzle: 'none' | |
| } | |
| }; | |
| function initThreeJS() { | |
| // Scene | |
| scene = new THREE.Scene(); | |
| scene.background = new THREE.Color(0x222222); | |
| scene.add(new THREE.AmbientLight(0x404040)); | |
| // Camera | |
| camera = new THREE.PerspectiveCamera(75, document.getElementById('gun-container').clientWidth / document.getElementById('gun-container').clientHeight, 0.1, 1000); | |
| camera.position.z = 5; | |
| // Renderer | |
| renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('gun-canvas'), antialias: true }); | |
| renderer.setSize(document.getElementById('gun-container').clientWidth, document.getElementById('gun-container').clientHeight); | |
| // Lighting | |
| const directionalLight = new THREE.DirectionalLight(0xffffff, 1); | |
| directionalLight.position.set(1, 1, 1); | |
| scene.add(directionalLight); | |
| const light2 = new THREE.DirectionalLight(0xffffff, 0.5); | |
| light2.position.set(-1, -1, -1); | |
| scene.add(light2); | |
| // Controls | |
| controls = new THREE.OrbitControls(camera, renderer.domElement); | |
| controls.enableDamping = true; | |
| controls.dampingFactor = 0.25; | |
| controls.screenSpacePanning = false; | |
| controls.maxPolarAngle = Math.PI / 2; | |
| controls.minDistance = 2; | |
| controls.maxDistance = 10; | |
| // Load initial gun model | |
| loadGunModel(gunConfig.model); | |
| // Handle window resize | |
| window.addEventListener('resize', onWindowResize); | |
| } | |
| function onWindowResize() { | |
| const container = document.getElementById('gun-container'); | |
| camera.aspect = container.clientWidth / container.clientHeight; | |
| camera.updateProjectionMatrix(); | |
| renderer.setSize(container.clientWidth, container.clientHeight); | |
| } | |
| function animate() { | |
| requestAnimationFrame(animate); | |
| controls.update(); | |
| renderer.render(scene, camera); | |
| } | |
| function loadGunModel(modelName) { | |
| // Remove existing model | |
| if (gunModel) { | |
| scene.remove(gunModel); | |
| gunModel = null; | |
| } | |
| const loader = new GLTFLoader(); | |
| const dracoLoader = new DRACOLoader(); | |
| dracoLoader.setDecoderPath('https://www.gstatic.com/draco/versioned/decoders/1.4.1/'); | |
| loader.setDRACOLoader(dracoLoader); | |
| let modelUrl; | |
| switch(modelName) { | |
| case 'glock17': | |
| modelUrl = 'https://threejs.org/examples/models/gltf/LittlestTokyo.glb'; | |
| break; | |
| case 'm4a1': | |
| modelUrl = 'https://threejs.org/examples/models/gltf/DamagedHelmet/glTF/DamagedHelmet.gltf'; | |
| break; | |
| case 'awp': | |
| modelUrl = 'https://threejs.org/examples/models/gltf/FlightHelmet/glTF/FlightHelmet.gltf'; | |
| break; | |
| case 'mp5': | |
| modelUrl = 'https://threejs.org/examples/models/gltf/LeePerrySmith/LeePerrySmith.glb'; | |
| break; | |
| case '870': | |
| modelUrl = 'https://threejs.org/examples/models/gltf/kira.glb'; | |
| break; | |
| case 'x26': | |
| modelUrl = 'https://threejs.org/examples/models/gltf/duck/glTF/Duck.gltf'; | |
| break; | |
| } | |
| loader.load( | |
| modelUrl, | |
| function (gltf) { | |
| gunModel = gltf.scene; | |
| scene.add(gunModel); | |
| // Set initial position and scale based on model | |
| if (modelName === 'glock17') { | |
| gunModel.scale.set(1, 1, 1); | |
| gunModel.position.y = -1; | |
| } else if (modelName === 'x26') { | |
| gunModel.scale.set(0.01, 0.01, 0.01); | |
| gunModel.position.y = -0.5; | |
| } else { | |
| gunModel.scale.set(0.5, 0.5, 0.5); | |
| gunModel.position.y = -1; | |
| } | |
| // Add initial material variation | |
| gunModel.traverse(function(child) { | |
| if (child.isMesh) { | |
| child.material = new THREE.MeshStandardMaterial({ | |
| color: getColorForPart(child.name), | |
| metalness: 0.7, | |
| roughness: 0.3 | |
| }); | |
| } | |
| }); | |
| }, | |
| undefined, | |
| function (error) { | |
| console.error('Error loading model:', error); | |
| } | |
| ); | |
| function getColorForPart(partName) { | |
| if (!partName) return new THREE.Color('#777777'); | |
| partName = partName.toLowerCase(); | |
| if (partName.includes('frame') || partName.includes('body')) { | |
| return new THREE.Color(gunConfig.colors.frame); | |
| } else if (partName.includes('slide')) { | |
| return new THREE.Color(gunConfig.colors.slide); | |
| } else if (partName.includes('grip')) { | |
| return new THREE.Color(gunConfig.colors.grip); | |
| } else if (partName.includes('barrel')) { | |
| return new THREE.Color(gunConfig.colors.barrel); | |
| } else if (partName.includes('trigger')) { | |
| return new THREE.Color(gunConfig.colors.trigger); | |
| } | |
| return new THREE.Color('#777777'); | |
| } | |
| gunConfig.model = modelName; | |
| updateCurrentModelText(modelName); | |
| // Position camera based on gun size | |
| camera.position.z = modelName === 'awp' ? 8 : 5; | |
| controls.minDistance = modelName === 'awp' ? 5 : 2; | |
| controls.maxDistance = modelName === 'awp' ? 15 : 10; | |
| } | |
| function updateCurrentModelText(modelName) { | |
| let displayName = ''; | |
| switch(modelName) { | |
| case 'glock17': displayName = 'Tokyo Model'; break; | |
| case 'm4a1': displayName = 'Damaged Helmet'; break; | |
| case 'awp': displayName = 'Flight Helmet'; break; | |
| case 'mp5': displayName = 'Character Head'; break; | |
| case '870': displayName = 'Kira Model'; break; | |
| case 'x26': displayName = 'Duck Model'; break; | |
| } | |
| document.getElementById('current-model').textContent = displayName; | |
| } | |
| // UI Event Handlers | |
| function setupEventListeners() { | |
| // Tab switching | |
| document.querySelectorAll('.tab-button').forEach(button => { | |
| button.addEventListener('click', function() { | |
| const tabName = this.getAttribute('data-tab'); | |
| // Play click sound | |
| document.getElementById('clickSound').play(); | |
| // Update active tab | |
| document.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active')); | |
| this.classList.add('active'); | |
| // Show correct tab content | |
| document.querySelectorAll('.tab-content').forEach(content => content.classList.add('hidden')); | |
| document.getElementById(`${tabName}-tab`).classList.remove('hidden'); | |
| }); | |
| }); | |
| // Model selection | |
| document.querySelectorAll('.model-select-btn').forEach(button => { | |
| button.addEventListener('click', function() { | |
| const model = this.getAttribute('data-model'); | |
| document.getElementById('clickSound').play(); | |
| loadGunModel(model); | |
| }); | |
| }); | |
| // Part selection | |
| document.querySelectorAll('.part-btn').forEach(button => { | |
| button.addEventListener('click', function() { | |
| document.getElementById('clickSound').play(); | |
| // Highlight selected part | |
| document.querySelectorAll('.part-btn').forEach(btn => btn.classList.remove('selected')); | |
| this.classList.add('selected'); | |
| // Update part options display | |
| const part = this.getAttribute('data-part'); | |
| document.querySelector('#part-options h3').textContent = `SELECT ${part.toUpperCase()} TYPE`; | |
| // In a real app, we would update the options shown based on the selected part | |
| }); | |
| }); | |
| // Part options | |
| document.querySelectorAll('.part-option').forEach(option => { | |
| option.addEventListener('click', function() { | |
| document.getElementById('clickSound').play(); | |
| // Highlight selected option | |
| document.querySelectorAll('.part-option').forEach(opt => opt.classList.remove('selected')); | |
| this.classList.add('selected'); | |
| // In a real app, we would update the gun model with the selected part | |
| }); | |
| }); | |
| // Color part selection | |
| document.querySelectorAll('.color-part-btn').forEach(button => { | |
| button.addEventListener('click', function() { | |
| document.getElementById('clickSound').play(); | |
| // Highlight selected part | |
| document.querySelectorAll('.color-part-btn').forEach(btn => btn.classList.remove('selected')); | |
| this.classList.add('selected'); | |
| }); | |
| }); | |
| // Color selection | |
| document.querySelectorAll('.color-swatch').forEach(swatch => { | |
| swatch.addEventListener('click', function() { | |
| const color = this.getAttribute('data-color'); | |
| // Highlight selected color | |
| document.querySelectorAll('.color-swatch').forEach(s => s.classList.remove('selected')); | |
| this.classList.add('selected'); | |
| // Get currently selected part | |
| const selectedPartBtn = document.querySelector('.color-part-btn.selected'); | |
| if (selectedPartBtn) { | |
| const part = selectedPartBtn.getAttribute('data-part'); | |
| gunConfig.colors[part] = color; | |
| // Update model colors | |
| if (gunModel) { | |
| gunModel.traverse(function(child) { | |
| if (child.isMesh) { | |
| const color = getColorForPart(child.name); | |
| child.material.color.copy(color); | |
| } | |
| }); | |
| } | |
| } | |
| }); | |
| }); | |
| // Attachment slot selection | |
| document.querySelectorAll('.attachment-btn').forEach(button => { | |
| button.addEventListener('click', function() { | |
| document.getElementById('clickSound').play(); | |
| // Highlight selected slot | |
| document.querySelectorAll('.attachment-btn').forEach(btn => btn.classList.remove('selected')); | |
| this.classList.add('selected'); | |
| // In a real app, we would filter/sort available attachments based on the slot | |
| }); | |
| }); | |
| // Attachment selection | |
| document.querySelectorAll('.attachment-option').forEach(option => { | |
| option.addEventListener('click', function() { | |
| document.getElementById('clickSound').play(); | |
| // Highlight selected option | |
| document.querySelectorAll('.attachment-option').forEach(opt => opt.classList.remove('selected')); | |
| this.classList.add('selected'); | |
| // In a real app, we would update the gun model with the selected attachment | |
| }); | |
| }); | |
| // Reset buttons | |
| document.getElementById('reset-colors').addEventListener('click', function() { | |
| document.getElementById('clickSound').play(); | |
| resetColors(); | |
| }); | |
| document.getElementById('reset-parts').addEventListener('click', function() { | |
| document.getElementById('clickSound').play(); | |
| resetParts(); | |
| }); | |
| // Save design | |
| document.getElementById('save-design').addEventListener('click', function() { | |
| document.getElementById('clickSound').play(); | |
| saveDesign(); | |
| }); | |
| // Music control | |
| document.getElementById('music-control').addEventListener('click', function() { | |
| const music = document.getElementById('lofiMusic'); | |
| if (music.paused) { | |
| music.play(); | |
| this.innerHTML = '<i class="fas fa-music text-white text-xl"></i>'; | |
| } else { | |
| music.pause(); | |
| this.innerHTML = '<i class="fas fa-volume-mute text-white text-xl"></i>'; | |
| } | |
| }); | |
| } | |
| function resetColors() { | |
| // Reset to default colors | |
| gunConfig.colors = { | |
| frame: '#222', | |
| slide: '#777', | |
| grip: '#333', | |
| barrel: '#444', | |
| trigger: '#888' | |
| }; | |
| // Update UI | |
| document.querySelectorAll('.color-swatch').forEach(swatch => { | |
| swatch.classList.remove('selected'); | |
| if (swatch.getAttribute('data-color') === '#222') { | |
| swatch.classList.add('selected'); | |
| } | |
| }); | |
| // In a real app, we would update the gun model materials | |
| } | |
| function resetParts() { | |
| // Reset to default parts | |
| gunConfig.parts = { | |
| barrel: 'standard', | |
| slide: 'standard', | |
| grip: 'standard', | |
| trigger: 'standard', | |
| magazine: 'standard' | |
| }; | |
| // Update UI | |
| document.querySelectorAll('.part-option').forEach(option => { | |
| option.classList.remove('selected'); | |
| if (option.getAttribute('data-option') === 'standard') { | |
| option.classList.add('selected'); | |
| } | |
| }); | |
| // In a real app, we would update the gun model | |
| } | |
| function saveDesign() { | |
| // In a real app, this would save the configuration to localStorage or server | |
| alert('Design saved! (This would actually save in a real implementation)'); | |
| console.log('Current gun configuration:', gunConfig); | |
| } | |
| // Initialize the app when DOM is loaded | |
| document.addEventListener('DOMContentLoaded', function() { | |
| initThreeJS(); | |
| setupEventListeners(); | |
| animate(); | |
| // Start with music paused | |
| document.getElementById('lofiMusic').pause(); | |
| }); | |
| </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=bsd121/3d-gun-builder" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
| </html> |