Skip to content

Instantly share code, notes, and snippets.

@lardratboy
Created February 2, 2025 22:02
Show Gist options
  • Save lardratboy/f6f52c15da926ca23bb7421e9f7e0a2a to your computer and use it in GitHub Desktop.
Save lardratboy/f6f52c15da926ca23bb7421e9f7e0a2a to your computer and use it in GitHub Desktop.
Tool to view bytes as points supports images and binary data
<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