Coloring With Code — A Programmatic Approach To Design

Color is powerful — it can radically shift our mood, inspire us, and help us express ourselves in a way that few other things can. It is a fundamental building block of design, but it can also be a little intimidating.

Often when given the opportunity to play with color, we freeze. Choosing just one can be enough to trigger a kind of iridescent nightmare, not to mention combining lots of them! The options are infinite, and the “rules” somewhat hazy… a potentially overwhelming combination, particularly for those of us used to the (often) more definite world of code.

In this tutorial, we will be learning how to use familiar tools — a text editor and web browser — to make the process of creating striking color palettes a lot less scary and (most importantly!) fun. 

Let’s do it! 

Intended audience

This article is perfect for folks who already have a good grasp of HTML, CSS (knowledge of HSL and RGB colors will be helpful), and JavaScript. If you love to make things on the web, but often reach for a pre-curated selection or an automatic “generator” when adding color, you’re in the right spot. 

Tutorial format 

We won’t be building one single, strictly defined project here. Instead, we will be learning to create three special JavaScript functions, all uniquely suited to generating beautiful color palettes. Once written, these functions will form a solid foundation for our very own suite of programmatic color tools, which can be carried from project to project and iterated on/personalized over time.

A short introduction to LCH color

Throughout this tutorial, we will be working almost exclusively with LCH colors. LCH stands for lightness (how dark/light a color is), chroma (how vivid/saturated a color is), and hue (whether a color is red, green, blue…).

In short, LCH is a way of representing color just like RGB or HSL, but with a few notable advantages — the most important for this tutorial being its perceptual uniformity. I know this sounds a little scary, but I promise it’s not; let me show you what it means!

To start, take a look at these two pairs of HSL colors: 

Two pairs of colored rectangles, with accompanying text describing the HSL value needed to create them. The two pairs of colors both have 20 degree hue difference in their HSL definition, but the rate of change between them is wildly different.

Notice how, despite both the top and bottom pairs having the same 20-degree hue variance, the difference we actually see is wildly different? This imbalance exists because HSL is not perceptually uniform.

Now take a look at the same experiment, this time using LCH colors:

Two pairs of colored rectangles, with accompanying text describing the LCH value needed to create them. The two pairs of colors both have 20 degree hue difference in their LCH definition, and the rate of change between them is consistent.

👋hue values do not align perfectly between HSL and LCH. In LCH, a hue of 0 is more pink, while in HSL, it is a pure red. 

Ah, much better! The change in hue seen here is far more balanced because LCH is perceptually uniform

Next, let’s take a peek at another two HSL colors: 

Two colored rectangles, and the relevant HSL values needed to create them. The two HSL values have identical lightness definitions, but appear very different. The left rectangle is far brighter.

These two colors have identical lightness values but appear very different to our human eyes. The yellow on the left is far “brighter” than the blue on the right.

Here’s a similar setup, but with LCH colors rather than HSL: 

Two colored rectangles, and the relevant LCH values needed to create them. The two LCH values have identical lightness definitions, and reflect this visually, being similarly bright.

That’s more like it! As demonstrated by the image above, lightness values in LCH are far more accurate representations of what we perceive — this, in combination with LCH’s uniform hue distribution, will make our lives a lot easier when creating harmonious color palettes. 

For now, this is all we need to know, but if you would like to learn more, I highly recommend this article by Lea Verou

👋 — We will be using a library in this tutorial, but native LCH support is heading to the browser! In fact, it is already in Safari, with other browsers currently working on it.

Following along

Before we write any code, we need a simple development environment. This setup is entirely your choice, but I recommend spinning up a CodePen to follow along with the examples, then moving to a custom setup/repository as and when you need to. Really, all we need here is an HTML/JavaScript file, and we will be using Skypack for all library imports, so there’s no need for any fancy build processes, etc.

Function #1 — “Scientific”

OK! First off, we are generating colors using “traditional” color theory. To get started with this method, let’s take a look at something called a color wheel:

12 colors, arranged in a "rainbow" around a 360 degree color wheel.

Look familiar?

A color wheel is a visual representation of the hues in a color space. The wheel above represents the hues in LCH, incrementing in 30-degree steps, from 0 to 360-degrees — a well-established format. In fact, for hundreds of years, we have used wheels to find colors that work well together!

Here’s how:

We start with a base color. Then, we rotate around the wheel by a certain number of degrees a certain number of times; for a perfect complementary palette, we move 180 degrees once: 

A 12 item color wheel, with 2 hues highlighted. The two highlighted hues appear at 0, and 180 degrees on the wheel (the top and bottom) - the colors themselves are a shade of pink and green.

Lovely! For a triadic palette, we move 120 degrees, twice: 

A 12 item color wheel, with 3 hues highlighted. The 3 highlighted hues appear at 0, and 120, and 240 degrees. The colors themselves are a shade of pink and green, and blue.

See where this is going? By altering the number of steps and rotation amount, we can create several “classic” color palettes:

A selection of 5 traditional color palettes plotted around color wheels.

Cool! Let’s take this method and turn it into 1s and 0s.

To keep things moving throughout this tutorial, I’ll show you the code, then break it down step-by-step:

The code

function adjustHue(val) {
  if (val < 0) val += Math.ceil(-val / 360) * 360;

  return val % 360;
}

function createScientificPalettes(baseColor) {
  const targetHueSteps = {
    analogous: [0, 30, 60],
    triadic: [0, 120, 240],
    tetradic: [0, 90, 180, 270],
    complementary: [0, 180],
    splitComplementary: [0, 150, 210]
  };

  const palettes = {};

  for (const type of Object.keys(targetHueSteps)) {
    palettes[type] = targetHueSteps[type].map((step) => ({
      l: baseColor.l,
      c: baseColor.c,
      h: adjustHue(baseColor.h + step),
      mode: "lch"
    }));
  }

  return palettes;
}

To break this down:

  1. Define a function createScientificPalettes that expects a single baseColor argument.
  2. Define the hue steps for several “classic” color palettes.
  3. For each palette type: iterate over each hue step, add the step value to the base hue, and store the resulting color — making sure its chroma and lightness values match the base. Use a small adjustHue function to ensure all hue values are between 0 and 360.
  4. Return the palettes in LCH format.  

Usage

Awesome! We can call our createScientificPalettes function like so:

const baseColor = {
  l: 50,
  c: 100,
  h: 0,
  mode: "lch"
};

const palettes = createScientificPalettes(baseColor);

In the example above, we pass a baseColor object, and the function returns a variety of palettes, all centered around that base. Thanks to LCH, the lightness and intensity of the colors in these palettes will be visually consistent, and the hue modulations highly accurate; this is great for accessibility, as, unlike other color spaces, each color in the palette will have the same perceived contrast. 

Cool! All that’s left to do now is convert the LCH colors to a more usable format. To do so, we can use Culori — an excellent color utility library used throughout this tutorial — to transform the LCH objects to, say, HEX:

import { formatHex } from "https://cdn.skypack.dev/culori@2.0.0";

const baseColor = {
  l: 50,
  c: 100,
  h: 0,
  mode: "lch"
};

const palettes = createScientificPalettes(baseColor);
const triadicHex = palettes.triadic.map((colorLCH) => formatHex(colorLCH));

// ["#ff007c", "#1f8a00", "#0091ff"]

👋 — Culori requires an explicit mode on all color objects. You will notice this included in the code examples throughout this tutorial. 

For our first function, that’s it! Let’s take a look at how we can use it in real life.

Practical application

One benefit of creating our color palettes with code (programmatically) is that it makes rapid prototyping/experimentation super easy. Say, for example, we were working on a design and got completely stuck with what color palette to use. Using our createScientificPalettes function, alongside some simple CSS custom properties, we can generate near-infinite palettes and test them with our UI in real-time! 

Here’s a CodePen to demonstrate:

light

Challenge

Right now, our createScientificPalettes function accounts for all palette types, apart from monochromatic. Can you update it to support monochromatic palettes

Function #2 — “Discovery”

So, this function is similar to the previous one but with quite a twist. We are still generating “classic” color combinations, but rather than calculating them scientifically (adding set “steps” to the hue of a base color), we are discovering them! That’s right; our discovery function will take an array of colors and find the best palette matches within it — analogous, triadic, tetradic, etc. 

Here’s an illustrated example:

A selection of colors, and a color wheel. Two colors on the wheel (at 0 and 180 degrees) are highlighted, and the closest matches for these two within the selection of colors is also highlighted, with lines drawn to their closest match.

Using this function, we can discover beautiful palettes within images, color datasets, and more! Let’s see how it works.

The code

import {
  nearest,
  differenceEuclidean,
} from "https://cdn.skypack.dev/culori@2.0.0";

function isColorEqual(c1, c2) {
  return c1.h === c2.h && c1.l === c2.l && c1.c === c2.c;
}

function discoverPalettes(colors) {
  const palettes = {};

  for (const color of colors) {
    const targetPalettes = createScientificPalettes(color);

    for (const paletteType of Object.keys(targetPalettes)) {
      const palette = [];
      let variance = 0;

      for (const targetColor of targetPalettes[paletteType]) {
        // filter out colors already in the palette
        const availableColors = colors.filter(
          (color1) => !palette.some((color2) => isColorEqual(color1, color2))
        );

        const match = nearest(
          availableColors,
          differenceEuclidean("lch")
        )(targetColor)[0];

        variance += differenceEuclidean("lch")(targetColor, match);

        palette.push(match);
      }

      if (!palettes[paletteType] || variance < palettes[paletteType].variance) {
        palettes[paletteType] = {
          colors: palette,
          variance
        };
      }
    }
  }

  return palettes;
}

To break this down: 

  1. Pass an array of LCH colors to the discoverPalettes function. 
  2. For every color, create the “optimum” target palettes based on it using our createScientificPalettes function. 
  3. For every palette, find the closest match for each of its colors. We calculate color matches here using Culori’s nearest and differenceEuclidian functions. 
  4. Determine how similar/different the “discovered” palette is to the target. Keep a record of the closest palette matches. 
  5. Return the closest match of each palette type! 

Awesome! This method is super exciting, as it operates much as a human would — looking at a selection of colors and finding the best (but never perfect) palettes; this is great, as sometimes, purely mathematic color theory can appear a touch sterile/predictable. 

Usage 

As a quick reference, here’s how we could use discoverPalettes with an array of HEX colors: 

import {
  converter,
} from "https://cdn.skypack.dev/culori@2.0.0";

const toLCH = converter("lch");

const baseColors = [
  "#FFB97A",
  "#FF957C",
  "#FF727F",
  "#FF5083",
  "#F02F87",
  "#C70084",
  "#9A007F",
  "#6A0076",
  "#33006B"
];

const baseColorsLCH = baseColors.map((color) => toLCH(color));

const palettes = discoverPalettes(baseColorsLCH);

// { analogous: [...], complementary: [...], ... }

👋  discoverPalettes expects a minimum of four colors to function correctly.

Practical application

One of the most compelling aspects of discoverPalettes is its ability to pull coherent color combinations out of just about any source. Here it is, discovering palettes based on images from Unsplash:

light

Cool eh? Extracting palettes from photographs is a fantastic way of working when stuck for ideas, and discoverPalettes makes the process incredibly easy. This kind of approach, previously available only through “magic” color generators/apps, is now right at our fingers and ready to be tweaked, iterated, and improved to suit our own personal use-cases and preferences!

Challenge

Right now, our discoverPalettes function finds the best matches it can in an array of colors, but it isn’t too easy to control. Can you add a degree of bias/weighting to its selection? How might you modify the function to prioritize brighter colors, for example?

Function #3 — “Hue Shift”

For our third and final function, we will be taking inspiration from the world of pixel art!

Often when adding shades/highlights to a sprite, pixel artists will not only modulate the lightness/chroma of a color (saturation if working with HSL) but also shift its hue. Here’s an excellent video on the subject, but in short, this is what it looks like:

So pretty! As a color becomes lighter, its hue shifts up; as it becomes darker, it shifts down. When applied subtly, this technique helps ensure shades/tints of a color are vivid and impactful. When “dialed up” a little, it is a fantastic way of generating stunning standalone color palettes.

The code

function adjustHue(val) {
  if (val < 0) val += Math.ceil(-val / 360) * 360;

  return val % 360;
}

function map(n, start1, end1, start2, end2) {
  return ((n - start1) / (end1 - start1)) * (end2 - start2) + start2;
}

function createHueShiftPalette(opts) {
  const { base, minLightness, maxLightness, hueStep } = opts;

  const palette = [base];

  for (let i = 1; i < 5; i++) {
    const hueDark = adjustHue(base.h - hueStep * i);
    const hueLight = adjustHue(base.h + hueStep * i);
    const lightnessDark = map(i, 0, 4, base.l, minLightness);
    const lightnessLight = map(i, 0, 4, base.l, maxLightness);
    const chroma = base.c;

    palette.push({
      l: lightnessDark,
      c: chroma,
      h: hueDark,
      mode: "lch"
    });

    palette.unshift({
      l: lightnessLight,
      c: chroma,
      h: hueLight,
      mode: "lch"
    });
  }

  return palette;
}

To break this down into steps: 

  1. Pass a base color, min/max lightness, and hue step parameters to a createHueShiftPalette function. The min/max lightness values determine how dark/light our palette will be at either extreme. The step controls how much the hue will shift at each color.
  2. Store the base color in an array. In the illustration above, this is the middle color. 
  3. Create a loop that iterates four times. Each iteration, add a darker shade to the start of the array and a lighter tint to the end. Here, we use map to calculate our lightness values — a function that takes a number that usually exists in one range and converts it to another — and increase or decrease the hue using our hueStep variable. Again, adjustHue is used here to ensure all hue values are between 0 and 360.
  4. Return the palette! 

Usage 

Once our createHueShiftPalette function is defined, we can use it like so:

import { formatHex } from "https://cdn.skypack.dev/culori@2.0.0";

const hueShiftPalette = createHueShiftPalette({
  base: {
    l: 55,
    c: 75,
    h: 0,
    mode: "lch"
  },
  minLightness: 10,
  maxLightness: 90,
  hueStep: 12
});

const hueShiftPaletteHex = hueShiftPalette.map((color) => formatHex(color));

// ["#ffb97a", "#ff957c", "#ff727f", "#ff5083", "#f02f87", "#c70084", "#9a007f", "#6a0076", "#33006b"]

Practical application

The palettes generated by createHueShiftPalette work fantastically for patterns/graphics; here’s an example using it to create random/generative patterns that differ ever-so-slightly each time they render: 

light

Cool, right? As just one example using this approach, we can create UI elements that are always fresh and unique to the current user — a lovely way to bring a little joy to the folks who use our websites/applications! 

Challenge

Right now, the lightness/hue values scale linearly in our createHueShiftPalette function. Could you apply some easing to them? Perhaps, starting with a larger/smaller hue shift and reducing/increasing it with each step?

Wrapping up

Well, folks, that’s all for now! We have learned how to create three beautiful color generation functions, seen how they can be applied and considered how they could be improved/changed. From here, I hope you take these functions and change them to suit you, and hopefully, even write your own!

As developers, we have a unique skill set that is perfect for creating truly innovative, stunning design. Whether that means creating a color generation tool for designers you work with or adding mind-blowing generative palettes to your website — we should all feel confident in our ability to work with color.

Until next time! 

The post Coloring With Code — A Programmatic Approach To Design appeared first on Codrops.

Embrace the Unpredictable

In nature, no two things are ever the same. Life is imperfect, unpredictable, and beautiful. We can walk through the same forest every day and see differently colored leaves. We can look up at the clouds every minute and watch a whole new formation. The physical world is transient and ever-changing. What if our designs were a little more like this?

Often, we spend hours, weeks, even months carefully crafting our websites/applications, sculpting every last pixel until they are just right. Then, we set them free into the world — a perfectly formed, yet static snapshot of something that once was a living, evolving thing.

There is (of course!) nothing wrong with this way of working. But what if we let go of the idea that there can be only one final version of a design? What if our interfaces were free to take more than one form?

I could write forever about this stuff, but I think it’s best to show you what I mean. Naturally, here’s a Pen:

Try clicking the “Regenerate” button above. Notice how the interface changes just a little every time? By parameterizing aspects of a design, then randomizing those parameters, we can create near-infinite variations of a single idea. For those familiar with generative art — art made using a system that includes an element of autonomy — this is likely a familiar concept.

For makers (particularly perfectionists like me!), this approach to design can be incredibly liberating.

For the folk who use the things we make, it creates an experience that is truly individual. In randomizing carefully chosen aspects of our interfaces, they become ephemeral, and to me, this is kind of magical. No two people will ever see the same version of our work.

The web can be a cold, sterile place. By embracing the unpredictable, we can add a joyful, organic touch to our creations — to me, this is the essence of generative UI design, and I would love if you gave it a try! SVG, Canvas, and CSS/Paint API are all excellent mediums for generative work, so pick the one that is most familiar and experiment.

Just remember: apply carefully, and always be mindful of accessibility/UX. Magically evolving designs are great, but only if they are great for everyone.

Creating Generative Patterns with The CSS Paint API

The browser has long been a medium for art and design. From Lynn Fisher’s joyful A Single Div creations to Diana Smith’s staggeringly detailed CSS paintings, wildly creative, highly skilled developers have — over the years — continuously pushed web technologies to their limits and crafted innovative, inspiring visuals.

CSS, however, has never really had an API dedicated to… well, just drawing stuff! As demonstrated by the talented folks above, it certainly can render most things, but it’s not always easy, and it’s not always practical for production sites/applications.

Recently, though, CSS was gifted an exciting new set of APIs known as Houdini, and one of them — the Paint API — is specifically designed for rendering 2D graphics. For us web folk, this is incredibly exciting. For the first time, we have a section of CSS that exists for the sole purpose of programmatically creating images. The doors to a mystical new world are well and truly open!

In this tutorial, we will be using the Paint API to create three (hopefully!) beautiful, generative patterns that could be used to add a delicious spoonful of character to a range of websites/applications.

Spellbooks/text editors at the ready, friends, let’s do some magic!

Intended audience

This tutorial is perfect for folks who are comfortable writing HTML, CSS, and JavaScript. A little familiarity with generative art and some knowledge of the Paint API/HTML canvas will be handy but not essential. We will do a quick overview before we get started. Speaking of which…

Before we start

For a comprehensive introduction to both the Paint API and generative art/design, I recommend popping over to the first entry in this series. If you are new to either subject, this will be a great place to start. If you don’t feel like navigating another article, however, here are a couple of key concepts to be familiar with before moving on.

If you are already familiar with the CSS Paint API and generative art/design, feel free to skip ahead to the next section.

What is generative art/design?

Generative art/design is any work created with an element of chance. We define some rules and allow a source of randomness to guide us to an outcome. For example, a rule could be “if a random number is greater than 50, render a red square, if it is less than 50, render a blue square*,”* and, in the browser, a source of randomness could be Math.random().

By taking a generative approach to creating patterns, we can generate near-infinite variations of a single idea — this is both an inspiring addition to the creative process and a fantastic opportunity to delight our users. Instead of showing people the same imagery every time they visit a page, we can display something special and unique for them!

What is the CSS Paint API?

The Paint API gives us low-level access to CSS rendering. Through “paint worklets” (JavaScript classes with a special paint() function), it allows us to dynamically create images using a syntax almost identical to HTML canvas. Worklets can render an image wherever CSS expects one. For example:

.worklet-canvas {
  background-image: paint(workletName);
}

Paint API worklets are fast, responsive, and play ever so well with existing CSS-based design systems. In short, they are the coolest thing ever. The only thing they are lacking right now is widespread browser support. Here’s a table:

This browser support data is from Caniuse, which has more detail. A number indicates that browser supports the feature at that version and up.

Desktop

ChromeFirefoxIEEdgeSafari
65NoNo79No

Mobile / Tablet

Android ChromeAndroid FirefoxAndroidiOS Safari
96No96No

A little thin on the ground! That’s OK, though. As the Paint API is almost inherently decorative, we can use it as a progressive enhancement if it’s available and provide a simple, dependable fallback if not.

What we are making

In this tutorial, we will be learning how to create three unique generative patterns. These patterns are quite simple, but will act as a wonderful springboard for further experimentation. Here they are in all their glory!

The demos in this tutorial currently only work in Chrome and Edge.

“Tiny Specks”

“Bauhaus”

“Voronoi Arcs”

Before moving on, take a moment to explore the examples above. Try changing the custom properties and resizing the browser window — watch how the patterns react. Can you guess how they might work without peeking at the JavaScript?

Getting set up

To save time and eliminate the need for any custom build processes, we will be working entirely in CodePen throughout this tutorial. I have even created a “starter Pen” that we can use as a base for each pattern!

I know, it’s not much to look at… yet.

In the starter Pen, we are using the JavaScript section to write the worklet itself. Then, in the HTML section, we load the JavaScript directly using an internal <script> tag. As Paint API worklets are special workers (code that runs on a separate browser thread), their origin must1 exist in a standalone .js file.

Let’s break down the key pieces of code here.

If you have written Paint API worklets before, and are familiar with CodePen, you can skip ahead to the next section.

Defining the worklet class

First things first: Let’s check out the JavaScript tab. Here we define a worklet class with a simple paint() function:

class Worklet {
  paint(ctx, geometry, props) {
    const { width, height } = geometry;
    ctx.fillStyle = "#000";
    ctx.fillRect(0, 0, width, height);
  }
}

I like to think of a worklet’s paint() function as a callback. When the worklet’s target element updates (changes dimensions, modifies custom properties), it re-runs. A worklet’s paint() function automatically has a few parameters passed when it executes. In this tutorial, we are interested in the first three:

  • ctx — a 2D drawing context very similar to that of HTML canvas
  • geometry — an object containing the width/height dimensions of the worklet’s target element
  • props — an array of CSS custom properties that we can “watch” for changes and re-render when they do. These are a great way of passing values to paint worklets.

Our starter worklet renders a black square that covers the entire width/height of its target element. We will completely rewrite this paint() function for each example, but it’s nice to have something defined to check things are working.

Registering the worklet

Once a worklet class is defined, it needs to be registered before we can use it. To do so, we call registerPaint in the worklet file itself:

if (typeof registerPaint !== "undefined") {
  registerPaint("workletName", Worklet);
}

Followed by CSS.paintWorklet.addModule() in our “main” JavaScript/HTML:

<script id="register-worklet">
  if (CSS.paintWorklet) {
    CSS.paintWorklet.addModule('https://codepen.io/georgedoescode/pen/bGrMXxm.js');
  }
</script>

We are checking registerPaint is defined before running it here, as our pen’s JavaScript will always run once on the main browser thread — registerPaint only becomes available once the JavaScript file is loaded into a worklet using CSS.paintWorklet.addModule(...).

Applying the worklet

Once registered, we can use our worklet to generate an image for any CSS property that expects one. In this tutorial, we will focus on background-image:

.worklet-canvas {
  background-image: paint(workletName);
}

Package imports

You may notice a couple of package imports dangling at the top of the starter pen’s worklet file:

import random from "https://cdn.skypack.dev/random";
import seedrandom from "https://cdn.skypack.dev/seedrandom";
Can you guess what they are?

Random number generators!

All three of the patterns we are creating in this tutorial rely heavily on randomness. Paint API worklets should, however, (almost) always be deterministic. Given the same input properties and dimensions, a worklet’s paint() function should always render the same thing.

Why?

  1. The Paint API may want to use a cached version of a worklet’s paint() output for better performance. Introducing an unpredictable element to a worklet renders this impossible!
  2. A worklet’s paint() function re-runs whenever the element it applies to changes dimensions. When coupled with “pure” randomness, this can result in significant flashes of content — a potential accessibility issue for some folks.

For us, all this renders Math.random() a little useless, as it is entirely unpredictable. As an alternative, we are pulling in random (an excellent library for working with random numbers) and seedrandom (a pseudo-random number generator to use as its base algorithm).

As a quick example, here’s a “random circles” worklet using a pseudo-random number generator:

And here’s a similar worklet using Math.random(). Warning: Resizing the element results in flashing imagery.

There’s a little resize handle in the bottom-right of both of the above patterns. Try resizing both elements. Notice the difference?

Setting up each pattern

Before beginning each of the following patterns, navigate to the starter Pen and click the “Fork” button in the footer. Forking a Pen creates a copy of the original the moment you click the button. From this point, it is yours to do whatever you like.

Once you have forked the starter Pen, there is a critical extra step to complete. The URL passed to CSS.paintWorklet.addModule must be updated to point to the new fork’s JavaScript file. To find the path for your fork’s JavaScript, take a peek at the URL shown in your browser. You want to grab your fork’s URL with all query parameters removed, and append .js — something like this:

Lovely. That’s the ticket! Once you have the URL for your JavaScript, make sure you update it here:

<script id="register-worklet">
  if (CSS.paintWorklet) {
    // ⚠️ hey friend! update the URL below each time you fork this pen! ⚠️
    CSS.paintWorklet.addModule('https://codepen.io/georgedoescode/pen/QWMVdPG.js');
  }
</script>

When working with this setup, you may occasionally need to manually refresh the Pen in order to see your changes. To do so, hit CMD/CTRL + Shift + 7.

Pattern #1 (Tiny Specks)

OK, we are ready to make our first pattern. Fork the starter Pen, update the .js file reference, and settle in for some generative fun!

As a quick reminder, here’s the finished pattern:

Updating the worklet’s name

Once again, first things first: Let’s update the starter worklet’s name and relevant references:

class TinySpecksPattern {
  // ...
}
if (typeof registerPaint !== "undefined") {
  registerPaint("tinySpecksPattern", TinySpecksPattern);
}
.worklet-canvas {
  /* ... */
  background-image: paint(tinySpecksPattern);
}

Defining the worklet’s input properties

Our “Tiny Specks” worklet will accept the following input properties:

  • --pattern-seed — a seed value for the pseudo-random number generator
  • --pattern-colors — the available colors for each speck
  • --pattern-speck-count — how many individual specks the worklet should render
  • --pattern-speck-min-size — the minimum size for each speck
  • --pattern-speck-max-size — the maximum size for each speck

As our next step, let’s define the inputProperties our worklet can receive. To do so, we can add a getter to our TinySpecksPattern class:

class TinySpecksPattern {
  static get inputProperties() {
    return [
      "--pattern-seed",
      "--pattern-colors",
      "--pattern-speck-count",
      "--pattern-speck-min-size",
      "--pattern-speck-max-size"
    ];
  }
  // ...
}

Alongside some custom property definitions in our CSS:

@property --pattern-seed {
  syntax: "<number>";
  initial-value: 1000;
  inherits: true;
}

@property --pattern-colors {
  syntax: "<color>#";
  initial-value: #161511, #dd6d45, #f2f2f2;
  inherits: true;
}

@property --pattern-speck-count {
  syntax: "<number>";
  initial-value: 3000;
  inherits: true;
}

@property --pattern-speck-min-size {
  syntax: "<number>";
  initial-value: 0;
  inherits: true;
}

@property --pattern-speck-max-size {
  syntax: "<number>";
  initial-value: 3;
  inherits: true;
}

We are using the Properties and Values API here (another member of the Houdini family) to define our custom properties. Doing so affords us two valuable benefits. First, we can define sensible defaults for the input properties our worklet expects. A tasty sprinkle of developer experience! Second, by including a syntax definition for each custom property, our worklet can interpret them intelligently.

For example, we define the syntax <color># for --pattern-colors. In turn, this allows us to pass an array of comma-separated colors to the worklet in any valid CSS color format. When our worklet receives these values, they have been converted to RGB and placed in a neat little array. Without a syntax definition, worklets interpret all props as simple strings.

Like the Paint API, the Properties and Values API also has limited browser support.

The paint() function

Awesome! Here’s the fun bit. We have created our “Tiny Speck” worklet class, registered it, and defined what input properties it can expect to receive. Now, let’s make it do something!

As a first step, let’s clear out the starter Pen’s paint() function, keeping only the width and height definitions:

paint(ctx, geometry, props) {
  const { width, height } = geometry;
}

Next, let’s store our input properties in some variables:

const seed = props.get("--pattern-seed").value;
const colors = props.getAll("--pattern-colors").map((c) => c.toString());
const count = props.get("--pattern-speck-count").value;
const minSize = props.get("--pattern-speck-min-size").value;
const maxSize = props.get("--pattern-speck-max-size").value;

Next, we should initialize our pseudo-random number generator:

random.use(seedrandom(seed));

Ahhh, predictable randomness! We are re-seeding seedrandom with the same seed value every time paint() runs, resulting in a consistent stream of random numbers across renders.

Finally, let’s paint our specks!

First off, we create a for-loop that iterates count times. In every iteration of this loop, we are creating one individual speck:

for (let i = 0; i < count; i++) {
}

As the first action in our for-loop, we define an x and y position for the speck. Somewhere between 0 and the width/height of the worklet’s target element is perfect:

const x = random.float(0, width);
const y = random.float(0, height);

Next, we choose a random size (for the radius):

const radius = random.float(minSize, maxSize);

So, we have a position and a size defined for the speck. Let’s choose a random color from our colors to fill it with:

ctx.fillStyle = colors[random.int(0, colors.length - 1)];

Alright. We are all set. Let’s use ctx to render something!

The first thing we need to do at this point is save() the state of our drawing context. Why? We want to rotate each speck, but when working with a 2D drawing context like this, we cannot rotate individual items. To rotate an object, we have to spin the entire drawing space. If we don’t save() and restore() the context, the rotation/translation in every iteration will stack, leaving us with a very messy (or empty) canvas!

ctx.save();

Now that we have saved the drawing context’s state, we can translate to the speck’s center point (defined by our x/y variables) and apply a rotation. Translating to the center point of an object before rotating ensures the object rotates around its center axis:

ctx.translate(x, y);
ctx.rotate(((random.float(0, 360) * 180) / Math.PI) * 2);
ctx.translate(-x, -y);

After applying our rotation, we translate back to the top-left corner of the drawing space.

We choose a random value between 0 and 360 (degrees) here, then convert it into radians (the rotation format ctx understands).

Awesome! Finally, let’s render an ellipse — this is the shape that defines our specks:

ctx.beginPath();
ctx.ellipse(x, y, radius, radius / 2, 0, Math.PI * 2, 0);
ctx.fill();

Here’s a simple pen showing the form of our random specks, a little closer up:

Perfect. Now, all we need to do is restore the drawing context:

ctx.restore();

That’s it! Our first pattern is complete. Let’s also apply a background-color to our worklet canvas to finish off the effect:

.worklet-canvas {
  background-color: #90c3a5;
  background-image: paint(tinySpecksPattern);
}

Next steps

From here, try changing the colors, shapes, and distribution of the specks. There are hundreds of directions you could take this pattern! Here’s an example using little triangles rather than ellipses:

Onwards!

Pattern #2 (Bauhaus)

Nice work! That’s one pattern down. Onto the next one. Once again, fork the starter Pen and update the worklet’s JavaScript reference to get started.

As a quick refresher, here’s the finished pattern we are working toward:

Updating the worklet’s name

Just like we did last time, let’s kick things off by updating the worklet’s name and relevant references:

class BauhausPattern {
  // ...
}

if (typeof registerPaint !== "undefined") {
  registerPaint("bauhausPattern", BauhausPattern);
}
.worklet-canvas {
  /* ... */
  background-image: paint(bauhausPattern);
}

Lovely.

Defining the worklet’s input properties

Our “Bauhaus Pattern” worklet expects the following input properties:

  • --pattern-seed — a seed value for the pseudo-random number generator
  • --pattern-colors — the available colors for each shape in the pattern
  • --pattern-size — the value used to define both the width and height of a square pattern area
  • --pattern-detail — the number of columns/rows to divide the square pattern into

Let’s add these input properties to our worklet:

class BahausPattern {
  static get inputProperties() {
    return [
      "--pattern-seed",
      "--pattern-colors",
      "--pattern-size",
      "--pattern-detail"
    ];
  }
  // ...
}

…and define them in our CSS, again, using the Properties and Values API:

@property --pattern-seed {
  syntax: "<number>";
  initial-value: 1000;
  inherits: true;
}

@property --pattern-colors {
  syntax: "<color>#";
  initial-value: #2d58b5, #f43914, #f9c50e, #ffecdc;
  inherits: true;
}

@property --pattern-size {
  syntax: "<number>";
  initial-value: 1024;
  inherits: true;
}

@property --pattern-detail {
  syntax: "<number>";
  initial-value: 12;
  inherits: true;
}

Excellent. Let’s paint!

The paint() function

Again, let’s clear out the starter worklet’s paint function, leaving only the width and height definition:

paint(ctx, geometry, props) {
  const { width, height } = geometry;
}

Next, let’s store our input properties in some variables:

const patternSize = props.get("--pattern-size").value;
const patternDetail = props.get("--pattern-detail").value;
const seed = props.get("--pattern-seed").value;
const colors = props.getAll("--pattern-colors").map((c) => c.toString());

Now, we can seed our pseudo-random number generator just like before:

random.use(seedrandom(seed));

Awesome! As you might have noticed, the setup for Paint API worklets is always somewhat similar. It’s not the most exciting process, but it’s an excellent opportunity to reflect on the architecture of your worklet and how other developers may use it.

So, with this worklet, we create a fixed-dimension square pattern filled with shapes. This fixed-dimension pattern is then scaled up or down to cover the worklet’s target element. Think of this behavior a bit like background-size: cover in CSS!

Here’s a diagram:

To achieve this behavior in our code, let’s add a scaleContext function to our worklet class:

scaleCtx(ctx, width, height, elementWidth, elementHeight) {
  const ratio = Math.max(elementWidth / width, elementHeight / height);
  const centerShiftX = (elementWidth - width * ratio) / 2;
  const centerShiftY = (elementHeight - height * ratio) / 2;
  ctx.setTransform(ratio, 0, 0, ratio, centerShiftX, centerShiftY);
}

And call it in our paint() function:

this.scaleCtx(ctx, patternSize, patternSize, width, height);

Now, we can work to a set of fixed dimensions and have our worklet’s drawing context automatically scale everything for us — a handy function for lots of use cases.

Next up, we are going to create a 2D grid of cells. To do so, we define a cellSize variable (the size of the pattern area divided by the number of columns/rows we would like):

const cellSize = patternSize / patternDetail;

Then, we can use the cellSize variable to “step-through” the grid, creating equally-spaced, equally-sized cells to add random shapes to:

for (let x = 0; x < patternSize; x += cellSize) {
  for (let y = 0; y < patternSize; y += cellSize) {
  }
}

Within the second nested loop, we can begin to render stuff!

First off, let’s choose a random color for the current shape:

const color = colors[random.int(0, colors.length - 1)];

ctx.fillStyle = color;

Next, let’s store a reference to the current cell’s center x and y position:

const cx = x + cellSize / 2;
const cy = y + cellSize / 2;

In this worklet, we are positioning all of our shapes relative to their center point. While we are here, let’s add some utility functions to our worklet file to help us quickly render center-aligned shape objects. These can live outside of the Worklet class:

function circle(ctx, cx, cy, radius) {
  ctx.beginPath();
  ctx.arc(cx, cy, radius, 0, Math.PI * 2);
  ctx.closePath();
}

function arc(ctx, cx, cy, radius) {
  ctx.beginPath();
  ctx.arc(cx, cy, radius, 0, Math.PI * 1);
  ctx.closePath();
}

function rectangle(ctx, cx, cy, size) {
  ctx.beginPath();
  ctx.rect(cx - size / 2, cy - size / 2, size, size);
  ctx.closePath();
}

function triangle(ctx, cx, cy, size) {
  const originX = cx - size / 2;
  const originY = cy - size / 2;
  ctx.beginPath();
  ctx.moveTo(originX, originY);
  ctx.lineTo(originX + size, originY + size);
  ctx.lineTo(originX, originY + size);
  ctx.closePath();
}

I won’t go into too much detail here, but here’s a diagram visualizing how each of these functions work:

If you get stuck on the graphics rendering part of any of the worklets in this tutorial, look at the MDN docs on HTML canvas. The syntax/usage is almost identical to the 2D graphics context available in Paint API worklets.

Cool! Let’s head back over to our paint() function’s nested loop. The next thing we need to do is choose what shape to render. To do so, we can pick a random string from an array of possibilities:

const shapeChoice = ["circle", "arc", "rectangle", "triangle"][
  random.int(0, 3)
];

We can also pick a random rotation amount in a very similar way:

const rotationDegrees = [0, 90, 180][random.int(0, 2)];

Perfect. We are ready to render!

To start, let’s save our drawing context’s state, just like in the previous worklet:

ctx.save();

Next, we can translate to the center point of the current cell and rotate the canvas using the random value we just chose:

ctx.translate(cx, cy);
ctx.rotate((rotationDegrees * Math.PI) / 180);
ctx.translate(-cx, -cy);

Now we can render the shape itself! Let’s pass our shapeChoice variable to a switch statement and use it to decide which shape rendering function to run:

switch (shapeChoice) {
  case "circle":
    circle(ctx, cx, cy, cellSize / 2);
    break;
  case "arc":
    arc(ctx, cx, cy, cellSize / 2);
    break;
  case "rectangle":
    rectangle(ctx, cx, cy, cellSize);
    break;
  case "triangle":
    triangle(ctx, cx, cy, cellSize);
    break;
}

ctx.fill();

Finally, all we need to do is restore() our drawing context ready for the next shape:

ctx.restore();

With that, our Bauhaus Grids worklet is complete!

Next steps

There are so many directions you could take this worklet. How could you parameterize it further? Could you add a “bias” for specific shapes/colors? Could you add more shape types?

Always experiment — following along with the examples we are creating together is an excellent start, but the best way to learn is to make your own stuff! If you are stuck for inspiration, take a peek at some patterns on Dribbble, look to your favorite artists, the architecture around you, nature, you name it!

As a simple example, here’s the same worklet, in an entirely different color scheme:

Pattern #3 (Voronoi Arcs)

So far, we have created both a chaotic pattern and one that aligns strictly to a grid. For our last example, let’s build one that sits somewhere between the two.

As one last reminder, here’s the finished pattern:

Before we jump in and write any code, let’s take a look at how this worklet… works.

A brief introduction to Voronoi tessellations

As suggested by the name, this worklet uses something called a Voronoi tessellation to calculate its layout. A Voronoi tessellation (or diagram) is, in short, a way to partition a space into non-overlapping polygons.

We add a collection of points to a 2D space. Then for each point, calculate a polygon that contains only it and no other points. Once calculated, the polygons can be used as a kind of “grid” to position anything.

Here’s an animated example:

The fascinating thing about Voronoi-based layouts is that they are responsive in a rather unusual way. As the points in a Voronoi tessellation move around, the polygons automatically re-arrange themselves to fill the space!

Try resizing the element below and watch what happens!

Cool, right?

If you would like to learn more about all things Voronoi, I have an article that goes in-depth. For now, though, this is all we need.

Updating the worklet’s name

Alright, folks, we know the deal here. Let’s fork the starter Pen, update the JavaScript import, and change the worklet’s name and references:

class VoronoiPattern {
  // ...
}

if (typeof registerPaint !== "undefined") {
  registerPaint("voronoiPattern", VoronoiPattern);
}
.worklet-canvas {
  /* ... */
  background-image: paint(voronoiPattern);
}

Defining the worklet’s input properties

Our VoronoiPattern worklet expects the following input properties:

  • --pattern-seed — a seed value for the pseudo-random number generator
  • --pattern-colors — the available colors for each arc/circle in the pattern
  • --pattern-background — the pattern’s background color

Let’s add these input properties to our worklet:

class VoronoiPattern {
  static get inputProperties() {
    return ["--pattern-seed", "--pattern-colors", "--pattern-background"];
  }
  // ...
}

…and register them in our CSS:

@property --pattern-seed {
  syntax: "<number>";
  initial-value: 123456;
  inherits: true;
}

@property --pattern-background {
  syntax: "<color>";
  inherits: false;
  initial-value: #141b3d;
}

@property --pattern-colors {
  syntax: "<color>#";
  initial-value: #e9edeb, #66aac6, #e63890;
  inherits: true;
}

Nice! We are all set. Overalls on, friends — let us paint.

The paint() function

First, let’s clear out the starter worklet’s paint() function, retaining only the width and height definitions. We can then create some variables using our input properties, and seed our pseudo-random number generator, too. Just like in our previous examples:

paint(ctx, geometry, props) {
  const { width, height } = geometry;

  const seed = props.get("--pattern-seed").value;
  const background = props.get("--pattern-background").toString();
  const colors = props.getAll("--pattern-colors").map((c) => c.toString());

  random.use(seedrandom(seed));
}

Before we do anything else, let’s paint a quick background color:

ctx.fillStyle = background;
ctx.fillRect(0, 0, width, height);

Next, let’s import a helper function that will allow us to quickly cook up a Voronoi tessellation:

import { createVoronoiTessellation } from "https://cdn.skypack.dev/@georgedoescode/generative-utils";

This function is essentially a wrapper around d3-delaunay and is part of my generative-utils repository. You can view the source code on GitHub. With “classic” data structures/algorithms such as Voronoi tessellations, there is no need to reinvent the wheel — unless you want to, of course!

Now that we have our createVoronoiTessellation function available, let’s add it to paint():

const { cells } = createVoronoiTessellation({
  width,
  height,
  points: [...Array(24)].map(() => ({
    x: random.float(0, width),
    y: random.float(0, height)
  }))
});

Here, we create a Voronoi Tessellation at the width and height of the worklet’s target element, with 24 controlling points.

Awesome. Time to render our shapes! Lots of this code should be familiar to us, thanks to the previous two examples.

First, we loop through each cell in the tessellation:

cells.forEach((cell) => {
});

For each cell, the first thing we do is choose a color:

ctx.fillStyle = colors[random.int(0, colors.length - 1)];

Next, we store a reference to the center x and y values of the cell:

const cx = cell.centroid.x;
const cy = cell.centroid.y;

Next, we save the context’s current state and rotate the canvas around the cell’s center point:

ctx.save();

ctx.translate(cx, cy);
ctx.rotate((random.float(0, 360) / 180) * Math.PI);
ctx.translate(-cx, -cy);

Cool! Now, we can render something. Let’s draw an arc with an end angle of either PI or PI * 2. To me and you, a semi-circle or a circle:

ctx.beginPath();
ctx.arc(
  cell.centroid.x,
  cell.centroid.y,
  cell.innerCircleRadius * 0.75,
  0,
  Math.PI * random.int(1, 2)
);
ctx.fill();

Our createVoronoiTessellation function attaches a special innerCircleRadius to each cell — this is the largest possible circle that can fit at its center without touching any edges. Think of it as a handy guide for scaling objects to the bounds of a cell. In the snippet above, we are using innerCircleRadius to determine the size of our arcs.

Here’s a simple pen highlighting what’s happening here:

Now that we have added a “primary” arc to each cell, let’s add another one, 25% of the time. This time, however, we can set the arc’s fill color to our worklets background color. Doing so gives us the effect of a little hole in the middle of some of the shapes!

if (random.float(0, 1) > 0.25) {
  ctx.fillStyle = background;
  ctx.beginPath();
  ctx.arc(
    cell.centroid.x,
    cell.centroid.y,
    (cell.innerCircleRadius * 0.75) / 2,
    0,
    Math.PI * 2
  );
  ctx.fill();
}

Great! All we need to do now is restore the drawing context:

ctx.restore();

And, that’s it!

Next steps

The beautiful thing about Voronoi tessellations is that you can use them to position anything at all. In our example, we used arcs, but you could render rectangles, lines, triangles, whatever! Perhaps you could even render the outlines of the cells themselves?

Here’s a version of our VoronoiPattern worklet that renders lots of small lines, rather than circles and semicircles:

Randomizing patterns

You may have noticed that up until this point, all of our patterns have received a static --pattern-seed value. This is fine, but what if we would like our patterns to be random each time they display? Well, lucky for us, all we need to do is set the --pattern-seed variable when the page loads to be a random number. Something like this:

document.documentElement.style.setProperty('--pattern-seed', Math.random() * 10000);

We touched on this briefly earlier, but this is a lovely way to make sure a webpage is a tiny bit different for everyone that sees it.

Until next time

Well, friends, what a trip!

We have created three beautiful patterns together, learned lots of handy Paint API tricks, and (hopefully!) had some fun, too. From here, I hope you feel inspired to make some more generative art/design with CSS Houdini! I’m not sure about you, but I feel like my portfolio site needs a new coat of paint…

Until next time, fellow CSS magicians!

Oh! Before you go, I have a challenge for you. There is a generative Paint API worklet running on this very page! Can you spot it?

  1. There are certainly ways around this rule, but they can be complex and not entirely suitable for this tutorial.

The post Creating Generative Patterns with The CSS Paint API appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

Conjuring Generative Blobs With The CSS Paint API

The CSS Paint API (part of the magical Houdini family) opens the door to an exciting new world of design in CSS. Using the Paint API, we can create custom shapes, intricate patterns, and beautiful animations — all with a touch of randomness — in a way that is portable, fast, and responsive.

We are going to dip our toes into the bubbling cauldron of generative CSS magic by creating my favorite shape, the blob. Random blobs are a great starting point for anyone new to generative art/design, and we will be learning the CSS Paint API as we go, so this is an ideal starting point for folks who are new to this world. You’ll be a generative CSS magician in no time!

Let’s hop on our broomsticks and conjure up some shapes.

Generative?

For some folks reading this, generative art may be an unfamiliar topic. If you are already comfortable with generative art/design, feel free to hop down to the next section. If not, here’s a little example:

Imagine, for a moment, that you are sitting at a desk. You have three stamps, some dice, and a piece of paper. Each of the stamps has a different shape on it. There is a square, a line, and a circle. You roll the dice. If the dice land on one, you use the square stamp on the page. If the dice lands on two, you use the line stamp. If it lands on three, you use the circle stamp. If the dice reads four, five, or six, you do nothing. You repeat the roll-and-stamp process until the page fills with shapes — this is generative art!

It can seem a little scary at first, but really, that’s all “generative” means — something created with an element of chance/unpredictability. We define some rules and let a source of randomness guide us to an outcome. In the “analog” example above, the randomness source is some dice. When we are working in the browser, it could be Math.random() or another similar function.

To bring things back to the land of ones and zeros for a moment, this is what the above example would look like if written in code:

Pretty cool, eh? By defining some simple rules and actioning them at random, we have created a unique pattern. In this tutorial series, we will use generative techniques just like this to create exciting user interfaces.

What is the CSS Paint API, and what’s a worklet?

The CSS Paint API allows us low-level access to CSS itself(!) through an HTML5 <canvas>-like drawing API. We can harness this power with something called a worklet.

Worklets, in short, are JavaScript classes. Each worklet class must have a paint() function. A worklet’s paint() function can programmatically create an image for any CSS property that expects one.

For example:

.my-element {
  background-image: paint(texture);
}

Here, we have a fictional texture worklet that generates a beautiful (I’ll leave this up to your imagination), programmatic texture. Where we might usually assign a url(...) value to the background-image property, we instead call paint(worklet_name) — this runs the worklet’s paint() function and renders the output to the target element.

We will be getting into how to write worklets in detail shortly, but I wanted to give you a quick primer on what they are before I start talking about them.

What we are building

So, in this tutorial, we will be building a generative blob worklet. Our worklet will take a few input parameters (as CSS Custom Properties, more on this a little later) and return a beautiful, random blob shape.

Let’s get started by checking out some examples of the finished worklet in action — if a picture paints a thousand words, a CodePen must paint a million, right?

The blob worklet, as a background image

First, here’s a demo of the blob worklet just hanging out on its own, generating a value for the background-image property of an element:

I encourage you to look at the CSS for the above CodePen, change the custom properties, resize the element, and see what happens. See how the shape resizes fluidly and updates when the custom properties change? Don’t worry about understanding how this works right now. At this stage, we are only concerned with what we are building.

Generative image masks, a practical use case

Awesome, now that we have seen the “standalone” worklet, let’s check out how we can use it. In this example, the worklet functions as a generative image mask:

The result (I think) is rather striking. The worklet adds a natural, eye-catching curve to the design. In addition, the mask shape is different each time the page loads, which is a fantastic way to keep the UI fresh and exciting — click “rerun” on the CodePen above to see this effect in action. This ever-changing behavior is subtle, for sure, but I hope it will bring folks who notice it a little bit of joy. The web can be quite a cold, sterile place, and generative touches like this can make it feel a lot more organic!

Note: I’m certainly not suggesting we all start making our entire interfaces change at random. That would be terrible for usability! This kind of behavior works best when applied sparingly and only to presentational elements of your website or app. Think blog post headers, hero images, subtle background patterns, etc.

Now, this is just one example (and simple one, at that), but I hope it gives you some ideas on how you could use the blob worklet in your own design and development. For anyone looking for some extra inspiration, a quick Dribbble search for “blobs” should give you a whole heap of ideas!

Wait, do I need the CSS Paint API to make blobs?

In short, no!

There are, in fact, a plethora of ways to make blobs to use in your UI design. You could reach for a tool like Blobmaker, do some magic with border-radius, use a regular <canvas> element, whatever! There are tons of roads leading to blob city.

None of these, however, are quite the same as using the CSS Paint API. Why?

Well, to name a few reasons…

It allows us to be expressive in our CSS

Instead of dragging around sliders, tweaking radii, or endlessly clicking “regenerate” in the hope that a perfect blob comes our way, we can use just a few human-readable values to get what we need.

For example, the blob worklet we will be building in this tutorial takes the following input properties:

.worklet-target {
  --blob-seed: 123456;
  --blob-num-points: 8;
  --blob-variance: 0.375;
  --blob-smoothness: 1;
  --blob-fill: #000;
}

Need your blobs to be super subtle and minimal? Reduce the --blob-variance custom property. Need them to be detailed and overstated? Bring it up!

Fancy redesigning your site in a more brutalist direction? No problem! Instead of re-exporting hundreds of assets or custom coding a bunch of border-radius properties, simply reduce the --blob-smoothness custom property to zero:

Handy, eh? The CSS Paint API, through worklets, allows us to create ever-unique UI elements that fit right in with a design system.

Note: I am using GSAP in the examples above to animate the input properties of the paint worklet we are building in this tutorial.

It is super performant

It just so happens that generative work can get a little heavy, computation-wise. We often find ourselves looping through lots of elements, performing calculations, and other fun stuff. When we factor in that we may need to create multiple programmatic, generative visuals on a page, performance issues could become a risk.

Luckily for us, CSS Paint API worklets do all their magic outside of the main browser thread. The main browser thread is where all of the JavaScript we usually write exists and executes. Writing code this way is perfectly OK (and generally preferable), **but it can have limitations. When we try and do too much on the main browser thread, the can UI become sluggish or even blocked.

As worklets run on a different thread to the main website or app, they will not “block” or slow down the interface. Additionally, this means that the browser can spin up lots of separate worklet instances that it can call on when needed — this is similar to containerization and results in blazing fast performance!

It won’t clutter the DOM

Because the CSS Paint API essentially adds an image to a CSS property, it doesn’t add any extra elements to the DOM. To me, this feels like a super clean approach to creating generative visual elements. Your HTML structure remains clear, semantic, and unpolluted, while your CSS handles how things look.

Browser support

It is worth noting that the CSS Paint API is a relatively new technology, and although support is growing, it is still unavailable in some major browsers. Here is a browser support table:

This browser support data is from Caniuse, which has more detail. A number indicates that browser supports the feature at that version and up.

Desktop

ChromeFirefoxIEEdgeSafari
65NoNo79No

Mobile / Tablet

Android ChromeAndroid FirefoxAndroidiOS Safari
92No92No

Although browser support is still a little thin on the ground — in this tutorial, we will be looking at how to use the css-paint-polyfill maintained by GoogleChromeLabs to make sure users in all browsers can enjoy our creations.

We will, additionally, be looking at how to “fail gracefully” when the CSS Paint API is unsupported. A polyfill means extra JavaScript weight, so for some folks, it is not a viable solution. If this is you, don’t worry. We will be exploring browser support options for everyone.

Let’s code!

OK, OK! We know what we are building and why the CSS Paint API rocks — now let’s get coding! First things first, let’s get a development environment spun up.

Note: if you get a little lost at any point during in this tutorial, you can view a finished version of the worklet.

A simple development environment

To get us started, I have created a worklet-starter-kit repository. As a first step, pop on over to GitHub and clone it. Once you have cloned and navigated inside the repo, run:

npm install

Followed by:

npm run start

Once you have run the above commands, a simple development server fires up in the current directory, and your default browser opens. As worklets must be loaded either over HTTPS or from localhost — this setup ensures that we can use our worklet without any CORS issues. The starter kit also handles automatically refreshing the browser when we make any changes.

As well as serving our content and providing a basic live reload, this repository features a simple build step. Powered by esbuild, this process bundles up any JavaScript imports inside our worklet and outputs the result to a worklet.bundle.js file. Any changes made in worklet.js are automatically reflected in worklet.bundle.js.

If you have a poke around the repository, you might notice that there is already some HTML and CSS kicking around. We have a simple index.html file with a single worklet-canvas div, alongside some CSS to center it on the page and scale it to the viewport. Think of this as a blank canvas for all of your worklet experimentation!

Initializing our worklet

OK, now that we have our development environment up and running, it’s time to create our worklet. Let’s start by navigating to the worklet.js file.

Note: Remember, worklet.bundle.js is automatically generated by our build step. We don’t ever want to edit this file directly.

In our worklet.js file, we can define our Blob class and register it with the registerPaint function. We pass two values to registerPaint — the name we would like our worklet to have (in our case, blob) and the class that defines it:

class Blob {}

registerPaint("blob", Blob);

Excellent! We just took the first step towards creating our blobs!

Adding a paint() function

Now, not much is happening yet, so let’s add a simple paint() function to our Blob class to check things are working OK:

paint(ctx, geometry, properties) {
  console.log(`Element size is ${geometry.width}x${geometry.height}`);

  ctx.fillStyle = "tomato";
  ctx.fillRect(0, 0, geometry.width, geometry.height);
}

We can think of this paint() function like a callback. It runs, initially, when the worklet’s target element first renders. After this, any time the element’s dimensions change or the worklet’s input properties update, it runs again.

When the paint() function is called, it automatically has a few values passed through it. In this tutorial, we are making use of the first three:

  1. context — a 2D drawing context similar to that of a <canvas> element, we use this to draw things.
  2. geometry — an object containing the width and height of the target element
  3. properties — an array of custom properties

Now that we have a simple paint() function defined, let’s pop over to the index.html file and load our worklet. To do so, we are going to add a new <script> just before our closing </body> tag:

<script>
  if (CSS["paintWorklet"] !== undefined) {
    CSS.paintWorklet.addModule("./worklet.bundle.js");
  }
</script>

Note: we are registering the bundled version of our worklet!

Excellent. Our blob worklet is now loaded and ready for use in our CSS. Let’s use it to generate a background-image for our worklet-canvas class:

.worklet-canvas {
  background-image: paint(blob);
}

Once you have added the above snippet, you should see a red square. Our worklet is alive! Nice work. If you resize the browser window, you should see the worklet-canvas element’s dimensions printed in the browser console. Remember, the paint() function runs whenever the worklet target’s dimensions change.

Defining the worklet’s input properties

To allow our worklet to generate beautiful blobs, we need to help it out and pass it some properties. The properties we need are:

  • --blob-seed — a “seed” value for a pseudorandom number generator; more on this in a moment
  • --blob-num-points — how detailed the blob is based on the number of points used along the shape
  • --blob-variance — how varied the blob’s control points are
  • --blob-smoothness — the smoothness/sharpness of the blob’s edges
  • --blob-fill — the blob’s fill color

Let’s tell our worklet that it will receive these properties and that it needs to watch them for changes. To do so, we can head back over to our Blob class and add an inputProperties getter:

static get inputProperties() {
  return [
    "--blob-seed",
    "--blob-num-points",
    "--blob-variance",
    "--blob-smoothness",
    "--blob-fill",
  ];
}

Cool. Now that our worklet knows what input properties to expect, we should add them to our CSS:

.worklet-canvas {
  --blob-seed: 123456;
  --blob-num-points: 8;
  --blob-variance: 0.375;
  --blob-smoothness: 1;
  --blob-fill: #000;
}

Now, at this point, we could use the CSS Properties and Values API (another member of the Houdini family) **to assign some defaults and make these custom properties a little easier to parse in our worklet. Unfortunately, however, at this moment, the Properties and Values API does not have the best browser support.

For now, to keep things simple, we are going to leave our custom properties as they are — relying on some basic parsing functions in our worklet instead.

Heading back to our worklet class for a moment, let’s add these utility functions:

propToString(prop) {
  return prop.toString().trim();
}

propToNumber(prop) {
  return parseFloat(prop);
}

In the absence of the Properties and Values API, these simple utility functions will help us convert the properties passed to paint() to usable values.

Using our new helper functions, we can parse properties and define some variables to use in our paint() function. Let’s remove the old “debug” code, too:

paint(ctx, geometry, properties) {
  const seed = this.propToNumber(properties.get("--blob-seed"));
  const numPoints = this.propToNumber(properties.get("--blob-num-points"));
  const variance = this.propToNumber(properties.get("--blob-variance"));
  const smoothness = this.propToNumber(properties.get("--blob-smoothness"));
  const fill = this.propToString(properties.get("--blob-fill"));
}

If you log any of these variables, you should see that the properties made available by the paint() function map exactly to the Custom Properties we defined in our CSS a moment ago.

If you open up dev-tools, inspect the worklet-canvas element, and change any of these custom properties — you should see that the logs re-run and reflect the updated value. Why? Our worklet reacts to any changes to its input properties and re-runs its paint() function when it detects them.

OK, folks, it’s time to start forming our blob shape. To do this, we need a way of generating random numbers. After all, this is what will make our blobs generative!

Now, you may be thinking, “Hey, we can use Math.random() for this!” and in many ways, you would be right on. There is, however, a problem with using a “regular” random number generator in CSS Paint API worklets. Let’s check it out.

The problem with Math.random()

We noticed earlier how a worklet’s paint() function runs rather often. If we use a method such as Math.random() to generate random values within paint() — they will be different each time the function executes. Different random numbers mean a different visual result every time the worklet re-renders. We do not want this at all. Sure, we want our blobs to be random, but only at the point of conception. They shouldn’t change once they exist on the page unless we explicitly tell them to do so.

I found this concept a little tricky to get my head around at first, so I have made a couple of CodePens (best viewed in a browser that natively supports the CSS Paint API) to help demonstrate. In the first example, we have a worklet that sets a random background color, using Math.random():

Warning: resizing the element below will result in a flash of color.

Try resizing the element above and notice how the background color changes as it updates. For some niche applications and fun demos, this might be what you want. In most practical use-cases, though, it isn’t. Aside from being visually jarring, behavior like this could be an accessibility issue for users who are sensitive to motion. Imagine that your worklet contained hundreds of dots that all started flying around and flashing whenever something on the page changed size!

Luckily for us, this issue is quite simple to fix. The solution? A pseudorandom number generator! Pseudorandom number generators (or PRNGs) generate random numbers based on a seed. Given the same seed value, a PRNG always returns the same sequence of random numbers — this is perfect for us, as we can re-initialize the PRNG every time the paint() function runs, ensuring the same sequence of random values!

Here’s a CodePen demonstrating how a PRNG works:

Click “generate” to choose some random numbers — then, click “generate” a few more times. Notice how the sequence of numbers is the same each time you click? Now, try changing the seed value, and repeat this process. The numbers will be different from the previous seed value, but consistent across generations. This is the beauty of a PRNG. Predictable randomness!

Here’s the random-background-color CodePen again, using a PRNG rather than Math.random():

Ah! Much better! The element has a random color set when the page loads, but the background color does not change when it resizes. Perfect! You can test this out by clicking “Rerun” on the CodePen above, and resizing the element.

Adding pseudorandom numbers to our worklet

Let’s go ahead and add a PRNG function above our Blob class definition:

// source: https://github.com/bryc/code/blob/master/jshash/PRNGs.md
function mulberry32(a) {
  return function () {
    a |= 0;
    a = (a + 0x6d2b79f5) | 0;
    var t = Math.imul(a ^ (a >>> 15), 1 | a);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}

Now, I’d be lying if I said I understand quite literally anything this function is doing. I discovered this beautiful little snippet of code through Jake Archibald’s excellent article on being predictably random with the CSS Paint API, and have used it in a ton of work since. You can find the original repository for this function over at GitHub — it includes a whole heap of excellent PRNGs and is certainly worth a look.

Note: while I don’t fully understand how this function works, I know how to use it. Often, when working in the generative world (if you are anything like me, anyway!), you will find yourself in this situation. When you do, don’t worry! It is absolutely OK to use a snippet of code to create some art/design without knowing exactly how it works. We can learn by doing, and that’s awesome.

OK, great, we have a PRNG function. Let’s add it to paint():

const random = mulberry32(seed);

In this snippet, we call mulberry32() with our --blob-seed custom property as its seed value, and it returns a brand new function. This new function — random — returns a random number between zero and one.

Lovely, let’s put our shiny new PRNG to use.

A quick aside: Drawing with the CSS Paint API

When working with CSS Paint API worklets, just like HTML <canvas>, we draw everything inside a 2D context. This context has a width and a height. For worklets, the width and height of this context always matches that of the element the worklet is painting to.

Say, for example, we wanted to add a point to the center of a 1920x1080px context, we could visualize it like so:

Points 0,0 in the top-left corner, 960, 40 in the center and 1920, 1040 in the bottom left written in purple. in an otherwise blank white space.

As we begin to write our “render” code, this is good to keep in mind.

How our blob is formed, an overview

Before we write any code, I’d like to show you a little SVG animation of how we will create our blob shape. If you are a visual learner like me, you may find an animated reference helpful for understanding this kind of thing:

To break this process down into three steps:

  1. Plot several equally spaced points around the radius of a circle.
  2. Pull each point a random amount towards the center of the circle.
  3. Draw a smooth curve through each of the points.

Now, things are about to get a tiny bit maths-y but don’t worry. We’ve got this!

Defining the blob’s control points

To start, let’s define the radius of our blob. The blob’s radius determines how large or small it is.

We want our blob shape to always “fit” inside the element it is painted on. To ensure this is the case, we check the width and height of the worklet’s target element and set the blob’s radius accordingly. Our blob is essentially a weird circle, and a circle’s total width/height will always be equal to its radius multiplied by two, so we divide this value to match. Let’s add some code to achieve this in our paint() function:

const radius = Math.min(geometry.width, geometry.height) / 2;

Here’s an image to help explain what is happening here:

Cool! Now that we know what the radius of our blob should be, we can initialize its points:

const points = [];

const center = {
  x: geometry.width / 2,
  y: geometry.height / 2,
};

const angleStep = (Math.PI * 2) / numPoints;

for (let i = 1; i <= numPoints; i++) {
  const angle = i * angleStep;
  const point = {
    x: center.x + Math.cos(angle) * radius,
    y: center.y + Math.sin(angle) * radius,
  };
}

Phew! In this snippet, we “walk” around the circumference of a circle, plopping down some equally spaced points as we go. How does this work?

To start, we define an angleStep variable. The maximum angle between two points on the circumference of a circle is Pi × 2. By dividing Pi × 2 by the number of “points” we would like to create, we have the desired (equally spaced) angle between each point.

Next, we loop over each point. For each of these points, we define an angle variable. This variable is our angleStep multiplied by the point’s index. Given a radius, an angle, and a center point for a circle, we can use Math.cos() and Math.sin() to plot each point.

Note: If you would like to learn a little more about trigonometric functions, I wholeheartedly recommend Michelle Barker’s excellent series!

Now that we have some perfect, beautiful, equally spaced points positioned around the circumference of a circle — we should mess them up. To do so, we can “pull” each one, a random amount, towards the circle’s center.

How can we do this?

First, let’s add a new lerp function (short for linear interpolation) just below where we defined mulberry32:

function lerp(position, target, amt) {
  return {
    x: (position.x += (target.x - position.x) * amt),
    y: (position.y += (target.y - position.y) * amt),
  };
}

This function takes a start-point, an end-point, and an “amount” value between zero and one. The return value of this function is a new point, placed somewhere between the start and end points.

In our worklet, just below where we define the point variable in our for-loop, we can use this lerp function to “pull” the point towards the center position. We store the modified point in our points array:

points.push(lerp(point, center, variance * random()));

For the linear interpolation amount, we use our --blob-variance property multiplied by a random number generated by random() — as random() always returns a value between zero and one, this amount will always be somewhere between zero, and our --blob-variance number.

Note: A higher --blob-variance will result in crazier blobs, as each point can end up closer to the center.

Drawing the curve

So, we have our blob’s points stored in an array. Right now, though, they aren’t used for anything! For the final step in our blob creation process, we will draw a smooth curve through each of them.

To draw this curve, we are going to use something called a Catmull-Rom spline. A Catmull-Rom spline is, in short, a great way of drawing a smooth Bézier curve through any number of { x, y } points. With a spline, we don’t have to worry about any tricky control point calculation. We pass in an array of points, and get a beautiful, organic curve back. No sweat.

Let’s head over to the start of our worklet.js file and add the following import:

import { spline } from "@georgedoescode/generative-utils";

Then install the package like so:

npm i @georgedoescode/generative-utils

This spline function is quite sizeable and a little complex. For this reason, I have packaged it up and added it to my generative-utils repository, a small collection of handy generative art utilities.

Once we have imported spline — we can use it in our worklet’s paint() function like this:

ctx.fillStyle = fill;

ctx.beginPath();
spline(points, smoothness, true, (CMD, data) => {
  if (CMD === "MOVE") {
    ctx.moveTo(...data);
  } else {
    ctx.bezierCurveTo(...data);
  }
});

ctx.fill();

Note: Place this snippet just after your for-loop!

We pass in our points, --blob-smoothness property, and a flag to let spline know it should return a closed shape. In addition, we use our --blob-fill custom property to set the fill color of the blob. Now, if we take a look at our browser window, we should see something like this!

Hooray! We did it! The spline function has successfully drawn a smooth curve through each of our points, thus making a gorgeous (and random) blob shape. If you would like your blob to be a little less rounded, try reducing the --blob-smoothness property.

Now, all we need to do is add a touch more randomness.

A random, random seed value

Right now, our blob’s PRNG seed is a fixed value. We defined this --blob-seed custom property in our CSS earlier, with a value of 123456 — this is great, but it means that the random numbers generated by random() and, therefore, the blob’s core shape, is always the same.

For some instances, this is ideal. You may not want your blobs to be random! You may want to choose some perfect seed values and use them across your site as part of a semi-generative design system. For other cases, though, you may want your blobs to be random — just like the image mask example I showed you earlier.

How can we do this? Randomize the seed!

Now, this isn’t quite as simple as it might seem. Initially, when I was working on this tutorial, I thought, “Hey, I can initialize the seed value in the Blob class’s constructor!” Unfortunately, though, I was wrong.

Since the browser may spin up multiple instances of a worklet to handle calls to paint() — one of several Blob classes may end up rendering the blob! If we initialize our seed value inside the worklet class, this value will be different across instances, and could lead to the visual “glitching” we discussed earlier.

To test this out, add a constructor function to your Blob class with the following code inside:

constructor() {
  console.log(`My seed value is ${Math.random()}`);
}

Now, check out your browser console, and resize the window. In most cases, you get multiple logs with different random values. This behavior is no good for us; we need our seed value to be constant.

To solve this issue, let’s add a little JavaScript on the main thread. I am popping this in the <script> tag we created earlier:

document
  .querySelector(".worklet-canvas")
  .style.setProperty("--blob-seed", Math.random() * 10000);

Excellent! Now when refreshing the browser window, we should see a new blob shape each time.

For our simple demo, this is perfect. In a “real” application, you may want to create a .blob class, target all instances of it on load, and update the seed value of each element. You could also experiment with setting the blob’s variance, number of points, and roundness properties to random values.

For this tutorial, though, that’s it! All we have left to do is make sure our code works OK for users in all browsers, or provide a suitable fallback for when it doesn’t.

Loading a polyfill

By adding a polyfill, our CSS Paint API code will work in all major browsers, with the cost of extra JavaScript weight. Here’s how we can update our CSS.paintWorklet.addModule code to add one to our example:

(async function () {
  if (CSS["paintWorklet"] === undefined) {
    await import("https://unpkg.com/css-paint-polyfill");
  }
  CSS.paintWorklet.addModule("./worklet.bundle.js");
})();

Using this snippet, we only load the polyfill if the current browser does not support the CSS Paint API. Nice!

A CSS-based fallback

If extra JavaScript weight isn’t your vibe, that’s cool. I totally get it. Luckily, using @supports, we can define a lightweight, CSS-only fallback for browsers that do not support the CSS Paint API. Here’s how:

.worklet-canvas {
  background-color: var(--blob-fill);
  border-radius: 49% 51% 70% 30% / 30% 30% 70% 70%;
}

@supports (background: paint(blob)) {
  .worklet-canvas {
    background-color: transparent;
    border-radius: 0;
    background-image: paint(blob);
  }
}

In this snippet, we apply a background-color and a blob-like border-radius (generated by fancy border radius) to the target element. If the CSS Paint API is supported, we remove these values and use our worklet to paint a generative blob shape. Awesome!

The end of the road

Well, folks, we’re all done. To quote the Grateful Dead — what a long, strange trip it’s been!

I know, there’s a lot to take in here. We have covered core generative art concepts, learned all about the CSS Paint API, and made some awesome generative blobs while we were at it. Not bad going at all, I say.

Now that we have learned the basics, though, we are ready to start creating all kinds of generative magic. Keep an eye out for more generative UI design tutorials from me soon, but in the meantime, try and take what we have learned in this tutorial and experiment! I’m sure you have a ton of fantastic ideas.

Until next time, fellow CSS magicians!


The post Conjuring Generative Blobs With The CSS Paint API appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.