Thursday, July 29, 2010

Canvas Rounded Corner Rectangles

I've recently started using the canvas tag and  needed to draw rounded corner rectangles. I did find a good post explaing how to do it but I didn't find a nice method so I decided to create one.  I took Futomi Hatano's code  and abstracted it into a method.

/**
 * Draws a rounded rectangle using the current state of the canvas. 
 * If you omit the last three params, it will draw a rectangle 
 * outline with a 5 pixel border radius 
 * @param {CanvasRenderingContext2D} ctx
 * @param {Number} x The top left x coordinate
 * @param {Number} y The top left y coordinate 
 * @param {Number} width The width of the rectangle 
 * @param {Number} height The height of the rectangle
 * @param {Number} radius The corner radius. Defaults to 5;
 * @param {Boolean} fill Whether to fill the rectangle. Defaults to false.
 * @param {Boolean} stroke Whether to stroke the rectangle. Defaults to true.
 */
function roundRect(ctx, x, y, width, height, radius, fill, stroke) {
  if (typeof stroke == "undefined" ) {
    stroke = true;
  }
  if (typeof radius === "undefined") {
    radius = 5;
  }
  ctx.beginPath();
  ctx.moveTo(x + radius, y);
  ctx.lineTo(x + width - radius, y);
  ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
  ctx.lineTo(x + width, y + height - radius);
  ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
  ctx.lineTo(x + radius, y + height);
  ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
  ctx.lineTo(x, y + radius);
  ctx.quadraticCurveTo(x, y, x + radius, y);
  ctx.closePath();
  if (stroke) {
    ctx.stroke();
  }
  if (fill) {
    ctx.fill();
  }        
}

And it can be used like the following:

// My html contains a canvas with id "rounded-rect" 500X350
var ctx = document.getElementById("rounded-rect").getContext("2d");
// Draw using default border radius, 
// stroke it but no fill (function's default values)
roundRect(ctx, 5, 5, 50, 50);
// To change the color on the rectangle, just manipulate the context
ctx.strokeStyle = "rgb(255, 0, 0)";
ctx.fillStyle = "rgba(255, 255, 0, .5)";
roundRect(ctx, 100, 5, 100, 100, 20, true);
// Manipulate it again
ctx.strokeStyle = "#2d6";
ctx.fillStyle = "#abc";
roundRect(ctx, 100, 200, 200, 100, 50, true);

Which will produce the following output:




For Firefox and Chrome (and maybe other browsers), you can do the following for syntactic sugar

CanvasRenderingContext2D.prototype.roundRect = 

function(x, y, width, height, radius, fill, stroke) {
  if (typeof stroke == "undefined" ) {
    stroke = true;
  }
  if (typeof radius === "undefined") {
    radius = 5;
  }
  this.beginPath();
  this.moveTo(x + radius, y);
  this.lineTo(x + width - radius, y);
  this.quadraticCurveTo(x + width, y, x + width, y + radius);
  this.lineTo(x + width, y + height - radius);
  this.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
  this.lineTo(x + radius, y + height);
  this.quadraticCurveTo(x, y + height, x, y + height - radius);
  this.lineTo(x, y + radius);
  this.quadraticCurveTo(x, y, x + radius, y);
  this.closePath();
  if (stroke) {
    this.stroke();
  }
  if (fill) {
    this.fill();
  }        
}

// Now you can just call
var ctx = document.getElementById("rounded-rect").getContext("2d");
ctx.roundRect(5, 5, 50, 50);

Here's a JsFiddle that you can edit freely

19 comments:

  1. Yay Juan! Good job! Gotta say, big bro, I don't understand a thing, but I came to show my love. Hahaha!
    Kiss kiss from your sis,

    ReplyDelete
  2. Hi,

    nice Idea, but isnt it possible to inherit your own Context class from canvas Context? So that you dont need to pass the context? Would be more elegant or?
    BTW. can i use the code for an Palm Web OS project i am working on?

    ReplyDelete
  3. @Matthias: Please do use it for anything you'd like. I don't think there's a way to inherit or augment the CanvasContext object returned by getContext() that works across browsers. Let me know if you know how to do it. I modified the post to show you how to do what you want in Firefox. By the way, thanks for the first comment on my blog that is not from a relative :)

    I knew I should have added a disclaimer. This is not the code I actually use in production. This is just the simplest way to do it so that others can benefit.

    ReplyDelete
  4. There's a bug on line 15 of the first listing:

    stroke = stroke === undefined ? true : false;

    should be

    stroke = stroke === (undefined || true) ? true : false

    ReplyDelete
  5. Thanks Brian. Fixed it with a clearer syntax.

    ReplyDelete
  6. Thanks! I've used your routine on my page: http://dbp-consulting.com/depthoffocus.html I changed a couple of things, I added ctx.save() at the beginning and ctx.restore() at the end, and switched the order of the stroke and fill at the end, since if you did both, the fill messed up the stroke a bit. In a comment in the routine, I give you credit, and have a link back to this blog.

    ReplyDelete
  7. Well the url for my page is different. It's:
    http://dbp-consulting.com/Photography/CircleOfConfusion.html
    I also made it so you can have an additional boolean arguments for the top left, top right, bottom right and bottom left corners. For each of the arguments, if specified as true, that corner will be rectangular. If unspecified it works just like yours.

    I have:
    var tl=arguments[7];
    var tr=arguments[8];
    var br=arguments[9];
    var bl=arguments[10];
    and then for each corner a section like:

    if(!tr){
    this.quadraticCurveTo(x+width,y,x+width,y+radius);
    }else{
    this.lineTo(x+width,y);
    this.lineTo(x+width,y+radius);
    }

    That way, if you don't specify them the test as false and you get the original specification. As an example of how I used that, look at the colored header on the CoC widget. I wanted to draw it so that the corners would match the canvas, and now I can by adding the correct arguments.

    ReplyDelete
  8. Hi Patrick, thanks for the link to to your usage, it's nice to see someone using it. I'll add your changes (support for multiple corner radii) to this post soon. Thanks for the link back. I should add a note explaining that all code I post here is free for the taking and any link backs are done out of generosity, they are not required.

    Nice job implementing dragging within the canvas tag. I've had to do it myself and it's a PITA.

    ReplyDelete
  9. You must be talking about the sliders I made. I have some really cool buttons too. I made a generic handler for events so that javascript objects could register for events and provide a hit() routine. When an event comes in for the canvas, it looks through the queue for that event and calls the hit() routine for each thing passing the x,y in canvas coordinates for the event. If hit returns true, the callback for the object gets called with the event. I figured that way complex graphic shapes could all use this, and each would be the expert on whether the event hit them. Once the framework was set up adding sliders and buttons et.al. turned out to be really easy. If you haven't checked out my site in a while (dbp-consulting.com) check it out, and look at the tutorials.

    ReplyDelete
  10. good work !
    I made a live example using this code at http://jsfiddle.net/d4JJ8/4/

    ReplyDelete
  11. Thanks for this...helped and works very well

    ReplyDelete
  12. just for the js challenged folks (like myself) that might stumble on this - I struggled with the the contextual call version - your "syntactical sugar" version - not working for the longest time. Finally decided to move the function definition in the js file above the location in the file where I get the context object and magically it started working! Not sure if that is browser specific behavior but I was getting the same behavior on both chrome and safari on mac - btw that approach does work on safari (at least the version I have (5.1.2) )

    ReplyDelete
  13. Great function. Can I use it in an open source data visualization library?
    https://github.com/bengarvey/evidensity.js/

    ReplyDelete
    Replies
    1. Cool stuff, feel free to use it, attribution not required, but appreciated. Love the name of your lib

      Delete
  14. Hi I am using this wpaint code in my application, but the save image options is not working after I merge the code in my application, can you please help me by giving the required changes for save image option to work. thanks in advance for your help :)

    ReplyDelete
    Replies
    1. Somu, I'm not sure why you think I can help with wPaint. You should ask at http://stackoverflow.com/

      Delete
  15. Inspiring writings and I greatly admired what you have to say , I hope you continue to provide new ideas for us all and greetings success always for you..Keep update more information..
    Website for school uk

    ReplyDelete