Created
February 2, 2025 22:02
-
-
Save lardratboy/f6f52c15da926ca23bb7421e9f7e0a2a to your computer and use it in GitHub Desktop.
Tool to view bytes as points supports images and binary data
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
<head> | |
<title>B2P - Bytes To Points</title> | |
<style> | |
body { | |
margin: 0; | |
} | |
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-family: Arial, sans-serif; | |
font-size: 24px; | |
} | |
#instructions { | |
position: fixed; | |
bottom: 20px; | |
left: 20px; | |
color: white; | |
font-family: Arial, sans-serif; | |
background: rgba(0, 0, 0, 0.5); | |
padding: 10px; | |
border-radius: 5px; | |
} | |
#objectList { | |
position: fixed; | |
top: 20px; | |
right: 20px; | |
color: white; | |
font-family: Arial, sans-serif; | |
background: rgba(0, 0, 0, 0.5); | |
padding: 15px; | |
border-radius: 5px; | |
max-height: 80vh; | |
overflow-y: auto; | |
} | |
.object-item { | |
margin: 5px 0; | |
padding: 5px; | |
background: rgba(255, 255, 255, 0.1); | |
border-radius: 3px; | |
display: flex; | |
justify-content: space-between; | |
align-items: center; | |
gap: 10px; | |
} | |
.delete-btn { | |
background: #ff4444; | |
border: none; | |
color: white; | |
padding: 2px 6px; | |
border-radius: 3px; | |
cursor: pointer; | |
} | |
.delete-btn:hover { | |
background: #ff6666; | |
} | |
.visibility-btn { | |
background: none; | |
border: none; | |
color: white; | |
cursor: pointer; | |
padding: 2px 6px; | |
} | |
.object-info { | |
flex-grow: 1; | |
} | |
#gridControls { | |
position: fixed; | |
top: 20px; | |
left: 20px; | |
color: white; | |
font-family: Arial, sans-serif; | |
background: rgba(0, 0, 0, 0.5); | |
padding: 15px; | |
border-radius: 5px; | |
} | |
.grid-toggle { | |
margin: 5px 0; | |
cursor: pointer; | |
} | |
#fileInput { | |
position: absolute; | |
top: 10px; | |
left: 10px; | |
z-index: 100; | |
} | |
</style> | |
</head> | |
<body> | |
<div id="dropZone">Drop data or images here</div> | |
<div id="instructions"> | |
Drag and drop files/images to view bytes/rgb as points<br> | |
Mouse: Left click + drag to rotate<br> | |
Mouse wheel to zoom | |
</div> | |
<input type="file" id="fileInput" multiple /> | |
<div id="gridControls"> | |
<label class="grid-toggle"><input type="checkbox" checked onchange="toggleGrid('xy')"> XY Plane | |
(Blue)</label><br> | |
<label class="grid-toggle"><input type="checkbox" checked onchange="toggleGrid('xz')"> XZ Plane | |
(Red)</label><br> | |
<label class="grid-toggle"><input type="checkbox" checked onchange="toggleGrid('yz')"> YZ Plane (Green)</label> | |
</div> | |
<div id="objectList"> | |
<h3 style="margin-top: 0">Loaded Objects</h3> | |
<div id="objectItems"></div> | |
</div> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> | |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/controls/OrbitControls.js"></script> | |
<script> | |
let scene, camera, renderer, controls; | |
let pointClouds = new Map(); | |
let objectCounter = 0; | |
let grids = {}; | |
function init() { | |
scene = new THREE.Scene(); | |
scene.background = new THREE.Color(0x111111); | |
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); | |
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5); | |
scene.add(ambientLight); | |
setupGrids(); | |
setupDropZone(); | |
animate(); | |
} | |
function setupGrids() { | |
const gridSize = 2; // Reduced grid size to match normalized data | |
const gridDivisions = 4; // More divisions for finer reference | |
// XY Plane (Blue) | |
const gridXY = new THREE.GridHelper(gridSize, gridDivisions, 0x0000ff, 0x0000ff); | |
gridXY.rotation.x = Math.PI / 2; | |
scene.add(gridXY); | |
grids.xy = gridXY; | |
// XZ Plane (Red) | |
const gridXZ = new THREE.GridHelper(gridSize, gridDivisions, 0xff0000, 0xff0000); | |
scene.add(gridXZ); | |
grids.xz = gridXZ; | |
// YZ Plane (Green) | |
const gridYZ = new THREE.GridHelper(gridSize, gridDivisions, 0x00ff00, 0x00ff00); | |
gridYZ.rotation.z = Math.PI / 2; | |
scene.add(gridYZ); | |
grids.yz = gridYZ; | |
} | |
function toggleGrid(plane) { | |
if (grids[plane]) { | |
grids[plane].visible = !grids[plane].visible; | |
} | |
} | |
function removeAlphaChannel(canvas, data) { | |
const imagePixelCount = canvas.width * canvas.height; | |
const rgbOnlyBytes = new Uint8ClampedArray(imagePixelCount * 3); | |
let nextImageIdx = 0; | |
let nextRGBIdx = 0; | |
for (let i = 0; i < imagePixelCount; i++) { | |
rgbOnlyBytes[nextRGBIdx] = data[nextImageIdx]; | |
rgbOnlyBytes[nextRGBIdx + 1] = data[nextImageIdx + 1]; | |
rgbOnlyBytes[nextRGBIdx + 2] = data[nextImageIdx + 2]; | |
nextImageIdx += 4; | |
nextRGBIdx += 3; | |
} | |
return rgbOnlyBytes; | |
} | |
function handleFiles(files) { | |
for (const file of files) { | |
const reader = new FileReader(); | |
reader.onload = function (e) { | |
if (file.type.startsWith('image/')) { | |
const img = new Image(); | |
img.onload = function () { | |
const canvas = document.createElement('canvas'); | |
const ctx = canvas.getContext('2d'); | |
canvas.width = img.width; | |
canvas.height = img.height; | |
ctx.drawImage(img, 0, 0); | |
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); | |
createPointCloud(removeAlphaChannel(canvas, imageData.data), file.name); | |
}; | |
img.src = e.target.result; | |
} else { | |
const arrayBuffer = e.target.result; | |
const uint8Array = new Uint8Array(arrayBuffer); | |
createPointCloud(uint8Array, file.name); | |
} | |
}; | |
if (file.type.startsWith('image/')) { | |
reader.readAsDataURL(file); | |
} else { | |
reader.readAsArrayBuffer(file); | |
} | |
} | |
} | |
// Handle clipboard operations | |
document.addEventListener('paste', (e) => { | |
e.preventDefault(); | |
const items = (e.clipboardData || window.clipboardData).items; | |
for (let item of items) { | |
if (item.kind === 'file') { | |
const file = item.getAsFile(); | |
handleFiles([file]); | |
} | |
} | |
}); | |
async function tryFetchAPI(src) { | |
try { | |
const response = await fetch(src); | |
if (response.ok) { | |
handleFiles([await response.blob()]); | |
} | |
} catch (_) { | |
} | |
} | |
function handleNonFileDrop(text) { | |
if (text.startsWith('<meta')) { | |
try { | |
const parser = new DOMParser(); | |
const doc = parser.parseFromString(text, 'text/html'); | |
const imgSrc = doc.querySelector('img').getAttribute('src'); | |
tryFetchAPI(imgSrc); | |
} catch (_) { | |
} | |
} | |
} | |
function handleDrop(e) { | |
const dt = e.dataTransfer; | |
const file = dt.files[0]; | |
if (file) { | |
handleFiles(e.dataTransfer.files); | |
return; | |
} | |
for (let i = 0; i < dt.items.length; i++) { | |
const item = dt.items[i]; | |
if (!(item.kind === 'string')) continue; | |
item.getAsString((s) => { | |
handleNonFileDrop(s); | |
}); | |
} | |
} | |
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'; | |
handleDrop(e); | |
}); | |
} | |
function getUniqueTuples(data) { | |
const bitArraySizeInUint32 = Math.ceil((256 * 256 * 256) / 32); | |
const tupleBitArray = new Uint32Array(bitArraySizeInUint32); | |
const resultTuples = []; | |
for (let i = 0; i < data.length; i += 3) { | |
const r = data[i]; | |
const g = data[i + 1]; | |
const b = data[i + 2]; | |
const index = (r << 16) | (g << 8) | b; | |
const elementIndex = index >> 5; | |
const bitPosition = index & 0x1F; | |
const mask = 1 << bitPosition; | |
if ((tupleBitArray[elementIndex] & mask) === 0) { | |
tupleBitArray[elementIndex] |= mask; | |
resultTuples.push(r, g, b); | |
} | |
} | |
return resultTuples; | |
} | |
function createPointCloud(buffer, fileName) { | |
const rgbData = new Uint8Array(getUniqueTuples(buffer)); | |
const pointCount = Math.floor(rgbData.length / 3); | |
const vertices = new Float32Array(pointCount * 3); | |
for (let i = 0; i < pointCount; i++) { | |
const rgbIndex = i * 3; | |
const vertexIndex = i * 3; | |
// Convert RGB values to normalized coordinates (-1 to 1) | |
const x = (rgbData[rgbIndex] / 255) * 2 - 1; | |
const y = (rgbData[rgbIndex + 1] / 255) * 2 - 1; | |
const z = (rgbData[rgbIndex + 2] / 255) * 2 - 1; | |
vertices[vertexIndex] = x; | |
vertices[vertexIndex + 1] = y; | |
vertices[vertexIndex + 2] = z; | |
} | |
const geometry = new THREE.BufferGeometry(); | |
geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3)); | |
geometry.setAttribute('color', new THREE.BufferAttribute(rgbData, 3, true)); | |
const material = new THREE.PointsMaterial({ | |
size: 0.005, // Smaller point size for normalized data | |
vertexColors: true | |
}); | |
const points = new THREE.Points(geometry, material); | |
const id = `cloud_${objectCounter++}`; | |
pointClouds.set(id, { | |
object: points, | |
name: fileName, | |
pointCount: pointCount | |
}); | |
scene.add(points); | |
updateObjectList(); | |
} | |
function updateObjectList() { | |
const container = document.getElementById('objectItems'); | |
container.innerHTML = ''; | |
pointClouds.forEach((cloud, id) => { | |
const item = document.createElement('div'); | |
item.className = 'object-item'; | |
const info = document.createElement('div'); | |
info.className = 'object-info'; | |
info.textContent = `${cloud.name} (${cloud.pointCount.toLocaleString()} points)`; | |
const visibilityBtn = document.createElement('button'); | |
visibilityBtn.className = 'visibility-btn'; | |
visibilityBtn.innerHTML = cloud.object.visible ? '👁️' : '👁️🗨️'; | |
visibilityBtn.onclick = () => toggleVisibility(id); | |
const deleteBtn = document.createElement('button'); | |
deleteBtn.className = 'delete-btn'; | |
deleteBtn.textContent = '×'; | |
deleteBtn.onclick = () => deletePointCloud(id); | |
item.appendChild(visibilityBtn); | |
item.appendChild(info); | |
item.appendChild(deleteBtn); | |
container.appendChild(item); | |
}); | |
} | |
function toggleVisibility(id) { | |
const cloud = pointClouds.get(id); | |
if (cloud) { | |
cloud.object.visible = !cloud.object.visible; | |
updateObjectList(); | |
} | |
} | |
function deletePointCloud(id) { | |
const cloud = pointClouds.get(id); | |
if (cloud) { | |
scene.remove(cloud.object); | |
pointClouds.delete(id); | |
updateObjectList(); | |
} | |
} | |
function animate() { | |
requestAnimationFrame(animate); | |
if (controls) controls.update(); | |
renderer.render(scene, camera); | |
} | |
init(); | |
window.addEventListener('resize', () => { | |
camera.aspect = window.innerWidth / window.innerHeight; | |
camera.updateProjectionMatrix(); | |
renderer.setSize(window.innerWidth, window.innerHeight); | |
}); | |
const argument = new URL(document.URL).searchParams.get('fetch'); | |
if (argument) { | |
console.log(argument); | |
tryFetchAPI(argument); | |
} | |
// Handle file input | |
document.getElementById('fileInput').addEventListener('change', function (e) { | |
handleFiles(e.target.files); | |
}); | |
</script> | |
</body> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment