wavetype-xyz-index2 / car-modular-customizer.html
vgrowhouse's picture
undefined - Initial Deployment
5b5deca verified
raw
history blame
24.9 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D Car Parts Customizer - Fantasy Rally</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&display=swap" rel="stylesheet">
<script async src="https://unpkg.com/[email protected]/dist/es-module-shims.js"></script>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/"
}
}
</script>
<style>
:root {
--primary-bg: #0d0c1d;
--secondary-bg: #131224;
--accent-color: #6c63ff;
--accent-glow: rgba(108, 99, 255, 0.5);
--text-primary: #e0e0e0;
--text-secondary: #a0a0a0;
--border-color: #3c3b53;
}
body {
font-family: 'Space Mono', monospace;
background-color: var(--primary-bg);
color: var(--text-primary);
overflow-x: hidden;
}
.hidden {
display: none;
}
/* === CRT Effect === */
.crt-effect::before {
content: " ";
display: block;
position: fixed;
top: 0; left: 0; bottom: 0; right: 0;
background: linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 0, 0, 0.25) 50%), linear-gradient(90deg, rgba(255, 0, 0, 0.06), rgba(0, 255, 0, 0.02), rgba(0, 0, 255, 0.06));
background-size: 100% 2px, 3px 100%;
z-index: 9999;
pointer-events: none;
opacity: 0.2;
}
/* === Header & Buttons === */
.header-btn {
background-color: rgba(108, 99, 255, 0.2);
border: 1px solid var(--accent-glow);
transition: all 0.3s ease;
}
.header-btn:hover {
background-color: rgba(108, 99, 255, 0.4);
box-shadow: 0 0 15px var(--accent-glow);
}
/* === 3D Viewer === */
#viewer-container {
width: 100%;
height: 500px;
position: relative;
border: 2px solid var(--accent-glow);
border-radius: 8px;
overflow: hidden;
background: rgba(19, 18, 36, 0.8);
}
#threed-canvas {
width: 100%;
height: 100%;
display: block;
}
#loading-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(13, 12, 29, 0.8);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 10;
}
.spinner {
width: 50px;
height: 50px;
border: 5px solid rgba(108, 99, 255, 0.3);
border-top: 5px solid var(--accent-color);
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 20px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* === Parts Panel === */
.parts-panel {
background: rgba(19, 18, 36, 0.8);
border: 1px solid var(--border-color);
border-radius: 8px;
max-height: 500px;
overflow-y: auto;
}
.part-category {
border-bottom: 1px solid var(--border-color);
}
.part-category:last-child {
border-bottom: none;
}
.part-item {
padding: 10px;
border-bottom: 1px solid rgba(60, 59, 83, 0.5);
cursor: pointer;
transition: all 0.2s ease;
}
.part-item:last-child {
border-bottom: none;
}
.part-item:hover {
background: rgba(108, 99, 255, 0.2);
}
.part-item.active {
background: rgba(108, 99, 255, 0.4);
border-left: 3px solid var(--accent-color);
}
/* === Controls Panel === */
.controls-panel {
background: rgba(19, 18, 36, 0.8);
border: 1px solid var(--border-color);
border-radius: 8px;
}
.control-group {
border-bottom: 1px solid var(--border-color);
}
.control-group:last-child {
border-bottom: none;
}
.color-picker {
width: 40px;
height: 40px;
border-radius: 50%;
border: 2px solid var(--border-color);
cursor: pointer;
}
/* === Modal Styles === */
.modal-overlay {
background-color: rgba(13, 12, 29, 0.9);
backdrop-filter: blur(10px);
}
.modal-content {
background-color: var(--secondary-bg);
border: 1px solid var(--border-color);
}
</style>
</head>
<body class="crt-effect">
<header class="p-4 flex flex-wrap justify-between items-center gap-4 bg-secondary-bg border-b border-border-color sticky top-0 z-20">
<h1 id="header-title" class="text-2xl font-bold tracking-wider text-accent">3D CAR PARTS CUSTOMIZER</h1>
<div class="flex items-center gap-2">
<button id="back-to-hub-btn" class="header-btn text-white font-bold py-2 px-4 rounded-lg"><i class="fas fa-arrow-left mr-2"></i>Back to Hub</button>
</div>
</header>
<main class="p-4 sm:p-6">
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6">
<!-- 3D Viewer -->
<div class="lg:col-span-2">
<div class="card-bg rounded-lg p-4">
<h2 class="text-xl font-bold mb-4">3D Viewer</h2>
<div id="viewer-container">
<div id="loading-overlay">
<div class="spinner"></div>
<p>Loading 3D Model...</p>
</div>
<canvas id="threed-canvas"></canvas>
</div>
</div>
</div>
<!-- Controls Panel -->
<div class="controls-panel rounded-lg p-4">
<h2 class="text-xl font-bold mb-4">Customization Controls</h2>
<div class="control-group mb-4 pb-4">
<h3 class="font-bold mb-2">Color Customization</h3>
<div class="grid grid-cols-3 gap-3">
<div>
<label class="block text-sm mb-1">Primary</label>
<input type="color" id="primary-color" class="color-picker w-full" value="#6c63ff">
</div>
<div>
<label class="block text-sm mb-1">Secondary</label>
<input type="color" id="secondary-color" class="color-picker w-full" value="#ff6b6b">
</div>
<div>
<label class="block text-sm mb-1">Accent</label>
<input type="color" id="accent-color-picker" class="color-picker w-full" value="#4ecdc4">
</div>
</div>
</div>
<div class="control-group mb-4 pb-4">
<h3 class="font-bold mb-2">Material</h3>
<select id="material-select" class="w-full p-2 bg-primary-bg border border-border-color rounded">
<option value="metal">Metallic</option>
<option value="matte">Matte</option>
<option value="chrome">Chrome</option>
<option value="carbon">Carbon Fiber</option>
</select>
</div>
<div class="control-group mb-4 pb-4">
<h3 class="font-bold mb-2">View Controls</h3>
<div class="grid grid-cols-2 gap-2">
<button id="reset-view-btn" class="header-btn py-2 px-3 rounded text-sm">Reset View</button>
<button id="toggle-wireframe-btn" class="header-btn py-2 px-3 rounded text-sm">Wireframe</button>
</div>
</div>
<div class="control-group">
<h3 class="font-bold mb-2">Export</h3>
<div class="grid grid-cols-2 gap-2">
<button id="export-png-btn" class="header-btn py-2 px-3 rounded text-sm">Export PNG</button>
<button id="export-glb-btn" class="header-btn py-2 px-3 rounded text-sm">Export GLB</button>
</div>
</div>
</div>
</div>
<!-- Parts Selection Panel -->
<div class="parts-panel rounded-lg p-4">
<h2 class="text-xl font-bold mb-4">Car Parts</h2>
<div class="part-category mb-4 pb-4">
<h3 class="font-bold mb-2">Body</h3>
<div class="part-item active" data-part="body">Main Body</div>
<div class="part-item" data-part="hood">Hood</div>
<div class="part-item" data-part="roof">Roof</div>
<div class="part-item" data-part="trunk">Trunk</div>
</div>
<div class="part-category mb-4 pb-4">
<h3 class="font-bold mb-2">Wheels</h3>
<div class="part-item" data-part="wheel_fl">Front Left Wheel</div>
<div class="part-item" data-part="wheel_fr">Front Right Wheel</div>
<div class="part-item" data-part="wheel_rl">Rear Left Wheel</div>
<div class="part-item" data-part="wheel_rr">Rear Right Wheel</div>
<div class="part-item" data-part="rim">Rim Style</div>
</div>
<div class="part-category mb-4 pb-4">
<h3 class="font-bold mb-2">Accessories</h3>
<div class="part-item" data-part="spoiler">Spoiler</div>
<div class="part-item" data-part="side_skirt">Side Skirts</div>
<div class="part-item" data-part="front_bumper">Front Bumper</div>
<div class="part-item" data-part="rear_bumper">Rear Bumper</div>
</div>
<div class="part-category">
<h3 class="font-bold mb-2">Lighting</h3>
<div class="part-item" data-part="headlight">Headlights</div>
<div class="part-item" data-part="taillight">Taillights</div>
<div class="part-item" data-part="neon">Neon Kit</div>
</div>
</div>
</main>
<!-- Confirmation Modal -->
<div id="confirmation-modal" class="fixed inset-0 z-50 flex items-center justify-center p-4 modal-overlay hidden">
<div class="modal-content rounded-lg shadow-2xl max-w-md w-full p-6">
<h3 class="text-xl font-bold mb-4">Export Ready</h3>
<p class="mb-6">Your customized car model has been prepared for export. What would you like to do next?</p>
<div class="flex justify-end space-x-3">
<button id="cancel-export-btn" class="header-btn py-2 px-4 rounded">Cancel</button>
<button id="confirm-export-btn" class="header-btn py-2 px-4 rounded bg-accent-color">Download</button>
</div>
</div>
</div>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
// DOM Elements
const backToHubBtn = document.getElementById('back-to-hub-btn');
const viewerContainer = document.getElementById('viewer-container');
const loadingOverlay = document.getElementById('loading-overlay');
const primaryColorPicker = document.getElementById('primary-color');
const secondaryColorPicker = document.getElementById('secondary-color');
const accentColorPicker = document.getElementById('accent-color-picker');
const materialSelect = document.getElementById('material-select');
const resetViewBtn = document.getElementById('reset-view-btn');
const toggleWireframeBtn = document.getElementById('toggle-wireframe-btn');
const exportPngBtn = document.getElementById('export-png-btn');
const exportGlbBtn = document.getElementById('export-glb-btn');
const partItems = document.querySelectorAll('.part-item');
const confirmationModal = document.getElementById('confirmation-modal');
const cancelExportBtn = document.getElementById('cancel-export-btn');
const confirmExportBtn = document.getElementById('confirm-export-btn');
// Three.js variables
let scene, camera, renderer, controls;
let carModel = null;
let selectedPart = 'body';
let wireframeMode = false;
// Initialize Three.js
function init() {
// Create scene
scene = new THREE.Scene();
scene.background = new THREE.Color(0x0d0c1d);
// Create camera
camera = new THREE.PerspectiveCamera(45, viewerContainer.clientWidth / viewerContainer.clientHeight, 0.1, 1000);
camera.position.set(0, 0, 5);
// Create renderer
renderer = new THREE.WebGLRenderer({
canvas: document.getElementById('threed-canvas'),
antialias: true,
alpha: true
});
renderer.setSize(viewerContainer.clientWidth, viewerContainer.clientHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
// Add orbit controls
controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.autoRotate = true;
controls.autoRotateSpeed = 0.5;
// Add lights
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(5, 5, 5);
scene.add(directionalLight);
// Create a simple car model
createCarModel();
// Hide loading overlay
loadingOverlay.style.display = 'none';
// Start animation loop
animate();
// Handle window resize
window.addEventListener('resize', onWindowResize);
}
// Create a simple car model
function createCarModel() {
// Car body
const bodyGeometry = new THREE.BoxGeometry(2, 0.5, 1);
const bodyMaterial = new THREE.MeshStandardMaterial({
color: primaryColorPicker.value,
metalness: 0.5,
roughness: 0.5
});
const body = new THREE.Mesh(bodyGeometry, bodyMaterial);
body.position.y = 0.5;
scene.add(body);
// Car top
const topGeometry = new THREE.BoxGeometry(1, 0.4, 0.8);
const topMaterial = new THREE.MeshStandardMaterial({
color: secondaryColorPicker.value,
metalness: 0.3,
roughness: 0.7
});
const top = new THREE.Mesh(topGeometry, topMaterial);
top.position.y = 1;
top.position.z = 0.1;
scene.add(top);
// Wheels
const wheelGeometry = new THREE.CylinderGeometry(0.2, 0.2, 0.1, 16);
wheelGeometry.rotateZ(Math.PI / 2);
const wheelMaterial = new THREE.MeshStandardMaterial({
color: accentColorPicker.value,
metalness: 0.8,
roughness: 0.2
});
const wheelPositions = [
{ x: 0.8, y: 0.2, z: 0.6 },
{ x: -0.8, y: 0.2, z: 0.6 },
{ x: 0.8, y: 0.2, z: -0.6 },
{ x: -0.8, y: 0.2, z: -0.6 }
];
wheelPositions.forEach(pos => {
const wheel = new THREE.Mesh(wheelGeometry, wheelMaterial);
wheel.position.set(pos.x, pos.y, pos.z);
scene.add(wheel);
});
carModel = new THREE.Group();
carModel.add(body);
carModel.add(top);
// Wheels are added directly to scene for simplicity
scene.add(carModel);
}
// Update materials based on selections
function updateMaterials() {
if (!carModel) return;
const primaryColor = new THREE.Color(primaryColorPicker.value);
const secondaryColor = new THREE.Color(secondaryColorPicker.value);
const accentColor = new THREE.Color(accentColorPicker.value);
carModel.traverse((child) => {
if (child.isMesh) {
// Determine which material to apply based on object name or position
let color;
if (child.geometry.parameters.height === 0.4) {
color = secondaryColor; // Top
} else {
color = primaryColor; // Body
}
// Create material based on selection
let material;
switch(materialSelect.value) {
case 'metal':
material = new THREE.MeshStandardMaterial({
color: color,
metalness: 0.8,
roughness: 0.2
});
break;
case 'matte':
material = new THREE.MeshStandardMaterial({
color: color,
metalness: 0.1,
roughness: 0.9
});
break;
case 'chrome':
material = new THREE.MeshStandardMaterial({
color: color,
metalness: 1,
roughness: 0
});
break;
case 'carbon':
material = new THREE.MeshStandardMaterial({
color: color,
metalness: 0.4,
roughness: 0.3
});
break;
default:
material = new THREE.MeshStandardMaterial({ color: color });
}
// Apply wireframe if enabled
material.wireframe = wireframeMode;
child.material = material;
}
});
// Update wheel colors
scene.traverse((child) => {
if (child.isMesh && child.geometry.type === 'CylinderGeometry') {
let material;
switch(materialSelect.value) {
case 'metal':
material = new THREE.MeshStandardMaterial({
color: accentColor,
metalness: 0.8,
roughness: 0.2
});
break;
case 'matte':
material = new THREE.MeshStandardMaterial({
color: accentColor,
metalness: 0.1,
roughness: 0.9
});
break;
case 'chrome':
material = new THREE.MeshStandardMaterial({
color: accentColor,
metalness: 1,
roughness: 0
});
break;
case 'carbon':
material = new THREE.MeshStandardMaterial({
color: accentColor,
metalness: 0.4,
roughness: 0.3
});
break;
default:
material = new THREE.MeshStandardMaterial({ color: accentColor });
}
material.wireframe = wireframeMode;
child.material = material;
}
});
}
// Handle window resize
function onWindowResize() {
camera.aspect = viewerContainer.clientWidth / viewerContainer.clientHeight;
camera.updateProjectionMatrix();
renderer.setSize(viewerContainer.clientWidth, viewerContainer.clientHeight);
}
// Animation loop
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}
// Event Listeners
backToHubBtn.addEventListener('click', () => {
window.location.href = 'index.html';
});
// Part selection
partItems.forEach(item => {
item.addEventListener('click', () => {
partItems.forEach(i => i.classList.remove('active'));
item.classList.add('active');
selectedPart = item.dataset.part;
});
});
// Color pickers
primaryColorPicker.addEventListener('input', updateMaterials);
secondaryColorPicker.addEventListener('input', updateMaterials);
accentColorPicker.addEventListener('input', updateMaterials);
// Material selection
materialSelect.addEventListener('change', updateMaterials);
// Reset view
resetViewBtn.addEventListener('click', () => {
controls.reset();
});
// Toggle wireframe
toggleWireframeBtn.addEventListener('click', () => {
wireframeMode = !wireframeMode;
toggleWireframeBtn.textContent = wireframeMode ? 'Solid' : 'Wireframe';
updateMaterials();
});
// Export PNG
exportPngBtn.addEventListener('click', () => {
// In a real implementation, this would trigger the export process
confirmationModal.classList.remove('hidden');
});
// Export GLB
exportGlbBtn.addEventListener('click', () => {
// In a real implementation, this would trigger the export process
confirmationModal.classList.remove('hidden');
});
// Modal controls
cancelExportBtn.addEventListener('click', () => {
confirmationModal.classList.add('hidden');
});
confirmExportBtn.addEventListener('click', () => {
// Simulate download
const link = document.createElement('a');
link.href = 'data:text/plain;charset=utf-8,Car customization export would be available here';
link.download = 'custom_car.txt';
link.click();
confirmationModal.classList.add('hidden');
});
// Close modal when clicking outside
confirmationModal.addEventListener('click', (e) => {
if (e.target === confirmationModal) {
confirmationModal.classList.add('hidden');
}
});
// Initialize the application
init();
</script>
</body>
</html>