This is exactly what I thought first. In reality, dart:ui doesn’t expose any API to build fully-fledged SkSL shaders. And Shader instances are just shallow wrappers over some built-in Skia shaders.

Bad news, it’s impossible to procedurally generate images with Flutter. We’re done gentlepeople. End of story.

This is exactly what I though on my second approach to this problem. And luckily, I was wrong.

Flutter Canvas and drawing images

Canvas has this method, drawImage, which accepts an instance of the Image object. So if we can just somehow manipulate the binary buffer behind that object to contain certain colours at certain pixels, and then draw resulting Image on the Canvas — we’re done. End of story?

Not yet. It’s impossible to just instantiate a new Image and pass in something from dart:typed_data package. The reason for that is that Image is created directly from Flutter Engine, which is native part of the framework with interop to Dart code. The only way in which Image could be obtained from the Dart side is by using instantiadeImageCodec utility. Which, as you may guess, does not return Image right away and also asynchronous. Most of the sources suggest to use it in the following way:

Which is 👌, as it’s well documented and works exactly as you’d expect it, despite being two lines more code than you might expect you needed for instantiating an Image.

The example above lacks one important detail, it doesn’t show you where byte data comes from. And in case of procedural texture generation, it’s quite important, because we’re not simply reading raw image source from disk, or network, but need to instantiate it in-memory from scratch. Formats which are supported by codec are JPEG, PNG, GIF, Animated GIF, WebP, Animated WebP, BMP, and WBMP. As you can see there’s no raw byte data in here.

Luckily, though, BMP is almost like raw bytes, right? And there’s probably a good library to generate BMP with Dart? Long story short, I've written a custom BMP encoder and started using it in my project before realizing that there’s a simpler, but less documented way to instantiate Images with dart:ui.

⚡️decodeImageFromPixels ⚡️

Sometimes it happens that you just can’t find some things on the Internet, no matter how hard you try. Sometimes it also happens that you’re just looking for a wrong thing. That happened to me as well, as I was completely focused on the docs from Image class stating that you have to use instantiateImageCodec for obtaining an Image instance. But literally, 48 lines below the source of instantiateImageCodec, there’s another function, decodeImageFromPixels, which I stumbled upon just by pure luck, and this one is not limited by the formats listed above and can accept raw pixel data. What a beauty!

With that, we can already have some fun. Here’s the playground I'm going to use for the rest of the article.

Playground for generating images

I assure you that this example have more length to it than complexity. But as a result, you can just run flutter create texture_generator and swap contents of main.dart with sources above. When run, it’ll look like:

Something not-yet-uniform

Those of you who dealt with shaders will immediately realise how awful this thing is, in reality. It runs on the CPU. On a mobile CPU.

The reason it’s not so good is simple, GPU exists exactly to do such things. So what we’re doing above is slightly inappropriate. So “Proceed at your own risk” ©.

Rendering with CPU (creds to Mythbusters)

Rendering with GPU (creds to Mythbusters)

That being said, let’s start tweaking our sample to get something more practical. In shader workflows, it’s common to operate with unit vector coordinates instead of absolute values.

Which will be rendered as:

We can modify it even so slightly with:

And it’s already something potentially usable by a real application.

Gradient with dots pattern

The benefits of such an approach would be mathematical preciseness. So if you’re required to produce a pixel perfect design, which yet adapts to all possible aspect ratios and pixel densities, this might even be an option for you. We’re doing it for fun here, however 🌕.

Now, let’s try different classics of shader tutorials: checkerboard pattern. To make our life a bit easier, let’s introduce a small helper to convert Vector3 to int representing RGBA color. Same type of int , we were returning in previous examples, as Colors.black.value or 0xffbada55 .

Now our code will look closer to what you can find in actual shader tutorials. To start with a checkerboard, let’s draw a gradient over X-axis.

It will draw a nice and smooth gradient from black to white over the horizontal axis.

As smooth as it could be for 8-bit color space

Next step is to add some repetition. One simple way to do it is to remap our uv vector so it repeats values between 0..1 multiple times when running code for pixels from 0 to Size.width . Implementing that couldn’t be easier, thanks Math!

This function will return a fractional part of Vector3 , so if we would pass something like uv.xxx in it, it would return pretty much the same exact thing. But, if we would pass a Vector(1.1, 2.2, 3.3) , it would return Vector3(0.1, 0.2, 0.3) which will always have unit size.

We could utilize it in the following fashion:

Please don’t use patterns like this in your apps directly 🙏

Let’s now add a repetition along Y-axis. To do so, we want to scale both x and y components of our unit coordinate vector. We also introduce frac2 for Vector2 specifically.

Rick & Morty grid

That is nice! But what if we want square tiles? That is also quite easy, we can just rescale our original uv based on the aspect ratio of the given Size .

“Perfect grid does not exi… 🤔”

Despite being quite cool already, it’s not a checkerboard just yet. To make it so, let’s use our gridUv coordinates. If gridUv represents top left or bottom right quarter of the grid cell, let’s return 0xffffff and 0xff000000 color otherwise.

That would be a long chess game!

Despite it’s cool that we are able to draw such thing with Flutter on a screen of mobile device with hot reload with true full control over each pixel, the pattern itself is sort of boring. So let’s level it up a notch!

Truchet tiles are a schema-based or random repetition of graphic fragments within a grid. Exactly what we did now! So, the first simple thing we can spice it up a bit is to swap which quarters of grid cell are white randomly.

Let’s render it, and…

Yeah, that’s how shaders are working.

We will just get a bunch of noise. It’s because of our code is executed for each pixel in isolation, and our gridUv abstraction is based solely on input coordinates.

The way out of this total and complete randomness is to make our decision on which tile to render based on the tile index. Alongside with that, we can also apply some pseudo-random transformations to input, so it doesn’t repeat too often.