Dotted Stroke in <Canvas>

dotted stroke in canvas

Fun question! I've written a custom implementation of dashed lines; you can try it out here. I took the route of Adobe Illustrator and allow you to specify an array of dash/gap lengths.

For stackoverflow posterity, here's my implementation (slightly altered for s/o line widths):

var CP = window.CanvasRenderingContext2D && CanvasRenderingContext2D.prototype;
if (CP && CP.lineTo){
CP.dashedLine = function(x,y,x2,y2,dashArray){
if (!dashArray) dashArray=[10,5];
if (dashLength==0) dashLength = 0.001; // Hack for Safari
var dashCount = dashArray.length;
this.moveTo(x, y);
var dx = (x2-x), dy = (y2-y);
var slope = dx ? dy/dx : 1e15;
var distRemaining = Math.sqrt( dx*dx + dy*dy );
var dashIndex=0, draw=true;
while (distRemaining>=0.1){
var dashLength = dashArray[dashIndex++%dashCount];
if (dashLength > distRemaining) dashLength = distRemaining;
var xStep = Math.sqrt( dashLength*dashLength / (1 + slope*slope) );
if (dx<0) xStep = -xStep;
x += xStep
y += slope*xStep;
this[draw ? 'lineTo' : 'moveTo'](x,y);
distRemaining -= dashLength;
draw = !draw;
}
}
}

To draw a line from 20,150 to 170,10 with dashes that are 30px long followed by a gap of 10px, you would use:

myContext.dashedLine(20,150,170,10,[30,10]);

To draw alternating dashes and dots, use (for example):

myContext.lineCap   = 'round';
myContext.lineWidth = 4; // Lines 4px wide, dots of diameter 4
myContext.dashedLine(20,150,170,10,[30,10,0,10]);

The "very short" dash length of 0 combined with the rounded lineCap results in dots along your line.

If anyone knows of a way to access the current point of a canvas context path, I'd love to know about it, as it would allow me to write this as ctx.dashTo(x,y,dashes) instead of requiring you to re-specify the start point in the method call.

HTML5 Canvas Draw Dashed Line

Your problem is that you are currently drawing a new Path at every mousemove. Dashes are made from the path starting point, to its end.

When you do move your mouse slowly, your are actually generating a lot of really small pathes, smaller than the 5px of your dash-array.
While when you move your mouse faster, the distance between two points is greater than 10px, and so, you can see the dash.

The solution for this problem is to store your points in an array, and redraw a single path from these stored points every time you do a redraw. This way, your pathes will simply get longer, and your dash will be working fine.

In the following example, I even save a new path at each mouseup, so they can have their own dashed property :

const ctx = c.getContext('2d');
const pathes = []; // this is where we will store all our patheslet mouse_down = false; // shall we draw ?c.onmousedown = e => { // add a new path object pathes.push({ pts: [], // an array of points dashed: check.checked // boolean }); mouse_down = true; // we should draw}c.onmouseup = c.onmouseleave = e => mouse_down = false;
c.onmousemove = throttle(e => { if (!mouse_down) { return; } else { const rec = c.getBoundingClientRect(); // add a new point addPoint(e.clientX - rec.left, e.clientY - rec.top); redraw(); // redraw everything }});
function redraw() { ctx.clearRect(0, 0, c.width, c.height); // we clear everything // and draw every pathes pathes.forEach(path => { ctx.setLineDash(path.dashed ? [5, 5] : [0]); ctx.beginPath(); path.pts.forEach(pt => ctx.lineTo(pt.x, pt.y)); ctx.stroke(); })}
function addPoint(x, y) { // append to the last one const points = pathes[pathes.length - 1].pts; points.push({ x: x, y: y });}

// just to avoid unnecessary drawingsfunction throttle(callback) { if (typeof callback !== 'function') throw 'A callback function must be passed'; var active = false; var evt; var handler = function() { active = false; callback(evt); }; return function handleEvent(e) { evt = e; if (!active) { active = true; requestAnimationFrame(handler); } };}
canvas {  border: 1px solid}
<label>dashed : <input type="checkbox" id="check" checked></label><br><canvas id="c" width="500" height="500"></canvas>

Glitch dotted lines in HTML Canvas using translate and scale

This is because every time you move the mouse, you are only drawing one line, either "normal" or "mirrored" but not both. You can see that your "dots" actually correspond to the dashes of the other side.

The switching happens because you apply the canvas transform once per draw call, but don't reset it, so at first mouse move, you mirror the context and at second one you'll finally draw mirrored and then switch back for the next move.

To do what you want, you could keep all your points in an Array, clear your context at the beginning of every frame and redraw the lines completely twice, once normal, and once mirrored.

function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
// trace in normal mode
points.forEach(({x, y}) => ctx.lineTo(x, y));
// mirror
ctx.translate(canvas.width, 0);
ctx.scale(-1, 1);
// move to first point so we can paint in a single path
ctx.moveTo(points[0].x, points[0].y);
// trace all the points mirrored
points.forEach(({x, y}) => ctx.lineTo(x, y));
// paint
ctx.stroke();
// reset the context transform
ctx.setTransform(1, 0, 0, 1, 0, 0);
}

HTML Canvas - Dotted stroke around circle

Live Demo

calcPointsCirc takes 4 arguments, the center x/y, the radius, and the length of the dashes. It returns an array of points, x,y,ex,ey. You can just loop through the points to draw the dashed circle. There's probably much more elegant ways to do this but figured Id give it a shot.

function calcPointsCirc( cx,cy, rad, dashLength)
{
var n = rad/dashLength,
alpha = Math.PI * 2 / n,
pointObj = {},
points = [],
i = -1;

while( i < n )
{
var theta = alpha * i,
theta2 = alpha * (i+1);

points.push({x : (Math.cos(theta) * rad) + cx, y : (Math.sin(theta) * rad) + cy, ex : (Math.cos(theta2) * rad) + cx, ey : (Math.sin(theta2) * rad) + cy});
i+=2;
}
return points;
}

var canvas = document.getElementById('canvas'),
ctx = canvas.getContext('2d');

canvas.width = canvas.height= 200;

var pointArray= calcPointsCirc(50,50,50, 1);
ctx.strokeStyle = "rgb(255,0,0)";
ctx.beginPath();

for(p = 0; p < pointArray.length; p++){
ctx.moveTo(pointArray[p].x, pointArray[p].y);
ctx.lineTo(pointArray[p].ex, pointArray[p].ey);
ctx.stroke();
}

ctx.closePath();

What is an alternative method for drawing dashed lines on an HTML5 Canvas?

You can create line segments.

The function will draw a dashed line from the info in the dashArr eg [2,2] will draw a line 2 pixels then a gap 2 pixels and repeat.

function dashedLine(x1,y1,x2,y2,dashArr){
// get the normalised line vector from start to end
var nx = x2 - x1;
var ny = y2 - y1;
const dist = Math.sqrt(nx * nx + ny * ny); // get the line length
nx /= dist;
ny /= dist;
var dashIdx = 0; // the index into the dash array
var i = 0; // the current line position in pixels
ctx.beginPath(); // start a path
while(i < dist){ // do while less than line length
// get the line seg dash length
var dashLen = dashArr[(dashIdx ++) % dashArr.length];
// draw the dash
ctx.moveTo(x1 + nx * i, y1 + ny * i);
i = Math.min(dist,i + dashLen);
ctx.lineTo(x1 + nx * i, y1 + ny * i);
// add the spacing
i += dashArr[(dashIdx ++) % dashArr.length];
if(i <= 0) { // something is wrong so exit rather than endless loop
break;
}
}
ctx.stroke(); // stroke
}

    function dashedLine(x1,y1,x2,y2,dashArr){
var nx = x2 - x1; var ny = y2 - y1; const dist = Math.sqrt(nx * nx + ny * ny); nx /= dist; ny /= dist; var dashIdx = 0; var i = 0; ctx.beginPath(); while(i < dist){ var dashLen = dashArr[(dashIdx ++) % dashArr.length]; ctx.moveTo(x1 + nx * i, y1 + ny * i); i = Math.min(dist,i + dashLen); ctx.lineTo(x1 + nx * i, y1 + ny * i); i += dashArr[(dashIdx ++) % dashArr.length]; if(i <= 0) { // something is wrong so exit rather than endless loop break; } } ctx.stroke() } const ctx = canvas.getContext("2d");dashedLine(0,0,300,150,[5,5]);
canvas { border : 2px solid black; }
<canvas id="canvas"></canvas>


Related Topics



Leave a reply



Submit