Yet Another Way to Draw Lines

Sure, why not, right?

One of the things I was trying to accomplish with the shaky drawing from my last post was to give the idea of hand-drawn lines. By adding in a bit of random variance, lines and shapes can seem like they are drawn by a human rather than perfectly rendered by a computer.

But at some point I was actually sketching something by hand and realized another big difference between computer drawing and human sketching. Very often when a person is sketching, rather than boldly drawing what they hope is a straight line from point A to B, they will instead make a number of very light lines that roughly go between the two points. You’ve seen it, you’ve done it, you know what I mean. So let’s replicate that.

The idea is that when we want to draw a line from x0, y0 to x1, y1 for example, we’ll actually draw multiple lines – not exactly to those to points, but from somewhere around the first point, to somewhere around the second one.

In this case, I’m going to dive right into a function. This was my first attempt:

function sketchLine(context, x0, y0, x1, y1, count, rand) {
  context.beginPath();
  for (let i = 0; i < count; i++) {
    let x = x0 + Math.random() * rand - rand / 2;
    let y = y0 + Math.random() * rand - rand / 2;
    context.moveTo(x, y);

    x = x1 + Math.random() * rand - rand / 2;
    y = y1 + Math.random() * rand - rand / 2;
    context.lineTo(x, y);
  }
  context.stroke();
}

We can then call this like so:

context.lineWidth = 0.2;
sketchLine(context, 100, 100, 700, 700, 5, 20);

Note that I set the line width down to 0.2 to make very light lines. We’ll let the multiple light lines build up to create the idea of a sketch, the same way you would do it by hand. So this will draw 5 lines with a variance of up to 10 pixels in any direction from the starting and ending points. The result:

Not that great, to be honest. The lines vary a bit, but they are still too uniform. I can bump up the randomness to, say, 50, but that looks even less like what a real person would sketch.

The thing with sketching, particularly in longer lines, is that you don’t always sketch the entire length of the line on each stroke. You might start around the first point and stroke maybe half way to the other point. And then you might stroke the middle third of the distance between them, and then something closer to the ending point. You’d do a bunch of strokes with varying start and end distances. Well, we can do something like that too.

function sketchLine(context, x0, y0, x1, y1, count, rand) {
  const dx = x1 - x0;
  const dy = y1 - y0;

  context.beginPath();
  for (let i = 0; i < count; i++) {
    let t0 = Math.random() * 0.5;
    let t1 = Math.random() * 0.5 + 0.5;
    
    let x = x0 + dx * t0 + Math.random() * rand - rand / 2;
    let y = y0 + dy * t0 + Math.random() * rand - rand / 2;
    context.moveTo(x, y);

    x = x0 + dx * t1 + Math.random() * rand - rand / 2;
    y = y0 + dy * t1 + Math.random() * rand - rand / 2;
    context.lineTo(x, y);
  }
  context.stroke();
}

This should look somewhat familiar if you’ve followed along with the other two posts in this series. First I get length of the line on the x and y axes – dx and dy. In the loop, I create a t value between 0 and 1. In this case, I create two of these – t0 and t1 – one for the start of the line and one for the end. t0 will range from 0.0 to 0.5 and t1 will go from 0.5 to 1.0. This should give us a random assortment of lines – some from very near the start point to very near the end point, some more in the first part of the line, some in the middle and some more near the end. Calling this again with:

context.lineWidth = 0.2;
sketchLine(context, 100, 100, 700, 700, 5, 20);

We get this image:

That’s something I could almost start to believe was done by hand. Let’s do a rectangle function!

function sketchRect(context, x, y, w, h, count, rand) {
  sketchLine(context, x, y, x + w, y, count, rand);
  sketchLine(context, x + w, y, x + w, y + h, count, rand);
  sketchLine(context, x + w, y + h, x, y + h, count, rand);
  sketchLine(context, x, y + h, x, y, count, rand);
}

Obviously, I just copied and pasted the shakyRect from the previous post and changed the method names and parameters. So let’s call this with:

context.lineWidth = 0.2;
sketchRect(context, 100, 100, 600, 600, 5, 20);

Pretty cool, but this brings up an issue we couldn’t immediately see when drawing a single line. All too often, the lines don’t reach all the way to those starting or ending points. Too many are falling right in the middle. We can shift our random parameters a bit to make up for that.

function sketchLine(context, x0, y0, x1, y1, count, rand) {
  const dx = x1 - x0;
  const dy = y1 - y0;

  context.beginPath();
  for (let i = 0; i < count; i++) {
    let t0 = Math.random() * 0.4 - 0.1;
    let t1 = Math.random() * 0.4 + 0.7;
    
    let x = x0 + dx * t0 + Math.random() * rand - rand / 2;
    let y = y0 + dy * t0 + Math.random() * rand - rand / 2;
    context.moveTo(x, y);

    x = x0 + dx * t1 + Math.random() * rand - rand / 2;
    y = y0 + dy * t1 + Math.random() * rand - rand / 2;
    context.lineTo(x, y);
  }
  context.stroke();
}

Now, t0 will range from -0.1 to +0.3, and t1 will go from 0.7 to 1.1. This makes it more likely that some lines will make it closer to one of the points, and may even overshoot it a bit, which is just what you’d do by hand. The results of this change:

Not bad, in my opinion. You’ll still have some rectangles with open corners, but not as often as before. Here’s one with a count of 20 and a rand of 40:

Finally, just for the heck of it, I combined the shaky and sketchy techniques with this function:

function sketchLine(context, x0, y0, x1, y1, count, rand) {
  const dx = x1 - x0;
  const dy = y1 - y0;

  context.beginPath();
  for (let i = 0; i < count; i++) {
    let t0 = Math.random() * 0.4 - 0.1;
    let t1 = Math.random() * 0.4 + 0.7;
    
    let xA = x0 + dx * t0 + Math.random() * rand - rand / 2;
    let yA = y0 + dy * t0 + Math.random() * rand - rand / 2;
    let xB = x0 + dx * t1 + Math.random() * rand - rand / 2;
    let yB = y0 + dy * t1 + Math.random() * rand - rand / 2;
    shakyLine(context, xA, yA, xB, yB, 20, 2);
  }
  context.stroke();
}

Here, I calculated the start and end points of each sketched line and rather than using moveTo and lineTo I called the shakyLine function from my last post. I hard coded it with a res of 20 and a rand of 2, which makes it just a little bit less than a perfectly straight line and possibly a little bit more like something hand drawn. Called with:

context.lineWidth = 0.2;
sketchRect(context, 100, 100, 600, 600, 10, 20);

it gives us:

Even that might be too much shake, but you can easily make it more subtle.

Here’s one final example. If this doesn’t look hand sketched, I don’t know what does.

Well, that’s that. I don’t have a ready-made library to share with you on this one, but hopefully you’ll find this useful or at least inspiring.

The rest of the How to Draw a Line series:

1. How to Draw a Line

2. More Ways to Draw a Line

3. Yet Another Way to Draw Lines

2 thoughts on “Yet Another Way to Draw Lines”

Leave a Reply

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