Table of contents
Every now and then I encountered strange behaviors in drawing paths in html5 canvas.
I started to collect them (in my memory) so I will recognize them and (possibly) avoid them.
But that had to come to an end so I thought it's time to go one level down into canvas, context and html5.
Here is what I found.
One more tip before commencing our descent.
The holy book of canvas and its 2D context is here: http://www.w3.org/TR/2dcontext/. Use it whenever you want to explore other dark corners.
Current default path
The current default path is the path context is using when drawing shapes.
There can be more than one paths in your context but there is only one current default path.
All the path altering actions you are doing will affect ONLY this path and not any other paths you might have.
This concept is very important so keep it in mind. The rest of the article is related to it.
Implicit path
When context is created it will create a default path and point “current default path” to it,
so you can simply skip the “beginPath” declaration as you already have a “current default path”
even if not created by you. ;)
function implicitPathExample(){ //obtain context var ctx = document.getElementById('quilt1').getContext('2d'); //Do not begin any path. Use the one created when context was created //move to a point ctx.moveTo(0,0); //draw a line to (100, 100) ctx.lineTo(100, 100); //stroke ctx.stroke(); }See bellow the code in action
Paths made from subpaths
All the paths are mare by zero or more subpaths. A subpath is “a list of one or more points, connected by straight or curved lines, and a flag indicating whether the subpath is closed or not.
A closed subpath is one where the last point of the subpath is connected to the first point of the subpath by a straight line. Subpaths with fewer than two points are ignored when painting the path.”
If a subpath is closed it does not mean that it was actually closed (by drawing a line to the beginning of the subpath) but it is only marked as closed....so the engine will know
how to treat it – for example when to fill it.
One more advice for subpaths: if you have a segment AB as a subpath you have to think about it like: point A, line from A to B and point B.
Trust me it will keep your mental sanity :)
beginPath
This method simply create a new empty path (No subpaths yet!) and mark it as current default path. The “link” to previous edited path is lost so that previous path is no longer editable.
If you forgot to stroke/fill previous path than say "good bye" to it....you will no longer be able to edit it.
closePath
Contrary to what a lot of people think or believe close path does not close a path. It might look like that but it operates at subpath level.
Here is what closePath() actually does:
- mark current subpath (from current default path) as closed
- creates a new subpath "whose first point is the same as the previous subpath's first point" (basically return to the initial point of previous subpath)
- add new subpath to current default path
function closePathExample(){ //obtain context var ctx = document.getElementById('quilt2').getContext('2d'); /*========================================================================*/ /*======================WE ARE IN FIRST SUBPATH===========================*/ //move to a point ctx.moveTo(10,10); //draw a line ctx.lineTo(100, 10); //draw a line ctx.lineTo(100, 60); /*Close path : * - mark previous subpath as closed * - create a new path * - add previous initial point (10,10) to the new subpath **/ ctx.closePath(); /*========================================================================*/ /*======================WE ARE IN SECOND SUBPATH==========================*/ //draw a line from previous point ctx.lineTo(50, 100); //stroke ctx.stroke(); }See bellow the code in action
stroke
stroke() method creates an union of all subpaths and stroke them as one. It will also use the last strokeStyle (along with last lineWidth, lineCap, lineJoin, mitterLimit) to outline to current default path.
This means that not matter how much you try to paint subpaths with different color (or style) you will not be able – you have to use different paths.
stroke() will stroke current default path of your context. It will not care about any other paths you created before but only about current one.
So if you previously created other paths and forgot to stroke them too bad, they are will not be stroked.
Example:
function strokeExample(){ //obtain context var ctx = document.getElementById('quilt3').getContext('2d'); /*========================================================================*/ /*======================WE ARE IN FIRST PATH==============================*/ //Begin a path (we could skip this) ctx.beginPath(); //move to a point ctx.moveTo(0,0); //draw a line ctx.lineTo(100, 100); //start another path ctx.beginPath(); /*========================================================================*/ /*======================WE ARE IN SECOND PATH=============================*/ //move to a point ctx.moveTo(200,100); //draw a line ctx.lineTo(50, 150); /*stroke it. *Second path is the current default path so it is going to be the only one stroked). *Fist path is simply ignored.*/ ctx.stroke(); }See bellow the code in action "Subpaths with fewer than two points are ignored when painting the path." - which makes a lot of sense.
moveTo
moveTo() function simply creates a new subpath inside the current default path. The new subpath contains, initially, ONLY the new point.
Previous subpath is simply stored in the current default path.
lineTo
This method draw a line from last point in subpath to new point (x,y) and then add new point to the subpath.
In case there is no subpath in current default path then it creates a new subpath that contains only the new point, being actually equivalent to moveTo() call.
In case there is no subpath in current default path then it creates a new subpath that contains only the new point, being actually equivalent to moveTo() call.
fill
fill() method simply fills current default path by iterating over its subpaths and fill them. If it finds an “open” subpath it will simply consider it closed and fill it.
Even if the subpath is implicitly closed it is not marked as closed.
Example:
Even if the subpath is implicitly closed it is not marked as closed.
Example:
function fillExample(){ //obtain context var ctx = document.getElementById('quilt4').getContext('2d'); //set fill color ctx.fillStyle = '#F0F000'; //Begin a path (we could skip this) ctx.beginPath(); //move to a point ctx.moveTo(10,10); //draw a line ctx.lineTo(100, 50); //draw another line ctx.lineTo(50, 100); /*fill what we have. *Context will close the subpath but without marking it as closed*/ ctx.fill(); /*stroke the subpath. *As the subpath were not marked as closed by previous fill the stroke will *not fully encircle the fill **/ ctx.stroke(); }See bellow the code in action If you want to know more about the filling algorithm read this non-zero winding number rule