Draw dashed and dotted rectangles on canvas in the same way css border works: draw 4 same edges

My use-case is to mimic css border rendering. Is it possible, using the CanvasRenderingContext2D::rect method with CanvasRenderingContext2D::setLineDash to simulate same border drawing as css renderer does, like border: 5px dashed red. Consider this example:

let canvas = document.querySelector('canvas');
let ctx = canvas.getContext('2d');
ctx.lineWidth = 5
ctx.strokeStyle = 'red'
ctx.lineCap = 'square'
ctx.setLineDash([10, 10]);
ctx.beginPath();
ctx.moveTo(2.5,2.5);
ctx.rect(2.5, 2.5, 195, 65);
ctx.stroke();
div {
  border: 5px dashed red;
  width: 200px;
  height: 70px;
  box-sizing: border-box;
  margin-bottom: 5px;
}

canvas {
  display: block;
  width: 200px;
  height: 70px;
}
<div></div>
<canvas width=200 height=70></canvas>

You may notice the problem is on edges.

Not pretty line joins on edges

I was trying to modify the gaps and dash sizes, but it seems impossible to get the same behaviour as in the css example: the lines on edges are bigger then the lines on the sides. As a workaround I can imagine to draw every side with a line, but I would like to use the rect method to draw in one stroke.

Thank you in advance.

1 answer

  • answered 2017-11-15 04:08 Kaiido

    CSS border-style: dashed algorithm is not tied by specs, so it will be impossible to render exactly the same in the canvas API.

    Then, you've got to know that even CSS renders it line by line: border is a shorthand for all the border-top-XXX, border-right-XXX, border-bottom-XXX, border-left-XXX.
    And that's why it behaves like that : each border has its line-dash set independently of the others.

    Anyway, if you want to do it with the canvas API, the easiest solution is to do the same, using four lines, and setting their line-dash separately.

    Here is a rough attempt at normalizing the dashes in order to get them always start and end at edges:

    var ctx = c.getContext('2d');
    ctx.lineCap = 'square';
    
    // returns a normalized dashArray per segment
    // This in no way does the same as any browser's implementation,
    // this is just a lazy way to always get dashes start and end at edges
    function getLineDash(x1, y1, x2, y2) {
      var length = Math.hypot((x2 - x1), (y2 - y1));
      var dash_length = length / 8;
      var nb_of_dashes = length / dash_length;
      var dash_gap = (dash_length * 0.66);
      dash_length -= dash_gap * 0.33;
      return [dash_length, dash_gap];
    }
    
    function draw() {
      ctx.lineWidth = lineWidth_.value;
      ctx.clearRect(0, 0, c.width, c.height);
    
      var points = [
        [x1_.value, y1_.value],
        [x2_.value, y2_.value],
        [x3_.value, y3_.value],
        [x4_.value, y4_.value]
      ];
    
      points.forEach(function(pt, i) {
        var next = points[(i + 1) % points.length];
        ctx.beginPath();
        ctx.moveTo(pt[0], pt[1]);
        ctx.lineTo(next[0], next[1]);
        ctx.setLineDash(getLineDash(pt[0], pt[1], next[0], next[1]));
        ctx.stroke();
      });
    
    }
    
    draw();
    document.oninput = function(e) {
      if (e.target.parentNode.parentNode === inputs_) {
        draw();
      }
    }
    label {
      display: inline-block;
    }
    
    input {
      max-width: 50px;
    }
    <div id="inputs_">
      <label>x1<input type="number" id="x1_" value="10"></label>
      <label>y1<input type="number" id="y1_" value="25"></label>
      <label>x2<input type="number" id="x2_" value="350"></label>
      <label>y2<input type="number" id="y2_" value="25"></label>
      <label>x3<input type="number" id="x3_" value="350"></label>
      <label>y3<input type="number" id="y3_" value="225"></label>
      <label>x4<input type="number" id="x4_" value="10"></label>
      <label>y4<input type="number" id="y4_" value="225"></label>
      <label>lineWidth<input type="number" id="lineWidth_" value="3"></label>
    
    </div>
    <canvas id="c" width="400" height="400"></canvas>


    So now, if you only want to use XXXRect, you can also create a single huge dash-array containing all of the dashes...

    var ctx = c.getContext('2d');
    ctx.lineCap = 'square';
    
    function getRectDashes(width, height) {
      var w_array = getLineDashes(width, 0, 0, 0);
      var h_array = getLineDashes(0, height, 0, 0);
      dashArray = [].concat.apply([], [w_array, 0, h_array, 0, w_array, 0, h_array]);
      return dashArray;
    }
    // same as previous snippet except that it does return all the segment's dashes
    function getLineDashes(x1, y1, x2, y2) {
      var length = Math.hypot((x2 - x1), (y2 - y1));
      var dash_length = length / 8;
      var nb_of_dashes = length / dash_length;
    
      var dash_gap = dash_length * 0.66666;
      dash_length -= dash_gap * 0.3333;
    
      var total_length = 0;
      var dasharray = [];
      var next;
      while (total_length < length) {
        next = dasharray.length % 2 ? dash_gap : dash_length;
        total_length += next;
        dasharray.push(next);
      }
      return dasharray;
    }
    
    function draw() {
      ctx.clearRect(0, 0, c.width, c.height);
      ctx.lineWidth = lineWidth_.value;
      var w = width_.value,
        h = height_.value;
      ctx.setLineDash(getRectDashes(w, h));
      ctx.strokeRect(20, 20, w, h);
    }
    draw();
    document.oninput = function(e) {
      if (e.target.parentNode.parentNode === inputs_)
        draw();
    };
    label {
      display: inline-block;
    }
    
    input {
      max-width: 50px;
    }
    <div id="inputs_">
      <label>width<input type="number" id="width_" value="200"></label>
      <label>height<input type="number" id="height_" value="225"></label>
      <label>lineWidth<input type="number" id="lineWidth_" value="3"></label>
    </div>
    <canvas id="c" width="400" height="400"></canvas>