Harry Roberts recently wrote an excellent blog post where he uses a CSS gradient to improve the perceived loading speed of an image on his website. Inspired by this, I wondered if I could extend Harry's technique, using multiple overlapping gradients to more closely mimic the source image. The end result is a Sass function I've named "Blurground". See it in action in this Pen:

The source image

First we need to reduce our source image to the desired grid size, using PhotoShop or similar. In this example I've reduced this image to 8x4 pixels. Blurground will turn each pixel into a radial gradient (or "blob"). So in this example we'll end up with an 8x4 grid of blobs. Each blob will blend from 100% opacity at its centre to 0% opacity at its edge.

Using PhotoShop's eyedropper tool we record the colour of each pixel, and save them in a Sass list, like this:

  $myColours: #9EA0A3, #9C9A97, #362F29, #15100D, #271E17, #241C17, #040201, #1C1C1C, #A9ABAC, #999999, #190E03, #000000, #6A4D44, #533C37, #000000, #141111, #AC9479, #746454, #0A0300, #3B3134, #715A60, #5C453E, #15100C, #0D0D0D, #A37A52, #49301A, #0D0D0D, #493B3A, #584D51, #624638, #1A0C06, #2A2A2C;

The function

Now we have our list of colours, let's build a function to turn it into a grid of blobs. Our function will need to know a few variables: the desired row and column lengths of our grid, and our list of colours.

  @function blurground($rowLength, $columnLength, $colours) {
  ...
}

blobWidth and blobHeight

We use our variables to calculate the size each blob will be:

  @function blurground($rowLength, $columnLength, $colours) {
  $blobWidth: 100%/$rowLength;
  $blobHeight: 100%/$columnLength;
}

xOffset and yOffset

Each blob will need to be centre-aligned within its grid square, so let's calculate that offset size:

  @function blurground($rowLength, $columnLength, $colours) {
  ...
  $xOffset: $blobWidth/2;
  $yOffset: $blobHeight/2;
}

xSize and ySize

Each blob will need to overlap slightly, otherwise we'll see some empty gaps. Here we can determine how much they overlap by: if $xSize is exactly equal to $blobWidth, there'll be zero overlap. In this example each blob will overlap its neighbours by 50%.

  @function blurground($rowLength, $columnLength, $colours) {
  ...
  $xSize: $blobWidth*2;
  $ySize: $blobHeight*2;
}

Finally, let's declare an empty string which will eventually contain our CSS:

  @function blurground($rowLength, $columnLength, $colours) {
  ...
  $result: "";
}

Here's what we've got so far:

  @function blurground($rowLength, $columnLength, $colours) {
  $blobWidth: 100%/$rowLength;
  $blobHeight: 100%/$columnLength;

  $xOffset: $blobWidth/2;
  $yOffset: $blobHeight/2;

  $xSize: $blobWidth*2;
  $ySize: $blobHeight*2;

  $result: "";
}

The loop

Now we need to loop through each colour in the list, creating a radial gradient (or blob) for each. We start with a Sass loop:

  @for $currentBlob from 1 through length($colours) {}

We need to calculate the current blob's X and Y co-ordinates. Before we can do that, we need to know what row we're currently on:

  @for $currentBlob from 1 through length($colours) {
  $row: ceil($currentBlob/$rowLength);
  $const: $row - 1;
}

The ceiling or ceil function rounds up to the nearest whole number. Let's assume we're currently on blob #10 of our 8x4 grid: the calculation would look something like this:

  $row: ceil(10/8);
// 10 divided by 8 is 1.25, which rounds up to 2: therefore we're on row 2.

Note the other variable, $const, which will be needed in the next step.

X coordinate

Now we know what row we're on, we can calculate the current blob's X coordinate:

  $xPosition: (($currentBlob - ($rowLength * $const)) * $blobWidth) - $xOffset;

Let's break down what's happening here. To figure out which grid column we're currently in, we take the current blob number (eg. 10), and subtract the row length multiplied by the current row minus 1 (eg. we're on row 2, so we subtract 8 multiplied by 1). This gives us the result 2: and yes, in an 8x4 grid, blob #10 would be in column 2. We multiply the result by a standard blob width, then subtract half a blob's width.

In our example the calculation can be simplified like this:

  $xPosition: ((10 - (8 * 1)) * 12.5%) - 6.25%;
// 18.75%

Y coordinate

Luckily, the Y position is a bit easier to figure out:

  $yPosition: ($blobHeight * $row) - $yOffset;

It's the height of a blob, multiplied by the current row, minus half a blob's height. This would simplify as follows:

  $yPosition: (25% * 2) - 12.5%;
// 37.5%

So now we have blob #10's coordinates: (18.75%, 37.5%).

The declaration

Still in the loop, we now build our CSS declaration for the current blob:

  $string: "radial-gradient(ellipse #{$xSize} #{$ySize} at #{$xPosition} #{$yPosition}, #{rgba(nth($colours,$currentBlob),1)}, #{rgba(nth($colours,$currentBlob),0)})";

It's a CSS radial gradient, shaped as an ellipse. Its width and height are $xSize and $ySize, its centre is at ($xPosition,$yPosition), and it blends from 100% to 0% opacity.

This would look something like the following:

  $string: "radial-gradient(ellipse 25% 50% at 18.75% 37.5%, #999999, rgba(153, 153, 153, 0))";

Comma-separation

The loop will iterate through our list of colours, create a gradient declaration for each one, and append it to our variable $result. CSS syntax requires that multiple gradient declarations are comma-separated, so let's quickly check that, and include a comma if necessary:

  $separator: "";
  @if ($currentBlob != 1) {
    $separator: ",";
  }
  $result: $result + $separator + $string;
}

Finally, the function outputs the completed string:

  @return unquote($result);

And with that, our function is ready! You can see the entire code, complete with inline comments, in the Pen.

Usage

Here's how you'd use the function:

  .page-head--masthead {
  background-image: blurground(8,4,$myColours);
}

And here it is in action:

Thanks for reading!

DISCLAIMER: This is an experiment; I would never suggest anyone use this code in production. The CSS it produces could potentially be heavier than the actual source image, therefore entirely missing the point of Harry's initial blog post. Also I can't imagine that having 32 (or more!) background gradients per node would be good for browser performance. It was fun to mess around with though :D


2,671 3 23