A couple of days ago, I found this video about the website with 1 million checkboxes. It came out during June (4 months ago) and I didn't really think much of it, except marvel at the optimization.
The video then revealed that a bunch of teens took decided to create art on it, with checkboxes. I felt like I was missing out. I immediately hopped onto their discord server and looked around. The owner was a pretty nice and active person, everyone there seemed to be quite friendly. They told me it wasn't possible to create art with the checkboxes anymore since the API had shut down. I was a bit disappointed, but soon-
I found out that there's a new page with 1 billion checkboxes. Obviously, I went on and decided to take apart all the javascript. It was a vite.js web-page. It was all obsfucated, so I went to the network requests. It was connected to a websocket and I saw that each request was a bunch of binary messages sent to a websocket. I could make very little of it. I had no idea what a byte was, let alone a "binary message". There were 4096 pages. Each page had 262143 checkboxes. It only rendered one chunk of one page when that part of the page was visible to us.
Eventually, I stumbled upon an interesting class.
class sn {
constructor() {}
isChecked(e) {
const t = e % en;
return this.bitmap.get(t)
}
toggle(e) {
console.log(e)
const t = e % en;
this.send({
msg: 19,
index: e
}),
this.bitmap.set(t, !this.bitmap.get(t))
}
get chunkIndex() {}
setChunkIndex(e) {}
getUint8Array() {}
openWebSocket() {}
onOpen() {}
onMessage(e) {}
deserialize(e) {}
send(e) {
if (!this.websocket)
return;
const t = this.serialize(e);
this.websocket.send(t)
}
serialize(e) {}
}
Now I've hidden all the properties except the ones that immediately caught my eye. The send
function.
All I had to find out now, was what I need to send to the send
function. So I hoooked a console.log(e)
to it and found that each pixel is simply identified by its index! For the 60,000th pixel, you need the index 59,999 etc.
I also managed to find out that you can access this class through app.$V.children.client
. Now, why would they both obsfucate their code and have something like this? I made a major oversight, and I'll get to that part.
I quickly set up a script to convert images into black/white and resize it to 60 pixels wide. I would've done it with sharp originally, but ChatGPT told me about "canvas" and now that's what I'll be using all the time- because it's a noticably faster. Then I set up a webscraper. Here's what it looked like:
// images is an array like this
let images = [
{
name: './image5.jpg',
threshold: 200
},
...
]
// Basically, all the images converted as an array of 1's and 0's, meaning checked and not checked.
// 0 means white, 1 means black
// This is an array of an array of binary, if that makes sense- (1 | 0)[][]
let binaryPixelList = []
// Render the images
for ( let index = 0; index < images.length; index++) {
let image = images[index];
// I render it so that I can save it.
convertToBlackAndWhite(image.name, 60, 0, image.threshold ).then(({ binaryPixels, width, height, }) => {
createBlackAndWhiteImage(binaryPixels, width, height, `./render_${index}.jpeg`);
});
// Then I re-render (lol why) it so I can append it to binary pixel list
let { binaryPixels, width, height } = await convertToBlackAndWhite( image.name, 60, startIndex, image.threshold );
binaryPixelList.push(binaryPixels)
}
await page.goto('https://bitmap.alula.me/', {waitUntil: "networkidle"});
// A weird method to go the menu, then go to the page we want.
await page.evaluate( async (startIndex) => {
let menu = document.querySelector('button.btn.btn-primary');
// @ts-ignore
menu.click();
await new Promise(r => setTimeout(r, 100));
let num = document.getElementsByName('num')[0];
// @ts-ignore
num.value = startIndex;
await new Promise(r => setTimeout(r, 100));
// @ts-ignore
document.querySelector('button[formaction="submit"]').click();
}, startIndex);
// This just means "index", i dont know why I named it this.
let imageIndex = 0;
while (true) {
await new Promise(r => setTimeout(r, 5000));
currentBinaryPixel = (imageIndex + 1) % binaryPixelList.length;
// This is essentially the black/white image in an array. 1=black, 0=white.
let chosenBinaryPixel = binaryPixelList[imageIndex];
await page.evaluate( async data => {
try {
data.chosenBinaryPixel.forEach(async (pixel: number, index: number) => {
index = index + data.startIndex;
// @ts-ignore
// See if the checkbox we need is checked
let isChecked = app.$V.children.client.isChecked(index);
// If it's supposed to be checked but it isn't checked, check it.
if (pixel === 1 && !isChecked) {
// @ts-ignore
app.$V.children.client.toggle(index)
}
// vice versa
if (pixel === 0 && isChecked) {
// @ts-ignore
app.$V.children.client.toggle(index)
}
});
}
catch (error) {
console.log(error);
process.exit(1)
}
}, {chosenBinaryPixel, startIndex} )
}
Now admittedly, this is not my first version. My first version only rendered 1 image over and over.
Here's how the image rendering went:
Big differece, right? When your resolution is that low, it's very hard to implement something like edge-detection. I realised I was better off just converting to b/w based on a regular color darkness threshold.
I hit the run button, and boom! She was there!
This was only the beginning though, I wanted to make more. I needed more images- a slideshow perhaps. So I made it a slideshow! This was also when I discovered that every image needs a specific threshold, so I manually assessed the images and adjusted their thresholds.
This is when I realised I made a major oof. The creator of the website told me there was already documentation for this and that their website itself was opensource. I hadn't bothered to check before, so I never figured it out. I found their github, which was also in typescript, so I copied their "client" class, the one we saw in the obfuscated code earlier and implemented it into mine.
So I re-wrote it like this
let binaryPixelList: Array<any> = [];
for ( let index = 0; index < images.length; index++) {
let image = images[index];
convertToBlackAndWhite(image.name, 60, 0, image.threshold ).then(({ binaryPixels, width, height, }) => {
createBlackAndWhiteImage(binaryPixels, width, height, `./render_${index}.jpeg`);
});
let { binaryPixels, width, height } = await convertToBlackAndWhite( image.name, 60, startIndex, image.threshold );
binaryPixelList.push(binaryPixels)
}
console.log(`Start: ${startIndex}\n End: ${binaryPixelList[0].length + startIndex}`);
async function navigateAndWait(client: BitmapClient, page: number, retries: number = 15000) {
// Wait for the websocket to open
let currentRetry = 0;
if (!client.websocketOpen) {
while (true) {
if (currentRetry >= retries) break;
if (client.websocketOpen) break;
currentRetry += 100;
await new Promise(r => setTimeout(r, 100));
}
}
// Wait for the chunk index to change
client.setChunkIndex(page - 1);
currentRetry = 0;
if (!client.chunkLoaded) {
while (true) {
if (currentRetry >= retries) break;
if (client.chunkLoaded) break;
currentRetry += 100;
await new Promise(r => setTimeout(r, 100));
}
}
}
let client = new BitmapClient();
await navigateAndWait(client, page);
let currentBinaryPixel = 0;
setInterval(() => {
currentBinaryPixel = (currentBinaryPixel + 1) % binaryPixelList.length;
}, 5000);
function drawImage() {
let chosenBinaryPixel = binaryPixelList[currentBinaryPixel]
return new Promise( (resolve, reject) => {
for (let index = 0; index < chosenBinaryPixel.length; index++) {
let pixel = chosenBinaryPixel[index];
let pixelIndex = startIndex + index;
let isChecked = client.isChecked(pixelIndex);
if (
pixel === 1 && !isChecked ||
pixel === 0 && isChecked
) {
client.toggle(pixelIndex);
}
}
resolve("finished")
})
}
while (true) {
try {
await drawImage()
await new Promise(r => setImmediate(r));;
}
catch (error) {
console.log(error);
process.exit(1)
}
}
I wanted to write a lot more but honestly I'm getting tired but yeah that's all I did, someone ran doom.