A WebGL Analog Clock

  
Your browser does not support the canvas tag. This is a static example of what would be seen.

In this example I’m going to try to build something that might be considered useful out of the WebGL experiments I’ve been putting together thus far. I’ve decided to build a simple animated clock, and I’m going to try to keep as much of the code as possible in the shader rather than submit lots of shapes and parameters as might be the more normal way to do this.

My clock is going to consist of five fixed elements. The clock will be circular and will have an outer edge. The clock will have three hands, for hours, minutes and seconds, and will have a small inner circle connecting the hands. My shader is going to represent these as two circles and three lines. Since everything is relative to the center I therefore just need to know the radius of each of the two circles and the position of the end of each line segment, and I’m happy to hard code anything that doesn’t move, which means three points is all the state I need to send to the GPU.

The rendering itself is going to use distance calculations only. I’m going to render a white canvas, but when the shader determines that any given pixel is near enough to one of the five elements it knows about we’ll darken the canvas forming soft black lines. Since we have two element types we only need to be able to calculate the distance to a circle, and the distance to a line segment, and we have all we need. Once we’ve done that for all five we take the darkest resulting colour and run with that.

Finally I’ve set the canvas to refresh every 100ms, which is more than enough to achieve a smooth result for a clock that ticks once per second.

The shader code that I’ve used above looks like this…


precision highp float;

varying vec4 v_texcoord;
uniform vec4 u_bigHand;
uniform vec4 u_smallHand;
uniform vec4 u_secHand;

/* get the squared distance between two points */
float length_squared(vec2 a, vec2 b) {
  return dot(a-b, a-b);
}

/* distance to line segment check */
float minimum_distance(vec2 v, vec2 w, vec2 p) {
  float l2 = length_squared(v, w);
  if (l2 == 0.0) return distance(p, v);
  float t = max(0.0, min(1.0, dot(p - v, w - v) / l2));
  vec2 projection = v + t * (w - v);
  return distance(p, projection);
}

/* clock shader */
void main(void) {
  vec2 pos = v_texcoord.xy;
  float dist = length(pos);
  float circle = abs((dist - 0.95) * 40.0);
  float bigHand  = minimum_distance(vec2(0.0, 0.0), u_bigHand.xy, pos) * 40.0;
  float smallHand  = minimum_distance(vec2(0.0, 0.0), u_smallHand.xy, pos) * 40.0;
  float secHand  = minimum_distance(vec2(0.0, 0.0), u_secHand.xy, pos) * 60.0;
  float middle = abs((dist - 0.02) * 40.0);
  float v = min(min(min(min(circle, bigHand), smallHand), secHand), middle);
  gl_FragColor = vec4(v, v, v, 1.0);
}

In JavaScript it’s fairly easy to get the current time, which we can then convert to the line segment end positions the shader needs by doing this sort of thing…


var d = new Date();
var hours = d.getHours();
var mins = d.getMinutes();
var secs = d.getSeconds();
var smallHand_deg = 360.0 * (hours / 12.0);
var bigHand_deg = 360.0 * (mins / 60.0);
var secHand_deg = 360.0 * (secs / 60.0);
var bigHand_rad = bigHand_deg * Math.PI / 180;
var smallHand_rad = smallHand_deg * Math.PI / 180;
var secHand_rad = secHand_deg * Math.PI / 180;
gl.uniform4f(shader.bigHand_loc,
  Math.sin(bigHand_rad) * 0.7, -Math.cos(bigHand_rad) * 0.7, 0.0, 0.0);    
gl.uniform4f(shader.smallHand_loc,
  Math.sin(smallHand_rad) * 0.5, -Math.cos(smallHand_rad) * 0.5, 0.0, 0.0);    
gl.uniform4f(shader.secHand_loc,
  Math.sin(secHand_rad) * 0.4, -Math.cos(secHand_rad) * 0.4, 0.0, 0.0);    

Leave a Reply

Your email address will not be published. Required fields are marked *