user1477388 user1477388 - 1 year ago 109
Javascript Question

Handdrawn circle simulation in HTML 5 canvas

The following code creates a circle in HTML 5 Canvas using jQuery:


//get a reference to the canvas
var ctx = $('#canvas')[0].getContext("2d");

DrawCircle(75, 75, 20);

//draw a circle
function DrawCircle(x, y, radius)
ctx.arc(x, y, radius, 0, Math.PI*2, true);
ctx.fillStyle = 'transparent';
ctx.lineWidth = 2;
ctx.strokeStyle = '#003300';

I am trying to simulate any of the following types of circles:


I have researched and found this article but was unable to apply it.

I would like for the circle to be drawn rather than just appear.

Is there a better way to do this? I'm sensing there's going to be a lot of math involved :)

P.S. I like the simplicity of PaperJs, maybe this would be the easiest approach using it's simplified paths?

Answer Source

There are already good solutions presented here. I wanted to add a variations of what is already presented - there are not many options beyond some trigonometry if one want to simulate hand drawn circles.

I would first recommend to actually record a real hand drawn circle. You can record the points as well as the timeStamp and reproduce the exact drawing at any time later. You could combine this with a line smoothing algorithm.

This here solution produces circles such as these:


You can change color, thickness etc. by setting the strokeStyle, lineWidth etc. as usual.

To draw a circle just call:

handDrawCircle(context, x, y, radius [, rounds] [, callback]);

(callback is provided as the animation makes the function asynchronous).

The code is separated into two segments:

  1. Generate the points
  2. Animate the points


function handDrawCircle(ctx, cx, cy, r, rounds, callback) {

    /// rounds is optional, defaults to 3 rounds    
    rounds = rounds ? rounds : 3;

    var x, y,                                      /// the calced point
        tol = Math.random() * (r * 0.03) + (r * 0.025), ///tolerance / fluctation
        dx = Math.random() * tol * 0.75,           /// "bouncer" values
        dy = Math.random() * tol * 0.75,
        ix = (Math.random() - 1) * (r * 0.0044),   /// speed /incremental
        iy = (Math.random() - 1) * (r * 0.0033),
        rx = r + Math.random() * tol,              /// radius X 
        ry = (r + Math.random() * tol) * 0.8,      /// radius Y
        a = 0,                                     /// angle
        ad = 3,                                    /// angle delta (resolution)
        i = 0,                                     /// counter
        start = Math.random() + 50,                /// random delta start
        tot = 360 * rounds + Math.random() * 50 - 100,  /// end angle
        points = [],                               /// the points array
        deg2rad = Math.PI / 180;                   /// degrees to radians

In the main loop we don't bounce around randomly but increment with a random value and then increment linearly with that value, reverse it if we are at bounds (tolerance).

for (; i < tot; i += ad) {
    dx += ix;
    dy += iy;

    if (dx < -tol || dx > tol) ix = -ix;
    if (dy < -tol || dy > tol) iy = -iy;

    x = cx + (rx + dx * 2) * Math.cos(i * deg2rad + start);
    y = cy + (ry + dy * 2) * Math.sin(i * deg2rad + start);

    points.push(x, y);

And in the last segment we just render what we have of points.

The speed is determined by da (delta angle) in the previous step:

    i = 2;

    /// start line    
    ctx.moveTo(points[0], points[1]);

    /// call loop

    function draw() {

        ctx.lineTo(points[i], points[i + 1]);

        ctx.moveTo(points[i], points[i + 1]);

        i += 2;

        if (i < points.length) {
        } else {
            if (typeof callback === 'function')

(see comments below in update section)

Tip: To get a more realistic stroke you can reduce globalAlpha to for example 0.7.

However, for this to work properly you need to draw solid to an off-screen canvas first and then blit that off-screen canvas to main canvas (which has the globalAlpha set) for each frame or else the strokes will overlap between each point (which does not look good).

For squares you can use the same approach as with the circle but instead of using radius and angle you apply the variations to a line. Offset the deltas to make the line non-straight.

I tweaked the values a little but feel free to tweak them more to get a better result.

To make the circle "tilt" a little you can first rotate the canvas a little:

rotate = Math.random() * 0.5;;
ctx.translate(cx, cy);
ctx.translate(-cx, -cy);

and when the loop finishes:

if (i < points.length) {
} else {

(included in the demo linked above).

The circle will look more like this:

Snapshot tilted


To deal with the issues mentioned (comment fields too small :-) ): it's actually a bit more complicated to do animated lines, especially in a case like this where you a circular movement as well as a random boundary.

Ref. comments point 1: the tolerance is closely related to radius as it defined max fluctuation. We can modify the code to adopt a tolerance (and ix/iy as they defines how "fast" it will variate) based on radius. This is what I mean by tweaking, to find that value/sweet-spot that works well with all sizes. The smaller the circle the smaller the variations. Optionally specify these values as arguments to the function.

Point 2: since we're animating the circle the function becomes asynchronous. If we draw two circles right after each other they will mess up the canvas as seen as new points are added to the path from both circles which then gets stroked criss-crossed.

We can get around this by providing a callback mechanism:

handDrawCircle(context, x, y, radius [, rounds] [, callback]);

and then when the animation has finished:

if (i < points.length) {

} else {
    if (typeof callback === 'function')
        callback();  /// call next function

Another issues one will run into with the code as-is (remember that the code is meant as an example not a full solution :-) ) is with thick lines:

When we draw segment by segment separately canvas does not know how to calculate the butt angle of the line in relation to previous segment. This is part of the path-concept. When you stroke a path with several segments canvas know at what angle the butt (end of the line) will be at. So here we to either draw the line from start to current point and do a clear in between or only small lineWidth values.

When we use clearRect (which will make the line smooth and not "jaggy" as when we don't use a clear in between but just draw on top) we would need to consider implementing a top canvas to do the animation with and when animation finishes we draw the result to main canvas.

Now we start to see part of the "complexity" involved. This is of course because canvas is "low-level" in the sense that we need to provide all logic for everything. We are basically building systems each time we do something more with canvas than just draw simple shapes and images (but this also gives the great flexibility).

I address a couple of issues in this updated fiddle but you will still need to tweak then a little bit (updated with top canvas, callback, dynamic tolerance).