uPlot icon indicating copy to clipboard operation
uPlot copied to clipboard

don't use center-aligned strokes for bars

Open leeoniya opened this issue 3 years ago • 1 comments

not quite sure if this is a net positive or not, but drawing crisp bars with canvas-native strokes is a real pain in the ass with a lot of accidental edge cases. for a long time i've been trying to figure out how to do performant masking in canvas (the opposite of clipping) for uPlot's purposes. today i finally figured it out.

  1. the first one is just what canvas does natively.-
  2. the second if we add some math, but it still bleeds stroke and can end up accidentally anti-aliased (non crisp). this is what uPlot currently does.
  3. the last is the holy grail, where the stroke is drawn using an additional fill with a mask. the math is simple and always exact, and never bleeds.

https://jsfiddle.net/eaLfo8zn/

image

<canvas id="can2" width="300" height="150"></canvas>
<canvas id="can3" width="300" height="150"></canvas>
<canvas id="can1" width="300" height="150"></canvas>
canvas {
  image-rendering: pixelated;
}
let strokeWidth = 9;

var c = document.getElementById("can1");
var ctx = c.getContext("2d");

let s = new Path2D();
let f = new Path2D();

s.rect(0, 0, 150, 100);
f.rect(0 + strokeWidth, 0 + strokeWidth, 150 - strokeWidth * 2, 100 - strokeWidth * 2);

ctx.strokeStyle = 'black';
ctx.lineWidth = 1;
ctx.moveTo(75 + 0.5, 0);
ctx.lineTo(75 + 0.5, 100);
ctx.stroke();

ctx.fillStyle = "rgba(255,0,0,0.5)";
ctx.fill(f);

ctx.fillStyle = "rgba(0,0,255,0.5)";
s.addPath(f);
ctx.fill(s, 'evenodd');


var c2 = document.getElementById("can2");
var ctx2 = c2.getContext("2d");

ctx2.strokeStyle = 'black';
ctx2.lineWidth = 1;
ctx2.moveTo(75 + 0.5, 0);
ctx2.lineTo(75 + 0.5, 100);
ctx2.stroke();

let s2 = new Path2D();

s2.rect(0, 0, 150, 100);

ctx2.lineWidth = strokeWidth;
ctx2.fillStyle = "rgba(255,0,0,0.5)";
ctx2.fill(s2);

ctx2.strokeStyle = "rgba(0,0,255,0.5)";
ctx2.stroke(s2);


var c3 = document.getElementById("can3");
var ctx3 = c3.getContext("2d");

ctx3.strokeStyle = 'black';
ctx3.lineWidth = 1;
ctx3.moveTo(75 + 0.5, 0);
ctx3.lineTo(75 + 0.5, 100);
ctx3.stroke();

let s3 = new Path2D();

s3.rect(0 + strokeWidth / 2, 0 + strokeWidth / 2, 150 - strokeWidth, 100 - strokeWidth);

ctx3.lineWidth = strokeWidth;
ctx3.fillStyle = "rgba(255,0,0,0.5)";
ctx3.fill(s3);

ctx3.strokeStyle = "rgba(0,0,255,0.5)";
ctx3.stroke(s3);

leeoniya avatar Nov 30 '21 23:11 leeoniya

made an attempt in https://github.com/leeoniya/uPlot/compare/stroke-via-fill-evenodd, mostly complete, but not for multi-path/disp bars.

works as advertised, but not sure it's pure win. the main downside is the reliance of stroking on filling. e.g if you need a stroke-only bar, you still need to create and apply the fill path, which adds all kinds of nonsense to the already tricky logic.

leeoniya avatar Sep 21 '23 04:09 leeoniya