whatwg / html

HTML Standard
https://html.spec.whatwg.org/multipage/
Other
8.1k stars 2.67k forks source link

Drawing crisp lines in canvas #3181

Open sc28 opened 6 years ago

sc28 commented 6 years ago

This issue concerns the possibility to draw crisp (i.e. non-antialiased) lines in an HTML canvas.

One use-case where this would be necessary is when using mouse events on canvas for identifying objects based on colors. More concretely, I am working with a D3.js implementation of parallel coordinates which draws lines on a canvas instead of as SVG objects. In order for the user to be able to interact with the chart's lines (e.g. to highlight them or obtain meta-information), I implemented a script that identifies the unique hex color code of the line and returns information based on this color code. The problem is, because canvas by default draws antialiased lines, there are always some edge-pixels which are slightly different than the intended color of the line, preventing the use of a unique color identifier. A demonstration of this use-case and its limitations can be observed here. I also opened a stackoverflow question illustrating the issue.

A partial work around that can be used is to stroke the line multiple times to reduce the antialiasing effect, but this doesn't remove the edge-pixels completely, and can become computationally intensive.

Is there any reason canvas doesn't support crisp drawing as an option?

domenic commented 6 years ago

/cc @whatwg/canvas. I kind of thought we had something in place here, maybe alpha: false?

sc28 commented 6 years ago

That would be great, but as far as I can tell this doesn't work: even with an opaque canvas (alpha:false), lines drawn are still not crisp (cf. fiddle).

I noticed that the "webgl" canvas does have an antialias: false attribute, so it's too bad that the "2d" doesn't (https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/getContext).

junov commented 6 years ago

The effect of { alpha: false } is that some implementations use this signal to trigger sub-pixel text anti-aliasing (as opposed to grayscale text anti-aliasing).

The idea of adding a control for anti-aliasing has been discussed a few times before on the WHATWG mailing list, and it did not fly in the past. IIRC, the rationale is that having anti-aliasing at all is not required by the spec. It is viewed as a "quality of implementation" issue, and therefore there is no mention of anti-aliasing at all in the spec. Basically. the idea is: user code tells the browser what to draw and the browser draws it as nice as it knows how. Obviously, this philosophy clashes with some practical concerns.

The decision to not expose any control over anti-aliasing is how we ended up with { alpha: false } being an undocumented backdoor for enabling sub-pixel text AA. FWIW, this removed a major roadblock for Google Sheets. The idea behind that backdoor is that sub-pixel text anti-aliasing is not compatible with transparency compositing, so if we know that the canvas is opaque we don't have to worry about compositing artifacts caused by LCD text filtering.

Anyways... over the years we started relaxing the notion of not exposing "quality of implementation" controls because sometimes it is not about quality, but about the author's intent. For example, there are situations where content authors want their upscaled images to look blocky instead of smoothly filtered, so we added the imageSmoothingEnabled attribute.

As far as controlling anti-aliasing of primitives is concerned, no objections from me (in fact I've advocated for it in the past). But there are important things to consider:

  1. Should the option be permanent (a context creation attribute) or should it be part of the context state (a rendering context attribute)?

  2. What should it affect (rects, paths, clipping, text, drawImage)?

Making it a context creation attribute (like webgl) means implementations could efficiently use GPU-assisted techniques like MSAA, but it would be less flexible.

My 2 cents: I think a context attribute would be a better choice, and I think text rendering should not be affected.

sc28 commented 6 years ago

Thanks for this useful context. Having recently began web development, I understand the purpose of achieving the author's intent rather than predefined assumptions about quality. In that sense, more control over the default anti-aliasing features would certainly be welcome.

As to your first consideration, if I understand correctly you distinguish between:

I'd find the latter more flexible and closer to traditional digital drawing software (paint, gimp...) where one can stroke paths either with a crisp "pencil" tool or an anti-aliased "brush" tool. But those are also just my 2 cents, and I lack some deeper knowledge of the possible implications.

Anyway, is there any hope that this topic could receive some further attention, if as you say it failed to fly in the past? Anything I could do to help (e.g. post a request elsewhere, specify more use-cases..)?

junov commented 6 years ago

My use of "context attribute" in my previous comment was unclear. What I meant is "an attribute of the rendering context object", as opposed to a "context creation attribute" (passed to getContext). Basically I agree with you. Lets wait for other browser vendors to chime-in to see if there is any will to do this.

In the mean time, here is a strawman proposal:

IDL:

partial interface CanvasCompositing {
  attribute boolean antiAliasingEnabled; // (default true)
};

Affects the behavior of drawImage, clearRect, fillRect, strokeRect, fillText, strokeText, fill, stroke, and clip. When set to false, graphics primitives must be rasterized without anti-aliasing. When set to true, graphics primitives may be rasterized with anti-aliasing.

Note: UserAgents are not required to support anti-aliasing on all types of graphics primitives, or at all.

qianduancaiji commented 3 years ago

After 12 years, this problem remains unsolved. https://stackoverflow.com/questions/195262/can-i-turn-off-antialiasing-on-an-html-canvas-element

Kaiido commented 3 years ago

@qianduancaiji you can now use svg filters to remove any non opaque pixels, resulting in something close to no antialiasing: https://stackoverflow.com/a/49357655/3702797

However it's true that it would be time to have a context option available directly, relying on such hacks (or on authors to draw their own path drawing methods) is terrible and it's a common enough problem that I believe we should finally do something about it.

For what it's worth, this was brought recently in https://github.com/fserb/canvas2D/issues/19 as ctx.shapeSmoothingEnabled & ctx.shapeSmoothingQuality, which would be similar to the current imageSmoothing... properties and which would map to SVG's shape-rendering.

qianduancaiji commented 3 years ago

@qianduancaiji you can now use svg filters to remove any non opaque pixels, resulting in something close to no antialiasing: https://stackoverflow.com/a/49357655/3702797

However it's true that it would be time to have a context option available directly, relying on such hacks (or on authors to draw their own path drawing methods) is terrible and it's a common enough problem that I believe we should finally do something about it.

For what it's worth, this was brought recently in fserb/canvas2D#19 as ctx.shapeSmoothingEnabled & ctx.shapeSmoothingQuality, which would be similar to the current imageSmoothing... properties and which would map to SVG's shape-rendering.

Thank you for your reply. As shown below, svg filters cannot meet the requirements. I want each pixel to be the expected pixel value. getImageData Output [255, 34, 34] when color is [255, 33, 33]. Notice that webgl has the antialias option. So finally, I chose the combination of webgl and canvas2D to meet the requirements temporarily.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <svg width="0" height="0" style="position:absolute;z-index:-1;">
    <defs>
      <filter id="remove-alpha" x="0" y="0" width="100%" height="100%">
        <feComponentTransfer>
          <feFuncA type="discrete" tableValues="0 1"></feFuncA>
        </feComponentTransfer>
        </filter>
    </defs>
  </svg>
  <canvas id="canvas" width="10" height="10"></canvas>
  <script>

    /*
    var canvas = document.querySelector('#canvas');
    var ctx = canvas.getContext('2d');
    var color = [255, 0, 0];
    ctx.save();
    ctx.lineWidth = 2;
    ctx.filter = 'url(#remove-alpha)';
    ctx.beginPath();
    ctx.moveTo(0, 0);
    ctx.lineTo(10, 10);
    ctx.strokeStyle = `rgb(${color.join(',')})`;
    ctx.stroke();
    ctx.restore();
    var imgdata = ctx.getImageData(0, 0, canvas.width, canvas.height);
    console.log(imgdata.data) // [255, 0, 0, 255, 255, 0, 0, 255, 0, ...]
    */

    /*
    var canvas = document.querySelector('#canvas');
    var ctx = canvas.getContext('2d');
    var color = [255, 33, 33];
    ctx.save();
    ctx.lineWidth = 2;
    ctx.filter = 'url(#remove-alpha)';
    ctx.beginPath();
    ctx.moveTo(0, 0);
    ctx.lineTo(10, 10);
    ctx.strokeStyle = `rgb(${color.join(',')})`;
    ctx.stroke();
    ctx.restore();
    var imgdata = ctx.getImageData(0, 0, canvas.width, canvas.height);
    console.log(imgdata.data) // [255, 34, 34, 255, 255, 34, 34, 255, 0, 0, ...]
    */

  </script>
</body>
</html>
nornagon commented 1 year ago

I've been experimenting with implementing this in Blink. Here's a draft CL: https://chromium-review.googlesource.com/c/chromium/src/+/4450410

This implements antialiasing as a canvas context attribute, e.g.

ctx.antiAlias = false
ctx.beginPath()
ctx.moveTo(0, 0)
ctx.lineTo(100, 40)
ctx.stroke()

ctx.antiAlias = true
ctx.beginPath()
ctx.moveTo(40, 0)
ctx.lineTo(140, 40)
ctx.stroke()

produces the following: an image of two diagonal lines. the one on the left is jagged and aliased, the one on the right is smooth and antialiased.

What would be the next step toward having this proposal formally considered?

Kaiido commented 1 year ago

A global ctx.antiAlias attribute seems problematic.
I didn't check your PR, but does it affect things like text rendering? What about filters or ctx.shadowBlur?

I think such an attribute should target only path drawings, stroke() and fill(), so I still believe the API should be more around ctx.shapeSmoothingEnabled.

As for the next step, it seems we're between step 5 and 6 of adding a new feature. So some feedback from @whatwg/canvas would be great.

nornagon commented 1 year ago

I checked, and it looks like the approach in my CL doesn't affect text rendering. I'm not sure what it would mean to not apply antialiasing to shadow blur, but my CL doesn't seem to change how shadow blur is applied.

I think shapeSmoothingEnabled (or perhaps just shapeSmoothing) makes sense as a name.

mysteryDate commented 1 year ago

@nornagon Awesome work! Disabling antialiasing for paths is a feature that's come up a few times recently among UA implementers. If I recall correctly, there is not strong opposition to it, though some implementers question such a features' utility. Blink's canvas team (speaking for us all) is certainly for it! If you wanted to take this feature over and get it shipped, we would love that.

The next steps for this feature would be:

nornagon commented 1 year ago

Sounds good! I created a first cut at #9192. I'm sure I've missed stuff, but it's a start!