HTML5 Canvas and Dashed Lines
I’ve been spending a lot of time with HTML5′s new Canvas tag, and I have to say, I’m quite excited with it. The gratification you can get with a small set of code rendering some images is great! It’s nothing like what you need to get going in C++ and OpenGL or even XNA – pure, simple, and of course, very limited.
Canvas is currently so limited that you cannot even draw a simple dashed line! Straight lines, no problem.
This is clearly a problem that needs fixing and luckily enough, I have just a solution.
So, how might we go about adding this functionality. Quite simply! Thanks to the ability for JavaScript’s prototypical nature, and maybe more accurately, it’s dynamic nature, we can extend the functionality of the Canvas object so that we can make our new functionality look like it’s built-in.
Ok, so how do we get started? First we need to define our protype code. I’ve created a new JavaScript file called canvas.js to where we’ll put our code.
CanvasRenderingContext2D.prototype.dashedLineTo = function (from, to, pattern) {}
That adds the ‘dashedLineTo’ function to our canvas protype.
Next we need to actually implement the function. I’m not going to actually go through and explain it all, but the idea of the code is to use the pattern parameter to define the length of each dash and each space, in alternating order, for your dashed line.
CanvasRenderingContext2D.prototype.dashedLineTo = function (fromX, fromY, toX, toY, pattern) {
// Our growth rate for our line can be one of the following:
// (+,+), (+,-), (-,+), (-,-)
// Because of this, our algorithm needs to understand if the x-coord and
// y-coord should be getting smaller or larger and properly cap the values
// based on (x,y).
var lt = function (a, b) { return a <= b; };
var gt = function (a, b) { return a >= b; };
var capmin = function (a, b) { return Math.min(a, b); };
var capmax = function (a, b) { return Math.max(a, b); };
var checkX = { thereYet: gt, cap: capmin };
var checkY = { thereYet: gt, cap: capmin };
if (fromY - toY > 0) {
checkY.thereYet = lt;
checkY.cap = capmax;
}
if (fromX - toX > 0) {
checkX.thereYet = lt;
checkX.cap = capmax;
}
this.moveTo(fromX, fromY);
var offsetX = fromX;
var offsetY = fromY;
var idx = 0, dash = true;
while (!(checkX.thereYet(offsetX, toX) && checkY.thereYet(offsetY, toY))) {
var ang = Math.atan2(toY - fromY, toX - fromX);
var len = pattern[idx];
offsetX = checkX.cap(toX, offsetX + (Math.cos(ang) * len));
offsetY = checkY.cap(toY, offsetY + (Math.sin(ang) * len));
if (dash) this.lineTo(offsetX, offsetY);
else this.moveTo(offsetX, offsetY);
idx = (idx + 1) % pattern.length;
dash = !dash;
}
};
To use this code, just include your new canvas.js script file in your .html file in a script tag, and voila!
<script type="text/javascript" src="canvas.js"><script>
If you know of a better way to do this, be sure to let me know!
Update: So, maybe I should have tested that version.
Updated the code to actually work and not rely on some other pieces of a library I’m working on.
I would be nice to see an example of a pattern instance and an explanation of it in more detail. Reading the code, I see pattern[idx] and pattern.length so I assume pattern is an array that contains numbers. Each number represents either a line or gap. So it would go something like…
- draw a line of length pattern[0]
- leave a gap of length pattern[1]
- draw a line of length pattern[2]
- leave a gap of length pattern[0] (assuming the pattern array had 3 elements)
- draw a line of length pattern[1]
- etc.
Don’t you need a beginPath and stroke in there as well?
I got this code working. Nice! Thanks.
I didn’t attach it to the canvas object itself but I have a renderer class that I use instead. I attached it to that instead and replaced references of this to ctx and passed the ctx in as a parameter.
Thanks, Works great! (thanks to @jacorb effect for the explanation!)
Is there a license to the code you have written? I can’t find anything about if it is free to use and how it is, if I want to use that code for a commercial product.
here’s a very basic dotted arc function:
CanvasRenderingContext2D.prototype.dottedArc = function(x,y,radius,startAngle,endAngle,anticlockwise) {
var g = Math.PI / radius / 2, sa = startAngle, ea = startAngle + g;
while(ea < endAngle) {
this.beginPath();
this.arc(x,y,radius,sa,ea,anticlockwise);
this.stroke();
sa = ea + g;
ea = sa + g;
}
};
Yes, could you please show us what you used for the pattern parameter? Is it a byte-specified pattern, or is it an image?
Thanks dude, You saved me an hour.I a using this code for my javascript library
Thanks once again
use [length of dash in px, length of gap in px] for pattern.
For example:
ctx.dashedLineTo(100,100,780,178,[5,5]);
Thanks for posting that – works excellently. I made a minor modification: a default pattern if none is supplied.
Hard to believe this isn’t part of the canvas standard.
Note that this will crash IE versions < 9 since they lack canvas-support. To fix this, surround the function with the following if-statement:
if (typeof CanvasRenderingContext2D !== 'undefined') {
CanvasRenderingContext2D.prototype.dashedLineTo = function (fromX, fromY, toX, toY, pattern) {
// code
}
}
thanks for the post – I needed to create a consistent dotted freehand line and this got me started…
code in case anyone needs it:
with(ctx) {
beginPath();
// draw dotted line if you’ve moved more then 3 pixels
if((e.pageX-offset.left)>(this.X+3) || (e.pageY-offset.top)>(this.Y+3) || (e.pageX-offset.left+3)<this.X || (e.pageY-offset.top+3)<this.Y)
{
dashedLineTo(this.X, this.Y,e.pageX-offset.left, e.pageY-offset.top,[1,5]);
strokeStyle = this.color;
stroke();
this.X = e.pageX-offset.left;
this.Y = e.pageY-offset.top;
}
}
My suggestion
function dash(ctx, points) {
var pattern = [20, 5];
var cnt = 0;
var b = 0;
var prev = points.shift();
ctx.moveTo(px, py);
while (points.length > 0) {
var cur = points.shift();
var px = prev.x;
var py = prev.y;
var dx = cur.x – prev.x;
var dy = cur.y – prev.y;
var len = Math.sqrt(dx * dx + dy * dy);
while (b < len) {
b += pattern[cnt % pattern.length];
var d = (b = len) { b -= d; break; }
cnt += 1;
}
prev = cur;
}
}
OK not sure of the magic to get code into a post .. email me and I give you the full contents
No, this should NOT have beginPath and stroke — a function like dashedLineTo() should behave just like a drop-in replacement to lineTo() and the user should manually issue the beginPath and stroke commands.
This comment was almost helpful for this Javascript newbie. I think the solution is the renderer class. However, I don’t know how to define / use the renderer class. Can someone help?
With experimentation and hints from jacorb, I found the answer. Here’s the modification of the function. First I changed the defininition to include “ctx” as the first parameter
function (ctx, fromX, fromY, toX, toY, pattern) {In the body of the function definition, this.moveTo (2 places) and this.lineTo were changed to ctx.moveTo and ctx.lineTo.
I called the function with:
canvas.beginPath(); canvas.dashedLineTo(canvas, [var.stub_width],[var.top],[var.stub_width],[var.bottom],[15,10]); canvas.stroke();I might note that the parameters to dashedLineTo are TBS (tinybutstrong) variables.
HTH
The code can get into an infinite loop for near-horizontal lines.
Here’s a possible fix:
// make sure we don’t get an infinite loop drawing eg
// y = -7.85046229341888E-17x
var EPSILON = 0.00000001;
var lt = function (a, b) { return a = b – EPSILON; };
Many thanks for the original code!
oops,a bit got lost:
var gt = function (a, b) { return a >= b – EPSILON; };