ChatGPT解决这个技术问题 Extra ChatGPT

Can I turn off antialiasing on an HTML <canvas> element?

I'm playing around with the <canvas> element, drawing lines and such.

I've noticed that my diagonal lines are antialiased. I'd prefer the jaggy look for what I'm doing - is there any way of turning this feature off?

I think that is rather browser-related. Maybe some additional info on what software you use would be helpful.
I'd prefer a cross-browser method, but a method that works on any single browser would still be interesting to me
I just wanted to see if there as been any change yet on this topic?
For what it's worth, I seem to remember that the chrome software rendering engine does support alias rendering, however, the browser still has it set to anti-aliased and it cannot be disabled. It's been a while since I mulled over the code and I couldn't tell you off the top where I noticed this, but I'm certain I saw it in there. I've thought about taking skia and trying to run it through emscripten, but it's hooked into the browser from the back side, so it's hard to say how much effort that would take. chromium.googlesource.com/external/skia/src/+/master/core

K
Kornel

For images there's now context.imageSmoothingEnabled= false.

However, there's nothing that explicitly controls line drawing. You may need to draw your own lines (the hard way) using getImageData and putImageData.


I wonder about the performance of a javascript line algorithm.. Might give Bresenham's a go at some point.
Browser vendors are touting new super-fast JS engines lately, so finally there would be a good use for it.
Does this really work? I'm drawing a line using putImageData but it still does aliasing of nearby pixels damn.
if I draw to a smaller canvas (cache) and then drawImage to another canvas with that setting off, will it work as intended?
a
allan

Draw your 1-pixel lines on coordinates like ctx.lineTo(10.5, 10.5). Drawing a one-pixel line over the point (10, 10) means, that this 1 pixel at that position reaches from 9.5 to 10.5 which results in two lines that get drawn on the canvas.

A nice trick to not always need to add the 0.5 to the actual coordinate you want to draw over if you've got a lot of one-pixel lines, is to ctx.translate(0.5, 0.5) your whole canvas at the beginning.


hmm, I'm having a trouble getting rid of anti-aliasing using this technique. Maybe, I'm miss understanding something? Would you mind posting an example some where?
This doesn't get rid of antialiasing, but does make antialiased lines look a lot better --- such as getting rid of those embarrassing horizontal or vertical lines that are two pixels thick when you actually wanted one pixel.
@porneL: No, lines are drawn between corners of pixels. When your line is 1 pixel wide, that extends half a pixel in either direction
Adding +0.5 works for me, but ctx.translate(0.5,0.5) didn't. on FF39.0
Thank you very much! I can't believe I have actual 1px lines for a change!
T
Tim Cooper

It can be done in Mozilla Firefox. Add this to your code:

contextXYZ.mozImageSmoothingEnabled = false;

In Opera it's currently a feature request, but hopefully it will be added soon.


cool. +1 for your contribution. i wonder if the disabling of AA speeds up linedrawing
The OP wants to un-anti-alias lines, but this only works on images. Per the spec, it determines "whether pattern fills and the drawImage() method will attempt to smooth images if their pixels don't line up exactly with the display, when scaling images up"
I
Izhaki

It must antialias vector graphics

Antialiasing is required for correct plotting of vector graphics that involves non-integer coordinates (0.4, 0.4), which all but very few clients do.

When given non-integer coordinates, the canvas has two options:

Antialias - paint the pixels around the coordinate based on how far the integer coordinate is from non-integer one (ie, the rounding error).

Round - apply some rounding function to the non-integer coordinate (so 1.4 will become 1, for example).

The later strategy will work for static graphics, although for small graphics (a circle with radius of 2) curves will show clear steps rather than a smooth curve.

The real problem is when the graphics is translated (moved) - the jumps between one pixel and another (1.6 => 2, 1.4 => 1), mean that the origin of the shape may jump with relation to the parent container (constantly shifting 1 pixel up/down and left/right).

Some tips

Tip #1: You can soften (or harden) antialiasing by scaling the canvas (say by x) then apply the reciprocal scale (1/x) to the geometries yourself (not using the canvas).

Compare (no scaling):

https://i.stack.imgur.com/127jr.png

with (canvas scale: 0.75; manual scale: 1.33):

https://i.stack.imgur.com/ccjt2.png

and (canvas scale: 1.33; manual scale: 0.75):

https://i.stack.imgur.com/fWWBp.png

Tip #2: If a jaggy look is really what you're after, try to draw each shape a few times (without erasing). With each draw, the antialiasing pixels get darker.

Compare. After drawing once:

https://i.stack.imgur.com/P96ie.png

After drawing thrice:

https://i.stack.imgur.com/eICmM.png


@vanowm feel free to clone and play with: github.com/Izhaki/gefri. All the images are screenshots from the /demo folder (with code slightly modified for tip #2). I'm sure you'll find it easy to introduce integer rounding to the drawn figures (took me 4 minutes) and then just drag to see the effect.
It may seem unbelievable but there are rare situations where you want antialias turned off. As a matter of fact, I just programmed a game where people must paint areas on a canvas and I need there to be only 4 colors and I'm stuck because of antialias. Repeating the pattern thrice did not solve the problem (I went up to 200) and there are still pixels of the wrong colors.
J
Jón Trausti Arason

I would draw everything using a custom line algorithm such as Bresenham's line algorithm. Check out this javascript implementation: http://members.chello.at/easyfilter/canvas.html

I think this will definitely solve your problems.


Exactly what I needed, the only thing I would add is that you need to implement setPixel(x, y); I used the accepted answer here: stackoverflow.com/questions/4899799/…
s
soshimee

Try something like canvas { image-rendering: pixelated; }.

This might not work if you're trying to only make one line not antialiased.

const canvas = document.querySelector("canvas"); const ctx = canvas.getContext("2d"); ctx.fillRect(4, 4, 2, 2); canvas { image-rendering: pixelated; width: 100px; height: 100px; /* Scale 10x */ } Canvas unsupported

I haven't tested this on many browsers though.


This should be marked as answer.
e
eri0o

I want to add that I had trouble when downsizing an image and drawing on canvas, it was still using smoothing, even though it wasn't using when upscaling.

I solved using this:

function setpixelated(context){
    context['imageSmoothingEnabled'] = false;       /* standard */
    context['mozImageSmoothingEnabled'] = false;    /* Firefox */
    context['oImageSmoothingEnabled'] = false;      /* Opera */
    context['webkitImageSmoothingEnabled'] = false; /* Safari */
    context['msImageSmoothingEnabled'] = false;     /* IE */
}

You can use this function like this:

var canvas = document.getElementById('mycanvas')
setpixelated(canvas.getContext('2d'))

Maybe this is useful for someone.


why not context.imageSmoothingEnabled = false ?
This didn't work at the time I wrote my answer. Does it work now?
it did, it's EXACTLY the same thing, in javascript writing obj['name'] or obj.name has always been, and will always be the same, an object is a collection of named values (tuples), using something that resembles a hash table, both notations will be treated the same way, there is no reason at all that your code would not have worked before, at worst it assigns a value that has no effect (because it's intended for another browser. a simple example: write obj = {a:123}; console.log(obj['a'] === obj.a ? "yes its true" : "no it's not")
I thought you meant why have all the other things, what I meant with my comment is that at the time browsers required different properties.
ok yes of course :) i was talking about the syntax, not the validity of the code itself (it works)
r
retepaskab
ctx.translate(0.5, 0.5);
ctx.lineWidth = .5;

With this combo I can draw nice 1px thin lines.


You don't need to set the lineWidth to .5 ... that will (or should) only make it half opacity.
M
Max Weber

Adding this:

image-rendering: pixelated; image-rendering: crisp-edges;

to the style attribute of the canvas element helped to draw crisp pixels on the canvas. Discovered via this great article:

https://developer.mozilla.org/en-US/docs/Games/Techniques/Crisp_pixel_art_look


this should be WAY higher up!!
S
StashOfCode

Notice a very limited trick. If you want to create a 2 colors image, you may draw any shape you want with color #010101 on a background with color #000000. Once this is done, you may test each pixel in the imageData.data[] and set to 0xFF whatever value is not 0x00 :

imageData = context2d.getImageData (0, 0, g.width, g.height);
for (i = 0; i != imageData.data.length; i ++) {
    if (imageData.data[i] != 0x00)
        imageData.data[i] = 0xFF;
}
context2d.putImageData (imageData, 0, 0);

The result will be a non-antialiased black & white picture. This will not be perfect, since some antialiasing will take place, but this antialiasing will be very limited, the color of the shape being very much like the color of the background.


C
Codesmith

I discovered a better way to disable antialiasing on path / shape rendering using the context's filter property:

The magic / TL;DR:

ctx = canvas.getContext('2d');

// make canvas context render without antialiasing
ctx.filter = "url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxmaWx0ZXIgaWQ9ImZpbHRlciIgeD0iMCIgeT0iMCIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgY29sb3ItaW50ZXJwb2xhdGlvbi1maWx0ZXJzPSJzUkdCIj48ZmVDb21wb25lbnRUcmFuc2Zlcj48ZmVGdW5jUiB0eXBlPSJpZGVudGl0eSIvPjxmZUZ1bmNHIHR5cGU9ImlkZW50aXR5Ii8+PGZlRnVuY0IgdHlwZT0iaWRlbnRpdHkiLz48ZmVGdW5jQSB0eXBlPSJkaXNjcmV0ZSIgdGFibGVWYWx1ZXM9IjAgMSIvPjwvZmVDb21wb25lbnRUcmFuc2Zlcj48L2ZpbHRlcj48L3N2Zz4=#filter)";

Demystified:

The data url is a reference to an SVG containing a single filter:

<svg xmlns="http://www.w3.org/2000/svg">
    <filter id="filter" x="0" y="0" width="100%" height="100%" color-interpolation-filters="sRGB">
        <feComponentTransfer>
            <feFuncR type="identity"/>
            <feFuncG type="identity"/>
            <feFuncB type="identity"/>
            <feFuncA type="discrete" tableValues="0 1"/>
        </feComponentTransfer>
    </filter>
</svg>

Then at the very end of the url is an id reference to that #filter:

"url(data:image/svg+...Zz4=#filter)";

The SVG filter uses a discrete transform on the alpha channel, selecting only completely transparent or completely opaque on a 50% boundary when rendering. This can be tweaked to add some anti-aliasing back in if needed, e.g.:

...
<feFuncA type="discrete" tableValues="0 0 0.25 0.75 1"/>
...

Cons / Notes / Gotchas

Note, I didn't test this method with images, but I can presume it would affect semi-transparent parts of images. I can also guess that it probably would not prevent antialiasing on images at differing color boundaries. It isn't a 'nearest color' solution but rather a binary transparency solution. It seems to work best with path / shape rendering since alpha is the only channel antialiased with paths.

Also, using a minimum lineWidth of 1 is safe. Thinner lines become sparse or may often disappear completely.

Edit:

I've discovered that, in Firefox, setting filter to a dataurl does not work immediately / synchronously: the dataurl has to 'load' first.

e.g. The following will not work in Firefox:

ctx.filter = "url(data:image/svg+xml;base64,...#filter)";

ctx.beginPath();
ctx.moveTo(10,10);
ctx.lineTo(20,20);
ctx.strokeStyle = 'black';
ctx.lineWidth = 2;
ctx.stroke();

ctx.filter = "none";

But waiting till the next JS frame works fine:

ctx.filter = "url(data:image/svg+xml;base64,...#filter)";
setTimeout(() => {
    ctx.beginPath();
    ctx.moveTo(10,10);
    ctx.lineTo(20,20);
    ctx.strokeStyle = 'black';
    ctx.lineWidth = 2;
    ctx.stroke();

    ctx.filter = "none";
}, 0);

This worked for me!
e
elliottdehn

Here is a basic implementation of Bresenham's algorithm in JavaScript. It's based on the integer-arithmetic version described in this wikipedia article: https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm

    function range(f=0, l) {
        var list = [];
        const lower = Math.min(f, l);
        const higher = Math.max(f, l);
        for (var i = lower; i <= higher; i++) {
            list.push(i);
        }
        return list;
    }

    //Don't ask me.
    //https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm
    function bresenhamLinePoints(start, end) {

        let points = [];

        if(start.x === end.x) {
            return range(f=start.y, l=end.y)
                        .map(yIdx => {
                            return {x: start.x, y: yIdx};
                        });
        } else if (start.y === end.y) {
            return range(f=start.x, l=end.x)
                        .map(xIdx => {
                            return {x: xIdx, y: start.y};
                        });
        }

        let dx = Math.abs(end.x - start.x);
        let sx = start.x < end.x ? 1 : -1;
        let dy = -1*Math.abs(end.y - start.y);
        let sy = start.y < end.y ? 1 : - 1;
        let err = dx + dy;

        let currX = start.x;
        let currY = start.y;

        while(true) {
            points.push({x: currX, y: currY});
            if(currX === end.x && currY === end.y) break;
            let e2 = 2*err;
            if (e2 >= dy) {
                err += dy;
                currX += sx;
            }
            if(e2 <= dx) {
                err += dx;
                currY += sy;
            }
        }

        return points;

    }

K
Kaiido

While we still don't have proper shapeSmoothingEnabled or shapeSmoothingQuality options on the 2D context (I'll advocate for this and hope it makes its way in the near future), we now have ways to approximate a "no-antialiasing" behavior, thanks to SVGFilters, which can be applied to the context through its .filter property.

So, to be clear, it won't deactivate antialiasing per se, but provides a cheap way both in term of implementation and of performances (?, it should be hardware accelerated, which should be better than a home-made Bresenham on the CPU) in order to remove all semi-transparent pixels while drawing, but it may also create some blobs of pixels, and may not preserve the original input color.

For this we can use a <feComponentTransfer> node to grab only fully opaque pixels.

const canvas = document.getElementById("canvas"); const ctx = canvas.getContext("2d"); ctx.fillStyle = "#ABEDBE"; ctx.fillRect(0,0,canvas.width,canvas.height); ctx.fillStyle = "black"; ctx.font = "14px sans-serif"; ctx.textAlign = "center"; // first without filter ctx.fillText("no filter", 60, 20); drawArc(); drawTriangle(); // then with filter ctx.setTransform(1, 0, 0, 1, 120, 0); ctx.filter = "url(#remove-alpha)"; // and do the same ops ctx.fillText("no alpha", 60, 20); drawArc(); drawTriangle(); // to remove the filter ctx.filter = "none"; function drawArc() { ctx.beginPath(); ctx.arc(60, 80, 50, 0, Math.PI * 2); ctx.stroke(); } function drawTriangle() { ctx.beginPath(); ctx.moveTo(60, 150); ctx.lineTo(110, 230); ctx.lineTo(10, 230); ctx.closePath(); ctx.stroke(); } // unrelated // simply to show a zoomed-in version const zoomed = document.getElementById("zoomed"); const zCtx = zoomed.getContext("2d"); zCtx.imageSmoothingEnabled = false; canvas.onmousemove = function drawToZoommed(e) { const x = e.pageX - this.offsetLeft, y = e.pageY - this.offsetTop, w = this.width, h = this.height; zCtx.clearRect(0,0,w,h); zCtx.drawImage(this, x-w/6,y-h/6,w, h, 0,0,w*3, h*3); }

For the ones that don't like to append an <svg> element in their DOM, and who live in the near future (or with experimental flags on), the CanvasFilter interface we're working on should allow to do this without a DOM (so from Worker too):

if (!("CanvasFilter" in globalThis)) { throw new Error("Not Supported", "Please enable experimental web platform features, or wait a bit"); } const canvas = document.getElementById("canvas"); const ctx = canvas.getContext("2d"); ctx.fillStyle = "#ABEDBE"; ctx.fillRect(0,0,canvas.width,canvas.height); ctx.fillStyle = "black"; ctx.font = "14px sans-serif"; ctx.textAlign = "center"; // first without filter ctx.fillText("no filter", 60, 20); drawArc(); drawTriangle(); // then with filter ctx.setTransform(1, 0, 0, 1, 120, 0); ctx.filter = new CanvasFilter([ { filter: "componentTransfer", funcA: { type: "discrete", tableValues: [ 0, 1 ] } } ]); // and do the same ops ctx.fillText("no alpha", 60, 20); drawArc(); drawTriangle(); // to remove the filter ctx.filter = "none"; function drawArc() { ctx.beginPath(); ctx.arc(60, 80, 50, 0, Math.PI * 2); ctx.stroke(); } function drawTriangle() { ctx.beginPath(); ctx.moveTo(60, 150); ctx.lineTo(110, 230); ctx.lineTo(10, 230); ctx.closePath(); ctx.stroke(); } // unrelated // simply to show a zoomed-in version const zoomed = document.getElementById("zoomed"); const zCtx = zoomed.getContext("2d"); zCtx.imageSmoothingEnabled = false; canvas.onmousemove = function drawToZoommed(e) { const x = e.pageX - this.offsetLeft, y = e.pageY - this.offsetTop, w = this.width, h = this.height; zCtx.clearRect(0,0,w,h); zCtx.drawImage(this, x-w/6,y-h/6,w, h, 0,0,w*3, h*3); };

Or you can also save the SVG as an external file and set the filter property to path/to/svg_file.svg#remove-alpha.


J
Jaewon.A.C

For those who still looking for answers. here is my solution.

Assumming image is 1 channel gray. I just thresholded after ctx.stroke().

ctx.beginPath();
ctx.moveTo(some_x, some_y);
ctx.lineTo(some_x, some_y);
...
ctx.closePath();
ctx.fill();
ctx.stroke();

let image = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height)
for(let x=0; x < ctx.canvas.width; x++) {
  for(let y=0; y < ctx.canvas.height; y++) {
    if(image.data[x*image.height + y] < 128) {
      image.data[x*image.height + y] = 0;
    } else {
      image.data[x*image.height + y] = 255;
    }
  }
}

if your image channel is 3 or 4. you need to modify the array index like

x*image.height*number_channel + y*number_channel + channel

M
Matías Moreno

Just two notes on StashOfCode's answer:

It only works for a grayscale, opaque canvas (fillRect with white then draw with black, or viceversa) It may fail when lines are thin (~1px line width)

It's better to do this instead:

Stroke and fill with #FFFFFF, then do this:

imageData.data[i] = (imageData.data[i] >> 7) * 0xFF

That solves it for lines with 1px width.

Other than that, StashOfCode's solution is perfect because it doesn't require to write your own rasterization functions (think not only lines but beziers, circular arcs, filled polygons with holes, etc...)