Shapes on a Plane

July 1st, 2008 by Jacob Rus

I’ve been making various little canvas graphics for an upcoming browser-based pure-JavaScript real-time multiplayer game, and decided to implement some shape primitives. Specifically, I created functions for drawing circles and regular polygons. Then, I decided I could use some curvy and straight stars, and, based on the regular polygon code, made two more functions, regularQuadraticStar and regularStar. Here’s an example of 50 of these shapes drawn on a canvas:

First, and most simply, a circle function. This one is not too much easier than just using the «context».arc function directly, but it makes code a tiny bit clearer, and also saves a couple of lines of code for each circle:

var circle = function (context, x, y, radius) {
  var c = context;
  c.beginPath();
  c.arc(x,y,radius,0,Math.PI*2, true)
  c.closePath();
}

Next, regular polygons can be easily drawn by translating, scaling, and rotating our drawing context, and then repeatedly drawing to the point (1, 0) and rotating the canvas. This is much easier to code and more readable than calculating the endpoints of a regular polygon. We remember to save the graphics context at the beginning of the function and restore it at the end:

var regularPolygon = function (context, x, y, radius,
                               angle, sides) {
  var c = context;
  var inner_angle = (2.0 * Math.PI) / sides
  c.save();
  c.beginPath();
  c.translate(x,y);
  c.rotate(angle);
  c.scale(radius, radius);
  c.moveTo(1,0);
  for (var i=0; i < sides; i++) {
    c.rotate(inner_angle);
    c.lineTo(1,0);
  };
  c.closePath();
  c.restore();
};

After drawing many pretty regular polygons, I decided that it might be nice to draw stars with the same corners as those regular polygons. To that end, I first made a regularQuadraticStar function which behaves identically, but adds a control point at (0, 0) between each pair of vertices:

var regularQuadraticStar = function (context, x, y, radius,
                                     angle, sides) {
  var c = context;
  var inner_angle = (2.0 * Math.PI) / sides
  c.save();
  c.beginPath();
  c.translate(x,y);
  c.rotate(angle);
  c.scale(radius, radius);
  c.moveTo(1,0);
  for (var i=0; i < sides; i++) {
    c.rotate(inner_angle);
    ctx.quadraticCurveTo(0,0,1,0);
  };
  c.closePath();
  c.restore();
};

And then, because sometimes normal pointy stars are needed, I made a regularStar function, which takes arguments like those of regularPolygon, along with one more, the “pointiness” of the star, which ranges from 0 (not at all pointy) to 1 (basically just radial lines or spokes):

var regularStar = function (context, x, y, radius,
                            angle, sides, pointiness) {
  var c = context;
  var half_inner_angle = (Math.PI) / sides
  c.save();
  c.beginPath();
  c.translate(x,y);
  c.rotate(angle);
  c.scale(radius, radius);
  c.moveTo(1,0);
  for (var i=0; i < sides; i++) {
    c.rotate(half_inner_angle);
    ctx.lineTo(1 - pointiness, 0);
    c.rotate(half_inner_angle);
    ctx.lineTo(1,0);
  };
  c.closePath();
  c.restore();
};

And then, to test the whole thing, I decided to generate a number of random shapes and colors, and then draw them on a canvas.

To do that, I needed a couple of simple helper functions, based on Math.random():

var uniform_random = function (low, high) {
  if (typeof(high) === 'undefined') {
    high = low;
    low = 0;
  }
  return low + (high - low) * Math.random()
};

var random_int = function (low, high) {
  return Math.floor(uniform_random(low, high))
}

var random_choice = function (array) {
  var choice = Math.floor(Math.random() * array.length);
  return array[choice];
};

var random_color = function() {
  var r = random_int(30, 256).toString();
  var g = random_int(30, 256).toString();
  var b = random_int(30, 256).toString();
  var a = uniform_random(0.2, 1.0).toString();
  return 'rgba(' + r + ', ' + g + ', ' + b + ', ' + a + ')'
}

And finally, using all of these functions, it was possible to make a function to create a random shape:

var randomShape = function (context, width, height, maxradius) {
  context.save()

  var minradius = 0.1 * maxradius;
  var minstroke = 0.5;
  var maxstroke = 5.0;

  var x = uniform_random(0, width);
  var y = uniform_random(0, height);
  var radius = uniform_random(minradius, maxradius);
  var angle = uniform_random(0.0, 2.0 * Math.PI);
  var sides = random_int(3,9);
  var pointiness = uniform_random(0.3, 0.9);

  var shape = random_choice([circle, regularPolygon,
                             regularQuadraticStar, regularStar]);

  context.fillStyle = random_color();
  context.strokeStyle = random_color();
  context.lineJoin = random_choice(['round', 'bevel', 'miter']);
  context.miterLimit = 10;
  context.lineWidth = uniform_random(minstroke, maxstroke);

  // actually draw the shape
  shape(context, x, y, radius, angle, sides, pointiness);
  context.fill();
  context.stroke();

  context.restore()
};

And then finally create a number of random shapes:

var canvasElement = document.getElementById('shapes');
canvasElement.width = 400;
canvasElement.height = 250;
var ctx = canvasElement.getContext('2d');

// fill our canvas with black
ctx.save();
ctx.fillStyle = 'rgb(0, 0, 0)';
ctx.fillRect(0,0,400,250);
ctx.restore();

for (var i=0; i < 50; i++) {
  randomShape(ctx, 400, 250, 30);
};

And there we have it—a canvas filled with 50 random shapes:

Leave a Reply