loading...
Cover image for Making Dynamic Animations with Shaders

Making Dynamic Animations with Shaders

trostcodes profile image Alex Trost Originally published at frontend.horse ・6 min read

I’ve been following ilithya’s shader demos for a while and wanted to write about her work weeks ago, but quickly learned that I was out of my depth with shaders. Instead, ilithya helped me write the Introduction to Shaders issue as a warm-up. Now it's race day!

If you're new to shaders and haven't read the Introduction to Shaders issue, that would be a good place to start. It provides a conceptual foundation that will make it easier to understand the shaders we're covering.

Summer Screen

Let's dive deep into how ilithya made this awesome shader, Summer Screen.

There's a few things going on here. First, we've got these vertical line sections that are shifting back and forth. Then we've got these gradients coming in from the top and bottom like color waves on a beach. Let's figure out these two effects.

First we'll check out a shortened version of the GLSL code so we can see what's going on. You might see a new language and some math here but don't worry! We'll trot through them nice and slow.

// Create three static colors
vec3 colorA = vec3(0.5, 0., 0.5); // Purple
vec3 colorB = vec3(1.0, 0.41, 0.71); // Pink
vec3 colorC = vec3(0.0, 1.0, 1.0); // Teal blue

// Create three dynamic colors
vec3 color1 = mix(colorA, colorB, fract(x*fr+cos(u_time)));
vec3 color2 = mix(colorB, colorC, (y-sin(u_time))*0.5);
vec3 color3 = mix(color1, color2, x*cos(u_time+2.));

// Our color output
gl_FragColor = vec4(color3, 1.0);

The first thing to address is that GLSL is a typed language, so when you declare a variable you also declare its type.

A float for our purposes can be thought of as a number with a decimal.

A vec3 for our purposes is kind of like a special JavaScript array with three values.

A vec4 has one more value than a vec3 and is what the output of a shader main() function needs to be. The four values there align to red, green, blue, alpha.

First ilithya creates colorA, colorB, and colorC. Each of these are static colors that aren't going to change.

The three colors used in this piece: Purple, pink and teal

Then she uses these colors and other variables to create dynamic colors that will change. We'll look at the vertical lines first.

Vertical Lines

For the vertical lines let's zoom in and see what's really going on here.

A close up of the vertical lines

The lines are really repeated gradients going from dark to light.

To make gradients with GLSL we use the mix function. mix takes three arguments: x: the start of the range, y: the end of the range, and a: the point between x and y.

// vec3 mix(vec3 x, vec3 y, vec3 a)
vec3 color1 = mix(colorA, colorB, fract(x*fr+cos(u_time)));

ilithya is making a gradient between the purple and pink, so those are her first two arguments. The colors get mixed in different amounts based on the value of a, which is a value from 0 to 1. If a is 0, we'll see 100% of the first color. If a is 1 we'll see 100% of the second color. Anything in between and we get a blend:

The gradient in steps

So that can give us one gradient, but how do we get repeating gradients?

To get the gradients to stop and start from 0 again, ilithya is using a GLSL function called fract. This returns the fraction of a number, dropping the integer. fract(x) is the same as doing x - Math.floor(x) or x % 1 in JavaScript. So as x increases, fract(x) will always be a float between 0 and 1.

I'm going to simplify it beyond what ilithya has in her piece to show how the fract function works:

Showing how the fract function works

As we can see, even though x keeps increasing, the result of fract(x) just returns the fraction, or whatever comes after the decimal, giving us that same purplish color. Translate that over the entire piece and you've got ilithya's cool vertical bars!

We'll touch on how all these pieces animate a bit later.

Pink & Blue Gradient

Now we move to color2.

vec3 color2 = mix(colorB, colorC, (y - sin(u_time)) * 0.5);

Another mix, this time between the pink and blue. No fract this time, so we're getting a smooth gradient. The y value makes it change color from bottom to top. We'll touch on the sin(u_time) bit soon.

Putting it All Together

Then she combines color1 and color2, using a mix amount that gets closer to 1 on the right side. The horizontal change is due to the x value. The cos(u_time+2) is for animation.

vec3 color3 = mix(color1, color2, x*cos(u_time+2.));

Here's a simplified version of the shader's parts and the final color output:

Showing how the different colors blend together to make the shader

You can see our simplified version looks similar to the finished shader, it's just missing animations.

Animation

There are three main animations happening in Summer Screen:

  1. The bars shifting left and right in color1
  2. The pink and blue gradient moving up and down in color2
  3. The mix value for color3 moving left and right

Each of these are using the value of the current time, and familiar math functions, sin and cos.

vec3 color1 = mix(colorA, colorB, fract(x*fr+cos(u_time)));
vec3 color2 = mix(colorB, colorC, (y-sin(u_time))*0.5);
vec3 color3 = mix(color1, color2, x*cos(u_time+2.));

That time value comes from a Three.js Clock and is how ilithya animates most of her shaders. It returns the elapsed time since the page loaded.

Don't worry if you don't know Three.js. The code ilithya uses is reusable boilerplate that makes the shader code simpler. We'd have to have a lot more GLSL code without the little bit of Three.js. It's a handy WebGL abstraction.

When ilithya calls either sin(u_time) or cos(u_time) she's going to get back a value between -1 and 1, and it'll transition smoothly. If you're familiar with CSS animations, it feels a lot like an ease-in-out.

Super Quick Sine Wave Refresher:

As we increase the value along the horizontal axis, the sine wave moves us up and peaks at 1, brings us down to 0 and bottoms out at -1 before climbing again. No matter high the horizontal value is, the sine of that value will always be between -1 and 1. Cosine works similarly but gives a different value.

Showing a sine wave

And there you have it! Three different parts being blended and animated together to make one awesome animation.

Break it Yourself

As she was helping me understand it, ilithya encouraged me to swap values out in the shader code. I highly recommend you go and do just that.

To see the vertical bars isolated, change the last line of the shader code to:

gl_FragColor = vec4(color1, 1.0);

To see the vertical pink and blue gradient:

gl_FragColor = vec4(color2, 1.0);

Or you can change that to colorA, colorB, or colorC to see a screen of solid color. Play with it, have fun, figure out how it works.

Check out Summer Screen →

Bubble Gum

I won't be diving into how this shader works, but it's just so dang soothing to stare at, I had to share it. Let me know if you'd like to read about it in a future issue.

Check out Bubble Gum →

Shader Resources

Ilithya shared some great resources for learning shaders over on her blog. The resource that everyone recommends is The Book of Shaders, so definitely check that out if you're interested.

Posted on by:

trostcodes profile

Alex Trost

@trostcodes

Writing Frontend Horse, a newsletter that teaches you the best techniques from Codepens, tutorials, and websites. Frontend Developer and Designer. Engineer at Digital Surgeons.

Discussion

pic
Editor guide