Skip to content

Latest commit

 

History

History
 
 

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 

Target title: Dynamic drawings

Script

<canvas id="my-canvas" width="500" height="500"></canvas>

<script type="text/javascript" src="/main.js"></script>

In the last lesson, we used the <canvas> element to draw a stick figure.

One thing we didn't talk about was... why should use using a <canvas> for drawing in the first place? Let's think about that for a bit.

If you simply wanted put an image on a webpage, you can draw it on a tool like Paint, or Photoshop, save it as an image file, and add it to your webpage using the <img> tag.

<img src="my-image.png" />

<script type="text/javascript" src="/main.js"></script>

...and that would be that. Drawing a stick figure in Photoshop is always going to be easier than writing the Javascript required to build that stick figure from scratch using commands. And once you're done drawing, there's very little additional work required to actually put it on the web-page. It's just one line of HTML.

Let's take a look at that <canvas> for a second.

<canvas id="my-canvas" width="500" height="500"></canvas>

<script type="text/javascript" src="/main.js"></script>

More specifically, let's look at the code that creates our image.

var c = document.getElementById("my-canvas");
var ctx = c.getContext("2d");

ctx.beginPath();
ctx.arc(250, 100, 50, 0, 2 * Math.PI);
ctx.stroke();

let drawLine = (startX, startY, endX, endY) => {
  ctx.beginPath();
  ctx.moveTo(startX, startY);
  ctx.lineTo(endX, endY);
  ctx.stroke();
};

drawLine(250, 150, 250, 350); // Main body
drawLine(250, 200, 150, 150); // Right Arm
drawLine(250, 200, 350, 150); // Left Arm
drawLine(250, 350, 225, 450); // Right Leg
drawLine(250, 350, 275, 450); // Left Leg

HIGHLIGHT THE drawLine CALLS

Notice how most of this is just numbers. It's just data. What we've done is convert an image into some data-points coupled with some commands to use that data.

HIGHLIGHT THE ENTIRE SCRIPT

And this program is something that we control. We decide what the image looks like. And we can change this at any time.

Let me demonstrate what I mean. Let's build an user avatar generator. What I'm talking about is an app that will generates a random face each time you visit the webpage.

Let's get started.

Since we're going focus on just the face in this lesson, let's delete the body, and make the face a lot bigger on our canvas.

I'll first delete the drawLine calls, and then move the circle over to the center of the canvas and increase its radius.

ctx.beginPath();
ctx.arc(250, 250, 150, 0, 2 * Math.PI);
ctx.stroke();

Switch to browser and refresh.

Yeah, that looks right. I've got a nice big empty face.

Before we start drawing inside the face, we have to make some decisions - what are the elements of a face?

Obviously a face has a lot of elements - you've got hair, eyes, ears, nose, mouth, chin, skin color, and a lot more. Let's just focus of three of those things for this lesson.

I'm going to pick eyes, nose, & mouth.

// Eyes
// Nose
// Mouth

When you work through this lesson, feel free to pick whatever facial elements you like.

All right, let's keep in mind that what we're creating is a random face generator. I'm going to write a function just to remind me of that.

let drawRandomFace = () => {};

Instead of writing any code in this function right now, let's try and build each of of the elements that we've picked, starting with the eyes. I'll create a drawEyes function that takes two arguments - the radius of the eye and the color of the eye.

// Eyes
let drawEyes = (radius, color) => {};

Now within this function, I have to draw both eyes. I'm just going to draw a circle for the left eye, and then another for the right eye.

// Eyes
let drawEyes = (radius, color) => {
  // Left eye
  // Right eye
};

One way I can go about drawing the circles for each eye is to copy the code from where we drew the face [HIGHLIGHT FACE JS], or knowing that I'll need to re-use this code, I can write a drawCircle function, just like we wrote drawLine [HIGHLIGHT drawLine FUNCTION]. Now, this new function needs to take all the arguments that are required to draw a circle. We know that it's the center co-ordinates of the circle, it's radius, and starting and ending angles.

let drawCircle = (centerX, centerY, radius, startAngle, endAngle) => {};

I'll just copy-paste the code from where we drew the circle for the face, and then modify it to use the arguments.

let drawCircle = (centerX, centerY, radius, startAngle, endAngle) => {
  ctx.beginPath();
  ctx.arc(centerX, centerY, radius, startAngle, endAngle);
  ctx.stroke();
};

Now we can use it to draw the face...

let drawCircle = (centerX, centerY, radius, startAngle, endAngle) => {
  ctx.beginPath();
  ctx.arc(centerX, centerY, radius, startAngle, endAngle);
  ctx.stroke();
};

drawCircle(250, 250, 150, 0, 2 * Math.PI);

...and finally we can draw the eyes. To start with, we can draw circles for the eyes. I'll put one circle to the left and one to the right.

// Eyes
let drawEyes = (radius, color) => {
  // Left eye
  drawCircle(300, 200, radius, 0, 2 * Math.PI);

  // Right eye
  drawCircle(200, 200, radius, 0, 2 * Math.PI);
};

But immediately, we can see a problem. We don't have any way to actually set the color that should go inside the circle. We haven't done that before.

Let's try to find out how that can be done.

I'm going to google for canvas fill circle with color

The first result [HTML Canvas fill() Method - W3Schools] looks promising, and if we take a look inside, it seems that this isn't actually talking about circle. It's actually talking about drawing rectangles.

But look at what it's telling us to do. Obviously rect is not a function we've used before, but this looks similar to how we the circle's arc function. What's different are these two lines:

  • There's a line which sets a fillStyle, which seems to be setting the color.
  • There's also a fill function which seems to apply the color.

Let's actually just copy these two lines into our drawCircle function, and then change the color that's written down with a new argument for the function - fill color.

let drawCircle = (
  centerX,
  centerY,
  radius,
  startAngle,
  endAngle,
  fillColor
) => {
  ctx.beginPath();
  ctx.arc(centerX, centerY, radius, startAngle, endAngle);
  ctx.fillStyle = fillColor;
  ctx.fill();
  ctx.stroke();
};

Now we do have a way to set the color of the eyes:

let drawEyes = (radius, color) => {
  // Left eye
  drawCircle(300, 200, radius, 0, 2 * Math.PI, color);

  // Right eye
  drawCircle(200, 200, radius, 0, 2 * Math.PI, color);
};

I also remember that we used this drawCircle function in one other location - to draw the face itself, so that call [HIGHLIGHT IT] needs to be updated to add the fillColor argument.

Now I don't actually want a fillColor for this circle, so I'm going to take a guess, and pass fillColor as transparent.

drawCircle(250, 250, 150, 0, 2 * Math.PI, "transparent");

We haven't yet called this drawEyes function anywhere, so let's call the function as well.

drawEyes(15, "brown");

Let's see whether that works.

Switch to browser and refresh.

Yup. We've got eyes.

Just like we did the nose, I'm going to add a drawNose function. In the interest of saving time, I'm simply going to paste in code that I've created before:

let drawNose = (type) => {
  // Nose type is either "crooked", or "button".
  if (type === "crooked") {
    drawLine(250, 225, 200, 275);
    drawLine(200, 275, 250, 275);
  } else {
    drawCircle(250, 250, 20, 0, Math.PI, "transparent");
    drawLine(230, 250, 250, 225);
    drawLine(270, 250, 250, 225);
  }
};

I haven't done anything new here, but you can see that I've decided that nose has a type, and it's either crooked or button. If it's crooked, I'll draw it with two lines, and if it's button, I'm drawing a semi-cricle with two lines on top of it. Let's take a look at how that works.

drawNose("button");

And if I switch to "crooked", we've got another kind of nose.

drawNose("crooked");

Finally, we can write the drawMouth function. Just like with the nose, I'm going to paste this in to save time:

// Mouth
let drawMouth = (expression) => {
  // Expression is either an 'surprise', 'happy' & 'flat'.
  if (expression === "surprise") {
    drawCircle(250, 325, 40, 0, 2 * Math.PI, "transparent");
  } else if (expression === "happy") {
    drawCircle(250, 300, 40, 0, Math.PI, "transparent");
  } else {
    drawLine(200, 325, 300, 325);
  }
};

Similar to the nose, I've created three different expressions - suprise, happy, and flat. Surprise is a full circle, happy is semi-circle, and flat is just a line. Let's see how that works by calling it:

drawMouth("happy");

First "happy", then "surprise", and then "flat". OK. Those work.

Now that we've written the code to generate a face, we still have an empty drawRandomFace function that we haven't written. However, this bit is actually going to be easy. Let's just move in these three function calls into the drawRandomFace function, and then call the function itself.

let drawRandomFace = () => {
  drawEyes(15, "brown");
  drawNose("crooked");
  drawMouth("flat");
};

drawRandomFace();

What's left to do is to set random values for these function calls.

Let's start with the radius of the eye. I'm going to say that I need a random number between 1 and 20px for the radius of the eye. Let's find out how to generate a random number. Let's google js random number.

The first result looks good.

And yes, it tells us that there's a Math.random() function. However, it also says that this function always returns a number between 0 & 1. Let's quickly try that out in the console.

Math.random();

So it appears to be giving us floating point numbers between 0 & 1. What we want is a integer random number between 1 and 20.

Thankfully, this same page tells us exactly what we need to do to get a random integer number. It says we should multiply the random number with the maximum number that we want, and then floor it. floor is a just a function that rounds a number down to nearest integer.

In fact, if we scroll down on this page, we find a very helpful section that tells us that it's a good idea to create a proper random function, and it even gives us the code for that function. This getRndInteger function takes a minimum value, a maximum value, and then returns a random number integer between those two values. This is exactly what we need, so let's copy and paste that into our code.

function getRndInteger(min, max) {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}

I'll change the way the function is written a bit, so that it matches our syntax for JS:

let getRndInteger = (min, max) => {
  return Math.floor(Math.random() * (max - min + 1)) + min;
};

Now we can use it to set the radius of the eye.

let drawRandomFace = () => {
  let eyeRadius = getRndInteger(1, 20);

  drawEyes(eyeRadius, "brown");
  drawNose("crooked");
  drawMouth("flat");
};

What about these string values though?

Let's actually use the same random number generator to pick an eye color for. I'll create an array of valid colors, and then pick a random color from it.

let drawRandomFace = () => {
  let eyeRadius = getRndInteger(1, 20);
  let eyeColor = ["brown", "blue", "green"][getRndInteger(0, 2)];

  drawEyes(eyeRadius, eyeColor);
  drawNose("crooked");
  drawMouth("flat");
};

Similar to that, I can pick the nose type and the mouth expression as well.

let drawRandomFace = () => {
  let eyeRadius = getRndInteger(1, 20);
  let eyeColor = ["brown", "blue", "green"][getRndInteger(0, 2)];
  let noseType = ["crooked", "button"][getRndInteger(0, 1)];
  let expression = ["surprise", "happy", "flat"][getRndInteger(0, 2)];

  drawEyes(eyeRadius, eyeColor);
  drawNose(noseType);
  drawMouth(expression);
};

That's actually it. Let's see how that works.

Switch to browser and refresh.

When we refresh the page, we get a new face.

There you go. That's a very simple random avatar generator.

What we've done here right now - that's not easy to do with Photoshop, or Paint. It's not easy to create random artwork like this manually.

What we're doing is using the data that defines what an image is, to build it from scratch.

In fact, let's just google random avatar generator and see what other people have built.

Click first result: Avataaars Generator (getavataaars.com)

Let's try clicking their Random button.

As you can see it's actually very similar to what we've built, except of course, it's a lot more sophisticated. It's got many more options, and it's much more polished. But at a fundamental level, both this web application, and our small webpage are doing exactly the same thing - taking input data and transforming it into something else.

That's it for this lesson. See you in the next one!