Last active
January 10, 2022 21:20
-
-
Save mildsunrise/c9275ba0abf7c34bfa60e1588ff94c59 to your computer and use it in GitHub Desktop.
🔬 Portable HTML file to test a bitmap with different strides (widths) and modes
This file contains 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> | |
<meta charset="utf-8"> | |
<title>Bitmap tester</title> | |
<style> | |
body { | |
margin: 20px; | |
font: 16px Roboto, sans-serif; | |
} | |
input, select { | |
font: inherit; | |
padding: .3em .2em; | |
} | |
.content { | |
} | |
#image-panel { | |
margin-top: 1em; | |
} | |
.controls { | |
display: flex; | |
flex-direction: column; | |
align-items: flex-start; | |
gap: .7em; | |
} | |
.stacked { | |
display: flex; | |
flex-direction: row; | |
align-items: center; | |
gap: 1em; | |
} | |
.canvas-wrapper { | |
display: inline-flex; | |
padding: 6px; | |
margin-top: 1em; | |
background-color: gray; | |
background-image: repeating-linear-gradient(45deg, transparent, transparent 1px, rgba(255,255,255,.5) 1px, rgba(255,255,255,.5) 2px); | |
} | |
label input, label select { | |
margin-left: .2em; | |
} | |
.mode-control { | |
font-weight: bold; | |
} | |
.width-control input, .height-control input, .pixels-control input { | |
font-size: 1em; | |
width: 5em; | |
} | |
#canvas { | |
image-rendering: pixelated; | |
} | |
</style> | |
</head> | |
<body> | |
<div class="content"> | |
<label class="data-control"> | |
Data: | |
<input id="data" type="text" placeholder="bitmap bytes (base64 or C array syntax)" size="30"> | |
</label> | |
<div id="image-panel" style="display: none"> | |
<div class="controls"> | |
<div class="stacked"> | |
<label class="mode-control"> | |
Mode: | |
<select id="mode"> | |
<option value="grayBitLE">1-bit gray (LE)</option> | |
<option value="grayBitBE">1-bit gray (BE)</option> | |
<option value="gray">8-bit gray</option> | |
<option value="rgb" selected>8-bit RGB</option> | |
</select> | |
</label> | |
<label class="zoom-control"> | |
Zoom: | |
<input id="zoom" type="number" min="1" max="16" step="1" value="4"> | |
</label> | |
<label class="overlap-control"> | |
<input id="overlap" type="checkbox" checked> | |
Overlap | |
</label> | |
</div> | |
<div class="stacked"> | |
<label class="width-control"> | |
Width: | |
<input id="width" type="number" min="1" step="1"> | |
</label> | |
<label class="height-control"> | |
Height: | |
<input id="height" type="number" readonly> | |
</label> | |
<label class="pixels-control"> | |
Pixels: | |
<input id="pixels" type="number" readonly> | |
</label> | |
</div> | |
</div> | |
<div class="canvas-wrapper"> | |
<canvas id="canvas"></canvas> | |
</div> | |
</div> | |
</div> | |
<script> | |
const dataInput = document.getElementById('data') | |
const imagePanel = document.getElementById('image-panel') | |
const canvas = document.getElementById('canvas') | |
const ctx = canvas.getContext('2d') | |
const modeInput = document.getElementById('mode') | |
const widthInput = document.getElementById('width') | |
const heightInput = document.getElementById('height') | |
const pixelsInput = document.getElementById('pixels') | |
const zoomInput = document.getElementById('zoom') | |
const overlapInput = document.getElementById('overlap') | |
let data | |
let imageData | |
setTimeout(() => { | |
const hash = decodeURIComponent(/^#(.*)$/.exec(document.location.hash || '#')[1]) | |
if (!hash) return dataInput.focus() | |
const payload = JSON.parse(hash) | |
dataInput.value = payload.data | |
modeInput.value = payload.mode | |
updateData() | |
widthInput.focus() | |
}, 0) | |
function serializeState() { | |
const payload = { data: data ? encodeBase64(data) : '', mode: modeInput.value } | |
document.location.hash = '#' + encodeURIComponent(JSON.stringify(payload)) | |
} | |
dataInput.addEventListener('input', () => updateData()) | |
modeInput.addEventListener('input', () => updatePreprocess()) | |
widthInput.addEventListener('input', () => renderImage()) | |
zoomInput.addEventListener('input', () => renderImage()) | |
overlapInput.addEventListener('input', () => renderImage()) | |
function updateData() { | |
data = parseBytes(dataInput.value) | |
imagePanel.style.display = data ? '' : 'none' | |
if (!data) return | |
updatePreprocess() | |
} | |
function updatePreprocess() { | |
serializeState() | |
imageData = modeImpl[modeInput.value](data) | |
pixelsInput.value = widthInput.max = imageData.length / 4 | |
if (!widthInput.value) | |
widthInput.value = Math.round(Math.sqrt(imageData.length / 4)) | |
renderImage() | |
} | |
function renderImage() { | |
const width = parseInt(widthInput.value) | |
if (isNaN(width) || width < 1 || width !== Math.floor(width)) return | |
const overlap = overlapInput.checked ? Math.round(width * 0.35) : 0 | |
const canvasWidth = width + overlap | |
const height = Math.ceil((imageData.length / 4) / width) | |
canvas.width = canvasWidth | |
canvas.height = height | |
heightInput.value = height | |
ctx.clearRect(0, 0, canvas.width, canvas.height) | |
const image = ctx.createImageData(width, height) | |
image.data.set(imageData) | |
ctx.putImageData(image, 0, 0) | |
ctx.putImageData(image, width, -1) | |
// set zoomed size | |
let zoom = parseInt(zoomInput.value) | |
if (isNaN(zoom)) zoom = 1 | |
canvas.style.width = (canvasWidth * zoom).toString() + 'px' | |
canvas.style.height = (height * zoom).toString() + 'px' | |
} | |
// PREPROCESSING | |
const grayImpl = (data) => data.flatMap(bit => [bit, bit, bit, 255]) | |
const getBit = (n, bit) => (n >> bit) & 1 | |
const grayBitImpl = (data, le) => grayImpl( | |
[...data].flatMap(byte => | |
[...Array(8)].map((_, i) => getBit(byte, le ? i : 7-i) * 255)) | |
) | |
const modeImpl = { | |
grayBitLE: (data) => grayBitImpl(data, true), | |
grayBitBE: (data) => grayBitImpl(data, false), | |
gray: (data) => grayImpl([...data]), | |
rgb: (data) => | |
[...Array(Math.floor(data.length / 3))] | |
.flatMap((_, i) => [...data.subarray(i*3, (i+1)*3), 255]), | |
} | |
// PARSING | |
const decodeBase64 = b => Uint8Array.from(atob(b), x => x.charCodeAt()) | |
const encodeBase64 = b => btoa(String.fromCharCode(...b)) | |
function parseBytes(data) { | |
data = data.replace(/\s/g, '') | |
// first try to parse as base64 | |
try { | |
if (/^[A-Za-z0-9+/=]+$/.test(data)) | |
return decodeBase64(data) | |
} catch (e) { | |
return | |
} | |
// remove delimiters | |
while (true) { | |
const m = /^\{(.*)\}$/.exec(data) || /^\((.*)\)$/.exec(data) || /^\[(.*)\]$/.exec(data) | |
if (!m) break | |
data = m[1] | |
} | |
// remove trailing comma, parse ints | |
const m = /^(.*),$/.exec(data) | |
data = m ? m[1] : data | |
data = data.split(',').map(x => parseInt(x)) | |
// validate | |
if (!(data.length && data.every(x => !isNaN(x) && x >= 0 && x <= 0xFF))) | |
return | |
return Uint8Array.from(data) | |
} | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment