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

Wait! This tutorial is rather old and I’ve given part one a major overhaul. You should really read (or at least start with) the new one here instead.

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:


This text is displayed if your browser does not support HTML5 Canvas.

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.

  • http://friendfeed.com/erisdiscord eris.discord

    Minor nitpick: JavaScript and its kin don’t have classes. Box is a constructor (hence why it’s declared as a function) and Box.prototype is its prototype, which gets cloned whenever you construct a new Box object. It’s mostly a matter of semantics in this case, but it can bite you on the ass when you assume other class-like behavior. :)

    Not trying to steal your thunder, though! This is a good tutorial. I’m gonna play with it myself.

    Apropos of nothing, there’s a neat trick you can use to make an object’s private data really private. If you add your methods within the constructor like so, you can use constructor-local variables since they will remain in scope. I’m using the dollar sign here as a convention; it doesn’t have any special syntactic meaning.

    function BlackBox() {
    	var $secret;
    	
    	this.secret = "nice try, son"
    	
    	this.newSecret = function() { $secret = Math.floor(100 * Math.random()) }
    	this.trySecret = function(guess) { return $secret === guess }
    }

    • Ven

      HI Simon,

      Would it be possible to identify text on canvas. I would like to display length of each vertex and change length of a side of rectangle on double click on side of rectangle or text that shows length of a side. Please suggest if you have an idea

      best
      Ven

  • gmtosh

    Thank you again Simon.
    I started designing the resize functionality for a modified version of your initial tutorial. Your approach is cleaner than what I had in mind. Great job.

  • Tim

    Oncea

  • Pingback: Making and Moving Selectable Shapes on an HTML5 Canvas: A Simple Example - simonsarris.com

  • Anonymous

    Great tutorial. Really helped me get a better feel for what the canvas can do. Do you have a starting point or any general info on what would need to be modified to be able to draw images with selectable handles on the canvas and perhaps other elements as well?

    Thanks.

  • http://twitter.com/esteban_martini Esteban Martini

    Great tutorial!

  • Sanjeev Singh

    must SAY EHAAAAAAAA!!

  • Miccet

    Hi Simon,
    Would it be hard to implement this with circles instead of rectangles?

  • http://www.facebook.com/people/Wis-Wxs/100001094937175 Wis Wxs

    hey…thank you v much that really helped
    but i wonder how can i add pngs and jpegs to the canvas rather than rectangles :D

  • Tony66281

    great

  • Brandon

    With StumbleUpon extension for chrome this causes a bad offset on the mouse (mx, my), making it unusable.

  • tina

    Hi there… excelent tutorial!!!

    However, I’ve been trying to draw lines as well but I can’t figure out how to calcule the selectionHandles for lines (for the resizing box).

    How can I do that? For rectangles I notice you use a 7 pointed box….

    Thx

  • Galib Anwar

    I found this version of 2d elements handling very clear and elegant.

    I managed to grasp a little of the logic and extended it with lines joining/indicating a target item, and displaying some text. And to add the circle bit as well, with bugs though – they refuse to move but only resize.
    I am trying to fix the circle issue and add arrowheads at the end of the lines, but not making much progress.

    Dear Simon the Great, if your schedules would allow you to put any more time, than you already have spontaneously put into this wonderful piece, it will be a boon for many like me.

    Already am grateful. Looking forward to being more so.

  • IT-Student

    Thanks this is really a great tutorial!!!
    I’ll use some this stuff in an phonegap app with xui touchevents and draggable images.

  • Vijay Akkineni

     Thank you this is great stuff!!!

  • sai swaminathan

    amazing stuff ,this is exactly what i was looking for !!! :)

  • http://www.facebook.com/jupiterpyter Pyter Alegrado-Isidro

    nice work, can this be modified and have an adjustable oval shape (or any other shapes) instead? please email me at pightzter@gmail.com I’m a big fan of html5 pro evolution!

  • http://twitter.com/el_keogh Isaac Zepeda

    thanks, it was very useful, I had to modify it so it could be done through objects, thanks a lot

  • Steve Widom

    Good stuff.  I modified the code slightly for the getMouse(e) function so that it would take into consideration scrolling offsets.  I use a mix of jQuery and a method I have been using for quite some time that appears to work in all browsers.  Here is the new code:

            var position = getMouseXY(e);

            var offset = $(canvas).offset();

            mx = position[0] – offset.left;
            my = position[1] – offset.top;
     
    And here is the method for getMouseXY(e):

    function getMouseXY(e) {
        var tempX = 0;
        var tempY = 0;

        var evt = window.event || e;
        if (evt.pageX || evt.pageY) {
            tempX = evt.pageX;
            tempY = evt.pageY;
        }
        else {
            tempX = evt.clientX +
              (document.documentElement.scrollLeft ||
               document.body.scrollLeft) –
               document.documentElement.clientLeft;
            tempY = evt.clientY +
              (document.documentElement.scrollTop ||
               document.body.scrollTop) –
               document.documentElement.clientTop;
        }

           if (tempX < 0) { tempX = 0; }
           if (tempY < 0) { tempY = 0; }
       

        return [tempX, tempY];
    }

    Hope this helps others.  Your tutorial is excellent, and I think this just makes it all that much better.

  • Anouman Ain

    Thanks man really helpful script.

  • Pingback: JS exercise 9, day 5 – Drag Square | Software Studies for Media Designers

  • Nikhil Dixit

    Great tutorial…!!
    Is it possible to use circle instead of rectangles?

  • K Saha

    its great

  • Oliver

    HI! Great stuff thx in dolby!
    Can you give me a hint, how i can create a new rect from outside the canvas like i.e. using a button?
    I tried:

    function createNewRect(){
    addRect(0, 0, 50, 50, ‘rgba(0,205,0,0.7)’);
    }

    within the head area of my html

    try to call it with

    I think that i have to call it in relation to the window class?! Means the function
    is need to be part within your window clss and i should do something like window.createNewRect() ??
    Hope for a hint

    Oli

    • Linda

      I would also be very interested in this feature – specifically so that a different image will be spawned on the canvas depending on which button you click.

      If you could help with this that would be fantastic, thanks

  • Alexander Mostovenko

    Just great!!! This is what i was looking for!!! Thanks man)

  • Vishal

    Hi
    This is Good but I need a modification here I want a single image on canvas which will be resizable and move with this handler (same )
    Please help
    Thanks

  • Vishal

    Hi
    This is Good but I need a modification here I want a single image on canvas which will be resizable and move with this handler (same )
    Please Reply
    Thanks

  • Kiran

    Why its not possible to draw images on canvas using boxes class. I have tried but its not working. Added below code in draw method but its not showing anything. neither there are any error

    var img = new Image();
    img.onload = function(){
    canvas.width = img.width;
    canvas.height = img.height;

    ctx.drawImage(img, 0, 0, 50, 50);
    }
    img.src = ‘cottage.jpg';

  • krisnarocks

    now, how to remove the block?

  • Tony Medina

    Very useful script, thanx, I’m looking for resizing selectable images too. Got to show and work with one image only, how to work with 3 or 4 images?

  • Kerry

    This is excellent; Donald Knuth would be proud.

  • Gustav

    I wondering if you could help me with a program. I going my last year at highschool and I have a project that includes javascript/canvas. One task is to draw a circle where you can make some rectangular shaped forms that change size and following the path around the circle.

    Is this possible to do? Please can U help me I’m stuck and can’t get so much help from the teachers.

  • Gromenawer

    It is possible to add some animation to the selection frame (for example, make it blink from red to green) that is not too much CPU consuming? I tried to modify myself and as soon you try to draw every t ms instead of the validating method the efficiency goes down significally.

  • Mahendra

    its great one but i have image on canvas while i want to draw angle/arrow over image and want redraw that angle/arrow ,how can i do this ,can you help regarding this

  • pera_nv

    can I add any figure and dimensioned ???? including amorphous figure?????

  • W Nirala

    How I get full code of this tutorial. Because I am new in Html5.

  • http://twitter.com/ihateiPhone4 ihateiPhone4

    i tried this in OSX browser, i cannot move the items

  • mahendra

    Its very good tutorial,but how can we apply same functionality to line or arrow,angle
    if you any idea about this please reply me.

  • K

    How would you go about adding text, images and doing shape rotation?

  • Andrew

    This is great! I really like how clean the new code in part one is so I went ahead and merged the code for resizing with that code base: https://github.com/amclark/shapesdemo

    Hope someone finds that useful!

  • Đàn Hiền

    Thanks for very great article. But I want to move and resize another shapes such as square, line, circle… Can you help me?

  • steve_davis

    I’ve been looking for this for some time now. However, what I REALLY need is an example of not only how to drag & drop with resizeable handles but also the ability to add text to the box and remove the box with the delete key. Finally, I need to combine all this with getting the coordinates of the drop location. The end result is the ability to create a box, drag it to a location, resize it, give it a name and save the name & coordinates to a variable that I could do something with later. I KNOW it’s possible, I just don’t know HOW – yet!!

  • Gnice!

    This is great!
    Can i ask you a question about Rotate item?

  • http://joshuaraichur.com/ Joshua Raichur

    This is neat. Thanks for this!

  • Muthu

    Thanks. Really a good one for starter.

  • Sabih Khan Afridi

    can any body help me to rotate these divs on different angles by using mouse

  • swati

    Hi Simon,
    Thanks for this useful article.I want to add connectors to connect two shapes on runtime can u help me for that.
    Thanks