Extending Canvas to Draw Rounded Rectangles
Recently, I have been working on some canvas rendered buttons and other UI elements for a little project. So, rather than downloading an image for the buttons, it can be rendered. One problem I encountered was the lack of rounded rectangle drawing methods in the canvas 2D context.I decided I'd like to make my job a little easier and add a couple of methods to the 2D context:
- fillRectR()
- strokeRectR()
If you are viewing this on an RSS reader, you probably won't be able to see any of the canvas elements, since they are rendered using JavaScript. To see the examples, please read this here.
Let's take a look at the 2D context object.
I can create a canvas 2D context using the following code:
//create a canvas 2d context object:
var context2d = document.createElement('canvas').getContext('2d');
To make things easy, I'll just use the JavaScript Console in Google Chrome. If I enter "context2d" in the console and hit enter; the console will respond by showing me the structure of the context2d object:
I'm interested in the last item in that list. "__proto__" is where all the methods for the 2d context exist in the object structure.
"__proto__" can be a complicated thing to deal with, so I'm not going to go into any detail about it in this article. For present needs, it will be suffice to say that "__proto__" is where we want to add our rounded rectangle drawing function.
By adding new methods to "__proto__", they will be available to any canvas element created after our code.
Update : Actually, it’s been pointed out that “__proto__” is now deprecated. The correct method to extend the canvas now, is to use a method of “Object”.
If you’re wondering what “Object” is, here is a quick description: “Object” is the basic object upon which all other objects are based.
(I was surprised that I couldn’t find a really simple description to link to. Please post in the comments if you find a good description to link to).
Anyway, the method we want to use is “Object.getPrototypeOf()”
This method will return the equivalent of “__proto__” using standardized JavaScript. It should be the safe and reliable method we can use to achieve our goals.
(I was surprised that I couldn’t find a really simple description to link to. Please post in the comments if you find a good description to link to).
Anyway, the method we want to use is “Object.getPrototypeOf()”
This method will return the equivalent of “__proto__” using standardized JavaScript. It should be the safe and reliable method we can use to achieve our goals.
Well, now I know enough about the 2D context that I can get started adding our new functions. I already know what my methods are going to be called, but still need to decide what arguments to use.
I'll use the arguments requred by the native fillRect() and strokeRect() methods as a base, and go from there.
Here is an example where I create a canvas, and draw a blue rectangle with a black outline using the native methods:
Result:
If you don't see a picture above, your browser probably doesn't support the canvas element.
So, fillRect() and strokeRect() both use the same set of arguments. I'll need to use all of those, and I'll have to add one more: radius.
The radius argument will define how wide the rounded corners of our rectangle will be.
I'll use the arguments requred by the native fillRect() and strokeRect() methods as a base, and go from there.
Here is an example where I create a canvas, and draw a blue rectangle with a black outline using the native methods:
//draw a blue rectangle with a black outline
var node = document.getElementById('canvas_target_1');
var x = 30;
var y = 10;
var width = 60;
var height = 40;
var canvas = document.createElement('canvas');
node.appendChild(canvas);
var context2d = canvas.getContext('2d');
context2d.fillStyle = "#00f";
context2d.strokeStyle = "#000";
context2d.fillRect(x,y,width,height);
context2d.strokeRect(x,y,width,height);
Result:
So, fillRect() and strokeRect() both use the same set of arguments. I'll need to use all of those, and I'll have to add one more: radius.
The radius argument will define how wide the rounded corners of our rectangle will be.
So, now I know what to call my new methods, what arguments are required, and where I need to create them. Let's take a look at my code:
That's a good start. All I need to do now is figure out how to draw a rectangle with rounded corners using the native canvas methods.
Fortunately, I've done that a few times before. So I can skip any guess work, and just write it up.
/* Basic code outline to implement
new methods on the canvas 2d context.
*/
Object.getPrototypeOf(document.createElement('canvas').getContext('2d')).fillRectR =
function(x,y,width,height,radius) {
//Draw a filled rounded rectangle here...
}
Object.getPrototypeOf(document.createElement('canvas').getContext('2d')).strokeRectR =
function(x,y,width,height,radius) {
//Draw a stroked rounded rectangle here...
}
That's a good start. All I need to do now is figure out how to draw a rectangle with rounded corners using the native canvas methods.
Fortunately, I've done that a few times before. So I can skip any guess work, and just write it up.
With the canvas I can create a vector path, using native methods of the 2D context object and fill it, stroke it or both. Here are the methods I'll be using to draw the rounded rects:
- beginPath() - start a vector path
- moveTo(x,y) - move the drawing point to a vertex. This could be considered as saying; "Start drawing from here."
- lineTo(x,y) - draw a line from the drawing point to this vector.
- quadraticCurveTo(v,w,x,y) - this is a more complicated method. The first pair of arguments defines the curve vector, and the second pair defines the destination point
- closePath() - end a vector path
- fill() - fill the current vector path
- stroke() - draw a line along the current vector path
Alright, now I have all the parts I need to finish creating these new rounded rectangle methods for a HTML5 canvas element.
Let's take a look at the final code listing:
As you can see, I've decided to rename all my arguments to single characters. I've also added some code to set the default radius to 5.
Well, let's try it out. Here's some code I've thrown together to render a few different rounded rectangles with different drawing styles:
And here is how it turns out:
Let's take a look at the final code listing:
/* Implement new methods on the canvas 2d context
fillRectR() draw a solid rounded rectangle
strokeRectR() draw an outline of a rounded rectangle
*/
Object.getPrototypeOf(document.createElement('canvas').getContext('2d')).fillRectR =
function(x,y,w,h,r) {
if (typeof r === "undefined") {
r = 5;
}
this.beginPath();
this.moveTo(x + r, y);
this.lineTo(x + w - r, y);
this.quadraticCurveTo(x + w, y, x + w, y + r);
this.lineTo(x + w, y + h - r);
this.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
this.lineTo(x + r, y + h);
this.quadraticCurveTo(x, y + h, x, y + h - r);
this.lineTo(x, y + r);
this.quadraticCurveTo(x, y, x + r, y);
this.closePath();
this.fill();
};
Object.getPrototypeOf(document.createElement('canvas').getContext('2d')).strokeRectR =
function(x,y,w,h,r) {
if (typeof r === "undefined") {
r = 5;
}
this.beginPath();
this.moveTo(x + r, y);
this.lineTo(x + w - r, y);
this.quadraticCurveTo(x + w, y, x + w, y + r);
this.lineTo(x + w, y + h - r);
this.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
this.lineTo(x + r, y + h);
this.quadraticCurveTo(x, y + h, x, y + h - r);
this.lineTo(x, y + r);
this.quadraticCurveTo(x, y, x + r, y);
this.closePath();
this.stroke();
};
As you can see, I've decided to rename all my arguments to single characters. I've also added some code to set the default radius to 5.
Well, let's try it out. Here's some code I've thrown together to render a few different rounded rectangles with different drawing styles:
var canvas = document.getElementById("canvas_final");
var context2d = canvas.getContext('2d');
context2d.fillStyle = 'rgba(255,128,0,0.33)';
for(var i=0; i<4; i++)
context2d.fillRectR(10 + (i*10),10 + (i*10),80,60,10);
var gradient = context2d.createLinearGradient(100,90,100,200);
gradient.addColorStop(0,'rgb(0,128,255)');
gradient.addColorStop(0.5,'rgb(255,255,255)');
gradient.addColorStop(1,'rgb(128,255,0)');
context2d.fillStyle = gradient;
context2d.fillRectR(120,90,220,110,20);
context2d.strokeRectR(140,120,60,60);
context2d.strokeRectR(166,150,60,40,7);
And here is how it turns out:
So, now I've got a way I can draw rounded rectangles on a canvas without mountains of code.
I've packaged the code to create the new methods in a single file: canvasExtend.sfa.js
Feel free to use it in your code or where ever. I'm not going to bother adding a license to the code, but if you use it I'd be happy to hear back about it.
I've packaged the code to create the new methods in a single file: canvasExtend.sfa.js
Feel free to use it in your code or where ever. I'm not going to bother adding a license to the code, but if you use it I'd be happy to hear back about it.
I do have a few more things I'd like to say about my experience with the canvas element over the last week or so. I'll save it for another post though.
It's likely I'll be going into some details on errors I encountered with some browser rendering engines. I'll probably post a few more blocks of code, too.
It's likely I'll be going into some details on errors I encountered with some browser rendering engines. I'll probably post a few more blocks of code, too.
Update : You can see the new post here.
I have also updated the article and code to useObject.getPrototypeOf(), rather than .__proto__ to apply the new 2d context methods.
I have also updated the article and code to use