Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>CSS Gen-Algo & 3D Experimental Lab</title> | |
| <!-- Import Icons --> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <!-- Three.js --> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> | |
| <style> | |
| :root { | |
| --bg-color: #050505; | |
| --panel-bg: rgba(20, 20, 25, 0.75); | |
| --accent-primary: #00f3ff; | |
| --accent-secondary: #bd00ff; | |
| --text-main: #ffffff; | |
| --text-muted: #888888; | |
| --border-color: rgba(255, 255, 255, 0.1); | |
| --card-ratio-w: 4; | |
| --card-ratio-h: 5; | |
| } | |
| * { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; | |
| } | |
| body { | |
| background-color: var(--bg-color); | |
| color: var(--text-main); | |
| overflow: hidden; /* App-like feel */ | |
| height: 100vh; | |
| width: 100vw; | |
| } | |
| /* --- 3D Background Canvas --- */ | |
| #bg-canvas { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| z-index: -1; | |
| pointer-events: none; /* Let clicks pass through */ | |
| } | |
| /* --- Header --- */ | |
| header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| padding: 1rem 2rem; | |
| background: rgba(0, 0, 0, 0.8); | |
| backdrop-filter: blur(10px); | |
| border-bottom: 1px solid var(--border-color); | |
| z-index: 100; | |
| } | |
| .brand { | |
| font-size: 1.2rem; | |
| font-weight: 700; | |
| letter-spacing: 2px; | |
| text-transform: uppercase; | |
| background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary)); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| } | |
| .anycoder-link { | |
| font-size: 0.8rem; | |
| color: var(--text-muted); | |
| text-decoration: none; | |
| transition: color 0.3s; | |
| } | |
| .anycoder-link:hover { color: var(--accent-primary); } | |
| /* --- Main Layout --- */ | |
| main { | |
| display: grid; | |
| grid-template-columns: 350px 1fr; | |
| height: calc(100vh - 70px); | |
| position: relative; | |
| } | |
| /* --- Sidebar / Controls --- */ | |
| aside { | |
| background: var(--panel-bg); | |
| backdrop-filter: blur(15px); | |
| border-right: 1px solid var(--border-color); | |
| padding: 1.5rem; | |
| overflow-y: auto; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 1.5rem; | |
| } | |
| h2 { | |
| font-size: 0.9rem; | |
| text-transform: uppercase; | |
| color: var(--accent-primary); | |
| margin-bottom: 0.8rem; | |
| border-bottom: 1px solid var(--border-color); | |
| padding-bottom: 0.3rem; | |
| } | |
| .control-group { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0.5rem; | |
| } | |
| label { | |
| font-size: 0.8rem; | |
| color: var(--text-muted); | |
| } | |
| textarea { | |
| background: rgba(0, 0, 0, 0.5); | |
| border: 1px solid var(--border-color); | |
| color: #00ff00; | |
| font-family: 'Courier New', monospace; | |
| font-size: 0.75rem; | |
| padding: 0.5rem; | |
| border-radius: 4px; | |
| resize: vertical; | |
| min-height: 80px; | |
| } | |
| textarea:focus { outline: 1px solid var(--accent-primary); } | |
| button { | |
| background: linear-gradient(45deg, var(--accent-secondary), #8000ff); | |
| border: none; | |
| color: white; | |
| padding: 0.8rem; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| font-weight: 600; | |
| text-transform: uppercase; | |
| font-size: 0.8rem; | |
| transition: transform 0.2s, box-shadow 0.2s; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 0.5rem; | |
| } | |
| button:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 4px 15px rgba(189, 0, 255, 0.4); | |
| } | |
| button.secondary { | |
| background: transparent; | |
| border: 1px solid var(--accent-primary); | |
| color: var(--accent-primary); | |
| } | |
| button.secondary:hover { | |
| background: rgba(0, 243, 255, 0.1); | |
| box-shadow: 0 0 10px rgba(0, 243, 255, 0.2); | |
| } | |
| /* --- 3D Scanner Preview --- */ | |
| #scanner-preview { | |
| width: 100%; | |
| height: 150px; | |
| background: #000; | |
| border: 1px solid var(--border-color); | |
| border-radius: 4px; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| #scanner-preview canvas { | |
| width: 100%; | |
| height: 100%; | |
| } | |
| /* --- Performance Curve --- */ | |
| #chart-canvas { | |
| width: 100%; | |
| height: 100px; | |
| background: rgba(0,0,0,0.3); | |
| border: 1px solid var(--border-color); | |
| border-radius: 4px; | |
| } | |
| /* --- Gallery / Workspace --- */ | |
| #workspace { | |
| padding: 2rem; | |
| overflow: hidden; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| .toolbar { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 1rem; | |
| } | |
| .zoom-control { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| input[type="range"] { | |
| accent-color: var(--accent-primary); | |
| } | |
| /* --- Gallery Grid --- */ | |
| #gallery-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); | |
| gap: 1.5rem; | |
| overflow-y: auto; | |
| flex-grow: 1; | |
| padding-right: 10px; | |
| padding-bottom: 20px; | |
| align-content: start; | |
| } | |
| /* Custom Scrollbar */ | |
| #gallery-grid::-webkit-scrollbar { | |
| width: 8px; | |
| } | |
| #gallery-grid::-webkit-scrollbar-track { | |
| background: rgba(255,255,255,0.05); | |
| } | |
| #gallery-grid::-webkit-scrollbar-thumb { | |
| background: var(--border-color); | |
| border-radius: 4px; | |
| } | |
| /* --- The Card (4:5) --- */ | |
| .card { | |
| aspect-ratio: 4 / 5; | |
| background: rgba(20,20,20,0.8); | |
| border: 1px solid var(--border-color); | |
| border-radius: 8px; | |
| position: relative; | |
| display: flex; | |
| flex-direction: column; | |
| overflow: hidden; | |
| transition: all 0.3s ease; | |
| cursor: pointer; | |
| box-shadow: 0 4px 6px rgba(0,0,0,0.3); | |
| } | |
| .card:hover { | |
| transform: translateY(-5px) scale(1.02); | |
| z-index: 10; | |
| box-shadow: 0 10px 30px rgba(0,0,0,0.5); | |
| } | |
| /* Card Visuals (Generated CSS applies here) */ | |
| .card-visual { | |
| flex-grow: 1; | |
| position: relative; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| overflow: hidden; | |
| } | |
| /* The "Shrunk" 3D Model representation in card */ | |
| .card-model-placeholder { | |
| width: 60%; | |
| height: 60%; | |
| background: rgba(255,255,255,0.05); | |
| border-radius: 50%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 2rem; | |
| color: rgba(255,255,255,0.2); | |
| transition: all 0.5s; | |
| } | |
| .card:hover .card-model-placeholder { | |
| transform: rotate(45deg) scale(1.1); | |
| color: var(--accent-primary); | |
| } | |
| .card-info { | |
| padding: 1rem; | |
| background: rgba(0,0,0,0.9); | |
| border-top: 1px solid var(--border-color); | |
| } | |
| .card-title { | |
| font-size: 0.8rem; | |
| font-weight: bold; | |
| margin-bottom: 0.3rem; | |
| white-space: nowrap; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| } | |
| .card-meta { | |
| font-size: 0.6rem; | |
| color: var(--text-muted); | |
| display: flex; | |
| justify-content: space-between; | |
| } | |
| .save-btn { | |
| position: absolute; | |
| top: 10px; | |
| right: 10px; | |
| background: rgba(0,0,0,0.6); | |
| border: 1px solid var(--text-muted); | |
| color: white; | |
| width: 30px; | |
| height: 30px; | |
| border-radius: 50%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| cursor: pointer; | |
| opacity: 0; | |
| transition: opacity 0.2s; | |
| } | |
| .card:hover .save-btn { opacity: 1; } | |
| .save-btn:hover { background: var(--accent-primary); border-color: var(--accent-primary); color: black; } | |
| /* --- Toast Notification --- */ | |
| #toast-container { | |
| position: fixed; | |
| bottom: 20px; | |
| right: 20px; | |
| z-index: 1000; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 10px; | |
| } | |
| .toast { | |
| background: rgba(10, 10, 15, 0.95); | |
| border-left: 4px solid var(--accent-primary); | |
| color: white; | |
| padding: 1rem 1.5rem; | |
| border-radius: 4px; | |
| box-shadow: 0 5px 15px rgba(0,0,0,0.5); | |
| animation: slideIn 0.3s ease-out; | |
| font-size: 0.9rem; | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| @keyframes slideIn { | |
| from { transform: translateX(100%); opacity: 0; } | |
| to { transform: translateX(0); opacity: 1; } | |
| } | |
| /* Responsive */ | |
| @media (max-width: 900px) { | |
| main { grid-template-columns: 1fr; grid-template-rows: auto 1fr; overflow-y: auto; height: auto;} | |
| aside { height: auto; max-height: 300px; border-right: none; border-bottom: 1px solid var(--border-color); } | |
| #workspace { height: 600px; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- 3D Background --> | |
| <canvas id="bg-canvas"></canvas> | |
| <!-- Header --> | |
| <header> | |
| <div class="brand"><i class="fa-solid fa-dna"></i> CSS GEN-ALGO LAB</div> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="anycoder-link"> | |
| Built with anycoder <i class="fa-solid fa-external-link-alt"></i> | |
| </a> | |
| </header> | |
| <main> | |
| <!-- Sidebar Controls --> | |
| <aside> | |
| <!-- CSS Models Mixer --> | |
| <section> | |
| <h2><i class="fa-solid fa-code"></i> CSS Models (Import)</h2> | |
| <div class="control-group"> | |
| <label>Model A (Base)</label> | |
| <textarea id="css-model-a" spellcheck="false"> | |
| .card-visual { | |
| background: radial-gradient(circle, #2a2a2a, #000); | |
| border: 2px solid #00f3ff; | |
| } | |
| .card-model-placeholder { | |
| box-shadow: 0 0 15px #00f3ff; | |
| border: 1px dashed #00f3ff; | |
| border-radius: 10px; | |
| }</textarea> | |
| </div> | |
| <div class="control-group"> | |
| <label>Model B (Variant)</label> | |
| <textarea id="css-model-b" spellcheck="false"> | |
| .card-visual { | |
| background: linear-gradient(135deg, #1a001a, #000); | |
| border: 1px solid #bd00ff; | |
| } | |
| .card-model-placeholder { | |
| box-shadow: 0 0 20px #bd00ff; | |
| border-radius: 50%; | |
| background: rgba(189, 0, 255, 0.1); | |
| }</textarea> | |
| </div> | |
| <button id="btn-mix" style="margin-top: 10px;"> | |
| <i class="fa-solid fa-shuffle"></i> Mix & Regenerate | |
| </button> | |
| </section> | |
| <!-- 3D Scanner / Source Code --> | |
| <section> | |
| <h2><i class="fa-solid fa-cube"></i> 3D Source Scanner</h2> | |
| <div class="control-group"> | |
| <label>Source Code / URL / ID</label> | |
| <textarea id="source-input" placeholder="Paste code, URL (.obj/.gltf), or ID here..."></textarea> | |
| </div> | |
| <button id="btn-scan" class="secondary"> | |
| <i class="fa-solid fa-radar"></i> Scan & Extract | |
| </button> | |
| <div id="scanner-preview"> | |
| <!-- Mini 3D Viewport --> | |
| </div> | |
| </section> | |
| <!-- Performance Curve --> | |
| <section> | |
| <h2><i class="fa-solid fa-chart-line"></i> Performance</h2> | |
| <canvas id="chart-canvas"></canvas> | |
| </section> | |
| </aside> | |
| <!-- Gallery Workspace --> | |
| <section id="workspace"> | |
| <div class="toolbar"> | |
| <h3>Generated Gallery (4:5)</h3> | |
| <div class="zoom-control"> | |
| <i class="fa-solid fa-magnifying-glass-minus"></i> | |
| <input type="range" id="zoom-slider" min="0.5" max="1.5" step="0.1" value="1"> | |
| <i class="fa-solid fa-magnifying-glass-plus"></i> | |
| </div> | |
| </div> | |
| <div id="gallery-grid"> | |
| <!-- Cards will be injected here via JS --> | |
| </div> | |
| </section> | |
| </main> | |
| <!-- Toast Container --> | |
| <div id="toast-container"></div> | |
| <script> | |
| // --- 1. GLOBAL STATE & CONFIG --- | |
| const state = { | |
| cards: [], | |
| zoom: 1, | |
| mixedCSS: '', | |
| scannedModelType: 'cube' // cube, sphere, complex | |
| }; | |
| const galleryGrid = document.getElementById('gallery-grid'); | |
| const zoomSlider = document.getElementById('zoom-slider'); | |
| // --- 2. UTILITIES --- | |
| function showToast(message, type = 'info') { | |
| const container = document.getElementById('toast-container'); | |
| const toast = document.createElement('div'); | |
| toast.className = 'toast'; | |
| toast.innerHTML = `<i class="fa-solid ${type === 'success' ? 'fa-check-circle' : 'fa-info-circle'}"></i> ${message}`; | |
| container.appendChild(toast); | |
| setTimeout(() => { | |
| toast.style.opacity = '0'; | |
| setTimeout(() => toast.remove(), 300); | |
| }, 3000); | |
| } | |
| function randomId() { | |
| return Math.random().toString(36).substr(2, 9); | |
| } | |
| // --- 3. CSS GENETIC ALGORITHM (MIXER) --- | |
| function mixCSSModels(cssA, cssB) { | |
| // Split into blocks (separated by }) | |
| const blocksA = cssA.split('}').filter(b => b.trim() !== ''); | |
| const blocksB = cssB.split('}').filter(b => b.trim() !== ''); | |
| const mixedBlocks = []; | |
| // Heuristic: Mix lines/blocks randomly | |
| const maxLen = Math.max(blocksA.length, blocksB.length); | |
| for(let i=0; i < maxLen; i++) { | |
| const pickA = Math.random() > 0.5; | |
| let block = ''; | |
| if(pickA && blocksA[i]) { | |
| block = blocksA[i] + '}'; | |
| } else if (!pickA && blocksB[i]) { | |
| block = blocksB[i] + '}'; | |
| } else if (blocksA[i]) { | |
| block = blocksA[i] + '}'; | |
| } else if (blocksB[i]) { | |
| block = blocksB[i] + '}'; | |
| } | |
| // Randomly mutate a property value (Simulation) | |
| if(block.includes('px') || block.includes('deg')) { | |
| block = block.replace(/(\d+(\.\d+)?)/g, (match) => { | |
| if(Math.random() > 0.8) return parseFloat(match) * (0.8 + Math.random() * 0.4); | |
| return match; | |
| }); | |
| } | |
| mixedBlocks.push(block); | |
| } | |
| return mixedBlocks.join('\n'); | |
| } | |
| // --- 4. CARD GENERATION --- | |
| function createCard(styleString, index) { | |
| const card = document.createElement('div'); | |
| card.className = 'card'; | |
| // Apply zoom via CSS variable or inline style handled by container, | |
| // but specific mix styles need a scope. | |
| // We will use a unique ID for the style tag to scope it to just this card or use inline styles. | |
| // For simplicity and "mixing lines" demo, we'll create a scoped style element or inject classes. | |
| // Better: Inject a style block specifically for this card's ID. | |
| const cardId = `card-${randomId()}`; | |
| card.id = cardId; | |
| // Extract relevant parts or wrap the mixed CSS | |
| // Assuming the mixed CSS targets .card-visual or .card-model-placeholder | |
| const scopedStyle = styleString.replace(/\.card-visual/g, `#${cardId} .card-visual`) | |
| .replace(/\.card-model-placeholder/g, `#${cardId} .card-model-placeholder`); | |
| const styleEl = document.createElement('style'); | |
| styleEl.innerHTML = scopedStyle; | |
| card.appendChild(styleEl); | |
| // Content | |
| const visual = document.createElement('div'); | |
| visual.className = 'card-visual'; | |
| // 3D Placeholder (Shrunk model) | |
| const placeholder = document.createElement('div'); | |
| placeholder.className = 'card-model-placeholder'; | |
| // Icon based on scanned type | |
| let iconClass = 'fa-cube'; | |
| if(state.scannedModelType === 'sphere') iconClass = 'fa-globe'; | |
| if(state.scannedModelType === 'complex') iconClass = 'fa-shapes'; | |
| placeholder.innerHTML = `<i class="fa-solid ${iconClass}"></i>`; | |
| visual.appendChild(placeholder); | |
| const info = document.createElement('div'); | |
| info.className = 'card-info'; | |
| info.innerHTML = ` | |
| <div class="card-title">Product #${index.toString().padStart(4, '0')}</div> | |
| <div class="card-meta"> | |
| <span>Mix v${Math.floor(Math.random()*10)}</span> | |
| <span style="color:var(--accent-primary)">Active</span> | |
| </div> | |
| `; | |
| const saveBtn = document.createElement('div'); | |
| saveBtn.className = 'save-btn'; | |
| saveBtn.innerHTML = '<i class="fa-solid fa-floppy-disk"></i>'; | |
| saveBtn.onclick = (e) => { | |
| e.stopPropagation(); | |
| showToast(`Design ${index} saved to library!`, 'success'); | |
| }; | |
| card.appendChild(saveBtn); | |
| card.appendChild(visual); | |
| card.appendChild(info); | |
| return card; | |
| } | |
| function generateGallery() { | |
| const cssA = document.getElementById('css-model-a').value; | |
| const cssB = document.getElementById('css-model-b').value; | |
| // Mix | |
| const mixed = mixCSSModels(cssA, cssB); | |
| state.mixedCSS = mixed; | |
| // Clear current but keep some for effect? No, regenerate usually means new batch. | |
| galleryGrid.innerHTML = ''; | |
| // Generate 8 cards | |
| for(let i=0; i<8; i++) { | |
| // Slight variation for each card | |
| const variedMixed = mixCSSModels(mixed, cssB); // Mix again slightly | |
| const card = createCard(variedMixed, i+1); | |
| galleryGrid.appendChild(card); | |
| } | |
| // Update Chart | |
| drawPerformanceChart(); | |
| showToast('Gallery Regenerated with Mixed CSS'); | |
| } | |
| // --- 5. CHARTING (Performance Curve) --- | |
| function drawPerformanceChart() { | |
| const canvas = document.getElementById('chart-canvas'); | |
| const ctx = canvas.getContext('2d'); | |
| const w = canvas.width = canvas.offsetWidth; | |
| const h = canvas.height = canvas.offsetHeight; | |
| ctx.clearRect(0, 0, w, h); | |
| // Grid | |
| ctx.strokeStyle = 'rgba(255,255,255,0.1)'; | |
| ctx.beginPath(); | |
| for(let i=0; i<w; i+=20) { ctx.moveTo(i,0); ctx.lineTo(i,h); } | |
| ctx.stroke(); | |
| // Random Curve | |
| ctx.strokeStyle = '#00f3ff'; | |
| ctx.lineWidth = 2; | |
| ctx.beginPath(); | |
| let points = []; | |
| for(let i=0; i<=10; i++) { | |
| points.push({ | |
| x: (w / 10) * i, | |
| y: h - (Math.random() * h * 0.8 + h * 0.1) | |
| }); | |
| } | |
| ctx.moveTo(points[0].x, points[0].y); | |
| // Catmull-Rom or simple Bezier smoothing | |
| for (let i = 0; i < points.length - 1; i ++) { | |
| const x_mid = (points[i].x + points[i + 1].x) / 2; | |
| const y_mid = (points[i].y + points[i + 1].y) / 2; | |
| const cp_x1 = (x_mid + points[i].x) / 2; | |
| const cp_x2 = (x_mid + points[i + 1].x) / 2; | |
| ctx.quadraticCurveTo(points[i].x, points[i].y, x_mid, y_mid); | |
| } | |
| ctx.lineTo(points[points.length-1].x, points[points.length-1].y); | |
| ctx.stroke(); | |
| // Fill | |
| ctx.lineTo(w, h); | |
| ctx.lineTo(0, h); | |
| ctx.fillStyle = 'linear-gradient(to top, rgba(0, 243, 255, 0.2), transparent)'; | |
| ctx.fill(); | |
| } | |
| // --- 6. 3D LOGIC (Background & Scanner) --- | |
| // -- A. Background Graph -- | |
| function initBackground3D() { | |
| const canvas = document.getElementById('bg-canvas'); | |
| const renderer = new THREE.WebGLRenderer({ canvas: canvas, alpha: true, antialias: true }); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| renderer.setPixelRatio(window.devicePixelRatio); | |
| const scene = new THREE.Scene(); | |
| const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); | |
| camera.position.z = 30; | |
| // Particles | |
| const geometry = new THREE.BufferGeometry(); | |
| const count = 500; | |
| const positions = new Float32Array(count * 3); | |
| for(let i=0; i<count*3; i++) { | |
| positions[i] = (Math.random() - 0.5) * 60; | |
| } | |
| geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); | |
| const material = new THREE.PointsMaterial({ color: 0x444444, size: 0.2 }); | |
| const particles = new THREE.Points(geometry, material); | |
| scene.add(particles); | |
| // Connecting Lines (Network) | |
| const lineMaterial = new THREE.LineBasicMaterial({ color: 0x222222, transparent: true, opacity: 0.3 }); | |
| const lineGeometry = new THREE.BufferGeometry(); | |
| // We'll update lines in animation loop or just use a wireframe object | |
| const wireGeo = new THREE.IcosahedronGeometry(15, 1); | |
| const wireframe = new THREE.LineSegments(new THREE.WireframeGeometry(wireGeo), lineMaterial); | |
| scene.add(wireframe); | |
| function animate() { | |
| requestAnimationFrame(animate); | |
| wireframe.rotation.x += 0.001; | |
| wireframe.rotation.y += 0.001; | |
| particles.rotation.y -= 0.0005; | |
| renderer.render(scene, camera); | |
| } | |
| animate(); | |
| window.addEventListener('resize', () => { | |
| camera.aspect = window.innerWidth / window.innerHeight; | |
| camera.updateProjectionMatrix(); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| }); | |
| } | |
| // -- B. Scanner 3D Viewer -- | |
| let scannerRenderer, scannerScene, scannerCamera, scannerMesh; | |
| function initScanner3D() { | |
| const container = document.getElementById('scanner-preview'); | |
| const w = container.offsetWidth; | |
| const h = container.offsetHeight; | |
| scannerRenderer = new THREE.WebGLRenderer({ alpha: true, antialias: true }); | |
| scannerRenderer.setSize(w, h); | |
| container.appendChild(scannerRenderer.domElement); | |
| scannerScene = new THREE.Scene(); | |
| scannerCamera = new THREE.PerspectiveCamera(50, w / h, 0.1, 100); | |
| scannerCamera.position.z = 4; | |
| // Lights | |
| const ambientLight = new THREE.AmbientLight(0xffffff, 0.5); | |
| scannerScene.add(ambientLight); | |
| const pointLight = new THREE.PointLight(0xffffff, 1); | |
| pointLight.position.set(2, 2, 2); | |
| scannerScene.add(pointLight); | |
| // Default Mesh | |
| updateScannerMesh('cube'); | |
| function animateScanner() { | |
| requestAnimationFrame(animateScanner); | |
| if(scannerMesh) { | |
| scannerMesh.rotation.x += 0.01; | |
| scannerMesh.rotation.y += 0.01; | |
| } | |
| scannerRenderer.render(scannerScene, scannerCamera); | |
| } | |
| animateScanner(); | |
| } | |
| function updateScannerMesh(type) { | |
| if(scannerMesh) scannerScene.remove(scannerMesh); | |
| let geometry; | |
| state.scannedModelType = type; | |
| if(type === 'sphere') geometry = new THREE.SphereGeometry(1, 32, 32); | |
| else if(type === 'complex') geometry = new THREE.TorusKnotGeometry(0.7, 0.2, 100, 16); | |
| else geometry = new THREE.BoxGeometry(1.5, 1.5, 1.5); | |
| // "Raytracing" look via Physical Material | |
| const material = new THREE.MeshPhysicalMaterial({ | |
| color: 0x222222, | |
| metalness: 0.9, | |
| roughness: 0.1, | |
| clearcoat: 1.0, | |
| clearcoatRoughness: 0.1, | |
| emissive: 0x000000 | |
| }); | |
| scannerMesh = new THREE.Mesh(geometry, material); | |
| scannerScene.add(scannerMesh); | |
| } | |
| // --- 7. EVENT LISTENERS & LOGIC --- | |
| // Zoom | |
| zoomSlider.addEventListener('input', (e) => { | |
| const val = e.target.value; | |
| // We scale the grid items via transform | |
| const cards = document.querySelectorAll('.card'); | |
| cards.forEach(card => { | |
| card.style.transform = `scale(${val})`; | |
| }); | |
| }); | |
| // Mix & Regenerate | |
| document.getElementById('btn-mix').addEventListener('click', generateGallery); | |
| // Scan Source Code | |
| document.getElementById('btn-scan').addEventListener('click', () => { | |
| const input = document.getElementById('source-input').value.toLowerCase(); | |
| // Intelligent Recognition Simulation | |
| let detected = 'cube'; | |
| if(input.includes('sphere') || input.includes('round') || input.includes('ball')) detected = 'sphere'; | |
| if(input.includes('complex') || input.includes('torus') || input.includes('knot') || input.includes('advanced')) detected = 'complex'; | |
| if(input.includes('http') && input.includes('.obj')) detected = 'complex'; // Assume complex if URL | |
| updateScannerMesh(detected); | |
| showToast(`Source Analyzed: Detected ${detected.toUpperCase()} geometry`, 'success'); | |
| // Also regenerate gallery to apply new model type to cards | |
| generateGallery(); | |
| }); | |
| // --- 8. INITIALIZATION --- | |
| window.addEventListener('load', () => { | |
| initBackground3D(); | |
| initScanner3D(); | |
| drawPerformanceChart(); | |
| generateGallery(); // Initial batch | |
| }); | |
| </script> | |
| </body> | |
| </html> |