cabinet / index.html
gladiopeace's picture
Add 1 files
fd20f78 verified
<!DOCTYPE html>
<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/[email protected]/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/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>