我正在玩<canvas>
元素,画线等。
我注意到我的对角线是抗锯齿的。对于我正在做的事情,我更喜欢锯齿状的外观 - 有没有办法关闭这个功能?
我正在玩<canvas>
元素,画线等。
我注意到我的对角线是抗锯齿的。对于我正在做的事情,我更喜欢锯齿状的外观 - 有没有办法关闭这个功能?
在坐标上画1-pixel
线,如ctx.lineTo(10.5, 10.5)
. 在该点上绘制一条单像素线(10, 10)
意味着1
该位置的该像素从9.5
到10.5
导致在画布上绘制两条线。
如果您有很多单像素线,则不必总是将其添加0.5
到要绘制的实际坐标中的一个好技巧是在开始时添加到ctx.translate(0.5, 0.5)
整个画布。
对于图像,现在有.context.imageSmoothingEnabled
= false
但是,没有任何东西可以明确控制线条绘制。您可能需要使用and来绘制自己的线条(很难)。getImageData
putImageData
它可以在 Mozilla Firefox 中完成。将此添加到您的代码中:
contextXYZ.mozImageSmoothingEnabled = false;
在 Opera 中,它目前是一个功能请求,但希望很快就会添加。
正确绘制涉及非整数坐标 (0.4, 0.4) 的矢量图形需要抗锯齿,除了极少数客户端之外,所有这些都需要。
当给定非整数坐标时,画布有两个选项:
后面的策略将适用于静态图形,尽管对于小图形(半径为 2 的圆)曲线将显示清晰的台阶而不是平滑的曲线。
真正的问题是当图形被平移(移动)时——一个像素和另一个像素之间的跳跃(1.6 => 2, 1.4 => 1)意味着形状的原点可能会相对于父容器跳跃(不断移动上/下和左/右 1 个像素)。
提示 #1:您可以通过缩放画布(例如按 x)软化(或硬化)抗锯齿,然后自己将倒数比例 (1/x) 应用于几何图形(不使用画布)。
比较(无缩放):
与(画布比例:0.75;手动比例:1.33):
和(画布比例:1.33;手动比例:0.75):
提示#2:如果你真的想要一个锯齿状的外观,试着画几次每个形状(不要擦除)。每次绘制时,抗锯齿像素都会变暗。
比较。绘制一次后:
画三次后:
我会使用自定义线算法(例如 Bresenham 的线算法)绘制所有内容。看看这个 javascript 实现: http: //members.chello.at/easyfilter/canvas.html
我认为这肯定会解决您的问题。
我想补充一点,我在缩小图像并在画布上绘图时遇到了麻烦,它仍在使用平滑,即使在放大时没有使用。
我用这个解决了:
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 细线。
尝试类似的东西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>
不过,我还没有在许多浏览器上测试过这个。
注意一个非常有限的技巧。如果要创建 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);
结果将是非抗锯齿的黑白图片。这不会是完美的,因为会发生一些抗锯齿,但这种抗锯齿将非常有限,形状的颜色非常类似于背景的颜色。
添加这个:
image-rendering: pixelated; image-rendering: crisp-edges;
canvas 元素的 style 属性有助于在画布上绘制清晰的像素。通过这篇精彩的文章发现:
https://developer.mozilla.org/en-US/docs/Games/Techniques/Crisp_pixel_art_look
filter
我发现了一种更好的方法来使用上下文的属性在路径/形状渲染上禁用抗锯齿:
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 是唯一使用路径抗锯齿的通道。
此外,至少使用lineWidth
1 是安全的。较细的线条变得稀疏或可能经常完全消失。
虽然我们在 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 = "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
.
对于那些仍在寻找答案的人。这是我的解决方案。
假设图像是 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
这是 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;
}
关于 StashOfCode 的回答只有两个注释:
最好这样做:
用 描边和填充#FFFFFF
,然后执行以下操作:
imageData.data[i] = (imageData.data[i] >> 7) * 0xFF
这解决了宽度为 1px 的线条。
除此之外,StashOfCode 的解决方案是完美的,因为它不需要编写自己的光栅化函数(不仅要考虑线条,还要考虑贝塞尔曲线、圆弧、带孔的填充多边形等......)