Using Canvas to create beautiful custom markers in Google Maps

I have been playing with Google Maps a little recently and I want to dynamically create a set of Markers without resorting to a server.  I noticed that the Marker class can take a string URL as a parameter, so it seemed to make sense that if it was a url to an icon, it could also be a dataUri to an image that I can control via a canvas.

To cut a long prologue short, it works.  More importantly I was quite pleased with the effect I generated and this is what this post is all about.

The markers that I wanted to create had to be rectangular, with rounded corners, distinct colours and a number right in the centre.

The first problem was how to create rounded rounded.  I didn’t have a lot of time for this but I used a lot of the code from http://js-bits.blogspot.com/2010/07/canvas-rounded-corner-rectangles.html as inspiration.  This simply draws a nice rounded rectangle.

The next problem to solve is how to create a nice range of colours that aren’t the same but are consistent.  This is where the new support for hsla (hue, saturation and luminance) support via CSS3 really pays off in a big way.  Using RGB there is no simple way to find a suite of colours that have the same tone but are uniquely distinct; with the HSL colour model if you keep the S and L values the same but modify the hue (H) you can produce a wide spectrum of colours that are of the same tone. See the Example image.

This creates an image that is a single colour, but it doesn’t look amazingly nice.  I decided to add a gradient to give it one of those effects you see all over the web at the moment (I am not sure of the name, its not a Gel Button, but it is close….ish).  The question is, how do you choose a colour for the gradient?  The answer was pretty simple in the end: use the same Hue and Saturation values as the first colour but reduce the Luminance by a couple of percentage.  The overall effect is pretty nice I think.

Centring the text was pretty simple too. The canvas API provides a nice little method called MeasureText, this allows you to get the pixel width of any arbitrary string (using the font-size and family you specify). From this it is a simple case of finding the centre of the text and the centre of the canvas and subtracting one from the other.

The final problem is pretty simple to solve.  How do you get the canvas image as a url?  canvas.toDataURL() will return an image url that can be attached to any “src” or css “url()” property.

It must be noted that this will only work with browsers that can render canvas elements.

The final code is follows for posterity:

var ButtonFactory = (function() {
  var width = 25;
  var height = 25;
  return new function() {

    var h = 1;
    var s = 78; // constant saturation
    var l = 63; // constant luminance
    var a = 1;

    var getColor = function(val, range) {
      h = Math.floor((360 / range) * val);

      return "hsla(" + h +"," + s + "%," + l +"%," + a +")";
    };

    var getColor1 = function() {
      return "hsla(" + h +"," + s + "%," + (l - 30) +"%," + a +")";
    };

    // draws a rounded rectangle
    var drawRect = function(context, x, y, width, height) {
      var radius = 10
      context.beginPath();
      context.moveTo(x + radius, y);
      context.lineTo(x + width - radius, y);
      context.quadraticCurveTo(x + width, y, x + width, y + radius);
      context.lineTo(x + width, y + height - radius);
      context.quadraticCurveTo(x + width, y + height, x + width -
      radius, y + height);
      context.lineTo(x + radius, y + height);
      context.quadraticCurveTo(x, y + height, x, y + height - radius);
      context.lineTo(x, y + radius);
      context.quadraticCurveTo(x, y, x + radius, y);
      context.closePath();
    }

    this.createCanvas = function(label, range) {
      var canvas = document.createElement("canvas");
      canvas.width = width;
      canvas.height = height;

      var context = canvas.getContext("2d");

      var val = parseInt(label);

      context.clearRect(0,0,width,height);

      var grad = context.createLinearGradient(0, 0, 0, height);

      var color0 = getColor(val, range);

      grad.addColorStop(0, color0);
      grad.addColorStop(1, getColor1());

      context.fillStyle = grad;
      context.strokeStyle = color0;

      drawRect(context, 0, 0, width, height);
      context.fill();
      context.stroke();

      context.fillStyle = "white";
      context.strokeStyle = "black"

      // Render Label
      context.font = "bold 12pt Arial";
      context.textBaseline  = "top";

      var textWidth = context.measureText(label);

      // centre the text.
      context.fillText(label,
        Math.floor((width / 2) - (textWidth.width / 2)),
        4
      );

      return canvas;

    };

    this.create = function(label, range) {
      var canvas = this.createCanvas(label, range);
      return canvas.toDataURL();
    };
  }
})();

Example usage:

for(var i = 1; i<100; i++) {
  document.body.appendChild(ButtonFactory.createCanvas(i,99));
}