Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Selectable Shapes Part 2: Resizable, Movable Shapes on HTML5 Canvas

DZone's Guide to

Selectable Shapes Part 2: Resizable, Movable Shapes on HTML5 Canvas

· Web Dev Zone
Free Resource

Tips, tricks and tools for creating your own data-driven app, brought to you in partnership with Qlik.

In the first tutorial I showed how to create a basic data structure for shapes on an HTML5 canvas and how to make them selectable, draggable, and movable. In this second part we’ll be reorganizing the code a little bit and adding selection handles so we can resize our Canvas objects.

The finished canvas will look like this:

Click to select a box. Click on a selection handle to resize. Double click to add new boxes.

This article’s code is written primarily to be easy to understand. It isn’t optimized for performance because a little bit of the drawing is set up so that more complex shapes can easily be added in the future.

In this tutorial we will add:

  1. Code for drawing the eight boxes that make up the selection handles
  2. Some small adjustments to the draw code
  3. Code to be run on every mouse move event
  4. Code for changing the mouse cursor when it is over a selection handle
  5. Code for resizing

Drawing the selection handles

The eight selection handles are unique in that each one allows you to resize an object in a different way. For instance clicking on the top-middle one will only let you make it taller or shorter, but the top-right one will allow you to make it taller, shorter, as well as more wide or narrow.

Like all decidedly unique things, we’ll want to keep track of them.

// New, holds the 8 tiny boxes that will be our selection handles
// the selection handles will be in this order:
// 0  1  2
// 3     4
// 5  6  7
var selectionHandles = [];

Previously we had the variables mySelColor and mySelWidth for the selection’s color and width. Now we also add variables for selection box color and size:

var mySelBoxColor = 'darkred'; // New for selection boxes
var mySelBoxSize = 6;

Draw’s new home

Draw is still its own function but most of the code has been moved out of it. We’re going to make our Box class start to look a little more classy by letting boxes draw themselves. If you haven’t seen this syntax before, it adds the draw function to all instances of the Box class, creating a someBox.draw() we can call on boxes. To clear up confusion, our old draw loop will be renamed mainDraw.

// New methods on the Box class
Box.prototype = {
  // we used to have a solo draw function
  // but now each box is responsible for its own drawing
  // draw() will call this with the normal canvas
  // myDown will call this with the ghost canvas with 'black'
  draw: function(context, optionalColor) {
    // ... (draw code) ...
  } // end draw
 
}

This draw code is lifted from the old draw method but with a few additions for the selection handles. We check to see if the current box is selected, and if it is, we draw the selection outline as well as the eight selection boxes, their places based on the selected object’s bounds.

In the Init() function we need to add the selectionHandles[] initialization as well as a new event. In the past, myMove was only activated if you pressed down with the mouse, and became deactivated as soon as the mouse was released. Now we need myMove to be active all the time.

// new code in init()
canvas.onmousemove = myMove;
 
// set up the selection handle boxes
for (var i = 0; i < 8; i ++) {
  var rect = new Box;
  selectionHandles.push(rect);
}

Our new main draw loop is now very slimmed down:

function mainDraw() {
  if (canvasValid == false) {
    clear(ctx);
 
    // draw all boxes
    var l = boxes.length;
    for (var i = 0; i < l; i++) {
      boxes[i].draw(ctx); // we used to call drawshape, but now each box draws itself
    }
 
    canvasValid = true;
  }
}

Doing this reorganization isn’t too important now, but it will be useful later on if we have many different types of objects draw themselves. After all, rectangles and (for instance) lines are not drawn in the same way, so if we can put all the custom drawing code in the object’s own class we can keep things better organized.

myMove revisited

Before I talk about myMove lets take a look at two new variables added to the top of our code that signal whether or not a box is being dragged and if so, from which selection handle.

var isResizeDrag = false;
var expectResize = -1; // New, will save the # of the selection handle if the mouse is over one.

isResizeDrag seems simple enough, it works almost identically to isDrag. expectResize will be a number between 0 and 7 to indicate which selection handle is currently active. If none is active (the default), we’ll set it to -1.

In most programs that have selection handles (such as the edges of your browser) it is nice to have the mouse cursor change to show that an action can be performed. To do this we are going to have to ask where the mouse is located all the time and see if it is over one of our eight selection handles. Remember that above we made myMove active all of the time and Now we have to add code to it:

// Happens when the mouse is moving inside the canvas
function myMove(e){
  if (isDrag) {
    getMouse(e);
 
    mySel.x = mx - offsetx;
    mySel.y = my - offsety;   
 
    // something is changing position so we better invalidate the canvas!
    invalidate();
  } else if (isResizeDrag) {
    // ... new code to talk about later.
  }
  getMouse(e);
  // if there's a selection see if we grabbed one of the selection handles
  if (mySel !== null && !isResizeDrag) {
    for (var i = 0; i < 8; i++) {
      // 0  1  2
      // 3     4
      // 5  6  7
 
      var cur = selectionHandles[i];
 
      // we dont need to use the ghost context because
      // selection handles will always be rectangles
      if (mx >= cur.x && mx <= cur.x + mySelBoxSize &&
          my >= cur.y && my <= cur.y + mySelBoxSize) {
        // we found one!
        expectResize = i;
        invalidate();
 
        switch (i) {
          case 0:
            this.style.cursor='nw-resize';
            break;
          case 1:
            this.style.cursor='n-resize';
            break;
          case 2:
            this.style.cursor='ne-resize';
            break;
          case 3:
            this.style.cursor='w-resize';
            break;
          case 4:
            this.style.cursor='e-resize';
            break;
          case 5:
            this.style.cursor='sw-resize';
            break;
          case 6:
            this.style.cursor='s-resize';
            break;
          case 7:
            this.style.cursor='se-resize';
            break;
        }
        return;
      }
 
    }
    // not over a selection box, return to normal
    isResizeDrag = false;
    expectResize = -1;
    this.style.cursor='auto';
  }

So if there is something selected and we are not already dragging, we will execute some code to see if the mouse position is over one of the selection boxes. If it is, give the mouse cursor the correct arrow. If the mouse isn’t over a selection box, make sure we change it back to the normal pointer.

You’ll also notice that at the start, after “if (isDrag)” we have a new test, “else if (isResizeDrag).” isResizeDrag becomes true if two conditions are met:

  1. expectResize is set to one of the selection handle numbers (is not -1)
  2. we have pressed down the mouse

In other words, it only happens if the mouse is over a selection handle and has been pressed. We add a tiny bit of code to myDown to make this work.

// Happens when the mouse is clicked in the canvas
function myDown(e){
  getMouse(e);
 
  //we are over a selection box
  if (expectResize !== -1) {
    isResizeDrag = true;
    return;
  }
 
  // ... the rest of myDown
 
}

Anyway, getting back to myMove. We are looking for the “else if (isResizeDrag)” to see what happens when this is true.

function myMove(e){
  if (isDrag) {
 
    // ...
 
  } else if (isResizeDrag) {
    // time ro resize!
    var oldx = mySel.x;
    var oldy = mySel.y;
 
    // 0  1  2
    // 3     4
    // 5  6  7
    switch (expectResize) {
      case 0:
        mySel.x = mx;
        mySel.y = my;
        mySel.w += oldx - mx;
        mySel.h += oldy - my;
        break;
      case 1:
        mySel.y = my;
        mySel.h += oldy - my;
        break;
      case 2:
        mySel.y = my;
        mySel.w = mx - oldx;
        mySel.h += oldy - my;
        break;
      case 3:
        mySel.x = mx;
        mySel.w += oldx - mx;
        break;
      case 4:
        mySel.w = mx - oldx;
        break;
      case 5:
        mySel.x = mx;
        mySel.w += oldx - mx;
        mySel.h = my - oldy;
        break;
      case 6:
        mySel.h = my - oldy;
        break;
      case 7:
        mySel.w = mx - oldx;
        mySel.h = my - oldy;
        break;
    }
 
    invalidate();
  }
 
  // ... rest of myMove
}

We see a bunch of arithmetic dealing with precisely how each handle will resize the box. Handle #6 is middle-bottom, so it only resizes the height of the box. Handle #1, on the other hand, is the middle top. It has to resize both the Y-axis co-ordinate as well as the height. If #1 only changed the Y-axis, then dragging it upwards would just look like the entire box is being dragged upwards. If it just resized the height, the top of the box would stay in the same position and we certainly don’t want that if the top is what we intended to move!

That’s pretty much everything. Try it out yourself above or see the demo and download the full source on this page.

So that wasn’t too bad. A few long chunks were added but not because of complexity, just because each of the eight selection handles is uniquely placed and does a unique resizing task.

If you would like to see this code enhanced in future posts (or have any fixes), let me know how in the comments.

 

Explore data-driven apps with less coding and query writing, brought to you in partnership with Qlik.

Topics:

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}