Draw Lines

Download the code for Drawing

An interactive environment lets us simulate real-world behaviors and interactions. This week, we'll simulate the act of drawing. We'll start with a simple example and move on to an app that lets you draw and paint on the screen. Let's get started.

Draw a line on the canvas with your mouse or finger.

You'll need a contemporary browser with javascript turned on to be able to see this page

The interaction is fairly simple for this example: whenever we click down and move the mouse, we draw a line that follows the mouse's movement; release the mouse and the drawing stops; click again and move the mouse to draw another line.

Drawer Behavior

We'll track mouse events, follow the mouse's position, and respond when the mouse is clicked and released. We'll do this in a behavior called Drawer. Its code looks like this:

//Drawer
//Click down the mouse to draw a line; the line follows the mouse's movement 
var Drawer = function(drawerArgs) {
  "use strict";	
  if (!drawerArgs) {
	  drawerArgs = {};
  }
  this.mouseDownCallback = drawerArgs.mouseDownCallback || function(){};
  this.mouseUpCallback = drawerArgs.mouseUpCallback || function(){};
  
  this.evtMgr = MyEventManager.getInstance();
  this.evtMgr.addMouseListener(this);
  
  this.mouseLoc = null; 
  this.name = "drawer";
  return this;	
};

Drawer.prototype.execute = function(sprite, context, time) {
  "use strict";	
  if (this.mouseLoc !== null) {
	sprite.mouseLoc = this.mouseLoc; 
	sprite.mouseIsDown = this.mouseIsDown;
  }
};

Drawer.prototype.mouseMove = function(mouseLoc, mouseVelocity) {
  "use strict"; 
  this.mouseLoc = mouseLoc; 
};

Drawer.prototype.mouseDown = function(mouseLoc) {
  "use strict"; 
  this.mouseIsDown = true;
  this.mouseDownCallback();
};

Drawer.prototype.mouseUp = function(mouseLoc) {
  "use strict"; 
  this.mouseIsDown = false;
  this.mouseUpCallback();
};

Drawer's Constructor

Let's focus on Drawer's constructor—the first function in our code listing. We pass a couple arguments to Drawer, both of them are callbacks: mouseDownCallback and mouseUpCallback. Remember that we use callbacks to be able to connect to objects outside the current object. Here we'll use the mouseDownCallback to tell a new Painter class—DrawLinePainter—to get ready to draw our line. We won't use the mouseUpCallback in this example.

After setting up our callbacks, we turn Drawer into a mouse-event listener. We do that by accessing MyEventManager and then adding a reference to Drawer to the manager's myMouseListener array. These two lines of code do that:

this.evtMgr = MyEventManager.getInstance();
this.evtMgr.addMouseListener(this);

When a behavior listens for mouse events, it also sets up a series of methods that are associated with each mouse event. Drawer has three such methods: mouseMove, mouseDown, and mouseUp.

Whenever a mouse event occurs, the event manager calls the appropriate method attached to the listening behavior. In the case of Drawer, as long its reference is stored inside the event manager's myMouseListener array, Drawer's mouse-event methods are called whenever the associated event occurs.

When we move the mouse, the event manager calls Drawer's mouseMove method. When we click down the mouse, event manager calls Drawer's mouseDown method. Release the mouse, the manager calls Drawer's mouseUp method. Inside each method we write the code that responds to a particular mouse event.

Drawer's execute Method

Drawer.prototype.execute = function(sprite, context, time) {
  "use strict";	
  if (this.mouseLoc !== null) {
	sprite.mouseLoc = this.mouseLoc; 
	sprite.mouseIsDown = this.mouseIsDown;
  }
};

Just a few lines of code here. Drawer tracks the mouse's position and stores it in its this.mouseLoc property. When the mouse clicks down, Drawer sets its mouseIsDown property to true; when the mouse is released, it sets mouseIsDown to false.

Drawerthis.mouseLoc isn't null, we update the sprite's mouseLoc and mouseIsDown with the values of Drawer's mouseLoc and mouseIsDown. The sprite properties are used by the sprite's painter, DrawLinePainter, to determine when and where to draw the line on the screen.

Mouse-Event Methods

Our last three methods are the mouse-event methods we mentioned above.

Drawer.prototype.mouseMove = function(mouseLoc, mouseVelocity) {
  "use strict"; 
  this.mouseLoc = mouseLoc; 
};

Drawer.prototype.mouseDown = function(mouseLoc) {
  "use strict"; 
  this.mouseIsDown = true;
  this.mouseDownCallback();
};

Drawer.prototype.mouseUp = function(mouseLoc) {
  "use strict"; 
  this.mouseIsDown = false;
  this.mouseUpCallback();
};

The mouseMove method keeps track of the mouse's current position, storing it in Drawer's this.mouseLoc for use in the execute method.

The mouseDown method sets Drawer's this.mouseIsDown to true. Like this.mouseLoc, this.mouseIsDown is stored and then used in the execute method. We also run the mouseDownCallback function in this method.

Finally the mouseUp method sets Drawer's this.mouseIsDown to false and runs the mouseUpCallback function. In this example, we didn't pass a mouseUpCallback to Drawer, so we call an empty anonymous function that doesn't run any code.

That's the code for this behavior. It tracks mouse movements and updates the sprite's mouseLoc and mouseIsDown properties based on the mouse's activity. It also runs the mouseDownCallback when the mouse is clicked. Let's look at that callback now.

Setup Script

To see the mouseDownCallback, open the setup script for this example, js/projects/drawing/draw-lines-setup.js.

//DrawLinesSetup
//Click down to start drawing a line, release the mouse to stop drawing
var DrawLinesSetup = function (setupArgs) {
  "use strict";
  //setupArgs passes in a reference to the project instantiation of the Main class
  if (!setupArgs) {
	  setupArgs = {};
  }
  //store a reference to "this" drawShapeSetUp object to use in callbacks below
  var myDrawing = this;
  this.myMain = setupArgs.myMain || {}; //store a reference to the Main object
  	
  //create a second canvas to draw on
  this.canvasDimensions = {w:1200, h:800}; //set default canvas dimensions
  if(setupArgs.myMain) { //if reference to Main object was passed, get the canvas dimensions from Main
	 this.canvasDimensions = setupArgs.myMain.getCanvasDimensions();
  } 	
		
  this.drawCanvas = document.createElement("canvas");
  this.drawContext = this.drawCanvas.getContext("2d"); 
  this.drawCanvas.width = this.canvasDimensions.w;
  this.drawCanvas.height = this.canvasDimensions.h;
 
 //line has it own particular Painter setupPaint method, you call it whenever you click down to draw a line
 //see behaviors Drawer's mouseDownCallback below
 
  this.lineDrawer = {
	spriteProps:{name:'lineDrawer', x: 0, y:0, w: 0, h: 0, type:"line", mouseLoc:null, mouseIsDown: false, strokeColor: "#000", lineWidth: 2, visible:true}, 
	painters:[new DrawLinePainter({canvas: this.drawCanvas, context: this.drawContext})], 
	behaviors:[new Drawer({mouseDownCallback: function(){myDrawing.lineDrawer.painters[0].setupPaint(myDrawing.lineDrawer.spriteProps);}})]
  };  
   
  this.lineDrawerSprite = new ShapeLineGraphic(this.lineDrawer);
  this.sprites = [this.lineDrawerSprite];

  return this;		
};

DrawLinesSetup.prototype.getSprites = function() {
  "use strict";
  return this.sprites;	
};

This setup script resembles many of the setup scripts we've seen in this course. Let's look at its unique aspects.

lineDrawer's Setup Data

You can find the mouseDownCallback function inside the highlighted this.linerDrawer. linerDrawer contains the sprite data we used to create a sprite and has the familiar three properties: spriteProps, painters, and behaviors.

Take a look at the spriteProps' mouseLoc and mouseIsDown properties. Those are the properties we update in Drawer's execute method. We also have strokeColor and lineWidth properties to help style the line's color and width.

Next look at the painters array. We're instantiating a DrawLinePainter. This is a new painter designed to help us draw a continuous line while the mouse is pressed down and moving around.

mouseDownCallback

Now look at the behaviors. We're instantiating our Drawer behavior and passing it a mouseDownCallback. Let's look at the callback's anonymous function. It'll be easier to read if we add a couple line breaks to the code:

function(){
  myDrawing.lineDrawer.painters[0].setupPaint(myDrawing.lineDrawer.spriteProps);
}

Inside our function we call a method on DrawLinePainter called setupPaint and pass that method the sprite's properties. Recall from last week, that we use closure to access our setup object when we're in another object. At the top of this setup script you'll see this line of code:

var myDrawing = this;

We've stored a reference to our setup script inside a variable called myDrawing. When you declare a variable inside a function, any function embedded inside that function has access to that variable. When we write an anonymous function within the main setup function, that anonymous function has access to the variable, myDrawing, no matter where it's run.

Let's look again at the line of code within our anonymous function:

myDrawing.lineDrawer.painters[0].setupPaint(myDrawing.lineDrawer.spriteProps);

We use myDrawing to connect to the setup script and then run a method that's tied to the painter stored inside this.lineDrawer. We also use myDrawing when we pass lineDrawer's sprite properties as an argument to setupPaint.

A Second Canvas

We'll talk more in a moment about DrawLinePainter and its setupPaint method. Before we do, let's highlight something new in our setup script.

Look toward the top of the script; you'll see these lines of code:

//create a second canvas to draw on
  this.canvasDimensions = {w:1200, h:800}; //set default canvas dimensions
  if(setupArgs.myMain) { //if reference to Main object was passed, get the canvas dimensions from Main
	 this.canvasDimensions = setupArgs.myMain.getCanvasDimensions();
  } 	
		
  this.drawCanvas = document.createElement("canvas");
  this.drawContext = this.drawCanvas.getContext("2d"); 
  this.drawCanvas.width = this.canvasDimensions.w;
  this.drawCanvas.height = this.canvasDimensions.h;

For most exercises, we create a single canvas and render everything on that canvas. For this exercise, though, we're drawing our line to a canvas that's separate from our main canvas. Why would we do that?

For this initial example, we could use just one canvas. But in the last two examples of this section, we have an interface that lets us choose different graphics to draw on the screen. To be able to render an interface using Canvas' shapes and also draw a line that starts with a mouse click and finishes with a mouse release, we need two canvases.

Drawing Shapes and Lines

Part of this comes down to the way we draw lines and shapes in Canvas. To start up a line or shape, we call the context's beginPath method. beginPath closes out any previous paths and starts up a new path. When we finish with a path, we run context's closePath method, which connects our path's end point to its beginning point, giving us a complete shape.

Since those methods—beginPath and closePath—belong to the canvas' context, when we have lots of shapes on the screen, as we will when we add an interface to our drawing project, we can't begin a line path and keep it open while the other shapes on screen need to start and finish their paths. We have just one context and if we run either beginPath or closePath, we'll affect the continuous line we hope to draw.

So we create a separate context for our line and render the interface in the original, main context.

Look back at lineDrawer's painters:

this.lineDrawer = {
	spriteProps:{name:'lineDrawer', x: 0, y:0, w: 0, h: 0, type:"line", mouseLoc:null, mouseIsDown: false, strokeColor: "#000", lineWidth: 2, visible:true}, 
	painters:[new DrawLinePainter({canvas: this.drawCanvas, context: this.drawContext})],
	behaviors:[new Drawer({mouseDownCallback: function(){myDrawing.lineDrawer.painters[0].setupPaint(myDrawing.lineDrawer.spriteProps);}})]
  };

When we instantiate DrawLinePainter, we pass our second canvas and context to DrawLinePainter. It draws our line to that second canvas and then draws that second canvas on to our main canvas. We'll see that code towards the end of this page.

DrawLinePainter

Let's look at our DrawLinePainter. You can find all the Painter classes in js/painters/all-painters.js. Recall that our mouseDownCallback runs DrawLinePainter's setupPaint method.

DrawPainters

DrawLinePainter inherits from DrawShapeLinePainter which in turn inherits from DrawPainter. They are all part of a set of classes that let us continuously draw lines, shapes, and bitmaps when we click down and drag our mouse around the screen.

The Structure of Painters

All Painter classes have a common set of methods: paint, setupPaint, startPaint, typePaint, and endPaint. The paint method typically calls the other four methods and the code in those methods varies based on the type of graphic we're painting.

If you're painting a rectangle, for example, you'll use the context.rect() to do that. If you're painting a circle, you'll use the context.arc(). Generally shape and graphic-specific code is placed inside typePaint—a method that changes based on the type of graphic we're painting.

Draw a Line

Let's talk about the process of drawing a line using DrawLinePainter. DrawLinePainter overrides its parent's paint method. Its own paint methods looks like this:

DrawLinePainter.prototype.paint = function(sprite, context) {
"use strict";
  if (sprite.mouseLoc && sprite.mouseIsDown) {
	this.startPaint(sprite, context);
	this.typePaint(sprite, context); //override with code specific to the graphic type
	this.endPaint(sprite, context);	
  }
};

Our Main object calls every sprite's paint method many times each second. The paint method generally calls four other methods: setupPaint, startPaint, typePaint, and endPaint.

setupPaint Method

setupPaint sets the properties of a line or shape and, most important, runs the context's beginPath method which clears out the current path and starts a new path.

DrawLinePainter calls setupPaint just once during the process of drawing a line because we need the same path to stick around as we draw the line on the screen.

If we call setupPaint each time the paint method is called by the Main loop, we'd constantly clear out the old path and start a new path and our line would never have a chance to grow into a full line path. (That's why we draw our line to a second canvas; we begin a path for that line when we click down and continue to add to that path until the mouse is released. We need a dedicated canvas to be able to keep that path open during the entire time we're drawing the line.)

endPaint Method

Another important difference in all our DrawPainter classes shows up in the endPaint method. Generally a Painter's endPaint method closes out the painting process. For lines and shapes that means we close out paths, fill in the color for shapes, stroke our lines, and restore the canvas. We do all of that in DrawLineShapePainter's endPaint method, but we also add something new. Since we're painting to a second canvas, before we end our paint process, we draw that additional canvas on to the main canvas. Here's the line of code that does that:

context.drawImage(this.drawCanvas, sprite.x, sprite.y);

We run the drawImage method on our main canvas' context and instead of passing it a bitmap as the first argument, we pass a reference to our second canvas, this.drawCanvas. The other two arguments set the position where the canvas is drawn on the main context.

startPaint and typePaint methods

Finally, to render the line, we rely on a few lines of code in the methods startPaint and typePaint. DrawLinePainter's startPaint method is housed in its grandparent, DrawPainter. Here's that startPaint code:

DrawPainter.prototype.startPaint = function(sprite, context) {
//start the paint process by saving out the current context and 
//translating the context to the mouse's current position
  "use strict";
  this.drawContext.save();
  if (sprite.alpha !== undefined) {
	  this.drawContext.globalAlpha = sprite.alpha;
  }
  
  if (sprite.mouseLoc) {
	this.drawContext.translate(sprite.mouseLoc.x || 0, sprite.mouseLoc.y || 0);
  }
  
  if (sprite.rotation) {
	  this.drawContext.rotate(sprite.rotation);
  }
  if (sprite.XYscale) {
	  this.drawContext.scale(sprite.XYscale[0] || 1.0, sprite.XYscale[1] || 1.0);
  }				
};

The highlighted code affects where the line is drawn. In startPaint, we translate the context to the mouse's current position which is saved inside the sprite's mouseLoc property. That places the context's top left corner at the mouse's position. Next we move the path to the context's top left corner. We do that in the DrawLinePainter's typePaint method. Here's that code:

DrawLinePainter.prototype.typePaint = function(sprite, context) {
  "use strict";
  if (sprite.mouseLoc && sprite.mouseIsDown) {
	//lineTo() method continually draws the line path while the mouse is down
	//we've already moved the context's top, left corner to the mouse's position, 
	//now we draw the path to that top, left corner
	this.drawContext.lineTo(0, 0); 
  }	 
};

We check that sprite's mouseLoc exists and that its mouseIsDown property is set to true. Then we run the second context's lineTo method, extending the path to the context's top left corner. Since we've already moved the context to the mouse's position in startPaint, lineTo(0,0) moves the path to mouse's current position, continuing the line.

Those are the main components of the DrawLinePainter and its parent classes. To learn more about the Painter classes, start with the parent of all Painter classes, GraphicPainter. Then head to DrawPainter, review DrawLineShapePainter, and study DrawLinePainter. All Painter classes are housed in one file, js/painters/all-painters.js.

Next we'll draw in color.