Programmatically Generate Images with CSS Painting API

A JavaScript API for dynamic image creation coupled with CSS

Viduni Wickramarachchi
Bits and Pieces

--

Images add color to an application. However, as we all know, having a lot of high-resolution images affects the page load time. For images of products, scenarios, and so on, we have no option but to include these images and optimize the application by caching them. But if you need a geometric image in your application, you don’t have to include it as an asset anymore.

You can programmatically generate geometric images on the fly using the CSS Painting API.

Let’s find out what this API is and how to generate an image using it.

Introduction to the CSS Painting API

The CSS Painting API enables developers to write JavaScript functions to draw images into CSS properties like background-image and border-image. It provides a set of APIs that gives developers access to the CSSOM. This is a part of CSS Houdini (Houdini — a collection of new browser APIs, gives developers lower-level access to CSS itself.).

The traditional approach to include an image is as follows.

div {
background-image: url('assets/background.jpg);
}

With the CSS Painting API, you can call the paint() function and pass in a worklet written in JS instead of the above.

div {
background-image: paint(background);
}

The workflow of this would be as follows.

You may have come across some unknown terms in the above section. For example, what are these worklets that we keep talking about?

In brief, the JavaScript code written to programmatically generate an image is called a Paint Worklet. A worklet is an extension point into the browser rendering pipeline. There are other types of worklets apart from paint worklets as well, such as animation worklets, layout worklets, etc.

Now let’s look at a step-by-step approach to generate an image programmatically.

Using the CSS Painting API in practice

In this article, we’ll look at how to create a bubble background.

Step 1: Add the CSS paint() function

First of all, you need to add the paint() function to the CSS property you need your image to be on.

.bubble-background {
width: 400px;
height: 400px;
background-image: paint(bubble);
}

bubble will be the worklet that we create to generate the images. This will be done in the next few steps.

Build better Component Libs and Design Systems

Share components across teams and projects to speed up development and make sure your users experience a consistent design at every touchpoint.

OSS Tools like Bit offer a great dev experience for building, sharing, and adopting components across teams and applications. Create a component hub for free give it a try →

An independently source-controlled and shared “card” component. On the right => its dependency graph, auto-generated by Bit.

Step 2: Defining the worklet

The worklets need to be kept in an external JS file. The paint worklet would be a class . E.g.:- class Bubble { .... } . This worklet needs to be registered using the registerPaint() method.

class Bubble {
paint(context, canvas, properties) {
........
}
}
registerPaint('bubble', Bubble);

The first parameter of the registerPaint() method should be the reference you included in CSS.

Now let’s draw the background.

class Bubble {
paint(context, canvas, properties) {
const circleSize = 10;
const bodyWidth = canvas.width;
const bodyHeight = canvas.height;

const maxX = Math.floor(bodyWidth / circleSize);
const maxY = Math.floor(bodyHeight / circleSize);

for (let y = 0; y < maxY; y++) {
for (let x = 0; x < maxX; x++) {
context.fillStyle = '#eee';
context.beginPath();
context.arc(x * circleSize * 2 + circleSize, y * circleSize * 2 + circleSize, circleSize, 0, 2 * Math.PI, true);
context.closePath();
context.fill();
}
}
}
}
registerPaint('bubble', Bubble);

The logic to create the image is inside the paint() method. You would need a bit of knowledge on canvas creation to draw images as above. Refer to the Canvas API documentation if you aren’t familiar with it.

Step 3: Invoke the worklet

The final step would be to invoke the worklet in the HTML file.

<div class="bubble-background"></div><script>
CSS.paintWorklet.addModule('https://codepen.io/viduni94/pen/ZEpgMja.js');
</script>

It’s done!

You have programmatically generated an image in just 3 steps.

Generated image

The output of what we created will look as follows.

View in Editor

What else can we do with this CSS Painting API?

The power of the CSS Painting API is not over yet. There are more things you can do with it.

1. You can create dynamic images

For example, you can dynamically change the color of the bubbles. CSS variables are used for this purpose. In order to use CSS variables, the browser should have prior knowledge that we are using it. We can use the inputProperties() method to do this.

registerPaint('bubble', class {
static get inputProperties() {
return ['--bubble-size', '--bubble-color'];
}

paint() {
/* ... */
}
});

The variables can be assigned using the third parameter passed to the paint() method.

paint(context, canvas, properties) {
const circleSize = parseInt(properties.get('--bubble-size').toString());
const circleColor = properties.get('--bubble-color').toString();

const bodyWidth = canvas.width;
const bodyHeight = canvas.height;

const maxX = Math.floor(bodyWidth / circleSize);
const maxY = Math.floor(bodyHeight / circleSize);

for (let y = 0; y < maxY; y++) {
for (let x = 0; x < maxX; x++) {
context.fillStyle = circleColor;
context.beginPath();
context.arc(x * circleSize * 2 + circleSize, y * circleSize * 2 + circleSize, circleSize, 0, 2 * Math.PI, true);
context.closePath();
context.fill();
}
}
}

2. You can generate random images using Math.random() in the paint() method.

// CSS
body {
width: 200px;
height: 200px;
background-image: paint(random);
}
// JS
function getRandomHexColor() {
return '#'+ Math.floor(Math.random() * 16777215).toString(16)
}
class Random {
paint(context, canvas) {
const color1 = getRandomHexColor();
const color2 = getRandomHexColor();

const gradient = context.createLinearGradient(0, 0, canvas.width, 0);
gradient.addColorStop(0, color1);
gradient.addColorStop(1, color2);

context.fillStyle = gradient;
context.fillRect(0, 0, canvas.width, canvas.height);
}
}
registerPaint('random', Random);

If you want to know more details about how to implement these, let me know in the comments section below.

It’s awesome, isn’t it?

But, every good thing has at least one bad side to it. This API has very limited support in browsers.

Browser Support

Source: Can I Use

Most browsers including Firefox have no support for the CSS Paint API. Only Chrome and Chromium-based browsers have full support for this so far. Let’s hope that browser support will improve in the near future.

Summary

The CSS Paint API is extremely useful to reduce the response time of network requests. This is achieved by generating some images programmatically rather than retrieving them via network requests.

On top of this, the main benefits in my opinion are as follows.

  • Ability to create fully customizable images as opposed to static images.
  • It creates resolution-independent images (no more bad quality images on your site).

An important point to note is that you can use a polyfill as a workaround to support the browsers like Firefox that’s yet to implement the CSS Painting API.

Let us know your thoughts on this too. Thanks for reading!

--

--