97

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

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

4

15 回答 15

72

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

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

于 2010-07-19T09:34:04.773 回答
71

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

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

于 2008-10-12T14:52:11.520 回答
23

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

contextXYZ.mozImageSmoothingEnabled = false;

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

于 2011-04-15T11:41:31.623 回答
13

它必须抗锯齿矢量图形

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

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

  • 抗锯齿- 根据整数坐标与非整数坐标的距离(即舍入误差)绘制坐标周围的像素。
  • Round - 对非整数坐标应用一些舍入函数(例如,1.4 将变为 1)。

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

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

一些技巧

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

比较(无缩放):

几个长方形

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

具有更柔和边缘的相同矩形

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

具有较暗边缘的相同矩形

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

比较。绘制一次后:

几条路

画三次后:

相同的路径但更暗且没有可见的抗锯齿。

于 2017-01-09T21:47:15.237 回答
11

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

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

于 2014-03-21T16:55:48.430 回答
8

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

我用这个解决了:

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

也许这对某人有用。

于 2015-09-26T14:31:47.710 回答
7
ctx.translate(0.5, 0.5);
ctx.lineWidth = .5;

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

于 2014-03-30T14:18:47.997 回答
7

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

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

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 */
}
<html>
  <head></head>
  <body>
    <canvas width="10" height="10">Canvas unsupported</canvas>
  </body>
</html>

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

于 2020-12-23T08:09:07.390 回答
5

注意一个非常有限的技巧。如果要创建 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);

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

于 2017-10-02T19:58:01.167 回答
5

添加这个:

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

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

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

于 2021-06-02T07:37:13.930 回答
3

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 的最后是一个 id 引用#filter

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

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

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

缺点/注释/陷阱

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

此外,至少使用lineWidth1 是安全的。较细的线条变得稀疏或可能经常完全消失。

于 2021-07-14T05:16:18.563 回答
2

虽然我们在 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 = "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);
}
<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="250" height="250" ></canvas>
<canvas id="zoomed" width="250" height="250" ></canvas>

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

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);
};
<canvas id="canvas" width="250" height="250" ></canvas>
<canvas id="zoomed" width="250" height="250" ></canvas>

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

于 2021-10-26T06:54:12.470 回答
1

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

假设图像是 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
于 2020-03-18T01:53:54.180 回答
1

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

    }
于 2020-03-23T03:25:25.930 回答
0

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

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

最好这样做:

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

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

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

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

于 2018-10-22T18:11:31.063 回答