class ASCII3DViewer { constructor() { this.scene = new THREE.Scene(); this.camera = new THREE.PerspectiveCamera(75, 1, 0.1, 1000); this.renderer = new THREE.WebGLRenderer(); this.asciiCanvas = document.getElementById('ascii-canvas'); this.context = this.asciiCanvas.getContext('2d'); this.currentModel = null; this.autoRotate = true; this.resolution = 50; this.charSet = '@%#*+=-:. '; this.colorMode = 'mono'; this.init(); this.setupEventListeners(); this.loadModel('cube'); this.animate(); } init() { this.camera.position.z = 5; this.scene.background = new THREE.Color(0x000000); // Add some ambient light const ambientLight = new THREE.AmbientLight(0x404040); this.scene.add(ambientLight); // Add directional light const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5); directionalLight.position.set(1, 1, 1); this.scene.add(directionalLight); this.resize(); window.addEventListener('resize', () => this.resize()); } resize() { const container = this.asciiCanvas.parentElement; const width = container.clientWidth; const height = container.clientHeight; this.asciiCanvas.width = width; this.asciiCanvas.height = height; this.camera.aspect = width / height; this.camera.updateProjectionMatrix(); } loadModel(modelType) { // Remove current model if exists if (this.currentModel) { this.scene.remove(this.currentModel); } let geometry; switch(modelType) { case 'sphere': geometry = new THREE.SphereGeometry(1, 32, 32); break; case 'torus': geometry = new THREE.TorusGeometry(1, 0.4, 16, 100); break; case 'teapot': // Simple teapot approximation geometry = new THREE.CylinderGeometry(0.5, 0.8, 1, 8); const lid = new THREE.CylinderGeometry(0.3, 0.5, 0.3, 8); lid.translate(0, 0.8, 0); geometry = THREE.BufferGeometryUtils.mergeBufferGeometries([geometry, lid]); break; case 'cube': default: geometry = new THREE.BoxGeometry(1, 1, 1); break; } const material = new THREE.MeshPhongMaterial({ color: 0xffffff, flatShading: true }); this.currentModel = new THREE.Mesh(geometry, material); this.scene.add(this.currentModel); document.getElementById('loading').style.display = 'none'; } renderToASCII() { const width = this.asciiCanvas.width; const height = this.asciiCanvas.height; // Temporary render to get pixel data this.renderer.setSize(width, height); this.renderer.render(this.scene, this.camera); // Create offscreen buffer const buffer = document.createElement('canvas'); buffer.width = width; buffer.height = height; const bufferCtx = buffer.getContext('2d'); bufferCtx.drawImage(this.renderer.domElement, 0, 0, width, height); const imageData = bufferCtx.getImageData(0, 0, width, height); const pixels = imageData.data; // Clear canvas this.context.fillStyle = '#000'; this.context.fillRect(0, 0, width, height); // Calculate step based on resolution const step = Math.max(1, Math.floor(100 / this.resolution)); const chars = this.charSet; const charCount = chars.length; // Render ASCII art this.context.font = `${step}px 'Courier New', monospace`; this.context.textAlign = 'center'; for (let y = 0; y < height; y += step * 2) { for (let x = 0; x < width; x += step) { const index = (y * width + x) * 4; const r = pixels[index]; const g = pixels[index + 1]; const b = pixels[index + 2]; if (r + g + b > 10) { // Skip very dark pixels const brightness = (r + g + b) / 3 / 255; const charIndex = Math.floor(brightness * (charCount - 1)); const char = chars[charIndex]; // Set color based on mode if (this.colorMode === 'color') { this.context.fillStyle = `rgb(${r},${g},${b})`; } else if (this.colorMode === 'grayscale') { const gray = (r + g + b) / 3; this.context.fillStyle = `rgb(${gray},${gray},${gray})`; } else { this.context.fillStyle = '#fff'; } this.context.fillText(char, x, y); } } } } animate() { requestAnimationFrame(() => this.animate()); if (this.autoRotate && this.currentModel) { this.currentModel.rotation.x += 0.01; this.currentModel.rotation.y += 0.01; } this.renderToASCII(); } setupEventListeners() { // Model selection document.getElementById('model-select').addEventListener('change', (e) => { document.getElementById('loading').style.display = 'block'; setTimeout(() => this.loadModel(e.target.value), 100); }); // Resolution slider const resolutionSlider = document.getElementById('resolution'); const resolutionValue = document.getElementById('resolution-value'); resolutionSlider.addEventListener('input', (e) => { this.resolution = parseInt(e.target.value); resolutionValue.textContent = this.resolution; }); // Character set document.getElementById('charset').addEventListener('change', (e) => { const sets = { 'default': '@%#*+=-:. ', 'dense': '█▓▒░ ', 'simple': '#*+-. ', 'numbers': '9876543210' }; this.charSet = sets[e.target.value]; }); // Color mode document.getElementById('color-mode').addEventListener('change', (e) => { this.colorMode = e.target.value; }); // Auto rotation document.getElementById('auto-rotate').addEventListener('change', (e) => { this.autoRotate = e.target.checked; }); // Reset camera document.getElementById('reset-camera').addEventListener('click', () => { if (this.currentModel) { this.currentModel.rotation.set(0, 0, 0); } }); // Mouse controls for manual rotation let isDragging = false; let previousMousePosition = {x: 0, y: 0}; this.asciiCanvas.addEventListener('mousedown', (e) => { isDragging = true; }); this.asciiCanvas.addEventListener('mouseup', () => { isDragging = false; }); this.asciiCanvas.addEventListener('mousemove', (e) => { if (!isDragging || !this.currentModel) return; const deltaMove = { x: e.offsetX - previousMousePosition.x, y: e.offsetY - previousMousePosition.y }; this.currentModel.rotation.y += deltaMove.x * 0.01; this.currentModel.rotation.x += deltaMove.y * 0.01; previousMousePosition = { x: e.offsetX, y: e.offsetY }; }); this.asciiCanvas.addEventListener('mouseleave', () => { isDragging = false; }); } } // Initialize the viewer when the page loads window.addEventListener('load', () => { new ASCII3DViewer(); }); // Polyfill for BufferGeometryUtils if needed if (!THREE.BufferGeometryUtils) { console.warn('THREE.BufferGeometryUtils not available. Some models may not work correctly.'); THREE.BufferGeometryUtils = { mergeBufferGeometries: (geometries) => geometries[0] }; }