// Global variables let scene, camera, renderer, controls; let model, cuttingPlaneMesh; let rotationSlider, autoRotateCheckbox, cuttingPlaneSlider; let sliceCanvas, sliceCtx; let isRecording = false; let mediaRecorder; let recordedChunks = []; let animationId; let sliceData = []; // Initialize Three.js scene function init3DScene() { const container = document.getElementById('canvas3d'); // Scene setup scene = new THREE.Scene(); scene.background = new THREE.Color(0x1a1a2e); // Camera setup camera = new THREE.PerspectiveCamera( 75, container.clientWidth / container.clientHeight, 0.1, 1000 ); camera.position.set(0, 0, 100); // Renderer setup renderer = new THREE.WebGLRenderer({ antialias: true, preserveDrawingBuffer: true }); renderer.setSize(container.clientWidth, container.clientHeight); renderer.localClippingEnabled = true; container.appendChild(renderer.domElement); // Controls controls = new THREE.OrbitControls(camera, renderer.domElement); controls.enableDamping = true; controls.dampingFactor = 0.05; // Lighting const ambientLight = new THREE.AmbientLight(0xffffff, 0.6); scene.add(ambientLight); const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8); directionalLight.position.set(10, 10, 10); scene.add(directionalLight); // Create 3D model (composite sphere with inner structures) createCompositeModel(); // Create cutting plane visualization createCuttingPlane(); // Event listeners setupEventListeners(); // Initialize slice canvas initSliceCanvas(); // Start render loop animate(); } // Create a composite 3D model function createCompositeModel() { // Main outer shell const outerGeometry = new THREE.SphereGeometry(30, 32, 32); const outerMaterial = new THREE.MeshPhongMaterial({ color: 0x4a90e2, transparent: true, opacity: 0.7, side: THREE.DoubleSide }); model = new THREE.Mesh(outerGeometry, outerMaterial); model.name = 'compositeModel'; scene.add(model); // Inner structures const innerGeometry1 = new THREE.SphereGeometry(15, 16, 16); const innerMaterial1 = new THREE.MeshPhongMaterial({ color: 0x7ed321, transparent: true, opacity: 0.8 }); const innerSphere1 = new THREE.Mesh(innerGeometry1, innerMaterial1); innerSphere1.position.set(5, 5, -5); model.add(innerSphere1); const innerGeometry2 = new THREE.BoxGeometry(15, 15, 15); const innerMaterial2 = new THREE.MeshPhongMaterial({ color: 0xf5a623, transparent: true, opacity: 0.8 }); const innerBox = new THREE.Mesh(innerGeometry2, innerMaterial2); innerBox.position.set(-8, -3, 8); innerBox.rotation.set(0.5, 0.3, 0.7); model.add(innerBox); // Additional small structures for complexity for (let i = 0; i < 5; i++) { const smallGeometry = new THREE.SphereGeometry(3 + Math.random() * 5, 8, 8); const smallMaterial = new THREE.MeshPhongMaterial({ color: new THREE.Color().setHSL(0.5 + Math.random() * 0.3, 0.8, 0.6), transparent: true, opacity: 0.9 }); const smallSphere = new THREE.Mesh(smallGeometry, smallMaterial); smallSphere.position.set( (Math.random() - 0.5) * 40, (Math.random() - 0.5) * 40, (Math.random() - 0.5) * 40 ); model.add(smallSphere); } // Update slice data for all z positions updateSliceData(); } // Generate slice data for 2D view function updateSliceData() { sliceData = []; const resolution = parseInt(document.getElementById('sliceResolution').value); const totalSlices = 100; for (let z = 0; z < totalSlices; z++) { const zPos = (z - totalSlices / 2) / (totalSlices / 100); const sliceArray = []; for (let y = 0; y < resolution; y++) { const row = []; for (let x = 0; x < resolution; x++) { // Convert canvas coordinates to 3D space const xPos = (x - resolution / 2) / (resolution / 100); const yPos = (y - resolution / 2) / (resolution / 100); // Calculate density based on distance from center for outer sphere const distanceFromCenter = Math.sqrt(xPos * xPos + yPos * yPos + zPos * zPos); let density = 0; // Outer sphere (main shell) if (distanceFromCenter <= 30) { density = 0.5 + (1 - distanceFromCenter / 30) * 0.5; // Inner sphere 1 const dist1 = Math.sqrt( (xPos - 5) * (xPos - 5) + (yPos - 5) * (yPos - 5) + (zPos + 5) * (zPos + 5) ); if (dist1 <= 15) { density = Math.max(density, 0.8 + (1 - dist1 / 15) * 0.2); } // Inner box (approximated) const boxDistX = Math.abs(xPos + 8); const boxDistY = Math.abs(yPos + 3); const boxDistZ = Math.abs(zPos - 8); if (boxDistX <= 12 && boxDistY <= 12 && boxDistZ <= 12) { density = Math.max(density, 0.7 + (1 - boxDistX / 12) * 0.3 * 0.3); density = Math.max(density, 0.7 + (1 - boxDistY / 12) * 0.3 * 0.3); density = Math.max(density, 0.7 + (1 - boxDistZ / 12) * 0.3 * 0.3); } // Small random structures for (let i = 0; i < 5; i++) { const posX = (Math.sin(i * 1.5) - 0.5) * 40; const posY = (Math.cos(i * 1.3) - 0.5) * 40; const posZ = (Math.sin(i) - 0.5) * 40; const smallDist = Math.sqrt( (xPos - posX) * (xPos - posX) + (yPos - posY) * (yPos - posY) + (zPos - posZ) * (zPos - posZ) ); const radius = 8 + i * 2; if (smallDist <= radius) { density = Math.max(density, 0.6 + (1 - smallDist / radius) * 0.4); } } } row.push(density); } sliceArray.push(row); } sliceData.push(sliceArray); } } // Initialize slice canvas function initSliceCanvas() { sliceCanvas = document.getElementById('sliceCanvas'); sliceCtx = sliceCanvas.getContext('2d'); const resolution = parseInt(document.getElementById('sliceResolution').value); sliceCanvas.width = resolution; sliceCanvas.height = resolution; } // Create cutting plane visualization function createCuttingPlane() { const planeGeometry = new THREE.PlaneGeometry(100, 100); const planeMaterial = new THREE.MeshBasicMaterial({ color: 0xff0000, transparent: true, opacity: 0.3, side: THREE.DoubleSide }); cuttingPlaneMesh = new THREE.Mesh(planeGeometry, planeMaterial); cuttingPlaneMesh.rotation.x = -Math.PI / 2; cuttingPlaneMesh.visible = true; scene.add(cuttingPlaneMesh); } // Update 2D slice view function updateSliceView(zPosition) { if (!sliceCtx || sliceData.length === 0) return; const resolution = parseInt(document.getElementById('sliceResolution').value); const sliceIndex = Math.floor((zPosition + 50) / 100 * (sliceData.length - 1)); if (sliceIndex < 0 || sliceIndex >= sliceData.length) return; const slice = sliceData[sliceIndex]; const imageData = sliceCtx.createImageData(resolution, resolution); const data = imageData.data; for (let y = 0; y < resolution; y++) { for (let x = 0; x < resolution; x++) { const density = slice[y][x]; const index = (y * resolution + x) * 4; if (density > 0) { // Color based on density const hue = 0.6 - density * 0.3; // Blue to green to yellow const rgb = hslToRgb(hue, 0.8, 0.5 + density * 0.3); data[index] = rgb[0]; // R data[index + 1] = rgb[1]; // G data[index + 2] = rgb[2]; // B data[index + 3] = Math.floor(density * 255); // A } else { data[index] = 0; data[index + 1] = 0; data[index + 2] = 0; data[index + 3] = 0; } } } sliceCtx.putImageData(imageData, 0, 0); // Update slice info document.getElementById('sliceInfo').textContent = `Z = ${zPosition.toFixed(1)}`; } // Convert HSL to RGB function hslToRgb(h, s, l) { const c = (1 - Math.abs(2 * l - 1)) * s; const x = c * (1 - Math.abs((h * 6) % 2 - 1)); const m = l - c / 2; let r, g, b; if (h < 1/6) { r = c; g = x; b = 0; } else if (h < 2/6) { r = x; g = c; b = 0; } else if (h < 3/6) { r = 0; g = c; b = x; } else if (h < 4/6) { r = 0; g = x; b = c; } else if (h < 5/6) { r = x; g = 0; b = c; } else { r = c; g = 0; b = x; } return [ Math.floor((r + m) * 255), Math.floor((g + m) * 255), Math.floor((b + m) * 255) ]; } // Setup event listeners function setupEventListeners() { rotationSlider = document.getElementById('rotationSlider'); autoRotateCheckbox = document.getElementById('autoRotate'); cuttingPlaneSlider = document.getElementById('cuttingPlane'); // Rotation slider rotationSlider.addEventListener('input', (e) => { const value = e.target.value; document.getElementById('rotationValue').textContent = value + '°'; if (model) { model.rotation.y = (value * Math.PI) / 180; } }); // Auto rotate checkbox autoRotateCheckbox.addEventListener('change', (e) => { controls.autoRotate = e.target.checked; }); // Cutting plane slider cuttingPlaneSlider.addEventListener('input', (e) => { const value = parseFloat(e.target.value); document.getElementById('planeValue').textContent = value.toFixed(1); updateCuttingPlane(value); updateSliceView(value); }); // Speed slider const speedSlider = document.getElementById('scanSpeed'); speedSlider.addEventListener('input', (e) => { document.getElementById('speedValue').textContent = e.target.value + 'x'; }); // Resolution selector document.getElementById('sliceResolution').addEventListener('change', () => { updateSliceData(); initSliceCanvas(); updateSliceView(parseFloat(cuttingPlaneSlider.value)); }); // Export button document.getElementById('exportBtn').addEventListener('click', exportScan); } // Update cutting plane position function updateCuttingPlane(zPosition) { if (cuttingPlaneMesh) { cuttingPlaneMesh.position.z = zPosition; } } // Animation loop function animate() { animationId = requestAnimationFrame(animate); controls.update(); renderer.render(scene, camera); } // Export scan as video async function exportScan() { if (isRecording) return; const exportBtn = document.getElementById('exportBtn'); const progressBar = document.getElementById('exportProgress'); const progressFill = progressBar.querySelector('.progress-fill'); const progressText = progressBar.querySelector('.progress-text'); exportBtn.disabled = true; progressBar.classList.remove('hidden'); isRecording = true; // Setup MediaRecorder const stream = sliceCanvas.captureStream(30); const options = { mimeType: 'video/webm;codecs=vp8,opus', videoBitsPerSecond: 2500000 }; try { mediaRecorder = new MediaRecorder(stream, options); recordedChunks = []; mediaRecorder.ondataavailable = (event) => { if (event.data.size > 0) { recordedChunks.push(event.data); } }; mediaRecorder.onstop = () => { const blob = new Blob(recordedChunks, { type: 'video/webm' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `3d-scan-${Date.now()}.webm`; a.click(); URL.revokeObjectURL(url); // Reset UI exportBtn.disabled = false; progressBar.classList.add('hidden'); isRecording = false; // Reset cutting plane cuttingPlaneSlider.value = 0; updateCuttingPlane(0); updateSliceView(0); }; // Start recording mediaRecorder.start(); // Animate scan from top to bottom const scanSpeed = parseFloat(document.getElementById('scanSpeed').value); const totalSteps = 100; const stepDelay = 1000 / scanSpeed / totalSteps; for (let i = 0; i <= totalSteps; i++) { const zPosition = 50 - (i * 100 / totalSteps); cuttingPlaneSlider.value = zPosition; updateCuttingPlane(zPosition); updateSliceView(zPosition); // Update progress const progress = (i / totalSteps) * 100; progressFill.style.width = progress + '%'; progressText.textContent = Math.floor(progress) + '%'; await new Promise(resolve => setTimeout(resolve, stepDelay)); } // Stop recording mediaRecorder.stop(); } catch (err) { console.error('Recording failed:', err); alert('Video recording failed. Please check browser permissions.'); exportBtn.disabled = false; progressBar.classList.add('hidden'); isRecording = false; } } // Initialize on load window.addEventListener('load', init3DScene); // Handle window resize window.addEventListener('resize', () => { const container = document.getElementById('canvas3d'); if (camera && renderer) { camera.aspect = container.clientWidth / container.clientHeight; camera.updateProjectionMatrix(); renderer.setSize(container.clientWidth, container.clientHeight); } });