Public
Edited
Jul 9, 2023
Fork of Pixel Input
1 fork
Importers
2 stars
Insert cell
Insert cell
viewof demo = PixelView({
height:5, width:4,
values: [1, 0, 0, 1, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1],
editable: true
})
Insert cell
demo.values
Insert cell
squeezed = PixelView({data:demo.squeezed()})
Insert cell
packed = demo.pack(new BigUint64Array(1), {dx:2, dy:1})
Insert cell
PixelView({data:PixelData.unpack(packed)[0]})
Insert cell
class PixelData {

static unpack(array, {height=8, width=8, stacked=false}={}) {
let bit = 0n, idx = 0;
const values = new Uint8Array(height * width);
const unpacked = [ ];
array.forEach((packed, k) => {
idx = 0;
for(let row=0; row<height; ++row) {
for(let col=0; col<width; ++col) {
bit = 1n << BigInt(8 * row + col);
if(!stacked) values[idx] = (packed & bit) ? 1 : 0;
else if(packed & bit) values[idx] = k+1;
idx++;
}
}
if(!stacked) unpacked.push(new PixelData(height, width, values));
});
return stacked ? new PixelData(height, width, values) : unpacked;
}
constructor(height, width, values) {
this.height = height;
this.width = width;
this.values = new Uint8Array(height * width);
if(!(values === undefined) && values.length == height * width) this.set(values);
}
set(values) {
for(let i=0; i < this.height * this.width; ++i) this.values[i] = values[i];
}
at(row, col) {
return this.values[row * this.width + col];
}
squeezed() {
// Remove any empty exterior rows and columns.
const rowSum = row => d3.range(this.width).reduce((sum,col) => sum + this.at(row,col), 0);
const colSum = col => d3.range(this.height).reduce((sum,row) => sum + this.at(row,col), 0);
let left,right,top,bottom;
for(top=0; top < this.height; ++top) if(rowSum(top)) break;
for(bottom=this.height-1; bottom >= 0; --bottom) if(rowSum(bottom)) break;
for(left=0; left < this.width; ++left) if(colSum(left)) break;
for(right=this.width-1; right >= 0; --right) if(colSum(right)) break;
const height = bottom - top + 1, width = right - left + 1;
//console.log([left,right,width], [top,bottom,height]);
if(height == this.height && width == this.width) return this;
const values = new Uint8Array(height * width);
for(let row = top; row <= bottom; ++row) {
for(let col = left; col <= right; ++col) {
values[(row - top) * width + (col - left)] = this.at(row, col);
}
}
return new PixelData(height, width, values);
}
isEqualTo(other) {
if(other.width != this.width || other.height != this.height) return false;
for(let row = 0; row < this.height; ++row) {
for(let col = 0; col < this.width; ++col) {
if(other.at(row,col) != this.at(row,col)) return false;
}
}
return true;
}
pack(array, {idx=0, dx=0, dy=0}) {
if(this.width > 8 || this.height > 8) throw new Error(`Cannot pack ${this.height} x ${this.width} pixels`);
array[idx] = 0n;
let bit = 0n;
for(let row=0; row < this.height; ++row) {
for(let col=0; col < this.width; ++col) {
if(this.at(row, col)) array[idx] |= 1n << BigInt(8 * (row+dy) + col + dx);
}
}
return array;
}
}
Insert cell
function PixelView({
height=7,
width=5,
values=[],
data,
size=20,
spacing=1,
fill = "currentColor",
stroke = "#ccc",
editable = false,
fillScheme
} = {}) {
data = data || new PixelData(height, width, values);
const canvas = document.createElement("canvas");
canvas.width = data.width * size + (data.width + 1) * spacing;
canvas.height = data.height * size + (data.height + 1) * spacing;
canvas.style.imageRendering = "-moz-crisp-edges";
canvas.style.imageRendering = "pixelated";
const context = canvas.getContext("2d");

Object.defineProperties(canvas, {
value: {
get() {
return data;
},
set(d) {
data.set(d.values);
context.clearRect(0, 0, canvas.width, canvas.height);
if (spacing) {
context.fillStyle = stroke;
for (let row = 0, y = 0; row <= data.height; ++row, y += spacing + size) {
context.fillRect(0, y, canvas.width, spacing);
}
for (let col = 0, x = 0; col <= data.width; ++col, x += spacing + size) {
context.fillRect(x, 0, spacing, canvas.height);
}
}
for (let row = 0, y = spacing; row < data.height; ++row, y += spacing + size) {
for (let col = 0, x = spacing; col < data.width; ++col, x += spacing + size) {
const val = data.at(row, col);
if (val) {
context.fillStyle = fillScheme ? fillScheme[(val-1)%fillScheme.length] : fill;
context.fillRect(x, y, size, size);
}
}
}
}
}
});

if(editable) {
canvas.addEventListener("click", event => {
const col = Math.floor((event.offsetX - spacing) / (size + spacing));
const row = Math.floor((event.offsetY - spacing) / (size + spacing));
if (col < 0 || col >= data.width || row < 0 || row >= data.height) return;
const v = 1 - data.at(row, col);
const x = spacing + col * (size + spacing);
const y = spacing + row * (size + spacing);
if (v) context.fillStyle = fill, context.fillRect(x, y, size, size);
else context.clearRect(x, y, size, size);
data.values[row * data.width + col] = v;
canvas.dispatchEvent(new CustomEvent("input"));
});
}

canvas.value = data;
return canvas;

}
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more