ChatGPT解决这个技术问题 Extra ChatGPT

我可以关闭 HTML <canvas> 元素的抗锯齿功能吗?

我正在玩 <canvas> 元素,画线等。

我注意到我的对角线是抗锯齿的。对于我正在做的事情,我更喜欢锯齿状的外观 - 有没有办法关闭这个功能?

我认为这与浏览器有关。也许有关您使用的软件的一些其他信息会有所帮助。
我更喜欢跨浏览器的方法,但适用于任何单个浏览器的方法对我来说仍然很有趣
我只是想看看这个话题是否有任何变化?
对于它的价值,我似乎记得 chrome 软件渲染引擎确实支持别名渲染,但是,浏览器仍然将其设置为抗锯齿并且无法禁用。自从我仔细研究代码以来已经有一段时间了,我无法告诉你我注意到这一点的顶部,但我确定我在那里看到了它。我曾考虑过使用skia 并尝试通过emscripten 运行它,但它是从背面连接到浏览器的,所以很难说需要付出多少努力。 chromium.googlesource.com/external/skia/src/+/master/core

K
Kornel

对于图像,现在有 context.imageSmoothingEnabled= false

但是,没有任何东西可以明确控制线条绘制。您可能需要使用 getImageDataputImageData 绘制自己的线条 (the hard way)。


我想知道 javascript 行算法的性能。在某些时候可能会让 Bresenham 尝试一下。
浏览器供应商最近都在吹捧新的超快速 JS 引擎,所以最终它会有一个很好的用途。
这真的有效吗?我正在使用 putImageData 画一条线,但它仍然会在附近像素混叠该死的。
如果我绘制到一个较小的画布(缓存),然后将 drawImage 绘制到另一个画布并关闭该设置,它会按预期工作吗?
a
allan

ctx.lineTo(10.5, 10.5) 等坐标上绘制 1-pixel 线。在点 (10, 10) 上绘制一条单像素线意味着该位置的此 1 像素从 9.5 延伸到 10.5,这导致在画布上绘制两条线。

如果您有很多单像素线,则不必总是将 0.5 添加到要绘制的实际坐标上,这是一个很好的技巧,即在开始时 ctx.translate(0.5, 0.5) 整个画布。


嗯,我在使用这种技术摆脱抗锯齿时遇到了麻烦。也许,我想念什么?你介意在哪里贴一个例子吗?
这并没有消除抗锯齿,但确实使抗锯齿线条看起来好多了——例如,当您真正想要一个像素时,摆脱那些厚两个像素的令人尴尬的水平或垂直线条。
@porneL:不,在像素角之间画线。当您的线条为 1 像素宽时,它会在任一方向上延伸半个像素
添加 +0.5 对我有用,但 ctx.translate(0.5,0.5) 没有。在FF39.0
非常感谢!我不敢相信我有实际的 1px 线来改变!
T
Tim Cooper

它可以在 Mozilla Firefox 中完成。将此添加到您的代码中:

contextXYZ.mozImageSmoothingEnabled = false;

在 Opera 中,它目前是一个功能请求,但希望很快就会添加。


凉爽的。 +1 为您的贡献。我想知道禁用 AA 是否会加快画线速度
OP 想要取消反锯齿线,但这仅适用于图像。根据 the spec,它确定 "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

它必须抗锯齿矢量图形

正确绘制涉及非整数坐标 (0.4, 0.4) 的矢量图形需要抗锯齿,除了极少数客户端之外,所有这些都需要。

当给定非整数坐标时,画布有两个选项:

抗锯齿 - 根据整数坐标与非整数坐标的距离(即舍入误差)绘制坐标周围的像素。

Round - 对非整数坐标应用一些舍入函数(例如,1.4 将变为 1)。

后面的策略将适用于静态图形,尽管对于小图形(半径为 2 的圆)曲线将显示清晰的台阶而不是平滑的曲线。

真正的问题是当图形被平移(移动)时——一个像素和另一个像素之间的跳跃(1.6 => 2, 1.4 => 1)意味着形状的原点可能会相对于父容器跳跃(不断移动上/下和左/右 1 个像素)。

一些技巧

提示#1:您可以通过缩放画布(例如按x)来软化(或硬化)抗锯齿,然后自己将倒数比例(1/x)应用于几何图形(不使用画布)。

比较(无缩放):

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

与(画布比例:0.75;手动比例:1.33):

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

和(画布比例:1.33;手动比例:0.75):

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

提示#2:如果你真的想要一个锯齿状的外观,试着画几次每个形状(不要擦除)。每次绘制时,抗锯齿像素都会变暗。

相比。绘制一次后:

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

画三次后:

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


@vanowm 可以随意克隆和使用:github.com/Izhaki/gefri。所有图像都是来自 /demo 文件夹的屏幕截图(针对技巧 #2 稍微修改了代码)。我相信您会发现很容易将整数舍入引入绘制的数字(花了我 4 分钟),然后只需拖动即可查看效果。
这似乎令人难以置信,但在极少数情况下您希望关闭抗锯齿功能。事实上,我刚刚编写了一个游戏,人们必须在画布上绘制区域,而我只需要 4 种颜色,但由于抗锯齿,我被卡住了。重复三次图案并没有解决问题(我增加到了 200 次)并且仍然有错误颜色的像素。
J
Jón Trausti Arason

我会使用自定义线算法(例如 Bresenham 的线算法)绘制所有内容。看看这个 javascript 实现:http://members.chello.at/easyfilter/canvas.html

我认为这肯定会解决您的问题。


正是我需要的,我唯一要补充的是您需要实现 setPixel(x, y);我在这里使用了公认的答案:stackoverflow.com/questions/4899799/…
s
soshimee

尝试类似 canvas { image-rendering: pixelated; }

如果您只想使一行不抗锯齿,这可能不起作用。

const canvas = document.querySelector("canvas"); const ctx = canvas.getContext("2d"); ctx.fillRect(4, 4, 2, 2);画布 { 图像渲染:像素化;宽度:100px;高度:100px; /* 缩放 10 倍 */ } 不支持画布

不过,我还没有在许多浏览器上测试过这个。


这应该被标记为答案。
e
eri0o

我想补充一点,我在缩小图像并在画布上绘图时遇到了麻烦,它仍在使用平滑,即使在放大时没有使用。

我用这个解决了:

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

你可以像这样使用这个函数:

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

也许这对某人有用。


为什么不 context.imageSmoothingEnabled = false ?
这在我写答案时不起作用。它现在有效吗?
确实如此,完全一样,在 javascript 中编写 obj['name'] 或 obj.name 一直是并且将永远是相同的,对象是命名值(元组)的集合,使用类似于哈希表,两种表示法将被以相同的方式处理,根本没有理由您的代码以前不会工作,最坏的情况是它分配了一个没有效果的值(因为它是为另一个浏览器设计的。一个简单的例子:写obj = {a:123}; console.log(obj['a'] === obj.a ? "是的,它是真的" : "不是的")
我以为您的意思是为什么要拥有所有其他东西,我的评论的意思是当时浏览器需要不同的属性。
好的,当然:)我在谈论语法,而不是代码本身的有效性(它有效)
r
retepaskab
ctx.translate(0.5, 0.5);
ctx.lineWidth = .5;

有了这个组合,我可以画出漂亮的 1px 细线。


您不需要将 lineWidth 设置为 .5 ...这将(或应该)仅使其变为一半不透明度。
M
Max Weber

添加这个:

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

canvas 元素的 style 属性有助于在画布上绘制清晰的像素。通过这篇精彩的文章发现:

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


这应该更高!
S
StashOfCode

注意一个非常有限的技巧。如果要创建 2 色图像,您可以使用颜色 #010101 在颜色 #000000 的背景上绘制您想要的任何形状。完成此操作后,您可以测试 imageData.data[] 中的每个像素并设置为 0xFF 任何值不是 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);

结果将是非抗锯齿的黑白图片。这不会是完美的,因为会发生一些抗锯齿,但这种抗锯齿将非常有限,形状的颜色非常类似于背景的颜色。


C
Codesmith

我发现了一种使用上下文的 filter 属性禁用路径/形状渲染的抗锯齿的更好方法:

魔法/ TL;DR:

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

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

揭秘:

数据 url 是对包含单个过滤器的 SVG 的引用:

<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>

然后在 url 的最后是对该 #filter 的 id 引用:

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

SVG 滤镜在 Alpha 通道上使用离散变换,在渲染时仅选择完全透明或在 50% 边界上完全不透明。如果需要,可以对其进行调整以添加一些抗锯齿,例如:

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

缺点/注释/陷阱

注意,我没有用图像测试这个方法,但我可以假设它会影响图像的半透明部分。我也可以猜测它可能不会阻止不同颜色边界的图像上的抗锯齿。它不是“最近的颜色”解决方案,而是二元透明度解决方案。它似乎最适合路径/形状渲染,因为 alpha 是唯一使用路径抗锯齿的通道。

此外,使用 1 的最小值 lineWidth 是安全的。较细的线条变得稀疏或可能经常完全消失。

编辑:

我发现,在 Firefox 中,将 filter 设置为 dataurl 不会立即/同步工作:dataurl 必须首先“加载”。

例如,以下内容在 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";

但是等到下一个 JS 框架工作正常:

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);

这对我有用!
e
elliottdehn

这是 Bresenham 算法在 JavaScript 中的基本实现。它基于此 Wikipedia 文章中描述的整数算术版本: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

虽然我们在 2D 上下文中仍然没有正确的 shapeSmoothingEnabledshapeSmoothingQuality 选项(我会提倡这一点,并希望它在不久的将来能够实现),但我们现在有办法近似“无抗锯齿” " 行为,这要归功于 SVGFilters,它可以通过其 .filter 属性应用于上下文。

因此,需要明确的是,它本身不会停用抗锯齿,但在实现和性能方面都提供了一种廉价的方式(?,它应该是硬件加速的,这应该比 CPU 上的自制 Bresenham 更好) 以便在绘制时移除所有半透明像素,但它也可能会创建一些像素块,并且可能无法保留原始输入颜色。

为此,我们可以使用 <feComponentTransfer> 节点仅抓取完全不透明的像素。

const canvas = document.getElementById("canvas"); const ctx = canvas.getContext("2d"); ctx.fillStyle = "#ABEDBE"; ctx.fillRect(0,0,canvas.width,canvas.height); ctx.fillStyle = "黑色"; ctx.font = "14px 无衬线"; ctx.textAlign = "中心"; // 首先没有过滤器 ctx.fillText("no filter", 60, 20);画弧();绘制三角形(); // 然后使用过滤器 ctx.setTransform(1, 0, 0, 1, 120, 0); ctx.filter = "url(#remove-alpha)"; // 并执行相同的操作 ctx.fillText("no alpha", 60, 20);画弧();绘制三角形(); // 移除过滤器 ctx.filter = "none";函数 drawArc() { ctx.beginPath(); ctx.arc(60, 80, 50, 0, Math.PI * 2); ctx.stroke(); } 函数 drawTriangle() { ctx.beginPath(); ctx.moveTo(60, 150); ctx.lineTo(110, 230); ctx.lineTo(10, 230); ctx.closePath(); ctx.stroke(); } // 无关 // 只是为了显示放大版本 const zoomed = document.getElementById("zoomed");常量 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, xw/6,yh/6,w, h, 0,0,w*3, h*3); }

对于那些不喜欢在他们的 DOM 中附加 <svg> 元素并且生活在不久的将来(或带有实验标志)的人,我们正在开发的 CanvasFilter 接口应该允许在没有 DOM 的情况下执行此操作(同样来自 Worker):

if (!("CanvasFilter" in globalThis)) { throw new Error("Not Supported", "请启用实验性 Web 平台功能,或稍等"); } const canvas = document.getElementById("canvas"); const ctx = canvas.getContext("2d"); ctx.fillStyle = "#ABEDBE"; ctx.fillRect(0,0,canvas.width,canvas.height); ctx.fillStyle = "黑色"; ctx.font = "14px 无衬线"; ctx.textAlign = "中心"; // 首先没有过滤器 ctx.fillText("no filter", 60, 20);画弧();绘制三角形(); // 然后使用过滤器 ctx.setTransform(1, 0, 0, 1, 120, 0); ctx.filter = new CanvasFilter([ { filter: "componentTransfer", funcA: { type: "discrete", tableValues: [ 0, 1 ] } } ]); // 并执行相同的操作 ctx.fillText("no alpha", 60, 20);画弧();绘制三角形(); // 移除过滤器 ctx.filter = "none";函数 drawArc() { ctx.beginPath(); ctx.arc(60, 80, 50, 0, Math.PI * 2); ctx.stroke(); } 函数 drawTriangle() { ctx.beginPath(); ctx.moveTo(60, 150); ctx.lineTo(110, 230); ctx.lineTo(10, 230); ctx.closePath(); ctx.stroke(); } // 无关 // 只是为了显示放大版本 const zoomed = document.getElementById("zoomed");常量 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, xw/6,yh/6,w, h, 0,0,w*3, h*3); };

或者,您也可以将 SVG 保存为外部文件并将 filter 属性设置为 path/to/svg_file.svg#remove-alpha


J
Jaewon.A.C

对于那些仍在寻找答案的人。这是我的解决方案。

假设图像是 1 通道灰度。我刚刚在 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;
    }
  }
}

如果您的图像通道是 3 或 4。您需要修改数组索引,例如

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

M
Matías Moreno

关于 StashOfCode 的回答只有两个注释:

它仅适用于灰度、不透明的画布(fillRect 用白色然后用黑色绘制,反之亦然)当线条很细时它可能会失败(~1px 线宽)

最好这样做:

#FFFFFF 描边和填充,然后执行以下操作:

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

这解决了宽度为 1px 的线条。

除此之外,StashOfCode 的解决方案是完美的,因为它不需要编写自己的光栅化函数(不仅要考虑线条,还要考虑贝塞尔曲线、圆弧、带孔的填充多边形等......)