Pixatar


I built a toy web app to generate pixel art images:

https://pixatar.djmetzle.io

My Github profile image is one of these avatar images, but i had created it “by hand” in GIMP.

Recently a coworker asked about this, and i thought it would be fun to create a simple web tool for generating these. I’ve also been working on learning Rust lately, so porque no los dos?

TL;DR

This is a Leptos app, running in webassembly, and served statically.

Here’s the source.

I’d like to share some of my learnings, and the tools explored.

Leptos

Leptos is a web UI framework written in Rust, targeting webassembly.

Think React, but Rust. Macros enable writing something that looks and feels a lot like React, or other client-side web framework components.

Leptos however uses signals, and thus has two-way data binding. This makes it a bit more similar to SolidJS (well, and every other non-React frontend framework).

Overall, the DX is pretty great. Any friction came from still being in the process of learning Rust, and the nature of the problem being solved.

Trunk

Trunk is a nifty tool for building and bundling static rust and webassembly sites.

Out of the box, it automagically set up a static build of my Leptos WASM project. We get watch and serve commands, to set up a hot reloading dev server, which worked great!

Trunk dev builds can be a little clunky (they’re not the fastest) and in a very few cases, a panic could break the hot reloading. But overall it worked 99.8% as i needed, right out the box, with zero configuration.

10/10, would recommend for this style of project.

The Code

The heart of this project is three things:

  • A UI to provide inputs for what it is you want generated
  • Converting the input into a pixel grid
  • Generating a PNG image to show to the user and allow for saving

UI

The UI is Leptos, so we’ve gone over that already. Im shocked at how snappy and fast it is! (the production release build anyhow)

The Pixel Grid

A decent little hacking puzzle sits at the heart of generating the pixel grid.

We take in a string, cast it to bytes, and return a pixel grid iterator, based on some of the inputs.

Do we walk in character order? Or bit position order? And what bit position comes first?

Here's some of the Rust code, for those not too faint of heart

Im decently proud of this!

pub struct Bytes {
    string: String,
    values: Vec<Vec<bool>>,
    orientation: Orientation,
    endian: Endian,
    current: u32,
}

impl Iterator for Bytes {
    type Item = Vec<bool>;

    fn next(&mut self) -> Option<Self::Item> {
        let byte_length = self.string.as_bytes().len() as u32;
        match self.orientation {
            Orientation::Horizontal => {
                if self.current == 8 {
                    return None;
                }
                let bit = self.current;
                let index = match self.endian {
                    Endian::Most => bit as usize,
                    Endian::Least => 7 - bit as usize,
                };
                let row = self
                    .values
                    .iter()
                    .map(|char_bits| char_bits[index])
                    .collect();
                self.current += 1;
                return Some(row);
            }
            Orientation::Vertical => {
                if self.current == byte_length {
                    return None;
                }
                let char = self.current;
                let row = self.values[char as usize].clone();
                self.current += 1;
                return match self.endian {
                    Endian::Most => Some(row),
                    Endian::Least => Some(row.into_iter().rev().collect()),
                };
            }
        }
    }
}

The PNG

The final image is generated with the PNG crate.

It’s nice an fast, and compiles to WASM to run entirely in the browser.

After we get out the cooked PNG image, we can base64 encode it, and dump it into the DOM.

That lets the user view the output on the fly, as well as right-click+save the image.

AI

I did use Gemini to add some easy touchup CSS.

Im a “function over form” kinda guy, so ive only included the bare minimum beyond the default client stylesheet.

CI/CD and IaC

Static sites are easy.

I’ve included the Github build and deploy action, as well as the terraform for hosting.

A note, Rust compilations are BRUTAL, so build caching is a must. Found that out the hard way!

Thoughts

I learned a lot through this project, and my biggest take away is just how fast frontend can be with WASM.

The result isnt something especially useful or practical,

Share: