Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>2D to 3D STL Generator</title> | |
<script src="https://cdn.tailwindcss.com"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> | |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/stl-export.min.js"></script> | |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css"> | |
<style> | |
.dropzone { | |
border: 2px dashed #4b5563; | |
transition: all 0.3s ease; | |
} | |
.dropzone.active { | |
border-color: #3b82f6; | |
background-color: rgba(59, 130, 246, 0.1); | |
} | |
#previewCanvas { | |
background-color: #f3f4f6; | |
border-radius: 0.5rem; | |
} | |
#3dViewer { | |
width: 100%; | |
height: 400px; | |
background-color: #f3f4f6; | |
border-radius: 0.5rem; | |
} | |
.slider-thumb::-webkit-slider-thumb { | |
-webkit-appearance: none; | |
width: 20px; | |
height: 20px; | |
border-radius: 50%; | |
background: #3b82f6; | |
cursor: pointer; | |
} | |
.slider-thumb::-moz-range-thumb { | |
width: 20px; | |
height: 20px; | |
border-radius: 50%; | |
background: #3b82f6; | |
cursor: pointer; | |
} | |
</style> | |
</head> | |
<body class="bg-gray-100 min-h-screen"> | |
<div class="container mx-auto px-4 py-8"> | |
<header class="text-center mb-8"> | |
<h1 class="text-4xl font-bold text-gray-800 mb-2">2D to 3D STL Generator</h1> | |
<p class="text-gray-600">Convert your 2D images into printable 3D STL files</p> | |
</header> | |
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8"> | |
<!-- Image Upload Section --> | |
<div class="bg-white rounded-xl shadow-md p-6"> | |
<h2 class="text-2xl font-semibold text-gray-800 mb-4">1. Upload 2D Image</h2> | |
<div id="dropzone" class="dropzone rounded-lg p-8 text-center cursor-pointer mb-6"> | |
<div class="flex flex-col items-center justify-center"> | |
<i class="fas fa-cloud-upload-alt text-4xl text-blue-500 mb-3"></i> | |
<p class="text-gray-600 mb-2">Drag & drop your image here</p> | |
<p class="text-sm text-gray-500 mb-4">or</p> | |
<label for="fileInput" class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-lg cursor-pointer transition"> | |
Select Image | |
</label> | |
<input type="file" id="fileInput" accept="image/*" class="hidden"> | |
</div> | |
</div> | |
<div id="imagePreviewContainer" class="hidden"> | |
<h3 class="text-lg font-medium text-gray-700 mb-3">Image Preview</h3> | |
<div class="flex justify-center"> | |
<canvas id="previewCanvas" class="w-full max-w-md h-auto"></canvas> | |
</div> | |
</div> | |
</div> | |
<!-- 3D Settings Section --> | |
<div class="bg-white rounded-xl shadow-md p-6"> | |
<h2 class="text-2xl font-semibold text-gray-800 mb-4">2. 3D Generation Settings</h2> | |
<div class="space-y-4"> | |
<div> | |
<label for="heightRange" class="block text-sm font-medium text-gray-700 mb-1">3D Height (mm)</label> | |
<div class="flex items-center"> | |
<input type="range" id="heightRange" min="1" max="50" value="10" class="w-full slider-thumb"> | |
<span id="heightValue" class="ml-3 text-gray-700 w-12 text-center">10</span> | |
</div> | |
</div> | |
<div> | |
<label for="smoothnessRange" class="block text-sm font-medium text-gray-700 mb-1">Edge Smoothness</label> | |
<div class="flex items-center"> | |
<input type="range" id="smoothnessRange" min="1" max="10" value="5" class="w-full slider-thumb"> | |
<span id="smoothnessValue" class="ml-3 text-gray-700 w-12 text-center">5</span> | |
</div> | |
</div> | |
<div> | |
<label for="baseThicknessRange" class="block text-sm font-medium text-gray-700 mb-1">Base Thickness (mm)</label> | |
<div class="flex items-center"> | |
<input type="range" id="baseThicknessRange" min="0" max="10" value="2" class="w-full slider-thumb"> | |
<span id="baseThicknessValue" class="ml-3 text-gray-700 w-12 text-center">2</span> | |
</div> | |
</div> | |
<div class="pt-4"> | |
<button id="generateBtn" class="w-full bg-blue-500 hover:bg-blue-600 text-white py-3 px-4 rounded-lg font-medium transition flex items-center justify-center"> | |
<i class="fas fa-cube mr-2"></i> Generate 3D Model | |
</button> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- 3D Preview Section --> | |
<div id="3dPreviewSection" class="hidden mt-8 bg-white rounded-xl shadow-md p-6"> | |
<div class="flex justify-between items-center mb-4"> | |
<h2 class="text-2xl font-semibold text-gray-800">3D Model Preview</h2> | |
<button id="downloadBtn" class="bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded-lg flex items-center"> | |
<i class="fas fa-download mr-2"></i> Download STL | |
</button> | |
</div> | |
<div id="3dViewer"></div> | |
<div class="mt-4 grid grid-cols-1 md:grid-cols-3 gap-4"> | |
<div class="bg-gray-50 p-4 rounded-lg"> | |
<h3 class="font-medium text-gray-700 mb-2">Model Info</h3> | |
<p class="text-sm text-gray-600">Height: <span id="modelHeight" class="font-medium">10</span> mm</p> | |
<p class="text-sm text-gray-600">Base: <span id="modelBase" class="font-medium">2</span> mm</p> | |
</div> | |
<div class="bg-gray-50 p-4 rounded-lg"> | |
<h3 class="font-medium text-gray-700 mb-2">Estimated Print Time</h3> | |
<p class="text-sm text-gray-600">Approx. <span id="printTime" class="font-medium">2-4</span> hours</p> | |
</div> | |
<div class="bg-gray-50 p-4 rounded-lg"> | |
<h3 class="font-medium text-gray-700 mb-2">File Size</h3> | |
<p class="text-sm text-gray-600"><span id="fileSize" class="font-medium">1.2</span> MB</p> | |
</div> | |
</div> | |
</div> | |
<!-- How It Works Section --> | |
<div class="mt-12 bg-white rounded-xl shadow-md p-6"> | |
<h2 class="text-2xl font-semibold text-gray-800 mb-4">How It Works</h2> | |
<div class="grid grid-cols-1 md:grid-cols-3 gap-6"> | |
<div class="text-center"> | |
<div class="bg-blue-100 w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-3"> | |
<i class="fas fa-upload text-blue-500 text-2xl"></i> | |
</div> | |
<h3 class="font-medium text-gray-800 mb-2">Upload Image</h3> | |
<p class="text-gray-600 text-sm">Upload any 2D image in JPG, PNG, or GIF format.</p> | |
</div> | |
<div class="text-center"> | |
<div class="bg-blue-100 w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-3"> | |
<i class="fas fa-sliders-h text-blue-500 text-2xl"></i> | |
</div> | |
<h3 class="font-medium text-gray-800 mb-2">Adjust Settings</h3> | |
<p class="text-gray-600 text-sm">Customize the height, smoothness, and base thickness.</p> | |
</div> | |
<div class="text-center"> | |
<div class="bg-blue-100 w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-3"> | |
<i class="fas fa-cube text-blue-500 text-2xl"></i> | |
</div> | |
<h3 class="font-medium text-gray-800 mb-2">Download STL</h3> | |
<p class="text-gray-600 text-sm">Generate and download your 3D model in STL format.</p> | |
</div> | |
</div> | |
</div> | |
</div> | |
<script> | |
document.addEventListener('DOMContentLoaded', function() { | |
// DOM Elements | |
const dropzone = document.getElementById('dropzone'); | |
const fileInput = document.getElementById('fileInput'); | |
const previewCanvas = document.getElementById('previewCanvas'); | |
const ctx = previewCanvas.getContext('2d'); | |
const imagePreviewContainer = document.getElementById('imagePreviewContainer'); | |
const generateBtn = document.getElementById('generateBtn'); | |
const downloadBtn = document.getElementById('downloadBtn'); | |
const heightRange = document.getElementById('heightRange'); | |
const heightValue = document.getElementById('heightValue'); | |
const smoothnessRange = document.getElementById('smoothnessRange'); | |
const smoothnessValue = document.getElementById('smoothnessValue'); | |
const baseThicknessRange = document.getElementById('baseThicknessRange'); | |
const baseThicknessValue = document.getElementById('baseThicknessValue'); | |
const modelHeight = document.getElementById('modelHeight'); | |
const modelBase = document.getElementById('modelBase'); | |
const printTime = document.getElementById('printTime'); | |
const fileSize = document.getElementById('fileSize'); | |
const dPreviewSection = document.getElementById('3dPreviewSection'); | |
const dViewer = document.getElementById('3dViewer'); | |
// Variables | |
let uploadedImage = null; | |
let generatedMesh = null; | |
let scene, camera, renderer, controls; | |
// Initialize Three.js viewer | |
function init3DViewer() { | |
scene = new THREE.Scene(); | |
scene.background = new THREE.Color(0xf3f4f6); | |
camera = new THREE.PerspectiveCamera(75, dViewer.clientWidth / dViewer.clientHeight, 0.1, 1000); | |
camera.position.z = 5; | |
renderer = new THREE.WebGLRenderer({ antialias: true }); | |
renderer.setSize(dViewer.clientWidth, dViewer.clientHeight); | |
dViewer.appendChild(renderer.domElement); | |
// Add lights | |
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5); | |
scene.add(ambientLight); | |
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8); | |
directionalLight.position.set(1, 1, 1); | |
scene.add(directionalLight); | |
// Add orbit controls | |
controls = new THREE.OrbitControls(camera, renderer.domElement); | |
controls.enableDamping = true; | |
controls.dampingFactor = 0.25; | |
// Handle window resize | |
window.addEventListener('resize', onWindowResize); | |
// Start animation loop | |
animate(); | |
} | |
function animate() { | |
requestAnimationFrame(animate); | |
controls.update(); | |
renderer.render(scene, camera); | |
} | |
function onWindowResize() { | |
camera.aspect = dViewer.clientWidth / dViewer.clientHeight; | |
camera.updateProjectionMatrix(); | |
renderer.setSize(dViewer.clientWidth, dViewer.clientHeight); | |
} | |
// Initialize the 3D viewer | |
init3DViewer(); | |
// Update slider values display | |
heightRange.addEventListener('input', function() { | |
heightValue.textContent = this.value; | |
}); | |
smoothnessRange.addEventListener('input', function() { | |
smoothnessValue.textContent = this.value; | |
}); | |
baseThicknessRange.addEventListener('input', function() { | |
baseThicknessValue.textContent = this.value; | |
}); | |
// Handle drag and drop | |
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { | |
dropzone.addEventListener(eventName, preventDefaults, false); | |
}); | |
function preventDefaults(e) { | |
e.preventDefault(); | |
e.stopPropagation(); | |
} | |
['dragenter', 'dragover'].forEach(eventName => { | |
dropzone.addEventListener(eventName, highlight, false); | |
}); | |
['dragleave', 'drop'].forEach(eventName => { | |
dropzone.addEventListener(eventName, unhighlight, false); | |
}); | |
function highlight() { | |
dropzone.classList.add('active'); | |
} | |
function unhighlight() { | |
dropzone.classList.remove('active'); | |
} | |
dropzone.addEventListener('drop', handleDrop, false); | |
function handleDrop(e) { | |
const dt = e.dataTransfer; | |
const files = dt.files; | |
handleFiles(files); | |
} | |
fileInput.addEventListener('change', function() { | |
handleFiles(this.files); | |
}); | |
function handleFiles(files) { | |
if (files.length > 0) { | |
const file = files[0]; | |
if (file.type.match('image.*')) { | |
const reader = new FileReader(); | |
reader.onload = function(e) { | |
const img = new Image(); | |
img.onload = function() { | |
uploadedImage = img; | |
displayImagePreview(img); | |
}; | |
img.src = e.target.result; | |
}; | |
reader.readAsDataURL(file); | |
} else { | |
alert('Please upload an image file (JPG, PNG, GIF)'); | |
} | |
} | |
} | |
function displayImagePreview(img) { | |
// Set canvas dimensions | |
const maxWidth = 500; | |
const maxHeight = 400; | |
let width = img.width; | |
let height = img.height; | |
if (width > maxWidth) { | |
height = (maxWidth / width) * height; | |
width = maxWidth; | |
} | |
if (height > maxHeight) { | |
width = (maxHeight / height) * width; | |
height = maxHeight; | |
} | |
previewCanvas.width = width; | |
previewCanvas.height = height; | |
// Draw image on canvas | |
ctx.drawImage(img, 0, 0, width, height); | |
// Show preview container | |
imagePreviewContainer.classList.remove('hidden'); | |
} | |
// Generate 3D model | |
generateBtn.addEventListener('click', function() { | |
if (!uploadedImage) { | |
alert('Please upload an image first'); | |
return; | |
} | |
// Show loading state | |
generateBtn.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i> Generating...'; | |
generateBtn.disabled = true; | |
// Simulate generation process (in a real app, this would be more complex) | |
setTimeout(function() { | |
generate3DModel(); | |
// Update model info | |
modelHeight.textContent = heightRange.value; | |
modelBase.textContent = baseThicknessRange.value; | |
// Estimate print time and file size (these would be calculated in a real app) | |
const height = parseInt(heightRange.value); | |
const base = parseInt(baseThicknessRange.value); | |
const estTime = Math.round(height * 0.2 + base * 0.1); | |
printTime.textContent = `${estTime}-${estTime + 2}`; | |
const fileSizeEst = (height * width * 0.001).toFixed(1); | |
fileSize.textContent = fileSizeEst; | |
// Show 3D preview section | |
dPreviewSection.classList.remove('hidden'); | |
// Reset button | |
generateBtn.innerHTML = '<i class="fas fa-cube mr-2"></i> Generate 3D Model'; | |
generateBtn.disabled = false; | |
// Scroll to 3D preview | |
dPreviewSection.scrollIntoView({ behavior: 'smooth' }); | |
}, 1500); | |
}); | |
function generate3DModel() { | |
// Clear previous mesh | |
if (generatedMesh) { | |
scene.remove(generatedMesh); | |
} | |
// Create a simple 3D shape from the image (simplified for demo) | |
// In a real app, you would use more sophisticated algorithms | |
const geometry = new THREE.BoxGeometry( | |
3, | |
parseFloat(baseThicknessRange.value) * 0.1, | |
parseFloat(heightRange.value) * 0.1 | |
); | |
// Create texture from image | |
const texture = new THREE.Texture(previewCanvas); | |
texture.needsUpdate = true; | |
const material = new THREE.MeshPhongMaterial({ | |
map: texture, | |
side: THREE.DoubleSide | |
}); | |
generatedMesh = new THREE.Mesh(geometry, material); | |
scene.add(generatedMesh); | |
// Reset camera position | |
camera.position.z = 5; | |
controls.reset(); | |
} | |
// Download STL | |
downloadBtn.addEventListener('click', function() { | |
if (!generatedMesh) { | |
alert('Please generate a 3D model first'); | |
return; | |
} | |
// Show loading state | |
downloadBtn.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i> Preparing Download...'; | |
downloadBtn.disabled = true; | |
// Simulate STL export (in a real app, you would use a proper STL exporter) | |
setTimeout(function() { | |
// Create a simplified STL export (demo only) | |
const exporter = new THREE.STLExporter(); | |
const stlString = exporter.parse(generatedMesh); | |
// Create download link | |
const blob = new Blob([stlString], { type: 'application/octet-stream' }); | |
const url = URL.createObjectURL(blob); | |
const link = document.createElement('a'); | |
link.href = url; | |
link.download = '3d-model.stl'; | |
document.body.appendChild(link); | |
link.click(); | |
document.body.removeChild(link); | |
// Reset button | |
downloadBtn.innerHTML = '<i class="fas fa-download mr-2"></i> Download STL'; | |
downloadBtn.disabled = false; | |
}, 1000); | |
}); | |
}); | |
</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=Mathieu2025/2dto3d-converter" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
</html> |