Skip to content

Instantly share code, notes, and snippets.

@mildsunrise
Last active January 10, 2022 21:20
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mildsunrise/c9275ba0abf7c34bfa60e1588ff94c59 to your computer and use it in GitHub Desktop.
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
<!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