Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>3D CNN Visualizer</title> | |
| <!-- Tailwind CSS --> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script> | |
| tailwind.config = { | |
| theme: { | |
| extend: { | |
| colors: { | |
| 'neon-green': '#00ff00', | |
| 'dark-green': '#002200', | |
| }, | |
| fontFamily: { | |
| mono: ['ui-monospace', 'SFMono-Regular', 'Menlo', 'Monaco', 'Consolas', 'monospace'], | |
| } | |
| } | |
| } | |
| } | |
| </script> | |
| <!-- Import Map for React & Three.js ecosystem --> | |
| <script type="importmap"> | |
| { | |
| "imports": { | |
| "react": "https://esm.sh/react@18.2.0", | |
| "react-dom/client": "https://esm.sh/react-dom@18.2.0/client", | |
| "three": "https://esm.sh/three@0.160.0", | |
| "@react-three/fiber": "https://esm.sh/@react-three/fiber@8.15.12?external=react,react-dom,three", | |
| "@react-three/drei": "https://esm.sh/@react-three/drei@9.96.1?external=react,react-dom,three,@react-three/fiber", | |
| "lucide-react": "https://esm.sh/lucide-react@0.303.0?external=react" | |
| } | |
| } | |
| </script> | |
| <!-- Babel for JSX --> | |
| <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script> | |
| <style> | |
| body { margin: 0; background-color: #000; overflow: hidden; color: white; } | |
| canvas { touch-action: none; } | |
| .hud-panel { | |
| background: rgba(0, 10, 0, 0.85); | |
| border: 1px solid #004400; | |
| box-shadow: 0 0 20px rgba(0, 255, 0, 0.1); | |
| backdrop-filter: blur(10px); | |
| } | |
| .btn-holo { | |
| background: linear-gradient(180deg, rgba(0,40,0,0.8) 0%, rgba(0,20,0,0.9) 100%); | |
| border: 1px solid #00ff00; | |
| color: #00ff00; | |
| text-shadow: 0 0 5px rgba(0,255,0,0.5); | |
| } | |
| .btn-holo:hover:not(:disabled) { | |
| background: #00ff00; | |
| color: #000; | |
| box-shadow: 0 0 15px #00ff00; | |
| } | |
| .btn-holo:disabled { | |
| border-color: #004400; | |
| color: #004400; | |
| cursor: not-allowed; | |
| } | |
| .neon-text { | |
| text-shadow: 0 0 10px rgba(0, 255, 0, 0.6); | |
| } | |
| /* Scanline effect */ | |
| .scanlines { | |
| position: fixed; | |
| top: 0; left: 0; width: 100%; height: 100%; | |
| background: linear-gradient(to bottom, rgba(255,255,255,0), rgba(255,255,255,0) 50%, rgba(0,0,0,0.1) 50%, rgba(0,0,0,0.1)); | |
| background-size: 100% 4px; | |
| pointer-events: none; | |
| z-index: 50; | |
| opacity: 0.3; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="root"></div> | |
| <div class="scanlines"></div> | |
| <script type="text/babel" data-type="module"> | |
| import React, { useState, useEffect, useRef, useMemo, useLayoutEffect } from 'react'; | |
| import { createRoot } from 'react-dom/client'; | |
| import { Canvas, useFrame } from '@react-three/fiber'; | |
| import { Text, Stars, Environment, Grid } from '@react-three/drei'; | |
| import * as THREE from 'three'; | |
| import { Play, RotateCcw, Activity, Layers, Cpu, Scan, Zap, BrainCircuit, Eraser } from 'lucide-react'; | |
| // --- 1. CONFIGURATION --- | |
| const ARCHITECTURE = [ | |
| { id: 'input', type: 'input', width: 28, height: 28, depth: 1, z: 0, label: 'INPUT (28x28)' }, | |
| { id: 'conv1', type: 'conv', width: 24, height: 24, depth: 4, z: -15, label: 'CONV LAYER 1' }, | |
| { id: 'pool1', type: 'pool', width: 12, height: 12, depth: 4, z: -25, label: 'POOLING 1' }, | |
| { id: 'conv2', type: 'conv', width: 8, height: 8, depth: 8, z: -35, label: 'CONV LAYER 2' }, | |
| { id: 'pool2', type: 'pool', width: 4, height: 4, depth: 8, z: -45, label: 'POOLING 2' }, | |
| { id: 'flat', type: 'flatten', width: 16, height: 8, depth: 1, z: -52, label: 'FLATTEN' }, | |
| { id: 'fc', type: 'fc', width: 1, height: 10, depth: 1, z: -62, label: 'CLASSIFICATION' } | |
| ]; | |
| // --- 2. MATH ENGINE (Synthethic Neural Network) --- | |
| // Define vector strokes for digits to generate "ideal" weights | |
| const DIGIT_STROKES = { | |
| 0: [[[10,2],[4,6],[4,14],[10,18],[16,14],[16,6],[10,2]]], | |
| 1: [[[10,2],[10,18]]], | |
| 2: [[[4,6],[6,2],[14,2],[16,6],[4,18],[18,18]]], | |
| 3: [[[4,4],[16,4],[10,10],[16,16],[4,16]]], | |
| 4: [[[14,2],[4,12],[18,12]],[[14,2],[14,18]]], | |
| 5: [[[16,4],[6,4],[4,10],[16,12],[4,18]]], | |
| 6: [[[16,4],[4,12],[4,16],[10,18],[16,14],[10,10]]], | |
| 7: [[[4,4],[16,4],[8,18]]], | |
| 8: [[[10,10],[4,6],[10,2],[16,6],[10,10],[4,14],[10,18],[16,14],[10,10]]], | |
| 9: [[[16,10],[10,4],[4,8],[10,12],[16,10],[10,18]]] | |
| }; | |
| const createGrid = (w, h, val = 0) => Array.from({ length: h }, () => Array(w).fill(val)); | |
| // Rasterize lines into grid | |
| const drawLine = (grid, p1, p2, intensity) => { | |
| let x0 = Math.round(p1[0]), y0 = Math.round(p1[1]); | |
| let x1 = Math.round(p2[0]), y1 = Math.round(p2[1]); | |
| const dx = Math.abs(x1 - x0), dy = Math.abs(y1 - y0); | |
| const sx = (x0 < x1) ? 1 : -1, sy = (y0 < y1) ? 1 : -1; | |
| let err = dx - dy; | |
| while(true) { | |
| if(y0 >= 0 && y0 < 20 && x0 >= 0 && x0 < 20) grid[y0][x0] = Math.max(grid[y0][x0], intensity); | |
| if(x0 === x1 && y0 === y1) break; | |
| const e2 = 2 * err; | |
| if(e2 > -dy) { err -= dy; x0 += sx; } | |
| if(e2 < dx) { err += dx; y0 += sy; } | |
| } | |
| }; | |
| const gaussianBlur = (grid) => { | |
| const size = 20; | |
| const result = createGrid(size, size); | |
| const kernel = [[0.06, 0.12, 0.06], [0.12, 0.25, 0.12], [0.06, 0.12, 0.06]]; | |
| for(let y=1; y<size-1; y++) { | |
| for(let x=1; x<size-1; x++) { | |
| let sum = 0; | |
| for(let ky=-1; ky<=1; ky++) for(let kx=-1; kx<=1; kx++) sum += grid[y+ky][x+kx] * kernel[ky+1][kx+1]; | |
| result[y][x] = sum; | |
| } | |
| } | |
| return result; | |
| }; | |
| // Initialize Brain | |
| let TRAINED_WEIGHTS = {}; | |
| const trainBrain = () => { | |
| Object.keys(DIGIT_STROKES).forEach(key => { | |
| const strokes = DIGIT_STROKES[key]; | |
| const grid = createGrid(20, 20); | |
| strokes.forEach(part => { | |
| for(let i=0; i<part.length-1; i++) drawLine(grid, part[i], part[i+1], 1.0); | |
| }); | |
| // Blur heavily to create a "probability field" | |
| TRAINED_WEIGHTS[key] = gaussianBlur(gaussianBlur(grid)); | |
| }); | |
| }; | |
| // Preprocessing | |
| const getBoundingBox = (grid) => { | |
| let minX = 28, minY = 28, maxX = 0, maxY = 0, hasPixels = false; | |
| for(let y=0; y<28; y++) for(let x=0; x<28; x++) if(grid[y][x] > 0.1) { | |
| hasPixels = true; | |
| if(x < minX) minX = x; | |
| if(x > maxX) maxX = x; | |
| if(y < minY) minY = y; | |
| if(y > maxY) maxY = y; | |
| } | |
| return { minX, minY, w: maxX - minX + 1, h: maxY - minY + 1, hasPixels }; | |
| }; | |
| const scaleToFit = (grid) => { | |
| const box = getBoundingBox(grid); | |
| if(!box.hasPixels) return grid; | |
| const canvas = document.createElement('canvas'); | |
| canvas.width = 28; canvas.height = 28; | |
| const ctx = canvas.getContext('2d'); | |
| // Draw original data to canvas | |
| const imgData = ctx.createImageData(28, 28); | |
| for(let i=0; i<28*28; i++) { | |
| const val = grid[Math.floor(i/28)][i%28] * 255; | |
| imgData.data[i*4+0] = val; imgData.data[i*4+1] = val; imgData.data[i*4+2] = val; imgData.data[i*4+3] = 255; | |
| } | |
| // Crop to temporary canvas | |
| const temp = document.createElement('canvas'); | |
| temp.width = box.w; temp.height = box.h; | |
| temp.getContext('2d').putImageData(imgData, -box.minX, -box.minY, 0, 0, 28, 28); | |
| // Scale and center back to 28x28 (target inner 20x20) | |
| ctx.clearRect(0,0,28,28); | |
| const scale = Math.min(20/box.w, 20/box.h); | |
| ctx.drawImage(temp, (28 - box.w*scale)/2, (28 - box.h*scale)/2, box.w*scale, box.h*scale); | |
| const finalData = ctx.getImageData(0,0,28,28).data; | |
| const finalGrid = createGrid(28,28); | |
| for(let i=0; i<28*28; i++) finalGrid[Math.floor(i/28)][i%28] = finalData[i*4+1] / 255; | |
| return finalGrid; | |
| }; | |
| const predict = (rawGrid) => { | |
| const scaled = scaleToFit(rawGrid); | |
| // Extract center 20x20 | |
| const inputVec = []; | |
| for(let y=4; y<24; y++) for(let x=4; x<24; x++) inputVec.push(scaled[y][x]); | |
| const inputMag = Math.sqrt(inputVec.reduce((acc, v) => acc + v*v, 0)); | |
| const scores = Array(10).fill(0); | |
| if(inputMag > 0.1) { | |
| for(let d=0; d<=9; d++) { | |
| const proto = TRAINED_WEIGHTS[d]; | |
| const protoVec = []; | |
| proto.forEach(row => row.forEach(v => protoVec.push(v))); | |
| let dot = 0, protoMag = 0; | |
| for(let i=0; i<400; i++) { | |
| dot += inputVec[i] * protoVec[i]; | |
| protoMag += protoVec[i] * protoVec[i]; | |
| } | |
| scores[d] = dot / (inputMag * Math.sqrt(protoMag) + 0.0001); | |
| } | |
| } | |
| // Softmax | |
| const exp = scores.map(s => Math.exp(s * 10)); // Amplify differences | |
| const sum = exp.reduce((a,b) => a+b, 0); | |
| const probs = sum > 0 ? exp.map(e => e/sum) : Array(10).fill(0); | |
| return { probs, scaled }; | |
| }; | |
| const convolve = (input, kernels) => { | |
| const h = input.length, w = input[0].length, kSize = kernels[0].length; | |
| return kernels.map(k => { | |
| const dim = h - kSize + 1; | |
| const layer = createGrid(dim, dim); | |
| for(let y=0; y<dim; y++) for(let x=0; x<dim; x++) { | |
| let sum = 0; | |
| for(let ky=0; ky<kSize; ky++) for(let kx=0; kx<kSize; kx++) sum += input[y+ky][x+kx] * k[ky][kx]; | |
| layer[y][x] = Math.max(0, sum); // ReLU | |
| } | |
| return layer; | |
| }); | |
| }; | |
| const maxPool = (inputs) => inputs.map(layer => { | |
| const h = layer.length, w = layer[0].length; | |
| const out = createGrid(Math.floor(w/2), Math.floor(h/2)); | |
| for(let y=0; y<out.length; y++) for(let x=0; x<out[0].length; x++) | |
| out[y][x] = Math.max(layer[y*2][x*2], layer[y*2][x*2+1], layer[y*2+1][x*2], layer[y*2+1][x*2+1]); | |
| return out; | |
| }); | |
| // --- 3. AUDIO ENGINE --- | |
| class AudioEngine { | |
| constructor() { | |
| this.ctx = null; | |
| this.master = null; | |
| } | |
| init() { | |
| if(this.ctx) return; | |
| const Ctx = window.AudioContext || window.webkitAudioContext; | |
| this.ctx = new Ctx(); | |
| this.master = this.ctx.createGain(); | |
| this.master.gain.value = 0.1; | |
| this.master.connect(this.ctx.destination); | |
| } | |
| playTone(freq, type, dur) { | |
| if(!this.ctx) return; | |
| if(this.ctx.state === 'suspended') this.ctx.resume(); | |
| const osc = this.ctx.createOscillator(); | |
| const gain = this.ctx.createGain(); | |
| osc.type = type; | |
| osc.frequency.setValueAtTime(freq, this.ctx.currentTime); | |
| gain.gain.setValueAtTime(0, this.ctx.currentTime); | |
| gain.gain.linearRampToValueAtTime(1, this.ctx.currentTime + 0.01); | |
| gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + dur); | |
| osc.connect(gain); | |
| gain.connect(this.master); | |
| osc.start(); | |
| osc.stop(this.ctx.currentTime + dur); | |
| } | |
| playStep(step) { | |
| if(step===0) this.playTone(400, 'sine', 0.1); | |
| if(step===1) this.playTone(150, 'sawtooth', 0.1); | |
| if(step===2) this.playTone(200, 'square', 0.1); | |
| if(step===3) this.playTone(300, 'sawtooth', 0.1); | |
| if(step===4) this.playTone(600, 'square', 0.1); | |
| if(step===5) this.playTone(800, 'triangle', 0.1); | |
| if(step===6) { this.playTone(440, 'sine', 0.3); setTimeout(()=>this.playTone(660,'sine',0.3), 100); } | |
| } | |
| } | |
| const audio = new AudioEngine(); | |
| // --- 4. 3D COMPONENTS --- | |
| const VoxelLayer = ({ config, data, active }) => { | |
| const meshRef = useRef(); | |
| const dummy = useMemo(() => new THREE.Object3D(), []); | |
| const color = useMemo(() => new THREE.Color(), []); | |
| // Calculate total instances needed | |
| const count = config.type === 'fc' ? 10 : (config.type === 'flatten' ? 128 : config.width * config.height * config.depth); | |
| useLayoutEffect(() => { | |
| if(!meshRef.current) return; | |
| let idx = 0; | |
| // --- VISUALIZATION LOGIC --- | |
| if(config.type === 'fc') { | |
| // Output bars | |
| for(let i=0; i<10; i++) { | |
| const val = data[0]?.[i]?.[0] || 0; | |
| dummy.position.set(0, (4.5 - i) * 0.7, 0); | |
| dummy.scale.set(val > 0.01 ? 0.5 + val * 4 : 0.5, 0.5, 0.5); | |
| dummy.updateMatrix(); | |
| meshRef.current.setMatrixAt(idx, dummy.matrix); | |
| if(active && val > 0.01) color.setHSL(0.33, 1, 0.5); | |
| else color.setHex(0x002200); | |
| meshRef.current.setColorAt(idx, color); | |
| idx++; | |
| } | |
| } else if (config.type === 'flatten') { | |
| // Flatten grid representation | |
| const flatData = []; | |
| if(data.length) data.forEach(slice => slice.forEach(row => row.forEach(v => flatData.push(v)))); | |
| for(let y=0; y<8; y++) { | |
| for(let x=0; x<16; x++) { | |
| const val = flatData[idx] || 0; | |
| dummy.position.set((x-8)*0.25, (y-4)*0.25, 0); | |
| if(val > 0.1) { | |
| dummy.scale.set(0.9, 0.9, 0.9); | |
| color.setHSL(0.35, 1, 0.2 + val*0.8); | |
| } else { | |
| dummy.scale.set(0,0,0); | |
| } | |
| dummy.updateMatrix(); | |
| meshRef.current.setMatrixAt(idx, dummy.matrix); | |
| meshRef.current.setColorAt(idx, color); | |
| idx++; | |
| } | |
| } | |
| } else { | |
| // Convolutional Volumes | |
| const gap = 0.3; | |
| const layerW = config.width * 0.2; | |
| const totalW = (layerW + gap) * config.depth; | |
| const startX = -totalW / 2; | |
| for(let z=0; z<config.depth; z++) { | |
| const zOff = startX + z * (layerW + gap); | |
| for(let y=0; y<config.height; y++) { | |
| for(let x=0; x<config.width; x++) { | |
| const val = data[z]?.[config.height-1-y]?.[x] || 0; | |
| dummy.position.set(zOff + x*0.2, (y - config.height/2)*0.2, 0); | |
| if(val > 0.1) { | |
| dummy.scale.set(0.9,0.9,0.9); | |
| color.setHSL(0.35, 1, 0.2 + val*0.8); | |
| } else { | |
| dummy.scale.set(0,0,0); | |
| } | |
| dummy.updateMatrix(); | |
| meshRef.current.setMatrixAt(idx, dummy.matrix); | |
| meshRef.current.setColorAt(idx, color); | |
| idx++; | |
| } | |
| } | |
| } | |
| } | |
| meshRef.current.instanceMatrix.needsUpdate = true; | |
| if(meshRef.current.instanceColor) meshRef.current.instanceColor.needsUpdate = true; | |
| }, [config, data, active]); | |
| return ( | |
| <group position={[0, 0, config.z]}> | |
| <Text position={[0, config.height * 0.12 + 2, 0]} fontSize={0.6} color={active?"#fff":"#004400"}> | |
| {config.label} | |
| </Text> | |
| <instancedMesh ref={meshRef} args={[undefined, undefined, count]}> | |
| <boxGeometry args={[0.2, 0.2, 0.2]} /> | |
| <meshStandardMaterial color="#0f0" transparent opacity={0.9} blending={THREE.AdditiveBlending} toneMapped={false} /> | |
| </instancedMesh> | |
| {config.type === 'fc' && Array.from({length:10}).map((_, i) => ( | |
| <group key={i} position={[0, (4.5 - i) * 0.7, 0]}> | |
| <Text position={[-1.5, 0, 0]} fontSize={0.4} color="#0f0">{i}</Text> | |
| <Text position={[4, 0, 0]} fontSize={0.4} color="#fff"> | |
| {((data[0]?.[i]?.[0] || 0) * 100).toFixed(1)}% | |
| </Text> | |
| </group> | |
| ))} | |
| </group> | |
| ); | |
| }; | |
| const CameraController = ({ step }) => { | |
| useFrame((state) => { | |
| const targetPos = new THREE.Vector3(); | |
| const targetLook = new THREE.Vector3(); | |
| if(step === -1) { | |
| targetPos.set(25, 10, 5); | |
| targetLook.set(0, 0, -35); | |
| } else { | |
| const zMap = {0:0, 1:-15, 2:-25, 3:-35, 4:-45, 5:-52, 6:-62}; | |
| const z = zMap[step] || 0; | |
| targetPos.set(18, 5, z + 8); | |
| targetLook.set(0, 0, z); | |
| } | |
| state.camera.position.lerp(targetPos, 0.05); | |
| const look = new THREE.Vector3(0,0,-1).applyQuaternion(state.camera.quaternion).add(state.camera.position); | |
| look.lerp(targetLook, 0.05); | |
| state.camera.lookAt(look); | |
| }); | |
| return null; | |
| }; | |
| // --- 5. DRAWING PAD --- | |
| const DrawingPad = ({ data, onChange, disabled }) => { | |
| const canvasRef = useRef(null); | |
| const [isDrawing, setIsDrawing] = useState(false); | |
| useEffect(() => { | |
| const ctx = canvasRef.current?.getContext('2d'); | |
| if(ctx && data.every(r=>r.every(v=>v===0))) { | |
| ctx.fillStyle = 'black'; ctx.fillRect(0,0,280,280); | |
| // Draw Grid | |
| ctx.strokeStyle = '#002200'; ctx.lineWidth = 1; | |
| ctx.beginPath(); | |
| for(let i=0; i<=280; i+=28) { ctx.moveTo(i,0); ctx.lineTo(i,280); ctx.moveTo(0,i); ctx.lineTo(280,i); } | |
| ctx.stroke(); | |
| } | |
| }, [data]); | |
| const getPos = (e) => { | |
| const r = canvasRef.current.getBoundingClientRect(); | |
| const x = (e.touches?e.touches[0].clientX:e.clientX) - r.left; | |
| const y = (e.touches?e.touches[0].clientY:e.clientY) - r.top; | |
| const scaleX = canvasRef.current.width / r.width; | |
| const scaleY = canvasRef.current.height / r.height; | |
| return { x: x*scaleX, y: y*scaleY }; | |
| }; | |
| const draw = (e) => { | |
| if(disabled || !isDrawing) return; | |
| const ctx = canvasRef.current.getContext('2d'); | |
| const {x,y} = getPos(e); | |
| ctx.strokeStyle = '#0f0'; | |
| ctx.lineWidth = 25; | |
| ctx.lineCap = 'round'; | |
| ctx.shadowBlur = 10; ctx.shadowColor = '#0f0'; | |
| ctx.lineTo(x,y); ctx.stroke(); | |
| ctx.beginPath(); ctx.moveTo(x,y); | |
| }; | |
| const start = (e) => { | |
| if(disabled) return; | |
| setIsDrawing(true); | |
| const {x,y} = getPos(e); | |
| const ctx = canvasRef.current.getContext('2d'); | |
| ctx.beginPath(); ctx.moveTo(x,y); | |
| }; | |
| const end = () => { | |
| if(!isDrawing) return; | |
| setIsDrawing(false); | |
| const ctx = canvasRef.current.getContext('2d'); | |
| const temp = document.createElement('canvas'); temp.width=28; temp.height=28; | |
| temp.getContext('2d').drawImage(canvasRef.current,0,0,28,28); | |
| const img = temp.getContext('2d').getImageData(0,0,28,28).data; | |
| const grid = createGrid(28,28); | |
| for(let i=0; i<28*28; i++) grid[Math.floor(i/28)][i%28] = img[i*4+1]/255; | |
| onChange(grid); | |
| }; | |
| return ( | |
| <canvas ref={canvasRef} width={280} height={280} | |
| className={`w-[220px] h-[220px] rounded border border-green-800 bg-black cursor-crosshair ${disabled ? 'opacity-50 pointer-events-none' : ''}`} | |
| onMouseDown={start} onMouseMove={draw} onMouseUp={end} onMouseLeave={()=>setIsDrawing(false)} | |
| onTouchStart={start} onTouchMove={draw} onTouchEnd={end} | |
| /> | |
| ); | |
| }; | |
| // --- 6. MAIN APP --- | |
| const App = () => { | |
| const [activations, setActivations] = useState({}); | |
| const [step, setStep] = useState(-1); | |
| const [processing, setProcessing] = useState(false); | |
| const [inputData, setInputData] = useState(createGrid(28,28)); | |
| useEffect(() => { | |
| trainBrain(); | |
| reset(); | |
| audio.init(); | |
| }, []); | |
| const reset = () => { | |
| setStep(-1); | |
| setActivations(ARCHITECTURE.reduce((acc,l) => ({...acc, [l.id]: []}), {})); | |
| setInputData(createGrid(28,28)); | |
| }; | |
| const delay = ms => new Promise(r => setTimeout(r, ms)); | |
| const run = async () => { | |
| if(processing) return; | |
| setProcessing(true); | |
| // Inference | |
| const { probs, scaled } = predict(inputData); | |
| // Pipeline Animation | |
| setActivations(prev => ({...prev, input: [scaled]})); | |
| setStep(0); audio.playStep(0); await delay(600); | |
| const k1 = [[[1,1,1],[0,0,0],[-1,-1,-1]], [[1,0,-1],[1,0,-1],[1,0,-1]], [[0,-1,0],[-1,4,-1],[0,-1,0]], [[-1,-1,-1],[-1,8,-1],[-1,-1,-1]]]; | |
| const c1 = convolve(scaled, k1); | |
| setActivations(prev => ({...prev, conv1: c1})); | |
| setStep(1); audio.playStep(1); await delay(800); | |
| const p1 = maxPool(c1); | |
| setActivations(prev => ({...prev, pool1: p1})); | |
| setStep(2); audio.playStep(2); await delay(800); | |
| const c2 = p1.map(layer => convolve(layer, [[[0.5,0.5],[0.5,0.5]]])[0]).concat(p1.map(l => l)); // Fake doubling depth | |
| setActivations(prev => ({...prev, conv2: c2})); | |
| setStep(3); audio.playStep(3); await delay(800); | |
| const p2 = maxPool(c2); | |
| setActivations(prev => ({...prev, pool2: p2})); | |
| setStep(4); audio.playStep(4); await delay(800); | |
| setActivations(prev => ({...prev, flat: p2})); | |
| setStep(5); audio.playStep(5); await delay(600); | |
| setActivations(prev => ({...prev, fc: [probs.map(p=>[p])]})); | |
| setStep(6); audio.playStep(6); | |
| await delay(3000); | |
| setProcessing(false); | |
| setStep(-1); | |
| }; | |
| return ( | |
| <div className="w-full h-screen relative bg-black font-mono"> | |
| <Canvas shadows camera={{ position: [25, 10, 5], fov: 45 }}> | |
| <CameraController step={step} /> | |
| <color attach="background" args={['#000200']} /> | |
| <fog attach="fog" args={['#000200', 20, 90]} /> | |
| <ambientLight intensity={0.2} /> | |
| <pointLight position={[10, 20, 10]} intensity={1.5} color="#00ff00" distance={50} /> | |
| <group> | |
| {ARCHITECTURE.map((cfg, i) => ( | |
| <VoxelLayer key={cfg.id} config={cfg} data={activations[cfg.id] || []} active={step===i} /> | |
| ))} | |
| <Grid args={[200, 200]} cellSize={1} cellThickness={1} sectionSize={5} sectionThickness={1.5} fadeDistance={60} sectionColor="#004400" cellColor="#001100" position={[0, -5, -30]} /> | |
| </group> | |
| <Stars radius={100} depth={50} count={3000} factor={4} saturation={0} fade speed={1} /> | |
| <Environment preset="city" /> | |
| </Canvas> | |
| {/* HUD UI */} | |
| <div className="absolute inset-0 pointer-events-none flex flex-col justify-between p-4 z-10"> | |
| {/* Header */} | |
| <div className="flex justify-between items-start"> | |
| <div className="hud-panel p-4 rounded-br-2xl border-l-4 border-l-green-500"> | |
| <h1 className="text-2xl md:text-4xl font-black tracking-tighter neon-text flex items-center gap-3"> | |
| <Cpu className="text-neon-green animate-pulse" /> DEEP <span className="text-neon-green">CNN</span> | |
| </h1> | |
| <div className="text-xs text-green-400 mt-2 flex items-center gap-2"> | |
| <Activity size={12} /> SYSTEM: {processing ? "PROCESSING TENSORS..." : "ONLINE"} | |
| </div> | |
| </div> | |
| </div> | |
| {/* Controls */} | |
| <div className="flex flex-col md:flex-row items-end gap-6 pointer-events-auto"> | |
| <div className="hud-panel p-4 rounded-tr-2xl backdrop-blur-xl max-w-sm"> | |
| <div className="flex justify-between items-center mb-2 text-green-400"> | |
| <div className="text-xs font-bold tracking-widest flex items-center gap-2"> | |
| <Scan size={14} /> INPUT SENSOR | |
| </div> | |
| <button onClick={reset} disabled={processing} className="hover:text-white transition-colors"> | |
| <RotateCcw size={16} /> | |
| </button> | |
| </div> | |
| <DrawingPad data={inputData} onChange={setInputData} disabled={processing} /> | |
| <button onClick={run} disabled={processing} className="w-full mt-4 py-3 rounded btn-holo flex justify-center items-center gap-2 font-bold transition-all"> | |
| {processing ? <Activity className="animate-spin" size={18} /> : <Play size={18} fill="currentColor" />} | |
| {processing ? 'CALCULATING...' : 'RUN INFERENCE'} | |
| </button> | |
| </div> | |
| {/* Pipeline Status */} | |
| <div className="hud-panel p-5 hidden md:block rounded-t-xl min-w-[260px] border-b-0"> | |
| <div className="text-xs text-green-500 font-bold mb-3 flex items-center gap-2"> | |
| <Layers size={14} /> PIPELINE STATUS | |
| </div> | |
| <div className="space-y-2"> | |
| {ARCHITECTURE.map((l, i) => ( | |
| <div key={l.id} className={`flex items-center gap-3 text-xs transition-all duration-300 ${step===i ? 'text-white translate-x-2' : 'text-green-900'}`}> | |
| <div className={`w-2 h-2 rounded-sm ${step===i ? 'bg-neon-green shadow-[0_0_8px_#0f0]' : 'bg-green-900'}`} /> | |
| <span className={step===i ? 'font-bold' : ''}>{l.label}</span> | |
| {step===i && <Zap size={10} className="ml-auto text-yellow-400 animate-pulse" />} | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| const root = createRoot(document.getElementById('root')); | |
| root.render(<App />); | |
| </script> | |
| </body> | |
| </html> |