Handdrawn circle simulation in HTML 5 canvas
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:
- Generate the points
- Animate the points
Initialization:
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.beginPath();
ctx.moveTo(points[0], points[1]);
/// call loop
draw();
function draw() {
ctx.lineTo(points[i], points[i + 1]);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(points[i], points[i + 1]);
i += 2;
if (i < points.length) {
requestAnimationFrame(draw);
} else {
if (typeof callback === 'function')
callback();
}
}
}
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.save();
ctx.translate(cx, cy);
ctx.rotate(-rotate);
ctx.translate(-cx, -cy);
and when the loop finishes:
if (i < points.length) {
requestAnimationFrame(draw);
} else {
ctx.restore();
}
(included in the demo linked above).
The circle will look more like this:
Update
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) {
requestAnimationFrame(draw);
} else {
ctx.restore();
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).
Here are some basics I created for this answer:
http://jsfiddle.net/Exceeder/TPDmn/
Basically, when you draw a circle, you need to account for hand imperfections. So, in the following code:
var img = new Image();
img.src="data:image/png;base64,...";
var ctx = $('#sketch')[0].getContext('2d');
function draw(x,y) {
ctx.drawImage(img, x, y);
}
for (var i=0; i<500; i++) {
var radiusError = +10 - i/20;
var d = 2*Math.PI/360 * i;
draw(200 + 100*Math.cos(d), 200 + (radiusError+80)*Math.sin(d) );
}
Pay attention how vertical radiusError changes when the angle (and the position) grows. You are welcome to play with this fiddle until you get a "feel" what component does what. E.g. it would make sense to introduce another component to radiusError that emulates "unsteady" hand by slowly changing it my random amounts.
There are many different ways to do this. I choose trig functions for the simplicity of the simulation, as speed is not a factor here.
Update:
This, for example, will make it less perfect:
var d = 2*Math.PI/360 * i;
var radiusError = +10 - i/20 + 10*Math.sin(d);
Obviously, the center of the circle is at (200,200), as the formula for drawing a circle (rather, ellipsis with vertical radius RY and horizontal radius RX) with trigonometric functions is
x = centerX + RX * cos ( angle )
y = centerY + RY * sin ( angle )
Your task seems to have 3 requirements:
- A hand-drawn shape.
- An “organic” rather than “ultra-precise” stroke.
- Revealing the circle incrementally instead of all-at-once.
To get started, check out this nice on-target demo by Andrew Trice.
This amazing circle is hand drawn by me (you can laugh now...!)
Andrew's demo does steps 1 and 2 of your requirements.
It lets you hand draw a circle (or any shape) using an organic looking “brush effect” instead of the usual ultra-precise lines normally used in canvas.
It achieves the “brush effect” by by repeated drawing a brush image between hand drawn points
Here’s the demo:
http://tricedesigns.com/portfolio/sketch/brush.html#
And the code is available on GitHub:
https://github.com/triceam/HTML5-Canvas-Brush-Sketch
Andrew Trice’s demo draws-and-forgets the lines that make up your circle.
Your task would be to impliment your third requirement (remembering strokes):
- Hand draw a circle of your own,
- Save each line segment that makes up your circle in an array,
- “Play” those segements using Andrew’s stylized brush technique.
Results: A hand-drawn and stylized circle that appears incrementally instead of all at once.
You have an interesting project…If you feel generous, please share your results!