The Art of Repepetition

October 22, 2012

/images/7_0.gif

Sometimes, differences everywhere aren't what you need. Constraints, like limited memory, bandwidth or performance, call for a different strategy.

Filling large areas of space when you can't pick a unique colour for each point means mastering the art of repetition.

You're probably already thinking I'm talking about shaders again. But I could just as well be referring to curtains, or wallpaper, a century ago, when the art of creating beautiful seamlessly-repeating patterns was (re)discovered.

Ok, ok, so this is going to be more shader stuff after all. I'm going to describe how to work with small canvases that get presented as tiles, for example with the CSS background-image property.

But before we dive in, some news. ThNdl Shader Editor has undergone a major upgrade to support this article. Naturally it needed tiling support if we're going to be playing with tiles. But there's more.

I added a gallery which you can use to browse existing shaders, and where you can publish your creations to share with a short URL, or for others to find and play with. The Image button gives you your creation as an image file you can save easily.

And finally, it's gone boldly into the 4th dimension (i.e. time - or you might say time travel back to the 90s) with support for the venerable, but (for some reason) not easily replaced, Animated GIF format. More about that one later.

Ok, back to the topic. Tiles. Well, tiles are of course rectangular or square. Since opposite edges get drawn next to other, it is of course easy to make tiled patterns with sharp lines at the edges. For example:

f=vec4(c.xy*.5+.5,c.x*c.y,1.)
/images/7_1.jpg

Easy, but...err..boring and very obviously repeating. Clearly the first thing to do is work out how to make it seamless.

One approach is to make it symmetrical within the tile about one axis so that opposite edges match. For example:

vec2 m=abs(c.xy);
f=vec4(m.xy,m.x*m.y,1.)
/images/7_2.jpg

Here we used abs to reflect the negative regions of c.xy, making it symmetrical around the centre. The result is smoother and a little nicer, but the edges are still quite visible.

If we want to get rid of the edges, we're going to need to make friends with mod, one of the two masters of repetition (the other is sin).

If you don't know mod, it's simply the remainer from a division operation. In shaders, the mod function takes two values, and calculates the remainer of the first divided by the second.

The reason this is useful for us is that mod lets us do maths and pretend the edges are not there, as long as we promise to make our results at the top and bottom of the mod range match up. Take a look:

vec2 m=mod(c.xy+c.x+c.y,1.);
f=vec4(m.xy,m.x*m.y,1.)
/images/7_3.jpg

First we skewed the original coordinate space, then we wrapped that in mod(1.0). That wouldn't have worked without the mod, but with the mod, the deformations on either side of the tile match up.

One thing, you can't do just any deformation - it needs to be compatible with the tile size to avoid creating breaks, which typically means integer multiples.

In this case we left the hard edges in, rather than matching them, but even now those edges are no longer identical to the tile shape. Tiling angled lines are just a short (smooth)step or two away:

vec2 m=mod(c.xy+c.x+c.y,1.);
f=vec4(smoothstep(.75,.5,m.y)*
smoothstep(.25,.5,m.y))
/images/7_4.jpg

Alternatively, to get rid of lines, we could use our old friend length:

vec2 m=mod(c.xy+c.x+c.y,2.)-1.;
f=vec4(length(m))
/images/7_5.jpg

Now this starts to get interesting. You might have spotted something else here - one of my favourite cheap tricks. Instead of mod(1.0), this used mod(2.0) and subtracted 1 from the result.

That puts this back into the range (-1,1), which is needed for symmetrical operations like creating a circle using length. Wrapping that in abs would create a zig-zag between 0-1 with no breaks - also very useful.

Time for something completely different... GIFs, GIFs, gimme more GIFs...

I promised to say more about GIFs in the ThNdl Shader Editor. If you click "JPEG" it will change to "GIF", and your world will go monochrome... huh?

Yeah, well, GIF, being so old, can only handle 256 colours. So, to avoid the need for dithering we only support B&W using the alpha value from the shader.

In return for this we get up to 40 frames of 10FPS animation (with quite miserable compression) that animates in most browsers. Good deal, huh?

The only other thing you need to do is use the value t in your shader. This is 0 in the first frame, and 1 in the last frame, smoothly changing in-between. You use it to drive your animation.

The interesting thing is that many of the rules that apply in 2D space for tiling, also apply to the animation tilling in the time dimension, if you want to get smooth animation that is.

Time for some examples:

vec2 m=mod(c.xy+c.x+c.y+2.*t,2.)-1.;
f.a=length(m)
/images/7_6.gif

By adding 2.*t to the deformation from the previous shader, now it scrolls smoothly forever, muhahaha:

float N=7.;float a=atan(c.x,c.y)+6.28319*t/N;
float b=6.28319/N;f.a=smoothstep(.5,.55,
cos(floor(.5+a/b)*b-a)*length(c.xy))
/images/7_7.gif

Making the heptagons slowly rotate just needs t scaled by 2/N PI and added to the angle. It only needs to turn by one segment in 40 frames so it is very smooth.

Also to be found in the gallery is the ball animation from the top of this article.

Go on, have a play with it and try out some of the repetition techniques. If you come up with something nice, don't forget to post it to the gallery for the rest of us to see.