|
|
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); |
|
|
|
|
|
|
|
|
const ambientLight = new THREE.AmbientLight(0x404040); |
|
|
this.scene.add(ambientLight); |
|
|
|
|
|
|
|
|
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) { |
|
|
|
|
|
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': |
|
|
|
|
|
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; |
|
|
|
|
|
|
|
|
this.renderer.setSize(width, height); |
|
|
this.renderer.render(this.scene, this.camera); |
|
|
|
|
|
|
|
|
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; |
|
|
|
|
|
|
|
|
this.context.fillStyle = '#000'; |
|
|
this.context.fillRect(0, 0, width, height); |
|
|
|
|
|
|
|
|
const step = Math.max(1, Math.floor(100 / this.resolution)); |
|
|
const chars = this.charSet; |
|
|
const charCount = chars.length; |
|
|
|
|
|
|
|
|
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) { |
|
|
const brightness = (r + g + b) / 3 / 255; |
|
|
const charIndex = Math.floor(brightness * (charCount - 1)); |
|
|
const char = chars[charIndex]; |
|
|
|
|
|
|
|
|
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() { |
|
|
|
|
|
document.getElementById('model-select').addEventListener('change', (e) => { |
|
|
document.getElementById('loading').style.display = 'block'; |
|
|
setTimeout(() => this.loadModel(e.target.value), 100); |
|
|
}); |
|
|
|
|
|
|
|
|
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; |
|
|
}); |
|
|
|
|
|
|
|
|
document.getElementById('charset').addEventListener('change', (e) => { |
|
|
const sets = { |
|
|
'default': '@%#*+=-:. ', |
|
|
'dense': '█▓▒░ ', |
|
|
'simple': '#*+-. ', |
|
|
'numbers': '9876543210' |
|
|
}; |
|
|
this.charSet = sets[e.target.value]; |
|
|
}); |
|
|
|
|
|
|
|
|
document.getElementById('color-mode').addEventListener('change', (e) => { |
|
|
this.colorMode = e.target.value; |
|
|
}); |
|
|
|
|
|
|
|
|
document.getElementById('auto-rotate').addEventListener('change', (e) => { |
|
|
this.autoRotate = e.target.checked; |
|
|
}); |
|
|
|
|
|
|
|
|
document.getElementById('reset-camera').addEventListener('click', () => { |
|
|
if (this.currentModel) { |
|
|
this.currentModel.rotation.set(0, 0, 0); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
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; |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
window.addEventListener('load', () => { |
|
|
new ASCII3DViewer(); |
|
|
}); |
|
|
|
|
|
|
|
|
if (!THREE.BufferGeometryUtils) { |
|
|
console.warn('THREE.BufferGeometryUtils not available. Some models may not work correctly.'); |
|
|
THREE.BufferGeometryUtils = { |
|
|
mergeBufferGeometries: (geometries) => geometries[0] |
|
|
}; |
|
|
} |