我正在玩 <canvas>
元素,画线等。
我注意到我的对角线是抗锯齿的。对于我正在做的事情,我更喜欢锯齿状的外观 - 有没有办法关闭这个功能?
对于图像,现在有 context.imageSmoothingEnabled
= false
。
但是,没有任何东西可以明确控制线条绘制。您可能需要使用 getImageData
和 putImageData
绘制自己的线条 (the hard way)。
在 ctx.lineTo(10.5, 10.5)
等坐标上绘制 1-pixel
线。在点 (10, 10)
上绘制一条单像素线意味着该位置的此 1
像素从 9.5
延伸到 10.5
,这导致在画布上绘制两条线。
如果您有很多单像素线,则不必总是将 0.5
添加到要绘制的实际坐标上,这是一个很好的技巧,即在开始时 ctx.translate(0.5, 0.5)
整个画布。
ctx.translate(0.5,0.5)
没有。在FF39.0
它可以在 Mozilla Firefox 中完成。将此添加到您的代码中:
contextXYZ.mozImageSmoothingEnabled = false;
在 Opera 中,它目前是一个功能请求,但希望很快就会添加。
"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"
它必须抗锯齿矢量图形
正确绘制涉及非整数坐标 (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
我会使用自定义线算法(例如 Bresenham 的线算法)绘制所有内容。看看这个 javascript 实现:http://members.chello.at/easyfilter/canvas.html
我认为这肯定会解决您的问题。
setPixel(x, y)
;我在这里使用了公认的答案:stackoverflow.com/questions/4899799/…
尝试类似 canvas { image-rendering: pixelated; }
。
如果您只想使一行不抗锯齿,这可能不起作用。
const canvas = document.querySelector("canvas"); const ctx = canvas.getContext("2d"); ctx.fillRect(4, 4, 2, 2);画布 { 图像渲染:像素化;宽度:100px;高度:100px; /* 缩放 10 倍 */ }
不过,我还没有在许多浏览器上测试过这个。
我想补充一点,我在缩小图像并在画布上绘图时遇到了麻烦,它仍在使用平滑,即使在放大时没有使用。
我用这个解决了:
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'))
也许这对某人有用。
ctx.translate(0.5, 0.5);
ctx.lineWidth = .5;
有了这个组合,我可以画出漂亮的 1px 细线。
添加这个:
image-rendering: pixelated; image-rendering: crisp-edges;
canvas 元素的 style 属性有助于在画布上绘制清晰的像素。通过这篇精彩的文章发现:
https://developer.mozilla.org/en-US/docs/Games/Techniques/Crisp_pixel_art_look
注意一个非常有限的技巧。如果要创建 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);
结果将是非抗锯齿的黑白图片。这不会是完美的,因为会发生一些抗锯齿,但这种抗锯齿将非常有限,形状的颜色非常类似于背景的颜色。
我发现了一种使用上下文的 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);
这是 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;
}
虽然我们在 2D 上下文中仍然没有正确的 shapeSmoothingEnabled
或 shapeSmoothingQuality
选项(我会提倡这一点,并希望它在不久的将来能够实现),但我们现在有办法近似“无抗锯齿” " 行为,这要归功于 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
。
对于那些仍在寻找答案的人。这是我的解决方案。
假设图像是 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
关于 StashOfCode 的回答只有两个注释:
它仅适用于灰度、不透明的画布(fillRect 用白色然后用黑色绘制,反之亦然)当线条很细时它可能会失败(~1px 线宽)
最好这样做:
用 #FFFFFF
描边和填充,然后执行以下操作:
imageData.data[i] = (imageData.data[i] >> 7) * 0xFF
这解决了宽度为 1px 的线条。
除此之外,StashOfCode 的解决方案是完美的,因为它不需要编写自己的光栅化函数(不仅要考虑线条,还要考虑贝塞尔曲线、圆弧、带孔的填充多边形等......)
putImageData
画一条线,但它仍然会在附近像素混叠该死的。