Skip to content

Instantly share code, notes, and snippets.

@SuppliedOrange
Last active September 4, 2024 00:13
Show Gist options
  • Save SuppliedOrange/600c873d44d4671f0e215b38fe9cf62c to your computer and use it in GitHub Desktop.
Save SuppliedOrange/600c873d44d4671f0e215b38fe9cf62c to your computer and use it in GitHub Desktop.

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.

image

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} )
    }

image

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:

image

image

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!

image

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.

brave_SfiWGYhYai-ezgif com-optimize

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.

output-onlinegiftools_2-ezgif com-optimize

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment