Last active
October 1, 2025 01:09
-
-
Save lardratboy/5489c7324463235b1f31c9fae1e0cdcd to your computer and use it in GitHub Desktop.
grayscale binary plane viewer
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <title>Binary Bit Viewer</title> | |
| <style> | |
| body { | |
| margin: 0; | |
| font-family: Arial, sans-serif; | |
| } | |
| canvas { | |
| width: 100%; | |
| height: 100%; | |
| } | |
| #dropZone { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| z-index: 1000; | |
| display: none; | |
| background: rgba(0, 0, 0, 0.7); | |
| color: white; | |
| justify-content: center; | |
| align-items: center; | |
| font-size: 24px; | |
| } | |
| #instructions { | |
| position: fixed; | |
| bottom: 20px; | |
| left: 20px; | |
| color: white; | |
| background: rgba(0, 0, 0, 0.5); | |
| padding: 10px; | |
| border-radius: 5px; | |
| } | |
| #stats { | |
| position: fixed; | |
| top: 20px; | |
| right: 20px; | |
| color: white; | |
| background: rgba(0, 0, 0, 0.5); | |
| padding: 15px; | |
| border-radius: 5px; | |
| font-family: monospace; | |
| } | |
| #fileInput { | |
| position: fixed; | |
| top: 20px; | |
| left: 20px; | |
| z-index: 100; | |
| background: white; | |
| padding: 8px; | |
| border-radius: 5px; | |
| } | |
| #exportButton { | |
| position: fixed; | |
| bottom: 20px; | |
| right: 20px; | |
| background: #44cc44; | |
| border: none; | |
| color: white; | |
| padding: 12px 20px; | |
| border-radius: 5px; | |
| cursor: pointer; | |
| font-size: 14px; | |
| } | |
| #exportButton:hover { | |
| background: #66dd66; | |
| } | |
| #exportButton:disabled { | |
| background: #666; | |
| cursor: not-allowed; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="dropZone">Drop binary file here</div> | |
| <div id="instructions"> | |
| Drag and drop a file to view bytes as grayscale points with bit satellites<br> | |
| Left click + drag to rotate | Mouse wheel to zoom | |
| </div> | |
| <input type="file" id="fileInput" /> | |
| <div id="stats"> | |
| Bytes: 0<br> | |
| Particles: 0<br> | |
| File: none | |
| </div> | |
| <button id="exportButton" disabled>Export as PLY</button> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/three@0.128/examples/js/controls/OrbitControls.js"></script> | |
| <script> | |
| let scene, camera, renderer, controls; | |
| let pointCloud; | |
| let currentFileName = 'none'; | |
| let byteData = null; | |
| function init() { | |
| scene = new THREE.Scene(); | |
| scene.background = new THREE.Color(0x000000); | |
| camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); | |
| camera.position.set(2, 2, 2); | |
| camera.lookAt(0, 0, 0); | |
| renderer = new THREE.WebGLRenderer({ antialias: false }); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| document.body.appendChild(renderer.domElement); | |
| controls = new THREE.OrbitControls(camera, renderer.domElement); | |
| setupDropZone(); | |
| setupFileInput(); | |
| setupExportButton(); | |
| animate(); | |
| } | |
| function setupDropZone() { | |
| const dropZone = document.getElementById('dropZone'); | |
| document.addEventListener('dragenter', (e) => { | |
| e.preventDefault(); | |
| dropZone.style.display = 'flex'; | |
| }); | |
| dropZone.addEventListener('dragleave', (e) => { | |
| e.preventDefault(); | |
| dropZone.style.display = 'none'; | |
| }); | |
| dropZone.addEventListener('dragover', (e) => { | |
| e.preventDefault(); | |
| }); | |
| dropZone.addEventListener('drop', (e) => { | |
| e.preventDefault(); | |
| dropZone.style.display = 'none'; | |
| const files = e.dataTransfer.files; | |
| if (files.length > 0) { | |
| handleFile(files[0]); | |
| } | |
| }); | |
| } | |
| function setupFileInput() { | |
| document.getElementById('fileInput').addEventListener('change', function(e) { | |
| if (e.target.files.length > 0) { | |
| handleFile(e.target.files[0]); | |
| } | |
| }); | |
| } | |
| function setupExportButton() { | |
| document.getElementById('exportButton').addEventListener('click', exportAsPLY); | |
| } | |
| function handleFile(file) { | |
| currentFileName = file.name; | |
| const reader = new FileReader(); | |
| reader.onload = function(e) { | |
| const buffer = e.target.result; | |
| const bytes = new Uint8Array(buffer); | |
| createBytePointCloud(bytes); | |
| }; | |
| reader.readAsArrayBuffer(file); | |
| } | |
| function updateStats(byteCount, particleCount, fileName) { | |
| const stats = document.getElementById('stats'); | |
| stats.innerHTML = `Bytes: ${byteCount.toLocaleString()}<br>Particles: ${particleCount.toLocaleString()}<br>File: ${fileName}`; | |
| // Enable export button | |
| document.getElementById('exportButton').disabled = false; | |
| } | |
| function createBytePointCloud(bytes) { | |
| byteData = bytes; | |
| const positions = []; | |
| const colors = []; | |
| let particleCount = 0; | |
| // Bit satellite positions (8 bits arranged around center) | |
| const bitPositions = [ | |
| [-1, 1, 0], // bit 0 | |
| [0, 1, 0], // bit 1 | |
| [1, 1, 0], // bit 2 | |
| [-1, 0, 0], // bit 3 | |
| [1, 0, 0], // bit 4 | |
| [-1, -1, 0], // bit 5 | |
| [0, -1, 0], // bit 6 | |
| [1, -1, 0] // bit 7 | |
| ]; | |
| const gridSize = Math.ceil(Math.sqrt(bytes.length)); | |
| const spacing = 0.3; | |
| for (let i = 0; i < bytes.length; i++) { | |
| const byte = bytes[i]; | |
| const row = Math.floor(i / gridSize); | |
| const col = i % gridSize; | |
| const gray = byte / 255; | |
| const baseX = (col - gridSize/2) * spacing; | |
| const baseY = (row - gridSize/2) * spacing; | |
| // Add center point (the byte value) | |
| positions.push(baseX, baseY, 0); | |
| colors.push(gray, gray, gray); | |
| particleCount++; | |
| // Add bit satellites (only for set bits) | |
| for (let bit = 0; bit < 8; bit++) { | |
| if ((byte & (1 << bit)) !== 0) { | |
| positions.push( | |
| bitPositions[bit][0] * 0.1 + baseX, | |
| bitPositions[bit][1] * 0.1 + baseY, | |
| bitPositions[bit][2] * 0.1 | |
| ); | |
| colors.push(gray, gray, gray); | |
| particleCount++; | |
| } | |
| } | |
| } | |
| updateStats(bytes.length, particleCount, currentFileName); | |
| // Remove old point cloud if exists | |
| if (pointCloud) { | |
| scene.remove(pointCloud); | |
| } | |
| const geometry = new THREE.BufferGeometry(); | |
| geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3)); | |
| geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3)); | |
| const material = new THREE.PointsMaterial({ | |
| size: 0.05, | |
| vertexColors: true | |
| }); | |
| pointCloud = new THREE.Points(geometry, material); | |
| scene.add(pointCloud); | |
| // Adjust camera to fit the data | |
| const box = new THREE.Box3().setFromObject(pointCloud); | |
| const boundingSphere = new THREE.Sphere(); | |
| box.getBoundingSphere(boundingSphere); | |
| const radius = boundingSphere.radius * 2.5; | |
| camera.position.set(0, 0, radius); | |
| camera.lookAt(boundingSphere.center); | |
| } | |
| function exportAsPLY() { | |
| if (!pointCloud) { | |
| console.log('No point cloud to export'); | |
| return; | |
| } | |
| const positions = pointCloud.geometry.getAttribute('position'); | |
| const colors = pointCloud.geometry.getAttribute('color'); | |
| // Header | |
| let ply = 'ply\n'; | |
| ply += 'format ascii 1.0\n'; | |
| ply += `element vertex ${positions.count}\n`; | |
| ply += 'property float x\n'; | |
| ply += 'property float y\n'; | |
| ply += 'property float z\n'; | |
| ply += 'property uchar red\n'; | |
| ply += 'property uchar green\n'; | |
| ply += 'property uchar blue\n'; | |
| ply += 'end_header\n'; | |
| // Vertex data | |
| for (let i = 0; i < positions.count; i++) { | |
| const x = positions.getX(i); | |
| const y = positions.getY(i); | |
| const z = positions.getZ(i); | |
| const r = Math.floor(colors.getX(i) * 255); | |
| const g = Math.floor(colors.getY(i) * 255); | |
| const b = Math.floor(colors.getZ(i) * 255); | |
| ply += `${x} ${y} ${z} ${r} ${g} ${b}\n`; | |
| } | |
| // Download | |
| const blob = new Blob([ply], { type: 'application/octet-stream' }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = currentFileName.replace(/\.[^/.]+$/, "") + "_bits.ply"; | |
| document.body.appendChild(a); | |
| a.click(); | |
| document.body.removeChild(a); | |
| URL.revokeObjectURL(url); | |
| } | |
| function animate() { | |
| requestAnimationFrame(animate); | |
| if (controls) controls.update(); | |
| renderer.render(scene, camera); | |
| } | |
| window.addEventListener('resize', () => { | |
| camera.aspect = window.innerWidth / window.innerHeight; | |
| camera.updateProjectionMatrix(); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| }); | |
| init(); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment