Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>3D Cabinet Generator</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.js"></script> | |
| <style> | |
| #modelContainer { | |
| width: 100%; | |
| height: 500px; | |
| background-color: #f5f3f0; | |
| border-radius: 8px; | |
| overflow: hidden; | |
| position: relative; | |
| } | |
| .loading-overlay { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: rgba(245, 243, 240, 0.8); | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: center; | |
| align-items: center; | |
| z-index: 10; | |
| } | |
| .progress-ring { | |
| width: 60px; | |
| height: 60px; | |
| } | |
| .progress-ring circle { | |
| transition: stroke-dashoffset 0.3s; | |
| } | |
| .controls-overlay { | |
| position: absolute; | |
| bottom: 20px; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| display: flex; | |
| gap: 10px; | |
| z-index: 5; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-50"> | |
| <div class="container mx-auto px-4 py-8"> | |
| <header class="mb-8 text-center"> | |
| <h1 class="text-3xl font-bold text-gray-800">3D Cabinet Generator</h1> | |
| <p class="text-gray-600 mt-2">Customize your cabinet with real-time 3D visualization</p> | |
| </header> | |
| <div class="grid grid-cols-1 lg:grid-cols-3 gap-8"> | |
| <!-- Controls --> | |
| <div class="bg-white rounded-lg shadow p-6"> | |
| <h2 class="text-xl font-semibold mb-4">Configuration</h2> | |
| <div class="mb-4"> | |
| <label class="block text-gray-700 mb-2">Cabinet Type</label> | |
| <select id="cabinetType" class="w-full p-2 border rounded"> | |
| <option value="base">Base Cabinet</option> | |
| <option value="wall">Wall Cabinet</option> | |
| <option value="tall">Tall Cabinet</option> | |
| </select> | |
| </div> | |
| <div class="mb-4"> | |
| <label class="block text-gray-700 mb-2">Width: <span id="widthValue">24</span>"</label> | |
| <input type="range" id="widthSlider" min="12" max="48" value="24" step="1" class="w-full"> | |
| </div> | |
| <div class="mb-4"> | |
| <label class="block text-gray-700 mb-2">Height: <span id="heightValue">36</span>"</label> | |
| <input type="range" id="heightSlider" min="12" max="96" value="36" step="1" class="w-full"> | |
| </div> | |
| <div class="mb-4"> | |
| <label class="block text-gray-700 mb-2">Depth: <span id="depthValue">12</span>"</label> | |
| <input type="range" id="depthSlider" min="12" max="24" value="12" step="1" class="w-full"> | |
| </div> | |
| <div class="mb-4"> | |
| <label class="block text-gray-700 mb-2">Material</label> | |
| <select id="material" class="w-full p-2 border rounded"> | |
| <option value="oak">Oak</option> | |
| <option value="maple">Maple</option> | |
| <option value="cherry">Cherry</option> | |
| <option value="laminate">Laminate</option> | |
| </select> | |
| </div> | |
| <div class="mb-4"> | |
| <label class="block text-gray-700 mb-2">Number of Doors</label> | |
| <div class="flex space-x-2"> | |
| <button id="oneDoor" class="flex-1 py-2 bg-blue-100 text-blue-700 rounded">1 Door</button> | |
| <button id="twoDoors" class="flex-1 py-2 bg-gray-100 text-gray-700 rounded">2 Doors</button> | |
| </div> | |
| </div> | |
| <div class="mb-4"> | |
| <label class="block text-gray-700 mb-2">Number of Shelves: <span id="shelvesValue">1</span></label> | |
| <input type="range" id="shelvesSlider" min="0" max="5" value="1" class="w-full"> | |
| </div> | |
| <div id="drawersContainer" class="mb-4 hidden"> | |
| <label class="block text-gray-700 mb-2">Number of Drawers: <span id="drawersValue">1</span></label> | |
| <input type="range" id="drawersSlider" min="1" max="3" value="1" class="w-full"> | |
| </div> | |
| <div class="mt-6 bg-gray-50 p-4 rounded-lg"> | |
| <h3 class="font-semibold mb-2">Price Estimation</h3> | |
| <div class="text-2xl font-bold text-blue-600" id="totalPrice">$299.99</div> | |
| </div> | |
| </div> | |
| <!-- 3D Preview --> | |
| <div class="lg:col-span-2"> | |
| <div id="modelContainer"> | |
| <div id="doorControls" class="controls-overlay hidden"> | |
| <button id="openBtn" class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 flex items-center"> | |
| <svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path> | |
| </svg> | |
| Open Doors | |
| </button> | |
| <button id="closeBtn" class="px-4 py-2 bg-gray-600 text-white rounded hover:bg-gray-700 flex items-center"> | |
| <svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7"></path> | |
| </svg> | |
| Close Doors | |
| </button> | |
| </div> | |
| <div id="loadingOverlay" class="loading-overlay"> | |
| <svg class="progress-ring" viewBox="0 0 60 60"> | |
| <circle class="text-gray-300" stroke="currentColor" stroke-width="4" fill="none" r="25" cx="30" cy="30"></circle> | |
| <circle class="text-blue-500" stroke="currentColor" stroke-width="4" fill="none" r="25" cx="30" cy="30" stroke-dasharray="157" stroke-dashoffset="157"></circle> | |
| </svg> | |
| <p class="mt-2 text-gray-600">Generating 3D model...</p> | |
| </div> | |
| </div> | |
| <div class="mt-4 flex justify-end space-x-2"> | |
| <button id="downloadBtn" class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 flex items-center"> | |
| <svg class="w-5 h-5 mr-2" fill="none stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path> | |
| </svg> | |
| Download 3D Model | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // Three.js variables | |
| let scene, camera, renderer, controls, cabinetGroup; | |
| const modelContainer = document.getElementById('modelContainer'); | |
| const loadingOverlay = document.getElementById('loadingOverlay'); | |
| const progressRing = document.querySelector('.progress-ring circle:last-child'); | |
| const doorControls = document.getElementById('doorControls'); | |
| const openBtn = document.getElementById('openBtn'); | |
| const closeBtn = document.getElementById('closeBtn'); | |
| // Door related variables | |
| let leftDoor, rightDoor, leftDoorHinge, rightDoorHinge; | |
| let isAnimating = false; | |
| let doorState = 'closed'; // 'closed', 'opening', 'open', 'closing' | |
| // Initialize Three.js | |
| function initThreeJS() { | |
| // Create scene | |
| scene = new THREE.Scene(); | |
| scene.background = new THREE.Color(0xf5f3f0); | |
| // Create camera | |
| camera = new THREE.PerspectiveCamera( | |
| 60, // fov | |
| modelContainer.clientWidth / modelContainer.clientHeight, // aspect | |
| 0.1, // near | |
| 1000 // far | |
| ); | |
| camera.position.set(40, 40, 40); | |
| // Create renderer | |
| renderer = new THREE.WebGLRenderer({ antialias: true }); | |
| renderer.setSize(modelContainer.clientWidth, modelContainer.clientHeight); | |
| renderer.shadowMap.enabled = true; | |
| modelContainer.appendChild(renderer.domElement); | |
| // Add lights | |
| const ambientLight = new THREE.AmbientLight(0xffffff, 0.6); | |
| scene.add(ambientLight); | |
| const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8); | |
| directionalLight.position.set(30, 50, 20); | |
| directionalLight.castShadow = true; | |
| directionalLight.shadow.mapSize.width = 2048; | |
| directionalLight.shadow.mapSize.height = 2048; | |
| scene.add(directionalLight); | |
| // Add helper light from the front | |
| const fillLight = new THREE.DirectionalLight(0xffffff, 0.3); | |
| fillLight.position.set(-30, 30, 30); | |
| scene.add(fillLight); | |
| // Add controls | |
| controls = new THREE.OrbitControls(camera, renderer.domElement); | |
| controls.enableDamping = true; | |
| controls.dampingFactor = 0.05; | |
| controls.minDistance = 10; | |
| controls.maxDistance = 200; | |
| // Create cabinet group | |
| cabinetGroup = new THREE.Group(); | |
| scene.add(cabinetGroup); | |
| // Add grid and axes helpers (for debugging) | |
| const gridHelper = new THREE.GridHelper(100, 100); | |
| gridHelper.position.y = -0.01; // Just below the cabinet | |
| scene.add(gridHelper); | |
| // Initial render | |
| updateCabinetModel(); | |
| // Animation loop | |
| function animate() { | |
| requestAnimationFrame(animate); | |
| controls.update(); | |
| renderer.render(scene, camera); | |
| } | |
| animate(); | |
| // Handle window resize | |
| window.addEventListener('resize', onWindowResize); | |
| } | |
| function onWindowResize() { | |
| camera.aspect = modelContainer.clientWidth / modelContainer.clientHeight; | |
| camera.updateProjectionMatrix(); | |
| renderer.setSize(modelContainer.clientWidth, modelContainer.clientHeight); | |
| } | |
| // Create cabinet model | |
| function updateCabinetModel() { | |
| showLoading(); | |
| // Clear previous cabinet | |
| while (cabinetGroup.children.length > 0) { | |
| cabinetGroup.remove(cabinetGroup.children[0]); | |
| } | |
| // Reset door variables | |
| leftDoor = null; | |
| rightDoor = null; | |
| leftDoorHinge = null; | |
| rightDoorHinge = null; | |
| // Get parameters | |
| const width = parseFloat(document.getElementById('widthSlider').value); | |
| const height = parseFloat(document.getElementById('heightSlider').value); | |
| const depth = parseFloat(document.getElementById('depthSlider').value); | |
| const materialType = document.getElementById('material').value; | |
| const isTwoDoors = document.getElementById('twoDoors').classList.contains('bg-blue-100'); | |
| const shelvesCount = parseInt(document.getElementById('shelvesSlider').value); | |
| const drawersCount = parseInt(document.getElementById('drawersSlider').value || 0); | |
| const cabinetType = document.getElementById('cabinetType').value; | |
| // Create materials | |
| const materials = { | |
| exterior: createMaterial(materialType), | |
| interior: new THREE.MeshStandardMaterial({ color: 0xF5F5F5, roughness: 0.7 }), | |
| shelf: new THREE.MeshStandardMaterial({ color: 0xEAEAEA, roughness: 0.6 }), | |
| hardware: new THREE.MeshStandardMaterial({ color: 0x333333 }) | |
| }; | |
| // Main cabinet structure | |
| createCabinetStructure(width, height, depth, materials, cabinetType, isTwoDoors); | |
| // Add shelves if needed | |
| if (shelvesCount > 0) { | |
| const availableHeight = height - 2; // account for frame | |
| const shelfSpacing = availableHeight / (shelvesCount + 1); | |
| for (let i = 0; i < shelvesCount; i++) { | |
| const shelfY = -height/2 + 1 + (i + 1) * shelfSpacing; | |
| createShelf(width - 2.5, 0.5, depth - 2, shelfY, materials.shelf); | |
| } | |
| } | |
| // Add drawers for base cabinets | |
| if (cabinetType === 'base' && drawersCount > 0) { | |
| const drawerHeight = (height / 3) / drawersCount; | |
| for (let i = 0; i < drawersCount; i++) { | |
| const drawerY = -height/2 + (i + 0.5) * drawerHeight; | |
| createDrawer(width * 0.8, drawerHeight * 0.8, depth * 0.7, drawerY, materials.exterior, materials.interior); | |
| } | |
| } | |
| // Update camera position based on cabinet size | |
| const maxDimension = Math.max(width, height, depth); | |
| camera.position.set(maxDimension * 1.5, maxDimension * 1.2, maxDimension * 1.5); | |
| controls.target.set(0, height/4, 0); | |
| controls.update(); | |
| // Show/hide door controls | |
| if (cabinetType !== 'base' || drawersCount === 0) { | |
| doorControls.classList.remove('hidden'); | |
| } else { | |
| doorControls.classList.add('hidden'); | |
| } | |
| // Hide loading after a short delay to ensure smooth UI | |
| setTimeout(hideLoading, 500); | |
| } | |
| // Create cabinet frame and panels | |
| function createCabinetStructure(width, height, depth, materials, type, isTwoDoors) { | |
| const thickness = 0.75; // standard material thickness | |
| // Bottom panel | |
| createBox(width, thickness, depth, [0, -height/2 + thickness/2, 0], materials.exterior); | |
| // Top panel (not for base cabinets with countertop) | |
| if (type !== 'base') { | |
| createBox(width, thickness, depth, [0, height/2 - thickness/2, 0], materials.exterior); | |
| } | |
| // Left side | |
| createBox(thickness, height - 2*thickness, depth, | |
| [-width/2 + thickness/2, 0, 0], materials.exterior); | |
| // Right side | |
| createBox(thickness, height - 2*thickness, depth, | |
| [width/2 - thickness/2, 0, 0], materials.exterior); | |
| // Back panel | |
| createBox(width - 2*thickness, height - 2*thickness, thickness, | |
| [0, 0, -depth/2 + thickness/2], materials.exterior); | |
| // Middle divider for two doors | |
| if (isTwoDoors) { | |
| createBox(thickness, height - 2*thickness, depth, [0, 0, 0], materials.exterior); | |
| // Create door hinges (invisible objects for rotation) | |
| leftDoorHinge = new THREE.Group(); | |
| leftDoorHinge.position.set(-width/2 + thickness, 0, depth/2 - thickness/2); | |
| cabinetGroup.add(leftDoorHinge); | |
| rightDoorHinge = new THREE.Group(); | |
| rightDoorHinge.position.set(width/2 - thickness, 0, depth/2 - thickness/2); | |
| cabinetGroup.add(rightDoorHinge); | |
| // Left door (rotates around left edge) | |
| leftDoor = createBox(width/2 - thickness, height - 2*thickness, thickness, | |
| [0, 0, -thickness/2], materials.exterior); | |
| leftDoorHinge.add(leftDoor); | |
| // Right door (rotates around right edge) | |
| rightDoor = createBox(width/2 - thickness, height - 2*thickness, thickness, | |
| [0, 0, -thickness/2], materials.exterior); | |
| rightDoorHinge.add(rightDoor); | |
| // Add handles | |
| // Left handle (positioned on the right side of left door) | |
| const leftHandle = createBox(0.2, 2, 1, [width/4 - thickness/2, 0, 0.75], materials.hardware); | |
| leftDoorHinge.add(leftHandle); | |
| // Right handle (positioned on the left side of right door) | |
| const rightHandle = createBox(0.2, 2, 1, [-width/4 + thickness/2, 0, 0.75], materials.hardware); | |
| rightDoorHinge.add(rightHandle); | |
| } else { | |
| // Single door (right side hinge) | |
| rightDoorHinge = new THREE.Group(); | |
| rightDoorHinge.position.set(width/2 - thickness/2, 0, depth/2 - thickness/2); | |
| cabinetGroup.add(rightDoorHinge); | |
| rightDoor = createBox(width - thickness, height - 2*thickness, thickness, | |
| [-width/2 + thickness/2, 0, -thickness/2], materials.exterior); | |
| rightDoorHinge.add(rightDoor); | |
| // Handle (positioned on the left side of the door) | |
| const handle = createBox(0.2, 2, 1, [-width/2 + thickness + 1, 0, 0.75], materials.hardware); | |
| rightDoorHinge.add(handle); | |
| } | |
| } | |
| function createShelf(width, thickness, depth, yPos, material) { | |
| createBox(width, thickness, depth, [0, yPos, 0], material); | |
| // Add shelf supports | |
| createBox(0.5, 1, 0.5, [-width/2 + 0.25, yPos - 0.75, 0], material); | |
| createBox(0.5, 1, 0.5, [width/2 - 0.25, yPos - 0.75, 0], material); | |
| } | |
| function createDrawer(width, height, depth, yPos, frontMaterial, interiorMaterial) { | |
| // Drawer front | |
| createBox(width * 1.1, height * 1.1, 0.5, [0, yPos, depth/2 + 0.25], frontMaterial); | |
| // Drawer box | |
| createBox(width, height, depth, [0, yPos, 0], interiorMaterial); | |
| // Handle (simplified as a horizontal bar) | |
| createBox(3, 0.5, 0.3, [0, yPos, depth/2 + 0.4], frontMaterial); | |
| } | |
| function createBox(width, height, depth, position, material) { | |
| const geometry = new THREE.BoxGeometry(width, height, depth); | |
| const mesh = new THREE.Mesh(geometry, material); | |
| mesh.position.set(position[0], position[1], position[2]); | |
| mesh.castShadow = true; | |
| mesh.receiveShadow = true; | |
| if (material.color.getHex() !== 0x333333) { // Don't add shadow to hardware | |
| mesh.castShadow = true; | |
| mesh.receiveShadow = true; | |
| } | |
| cabinetGroup.add(mesh); | |
| return mesh; | |
| } | |
| function createMaterial(type) { | |
| const materials = { | |
| oak: { | |
| color: 0xC19A6B, | |
| roughness: 0.5 | |
| }, | |
| maple: { | |
| color: 0xE6CEAC, | |
| roughness: 0.4 | |
| }, | |
| cherry: { | |
| color: 0x8B4513, | |
| roughness: 0.3 | |
| }, | |
| laminate: { | |
| color: 0xFFFFFF, | |
| roughness: 0.1, | |
| metalness: 0.1 | |
| } | |
| }; | |
| return new THREE.MeshStandardMaterial(materials[type]); | |
| } | |
| // Animate doors | |
| function animateDoors(targetAngle) { | |
| if (isAnimating) return; | |
| isAnimating = true; | |
| const duration = 1000; // ms | |
| const startTime = Date.now(); | |
| const isTwoDoors = document.getElementById('twoDoors').classList.contains('bg-blue-100'); | |
| // Determine current angles | |
| const leftStartAngle = leftDoorHinge ? leftDoorHinge.rotation.y : 0; | |
| const rightStartAngle = rightDoorHinge ? rightDoorHinge.rotation.y : 0; | |
| // Target angles | |
| let leftTarget = 0; | |
| let rightTarget = 0; | |
| if (targetAngle === 0) { // Closing | |
| leftTarget = 0; | |
| rightTarget = 0; | |
| doorState = 'closing'; | |
| } else { // Opening | |
| if (isTwoDoors) { | |
| leftTarget = -targetAngle; // Left door opens inward (negative rotation) | |
| rightTarget = targetAngle; // Right door opens inward (positive rotation) | |
| } else { | |
| rightTarget = targetAngle; // Single door opens inward (positive rotation) | |
| } | |
| doorState = 'opening'; | |
| } | |
| const animateFrame = () => { | |
| const elapsed = Date.now() - startTime; | |
| const progress = Math.min(elapsed / duration, 1); | |
| // Easing function | |
| const easeProgress = easeOutCubic(progress); | |
| if (leftDoorHinge) { | |
| leftDoorHinge.rotation.y = leftStartAngle + (leftTarget - leftStartAngle) * easeProgress; | |
| } | |
| if (rightDoorHinge) { | |
| rightDoorHinge.rotation.y = rightStartAngle + (rightTarget - rightStartAngle) * easeProgress; | |
| } | |
| if (progress < 1) { | |
| requestAnimationFrame(animateFrame); | |
| } else { | |
| isAnimating = false; | |
| doorState = targetAngle === 0 ? 'closed' : 'open'; | |
| } | |
| }; | |
| animateFrame(); | |
| } | |
| // Easing function for smooth animation | |
| function easeOutCubic(t) { | |
| return 1 - Math.pow(1 - t, 3); | |
| } | |
| // Open/close door functions | |
| function openDoors() { | |
| if (doorState === 'closed' || doorState === 'closing') { | |
| animateDoors(Math.PI/2); // 90 degrees in radians | |
| } | |
| } | |
| function closeDoors() { | |
| if (doorState === 'open' || doorState === 'opening') { | |
| animateDoors(0); | |
| } | |
| } | |
| function showLoading() { | |
| loadingOverlay.style.display = 'flex'; | |
| let progress = 0; | |
| const interval = setInterval(() => { | |
| progress += 10; | |
| const offset = 157 * (1 - progress/100); | |
| progressRing.style.strokeDashoffset = offset; | |
| if (progress >= 100) { | |
| clearInterval(interval); | |
| } | |
| }, 50); | |
| } | |
| function hideLoading() { | |
| loadingOverlay.style.display = 'none'; | |
| progressRing.style.strokeDashoffset = '157'; | |
| doorState = 'closed'; // Reset door state after loading | |
| } | |
| // Update price calculation | |
| function updatePrice() { | |
| const width = parseFloat(document.getElementById('widthSlider').value); | |
| const height = parseFloat(document.getElementById('heightSlider').value); | |
| const depth = parseFloat(document.getElementById('depthSlider').value); | |
| const material = document.getElementById('material').value; | |
| const isTwoDoors = document.getElementById('twoDoors').classList.contains('bg-blue-100'); | |
| const shelvesCount = parseInt(document.getElementById('shelvesSlider').value); | |
| const drawersCount = parseInt(document.getElementById('drawersSlider').value || 0); | |
| const cabinetType = document.getElementById('cabinetType').value; | |
| // Calculate base price | |
| let basePrice = 100 + (width * height * depth) / 100; | |
| // Adjust by type | |
| if (cabinetType === 'base') basePrice *= 1.3; | |
| else if (cabinetType === 'tall') basePrice *= 1.5; | |
| // Material multipliers | |
| if (material === 'maple') basePrice *= 1.2; | |
| else if (material === 'cherry') basePrice *= 1.4; | |
| // Features | |
| if (isTwoDoors) basePrice += 50; | |
| if (shelvesCount > 1) basePrice += (shelvesCount - 1) * 25; | |
| if (drawersCount > 0) basePrice += drawersCount * 60; | |
| document.getElementById('totalPrice').textContent = `$${basePrice.toFixed(2)}`; | |
| } | |
| // Event listeners | |
| openBtn.addEventListener('click', openDoors); | |
| closeBtn.addEventListener('click', closeDoors); | |
| document.getElementById('widthSlider').addEventListener('input', function() { | |
| document.getElementById('widthValue').textContent = this.value; | |
| updateCabinetModel(); | |
| updatePrice(); | |
| }); | |
| document.getElementById('heightSlider').addEventListener('input', function() { | |
| document.getElementById('heightValue').textContent = this.value; | |
| updateCabinetModel(); | |
| updatePrice(); | |
| }); | |
| document.getElementById('depthSlider').addEventListener('input', function() { | |
| document.getElementById('depthValue').textContent = this.value; | |
| updateCabinetModel(); | |
| updatePrice(); | |
| }); | |
| document.getElementById('material').addEventListener('change', function() { | |
| updateCabinetModel(); | |
| updatePrice(); | |
| }); | |
| document.getElementById('cabinetType').addEventListener('change', function() { | |
| // Adjust default dimensions | |
| if (this.value === 'base') { | |
| document.getElementById('heightSlider').value = 34.5; | |
| document.getElementById('depthSlider').value = 24; | |
| document.getElementById('heightValue').textContent = '34.5'; | |
| document.getElementById('depthValue').textContent = '24'; | |
| document.getElementById('drawersContainer').classList.remove('hidden'); | |
| } else { | |
| if (this.value === 'wall') { | |
| document.getElementById('heightSlider').value = 36; | |
| document.getElementById('depthSlider').value = 12; | |
| } else if (this.value === 'tall') { | |
| document.getElementById('heightSlider').value = 84; | |
| document.getElementById('depthSlider').value = 24; | |
| } | |
| document.getElementById('heightValue').textContent = document.getElementById('heightSlider').value; | |
| document.getElementById('depthValue').textContent = document.getElementById('depthSlider').value; | |
| document.getElementById('drawersContainer').classList.add('hidden'); | |
| } | |
| updateCabinetModel(); | |
| updatePrice(); | |
| }); | |
| document.getElementById('oneDoor').addEventListener('click', function() { | |
| this.classList.add('bg-blue-100', 'text-blue-700'); | |
| document.getElementById('twoDoors').classList.remove('bg-blue-100', 'text-blue-700'); | |
| updateCabinetModel(); | |
| updatePrice(); | |
| }); | |
| document.getElementById('twoDoors').addEventListener('click', function() { | |
| this.classList.add('bg-blue-100', 'text-blue-700'); | |
| document.getElementById('oneDoor').classList.remove('bg-blue-100', 'text-blue-700'); | |
| updateCabinetModel(); | |
| updatePrice(); | |
| }); | |
| document.getElementById('shelvesSlider').addEventListener('input', function() { | |
| document.getElementById('shelvesValue').textContent = this.value; | |
| updateCabinetModel(); | |
| updatePrice(); | |
| }); | |
| document.getElementById('drawersSlider').addEventListener('input', function() { | |
| document.getElementById('drawersValue').textContent = this.value; | |
| updateCabinetModel(); | |
| updatePrice(); | |
| }); | |
| document.getElementById('downloadBtn').addEventListener('click', function() { | |
| alert('In a complete implementation, this would export your cabinet as a 3D model file.'); | |
| }); | |
| // Initialize | |
| window.addEventListener('load', function() { | |
| initThreeJS(); | |
| updatePrice(); | |
| // Set one door as active by default | |
| document.getElementById('oneDoor').classList.add('bg-blue-100', 'text-blue-700'); | |
| }); | |
| </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=gladiopeace/cabinet" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
| </html> |