Visual Effect Analysis: Animated Raindrops

Roger Bongers
12 min readOct 15, 2018

For those not familiar with Dwitter, it is a new platform for sharing visual effects. Dwitter provides an abbreviated Javascript Canvas API, along with some math functions, and challenges users to see what they can create in only 140 characters.

Dwitter is great for experimenting with visual effects and I’ve been using it a lot recently. I’ve started looking into how some of the effects I’ve come across are made, and I thought I would share my findings.

The original raindrop effect

This Dweet of animated raindrops is one of the top rated on the website as of now, and it’s easy to see why. It’s got a non-abstract effect, it has lots of different events happening on the canvas at the same time, and it even has a nice startup phase, all in a few short statements. I recommend you look at the full effect, as it’s much more impressive than the short loop above. Dweets are also editable in real time, so as you follow along, I urge you to try to experiment with the code yourself.

Source code for the examples below is available on github.

Refactoring the Code

l=c.width+=i=0;h=400;for(;++i<200;)for(j=a=(t+C(i))%2;j++<50;)x.fillRect(l*C(i*i)+~~a*(w=a*h-h)*C(j),(~~a?h:a*h)*(C(i)*C(i)+2)+w*S(j)/4,3,3)

This is the original code for the Dweet. It’s formatted now in such a way to use as few characters as possible, so I’ll expand it so it’s a little easier to read.

l = c.width += i = 0
h = 400
for (; ++i < 200;) {
for (j = a = (t + C(i)) % 2; j++ < 50;) {
x.fillRect(
l * C(i * i) + ~~a * (w = a * h - h) * C(j),
(~~a ? h : a * h) * (C(i) * C(i) + 2) + w * S(j) / 4,
3,
3
);
}
}

A little better. In case you’re not familiar with the Dwitter API, I’ll go ahead and refactor it so you can see what’s going on. The rest of the code in this article will not work with Dwitter.

context.clearRect(0, 0, canvas.width, canvas.height);
l = canvas.width;
i = 0;
h = 400;
for (; ++i < 200;) {
for (j = a = (t + Math.cos(i)) % 2; j++ < 50;) {
context.fillRect(
l * Math.cos(i * i) + ~~a * (w = a * h - h) * Math.cos(j),
(~~a ? h : a * h) * (Math.cos(i) * Math.cos(i) + 2) + w * Math.sin(j) / 4,
3,
3
);
}
}

Again, better, but you still can’t probably can’t tell what’s going on at a glance with all of these tricks going on. I’ll start by breaking each important value used in the code into its own named variable.

context.clearRect(0, 0, canvas.width, canvas.height);
effect_size = 400;
for (i = 1; i < 200; i++) {
timing_variation = (time + Math.cos(i)) % 2;
for (j = timing_variation; j++ < 50;) {
burst_trigger = ~~timing_variation;
drop_size = timing_variation * effect_size - effect_size;
x_distribution = canvas.width * Math.cos(i * i);
x_burst_shape = Math.cos(j);
y_position = (
burst_trigger ? effect_size : timing_variation * effect_size
);
y_variation = Math.cos(i) * Math.cos(i) + 2;
y_drop_shape = Math.sin(j) / 4;
x_coord = (
x_distribution + burst_trigger * drop_size * x_burst_shape
);
y_coord = y_position * y_variation + drop_size * y_drop_shape;
context.fillRect(x_coord, y_coord, 3, 3);
}
}

That’s probably as far down as the code can be broken down. By just looking at the last few lines, you may be able to tell how the effect is made given all of the different variables. I’ll write more on how everything works together later, but first I’ll break down the overlying structure and how each variable works. I’ll be referencing this version of the code throughout the rest of the article.

Structure of the Code

Before we cover the structure unique to the code, we should probably cover how Dwitter behaves.

Dwitter starts by creating a Javascript canvas and then getting the context for that canvas. You don’t have to know what that means, just that a canvas is a drawable area and the canvas context provides additional functions for interacting with that area. The functions used by these Javascript objects should be self explanatory — clearRect clears a rectangular area of the canvas, and fillRect draws a rectangle on the canvas.

Dwitter will then call the code for the Dweet 60 times a second. We have to clear the canvas on each render using clearRect, as Dwitter doesn’t clear it for us. Dwitter provides us with a variable telling us how much time has passed, which I call time, allowing us to make our effects move and change as time passes.

Other than that, the structure of the Dweet is fairly simple, which we can see when we remove the non-structural lines other than those used to draw to the canvas.

context.clearRect(0, 0, canvas.width, canvas.height);
...
for (i = 1; i < 200; i++) {
timing_variation = (time + Math.cos(i)) % 2;
for (j = timing_variation; j++ < 50;) {
...
context.fillRect(x_coord, y_coord, 3, 3);
}
}

The structure consists of two loops, one with a normal incremental value and one which has a start position that changes over time. More on that later. The canvas is cleared at the beginning of the code using a rectangle which stretches from the origin to the end of the canvas. The particles themselves are 3x3 pixel rectangles drawn inside of the inner loop.

This is not clear just from looking at the loop structure, but the outer loop controls which raindrop is currently being drawn, while the inner loop controls which particle inside that raindrop is being drawn. The raindrops are always made up of different particles, even during the “falling” phase where they seem to form a concurrent line.

Now that I’ve covered the basic structure, I’ll break down each variable so we can determine how the effect gets its definitive shape.

Variable Analysis

Fundamental Variables

There are a few variables that are defined near the beginning and used throughout the code, so let’s start with how those work.

effect_size = 400;

This is simply a constant which controls the size of the effect. It is mostly used to control the vertical size of the effect, and is also used to control the size of the “burst” part of the effect.

timing_variation = (time + Math.cos(i)) % 2;

This is quite an interesting value that adds some variation between when each raindrop falls and bursts. I need to break this down further to illustrate how this value actually changes over time.

By using just time % 2, we get a value which will cycle over time from 0 to 2. The modulo (%) operator gives the remainder after dividing, which will always keep the value below the second number.

By adding Math.cos(i) to the inner part of this variable, we achieve a little bit of a different offset for each raindrop. We can see how this variation contributes to the way the rain falls by removing it.

The raindrops without any timing variation (source)

Not a very convincing effect without that variation. This lets us see a little bit of how this value is used, because we can now see where the variation is gone. All of the rain now falls at the same time and burst at the same place now.

timing_variation starts out negative at the beginning of the effect because of Math.cos(i), but the negative effect is cancelled out permanently when time becomes equal to 1. This causes the slight delay at the beginning of the original animation, which is a nice effect.

 burst_trigger = ~~timing_variation;

This value controls when the raindrops actually burst. Let’s look at how.

If you’re not familiar with it, the ~ operator flips all of the bits in a value. So, if you use it twice, it flips any integer back to its original value. If you use any decimal values, it will remove the decimal part of the number and turn it into an integer, which is special Javascript behaviour. The same effect can be achieved by using Math.ceil for negative values or Math.floor for positive values, but this is a clever way to reduce the number of characters used on Dwitter.

Since timing_variation effectively cycles between 0 and 2 non-inclusive, burst trigger will flip suddenly from 0 to 1. When the trigger is 1, the burst will be in effect. Since there is a little variation for each raindrop, each drop will burst at a different time.

With this knowledge, we can get more insight about how the effect works by looking at how it looks when the rain is always falling.

The raindrops stuck in falling mode (source)

As you can see, the drops actually get smaller before they get larger again, which can be hard to see in the original effect as they burst as soon as they get too small.

We can also see how it looks when the rain is always bursting.

The raindrops stuck in bursting mode (source)

Here, you can actually see that if the effect were to go along long enough, the bursts would cycle between large and small because of the use of sinusoidal functions. You can also see that, between the falling and burst phases, the raindrops completely stop.

drop_size = timing_variation * effect_size - effect_size;

This value controls the size of each drop, both when it is falling and when it is bursting.

The timing variation here gives each drop a slightly different size and causes the drop to change in size over time. The drop size is multiplied by the effect size, which makes it large enough to have a noticeable effect. The effect size is also subtracted, which is what causes the drop to get smaller until it bursts, at which point it gets larger. This is because the drop size goes from negative in the falling phase to positive in the burst phase.

If we change this value, we can see what it would look like if we made the drop size constant throughout the effect.

The raindrops with constant sizes (source)

You can now see clearly what exactly the timing variation adds to the size of each raindrop by comparing it with the original effect.

Horizontal Variables

Now that we know how the basic variables work, let’s look at the values that make up the horizontal position of the drops.

x_distribution = canvas.width * Math.cos(i * i);

This value controls how the drops are spread out horizontally across the canvas. The drops will be distributed across the canvas width in a pattern that, while trigonometric, appears pseudo-random because of how multiplying i by itself causes the pattern to wrap around.

Let’s see how it looks if we do not multiply i by itself.

The raindrops with a clearer horizontal distribution (source)

By removing the pseudo-random distribution, now you can see how each raindrop compares to the next sequential raindrop. However, we can go a step further and remove any horizontal distribution at all by making every raindrop fall in the middle of the canvas.

The raindrops all falling in the middle of the canvas (source)

This makes the effect take on a whole new shape. It actually looks like it could use a whole new analysis. However, if we just remove the timing variation like we did before, we can see that the new shape is just caused by the sinusoidal timing variation.

The raindrops falling in the middle without any timing variation (source)

Now, without any timing or horizontal variation between raindrops, we can pretty clearly see that each raindrop is falling and spreading out in the same way.

x_burst_shape = Math.cos(j);

This, quite simply, gives each burst the circular shape on the horizontal axis by using the Math.cos function. The j value is used, so the position changes for each particle.

By changing this variable, we can change the horizontal shape of each burst. For instance, we can multiply j by itself with Math.cos(j*j). This uses the same pseudo-random pattern as the horizontal distribution (Math.cos(i*i)), so the bursts take on a much more noisier pattern.

The raindrops with a noisy horizontal shape (source)

Vertical Variables

y_position = (
burst_trigger ? effect_size : timing_variation * effect_size
);

This variable simply determines the vertical position of the drops.

When the burst effect is active, the drops freeze in place at the predetermined height. When the effect is not active and timing_varation is less than 1, they will move at a multiple of the predetermined height.

Since the burst effect becomes active when timing_variation becomes 1, the drops freeze exactly in place.

Knowing what this does, we can now make the raindrops go up.

The raindrops falling up (source)

This is achieved by making the variable negative and adding 3 * effect_size.

y_variation = Math.cos(i) * Math.cos(i) + 2;

This adds a little variation to the vertical position of each raindrop.

By multiplying Math.cos(i) by itself, the value will always be positive, so the raindrops won’t burst too high. It will also cause a slightly different pattern than other variables used for variation.

The value of 2 here is simply a constant used to make sure all drops don’t fall too low or too high.

We can now see what it would look like if all of the raindrops fell with the same vertical variation.

The raindrops all bursting in the center of the canvas (source)

This shows all raindrops falling at a height of 2.

We can now remove all horizontal randomness, timing variation, and vertical position variation.

The raindrops all bursting in the center at the same time, evenly spread (source)

This can give us another clear picture of how the effect works.

y_drop_shape = Math.sin(j) / 4;

This variable, like the x_burst_shape, gives the burst its circular pattern. Unlike x_burst_shape, it also contributes to the shape of the drop while it is falling, causing it to get shorter as it falls.

The timing of the effect causes the drop to burst just as this vertical shape variable reaches 0, meaning that the burst will be triggered exactly when the raindrop becomes flat. This gives for a very nice transition.

This shape variable uses Math.sin, whereas the horizontal burst shape variable uses Math.cos. This is what causes the circular shape.

Finally, the constant 4 is used here to make the burst shorter than it is long, creating the perspective effect.

If we didn’t shorten the raindrop, this is what it would look like.

The raindrops without vertical squishing (source)

This is achieved by changing 4 to 1.

Putting it All Together

We’ve finally covered how each supplementary variable. Let’s look at they’re combined together to achieve the final effect.

x_coord = (
x_distribution + burst_trigger * drop_size * x_burst_shape
);
y_coord = y_position * y_variation + drop_size * y_drop_shape;

The x coordinate of each particle is first determined by taking the value which determines how they are spread horizontally. Then, whenever the burst effect is active, we add the value which determines the burst shape, which is also multiplied by the size of the drop, causing it to expand.

The y coordinate of each particle is determined by taking the value which determines the position and multiplying it by the variation for that raindrop. Then, we add the value which determines the shape of each drop and multiply it by the size of each drop, causing it to scale correctly and expand a bit more.

This effect is orchestrated perfectly to create a complex effect from a few simple elements. It still looks beautiful after analyzing each element.

I hope you now understand a little bit better how this effect works. If you have any corrections, suggestions, or questions, feel free to leave a comment. If there’s an effect you’d like to see analyzed, let me know and I might give it a shot!

--

--

Roger Bongers

I’m a professional software developer. My interests include writing chiptunes, video synthesis, mathematics, graphics programming, robotics, and more.