I’ve brought back TEH GLITTERMAP!!!1!. It’s been what, 5 years since it was offline?

It was a neat idea back then - all the maps were using PNG or JPG tiles (no vector yet), and maps could become animated by using animated GIFs for the tiles. The challenge was that the tiles loaded individually, and any kind of coordinated animation would break at the tiles edges, becoming a noticeable seam.

But there was an animation which could be done on a per-tile basis with no noticeable seams: 90’s glitter.

Teh glittermap back then was made by bulk downloading tiles and using some of the imagemagick techniques for making old-school glitter animations. Make a few glitter patterns for different base colours; then for each tile replace pixels from a predefined set of colours with the corresponding pixel from the glitter pattern for that colour. The result was good, and it worked in any web browser, but it needed lots of preprocessing. While it was possible to make some server-side on-the-fliy glitterification, it was not worth it for a hobby project.


Fast forward a few years and a couple dozen Leaflet plugins; most importantly L.TileLayer.GL. Take into account browser support for WebGL (98% at the time of this writing). The only thing that was missing was support for time-based animations in L.TileLayer.GL.

So, how does teh new glittermap work anyway? It tells your web browser to load map tiles, and then apply a WebGL transformation to each of the pixels in the image, every frame (30 times per second). If “WebGL transformationto each pixel” sounds alien and you want to learn about it, reading through the Book of Shaders is recommended.

One other fundamental part of all the process is some pseudo-random noise. ImageMagick can make random noise, but this is done only in the client (browser) side of things, so we need to provide some random noise. In particular, this one image:

That noise is made in such a way that the histograms for the red, green, and blue channels are more-or-less uniform: fetching a pixel at random will provide a (mostly) random value for the red, green, and blue channels.

After a lot of iterations, teh glittermap combines edge detection with HSV colour space and pseudo-random noise: the glitter appears only on highly saturated (vivid colour) pixels which are near pixels of a different colour. The exact code is as follows:


The GLSL code starts with some definitions:

precision highp float; 

Tell the WebGL stack to use 24-bit floating point numbers for everything.

uniform float uNow;          

This is a uniform: a value which is the same for all pixels. The javascript code will update this at every frame, filling it with the number of microseconds elapsed since page load.

varying vec2 vTextureCoords;

A varying is interpolated from values from the triangles (which L.TileLayer.GL handles behind the scenes). The vTextureCoords are the pixel coordinates of this fragment, needed to fetch the colours from the texture(s)

uniform sampler2D uTexture0;
uniform sampler2D uTexture1;

Pointers for the textures: the first one is for the map image, and the second one is for the noise texture.

const float distanceToNeighbourPixel = 2.0 / 256.0;                                                 
const mat3 kernel = mat3(-1, -1, -1,    -1,  8, -1,     -1, -1, -1);                                

Some constants for the edge detection algorithm. WebGL textures range from 0.0 (left/top) to 1.0 (right/bottom), they are not in pixel size. Therefore, the edge detection algorithm needs a constant with the distance to the pixels to compare with. A discussion of WebGL image handling kernels can be found elsewhere.

vec3 rgb2hsv(vec3 c) {                                                                              
	vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);                                                
	vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g));                               
	vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r));                               

	float d = q.x - min(q.w, q.y);                                                                  
	float e = 1.0e-10;                                                                              
	return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);                        
}                                                                                                   
																									vec3 hsv2rgb(vec3 c) {                                                                              
	vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);                                                  
	vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);                                               
	return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);                                       
}            

Conversion functions between RGB colour space and HSV colour space, found elsewhere. Not really interesting.

void main(void) {                                                                                   

For each pixel (technically, “fragment”), this function will run once.

	vec4 sample;                                                                                    
	vec3 acc = vec3(0,0,0);                                                                         

	// For each pixel in a 3x3 envelope around our core pixel                                       
	for (int x=-1; x<2; x++) {                                                                      
		for (int y=-1; y<2; y++) {                                                                  
			sample = texture2D(uTexture0, vTextureCoords.st + vec2(x,y) * distanceToNeighbourPixel );

			acc += sample.rgb * kernel[x][y];                                                       
		}                                                                                           
	}                    

Edge detection algorithm. Fetch colours around the current pixel, add each colour times kernel to an accumulator (acc).

	vec4 texelColour = texture2D(uTexture0, vTextureCoords.st);                                     
	vec4 noiseColour = texture2D(uTexture1, vTextureCoords.st);                                     

Fetch colour for the pixel, plus its corresponding pseudo-random pixel. Note that at this time, noiseColour is deterministic: it’s the same for each pixel in each render cycle.

	vec3 hsv = rgb2hsv(texelColour.rgb);                                      

Transform the current pixel colour to HSV colour space.

	// Calculate the glittered colour                                                               
	float pseudorandom = fract(noiseColour.r + noiseColour.b * uNow / 200.0);

Use the noise colour plus the current time in milliseconds to fetch colour from the noise image a second time. This provides some proper pseudo-random values (even if they are deterministic, they look random to the human eye because of how time is used).

	hsv.y *= 0.5 + pseudorandom * 1.0;                                                              
	hsv.z *= 0.5 + pseudorandom * 1.0;                                                              

The glittered colour is done by multiplying the saturation and value of the colour by a factor of the pseudorandom number. These numbers can be tweaked, but they feel about right.

	// How much glitter for this pixel? Put glitter only in edge pixels, 
	// and only on those with a saturaded (vivid) colour.                                                                  
	float amount = length(acc) * hsv.y * 1.5;                                                       

I only want glitter on saturated pixels (with a high value for the saturation, hsv.y) which are edges (a high absolute value for acc, which is actually a 3-component vector).

	gl_FragColor = vec4(mix( texelColour.rgb, hsv2rgb(hsv), amount), texelColour.a);  

Finally, set the actual output colour for that pixel. If amount is 0.0 then use the original texelColour.rgb, if it’s 1.0 then use the modified colour, if it’s in-between, then linearly interpolate those colours (mix(...)).

}                                                                                                   

…and that’s it! If you want to play more, you can tweak the shader code in the L.TileLayer.GL REPL.