如何提高我的Html5 Canvas路径质量?

时间:2016-06-24 09:57:13

标签: javascript html5 canvas

我一直在编写一个小的javascript插件,我在改进画布的渲染整体质量方面遇到了一些麻烦。我在这里和那里搜索过网络,但找不到任何有意义的东西。

从我的曲线创建的线条不平滑,如果你看下面的jsfiddle,你会明白我的意思。它有点像素化。有没有办法提高质量?或者是否有一个Canvas Framework已经使用某种方法来自动提高我在项目中可以使用的质量?

My Canvas Render

不确定这是否有帮助,但我在脚本开头使用此代码:

var c = document.getElementsByClassName("canvas");

  for (i = 0; i < c.length; i++) {
    var canvas = c[i];
    var ctx = canvas.getContext("2d");
    ctx.clearRect(0,0, canvas.width, canvas.height);
    ctx.lineWidth=1;
  }

}

提前致谢

我的曲线代码示例:

var data = {
diameter: 250,
slant: 20,
height: 290
};

for (i = 0; i < c.length; i++) {
  var canvas = c[i];
  var ctx = canvas.getContext("2d");
  ctx.beginPath();
    ctx.moveTo( 150 + ((data.diameter / 2) + data.slant ), (data.height - 3) );
    ctx.quadraticCurveTo( 150 , (data.height - 15), 150 - ((data.diameter / 2) + data.slant ), (data.height - 3));
    ctx.lineTo( 150 - ((data.diameter / 2) + data.slant ), data.height );
    ctx.quadraticCurveTo( 150 , (data.height + 5), 150 + ((data.diameter / 2) + data.slant ), data.height);
  ctx.closePath();
  ctx.stroke();
}

6 个答案:

答案 0 :(得分:28)

问题

这是其中一种情况,如果不手动调整它几乎不可能获得平稳的结果。

原因与分配平滑像素的最小空间有关。在这种情况下,我们在二次曲线中的每个部分之间只有一个像素高度。

如果我们查看没有平滑的曲线,我们可以更清楚地看到这个限制(没有平滑每个像素位于整数位置):

no smoothing

红线表示单个部分,我们可以看到上一部分和下一部分之间的过渡必须分布在一个像素的高度上。有关其工作原理,请参阅my answer here

平滑基于点的坐标转换为整数的剩余分数。由于平滑然后使用此分数来确定基于笔画主色和背景色的颜色和alpha 以添加阴影像素,我们将很快遇到限制,因为用于平滑的每个像素占据整个像素本身由于这里空间不足,阴影将非常粗糙,因而显露出来。

当长线从y到y +/- 1(或x到x +/- 1)时,端点之间没有单个像素会落在完美边界上,这意味着它们之间的每个像素都是阴影

如果我们仔细观察当前行中的几个片段,我们可以更清楚地看到阴影以及它如何影响结果:

closeup

另外

虽然这解释了一般的原则 - 其他问题(正如我几天前在这个答案的revision 1(最后一段)中几乎暗示的那样,但是删除并忘记了深入研究它)是一般来说,绘制在彼此之上的线条将有助于对比度,因为alpha像素将会混合,并且在某些部分会引入更高的对比度。

您必须检查代码以删除不需要的笔划,以便在每个位置获得一个笔划。例如,你有一些closePaths()会将路径的末端连接到开头并绘制双线等等。

这两者的组合应该在平滑和锐利之间取得很好的平衡。

平滑试验台

此演示允许您根据可用空间查看平滑分布的效果。

曲线弯曲越多,每个部分变得越短,并且需要更少的平滑。结果:线条更平滑。

&#13;
&#13;
var ctx = c.getContext("2d");
ctx.imageSmoothingEnabled = 
  ctx.mozImageSmoothingEnabled = ctx.webkitImageSmoothingEnabled = false; // for zoom!

function render() {
  ctx.clearRect(0, 0, c.width, c.height);
  !!t.checked ? ctx.setTransform(1,0,0,1,0.5,0.5):ctx.setTransform(1,0,0,1,0,0);
  ctx.beginPath();
  ctx.moveTo(0,1);
  ctx.quadraticCurveTo(150, +v.value, 300, 1);
  ctx.lineWidth = +lw.value;
  ctx.strokeStyle = "hsl(0,0%," + l.value + "%)";
  ctx.stroke();
  vv.innerHTML = v.value;
  lv.innerHTML = l.value;
  lwv.innerHTML = lw.value;
  ctx.drawImage(c, 0, 0, 300, 300, 304, 0, 1200, 1200);  // zoom
}

render();
v.oninput=v.onchange=l.oninput=l.onchange=t.onchange=lw.oninput=render;
&#13;
html, body {margin:0;font:12px sans-serif}; #c {margin-top:5px}
&#13;
<label>Bend: <input id=v type=range min=1 max=290 value=1></label>
<span id=vv></span><br>
<label>Lightness: <input id=l type=range min=0 max=60 value=0></label>
<span id=lv></span><br>
<label>Translate 1/2 pixel: <input id=t type=checkbox></label><br>
<label>Line width: <input id=lw type=range min=0.25 max=2 step=0.25 value=1></label>
<span id=lwv></span><br>
<canvas id=c width=580></canvas>
&#13;
&#13;
&#13;

解决方案

除非可以增加决议,否则没有好的解决办法。因此,我们不得不调整颜色和几何图形以获得更平滑的结果。

我们可以使用一些技巧来解决问题:

  1. 我们可以将线宽减小到0.5 - 0.75,这样我们就可以得到用于着色的不太明显的颜色渐变。
  2. 我们可以调暗颜色以降低对比度
  3. 我们可以翻译半像素。这在某些情况下有效,有些则不行。
  4. 如果锐度不是必需的,增加线宽可能有助于平衡阴影。示例值可以是1.5与浅色/灰色相结合。
  5. 我们也可以使用阴影,但这是一种性能匮乏的方法,因为它使用more memory as well as Gaussian blur, together with two extra composition steps并且相对较慢。我建议改用4)。
  6. 1)和2)在某种程度上相关,因为使用线宽<1。 1将强制整行上的子像素化,这意味着没有像素是纯黑色。这两种技术的目标是降低对比度以伪装阴影渐变,从而产生更清晰/更细的线条的幻觉。

    请注意,3)只会改善像素,因为它会落在精确的像素范围内。所有其他案件仍然模糊不清。在这种情况下,这个技巧对曲线几乎没有影响,但适用于矩形和垂直和水平线。

    如果我们使用上面的测试平台应用这些技巧,我们将获得一些可用的值:

    <强> Variation 1

    ctx.lineWidth = 1.25;              // gives some space for lightness
    ctx.strokeStyle = "hsl(0,0%,50%)"; // reduces contrast
    ctx.setTransform(1,0,0,1,0.5,0.5); // not so useful for the curve itself
    

    result2

    <强> Variation 2

    ctx.lineWidth = 0.5;               // sub-pixels all points
    ctx.setTransform(1,0,0,1,0.5,0.5); // not so useful for the curve itself
    

    result1

    我们可以通过试验线宽和笔触颜色/亮度来进一步微调。

    另一种是使用Photoshop或具有更好平滑算法的类似曲线为曲线生成更准确的结果,并将其用作图像而不是使用原生曲线。

答案 1 :(得分:13)

这个问题让我觉得有点奇怪。与高端渲染器相比,Canvas渲染虽然不是最好的但仍然非常好。那么为什么这个例子存在这样的问题呢。我准备离开它,但500点值得再看。从那以后,我可以给出两点建议,解决方案和替代方案。

首先,设计师及其设计必须包含媒体的限制。它可能听起来有些冒昧但你试图减少不可减少的,你无法摆脱位图显示的别名。

其次,总是写好评论的代码。这里有4个答案,没有人挑选出这个缺陷。这是因为所呈现的代码相当混乱且难以理解。我猜(几乎像我一样)其他人完全跳过你的代码,而不是弄清楚它做错了什么。

请注意

此问题的所有答案中的图像质量可能会被浏览器缩放(因此重新采样)。要进行真正的比较,最好在单独的页面上查看图像,以便不缩放它们。

按质量顺序研究问题的结果(在我看来)

遗传算法

我发现提高质量的最佳方法,一种通常与计算机图形无关的方法,是使用一种非常简单的遗传算法(GA)形式,通过对渲染过程进行细微的更改来搜索最佳解决方案

亚像素定位,线宽,滤镜选择,合成,重新采样和颜色更改可以对最终结果进行显着更改。目前数十亿种可能的组合,其中任何一种都可能是最好的。 GA非常适合寻找这些类型搜索的解决方案,尽管在这种情况下,适应性测试存在问题,因为质量是主观的,健康测试也必须是,因此需要人为输入。

经过多次迭代并占用了比我想要的更多的时间,我发现了一种非常适合这种类型图像的方法(许多细密的间距线)GA不适合公开发布。幸运的是,我们只对最适合的解决方案感兴趣,并且GA创建了一系列可重复且一致的步骤,以满足其运行的特定样式。

GA搜索的结果是。

Best result

有关处理步骤,请参阅注释1

结果远比我预期的要好得多,所以我必须有一个闭合的外观,并注意到2个不同的功能,使这个图像与这个和其他答案中呈现的所有其他图像区别开来。

  • 抗锯齿不均匀。在通常期望强度均匀变化的地方,这种方法会产生阶梯式渐变(为什么这会让它看起来更好我不知道)
  • 黑暗节点。就在从一行到下一行的过渡几乎完成的地方,对于几个像素,下方或上方的线显着变暗,然后当线适合该行时,恢复到较浅的色调。这种接缝可以补偿整条生产线的轻量化,因为它在两排之间分享了它的意图。

enter image description here

这给了我一些思考的东西,我将看看这些特征是否可以直接合并到线条和曲线扫描线渲染中。

注1 用于渲染上述图像的方法。屏幕外屏幕尺寸为1200 x 1200.按比例缩放4 ctx.setTransform(4,0,0,4,0,0),像素y偏移3 ctx.translate(0,3),线宽0.9像素在白色背景上渲染两次,1像素光子计数模糊重复3次(类似于卷积) 3 * 3高斯模糊,但沿对角线的重量稍微减少),使用光子计数装置通过2步下采样向下采样4次(每个像素通道是4(2乘2)采样的平方均值的平方根像素)。锐化一遍(遗憾的是,这是一个非常复杂的自定义锐化滤镜(如某些引脚/像素锐化滤镜))然后用ctx.globalCompositeOperation = "multiply"ctx.globalAlpha = 0.433分层一次,然后在画布上捕捉。所有处理都在Firefox上完成

代码修复

可怕的渲染结果实际上是由代码中的一些轻微渲染不一致引起的。

以下是代码修复之前和之后。正如您所看到的那样,有明显的改善。

Befor and after code fix.

那你做错了什么?

不是很多,问题是你在现有线的顶部渲染线。这具有增加这些线条的对比度的效果,渲染器不知道您不想要现有的颜色,因此增加了现有的抗锯齿,使不透明度加倍并破坏效果。

仅使用形状渲染来代码化您的代码。评论显示了这些变化。

ctx.beginPath();
// removed the top and bottom lines of the rectangle
ctx.moveTo(150, 0);
ctx.lineTo(150, 75);
ctx.moveTo(153, 0);
ctx.lineTo(153, 75);
// dont need close path
ctx.stroke();
ctx.beginPath();
ctx.moveTo((150 - (data.diameter / 2)), 80);
ctx.quadraticCurveTo(150, 70, 150 + (data.diameter / 2), 80);
ctx.lineTo(150 + (data.diameter / 2), 83);
ctx.quadraticCurveTo(150, 73, 150 - (data.diameter / 2), 83);
ctx.closePath();
ctx.stroke();
ctx.beginPath();
// removed the two quadratic curves that where drawing over the top of existing ones
ctx.moveTo(150 + (data.diameter / 2), 83);
ctx.lineTo(150 + ((data.diameter / 2) + data.slant), data.height);
ctx.moveTo(150 - ((data.diameter / 2) + data.slant), data.height);
ctx.lineTo(150 - (data.diameter / 2), 83);
// dont need close path
ctx.stroke();

ctx.beginPath();
// removed a curve
ctx.moveTo(150 + ((data.diameter / 2) + data.slant), (data.height - 3));
ctx.quadraticCurveTo(150, (data.height - 15), 150 - ((data.diameter / 2) + data.slant), (data.height - 3));
// dont need close path
ctx.stroke();

ctx.beginPath();
ctx.moveTo(150 + ((data.diameter / 2) + data.slant), data.height);
ctx.quadraticCurveTo(150, (data.height - 10), 150 - ((data.diameter / 2) + data.slant), data.height);
ctx.quadraticCurveTo(150, (data.height + 5), 150 + ((data.diameter / 2) + data.slant), data.height);
ctx.closePath();
ctx.stroke();

所以现在渲染效果要好得多。

主观眼

我认为代码修复是可以通过最少的努力实现的最佳解决方案。由于质量是主观的,我提出了几种方法,可能会或可能不会提高质量,取决于法官的眼睛。

向下采样

提高渲染质量的另一个原因是降低采样质量。这包括简单地以更高的分辨率渲染图像,然后以较低的分辨率重新渲染图像。然后每个像素的平均值为原始像素的2个或更多。

有许多下采样方法,但由于处理图像所花费的时间,许多方法没有任何实际用途。

最快的下采样可以通过GPU和本机画布渲染调用完成。只需以2倍或4倍的分辨率创建一个离屏画布,然后使用变换来缩放渲染的图像(这样您就不需要更改渲染代码)。然后,以所需的分辨率渲染该图像。

使用2D API和JS进行下采样的示例

var canvas = document.getElementById("myCanvas"); // get onscreen canvas
// set up the up scales offscreen canvas
var display = {};
display.width = canvas.width;
display.height = canvas.height;
var downSampleSize = 2;
var canvasUp = document.createElement("canvas");
canvasUp.width = display.width * downSampleSize;
canvasUp.height = display.height * downSampleSize;
var ctx = canvasUp.getContext("2D");
ctx.setTransform(downSampleSize,0,0,downSampleSize,0,0);

// call the render function and render to the offscreen canvas

Once you have the image just render it to you onscreen canvas

ctx = canvas.getContext("2d");
ctx.drawImage(canvasUp,0,0,canvas.width,canvas.height);

以下图像显示了4 *向下采样的结果,并将线宽从1.2像素降低到0.9像素(请注意,上采样线宽为4 *,即4.8,4.4,4和3.6)

enter image description here enter image description here

下一张图片4 *使用Lanczos resampling样本进行快速重采样(更适合图片)

enter image description here

下采样很快,只需要对原始代码进行很少修改即可。生成的图像将改善精细细节的外观并创建稍微更好的抗锯齿线。

向下采样还允许更精细地控制(表观)线宽。在显示分辨率下渲染在线宽的1/4像素变化下得到的结果很差。使用下采样,您可以将1/8和1/16精细细节加倍并翻两倍(请记住,在以亚像​​素分辨率渲染时会出现其他类型的混叠效果)

动态范围

数字媒体中的动态范围是指媒体可以处理的值范围。对于每个颜色通道范围为256(8位)的画布。人眼很难选择并发值(例如128和129)之间的差异,因此这个范围在计算机图形领域几乎无处不在。现代GPU虽然可以渲染更高的动态范围16bit,24bit,每通道32bit甚至双精度浮动64bit。

适当的8位范围适用于95%的情况,但是当渲染的图像被强制进入较低的动态范围时会受到影响。当您在接近线条颜色的颜色顶部渲染一条线时会发生这种情况。在问题中,图像不是在非常明亮的背景上渲染(例如#888),结果是抗锯齿仅具有将动态范围减半的7位范围。如果将图像渲染到通过改变alpha通道来实现抗锯齿的透明背景上,则导致引入第二级伪像,从而使问题更加复杂。

当您记住动态范围时,您可以设计图像以获得最佳结果(在设计约束内)。渲染和背景已知时,不要渲染到透明画布上,让硬件合成最终的屏幕输出,将背景渲染到画布上,然后渲染设计。尽量保持动态范围尽可能大,颜色差异越大,抗锯齿算法处理中间色的效果越好。

下面是渲染各种背景强度的示例,它们使用预渲染背景上的2 *向下采样进行渲染。 BG表示背景强度。

Comparing background intensity on the rendering result. 请注意,此图片非常适合页面,并且会被浏览器下采样,从而添加额外的工件。

TRUE TYPE,如

虽然这里有另一种方法。如果你认为屏幕由像素组成,每个像素有3个部分红色,绿色,蓝色,我们将它们分组,总是从红色开始。

但是像素开始的位置并不重要,重要的是像素有3种颜色rgb,它可以是gbr或brg。当您以这样的方式查看屏幕时,您可以有效地获得相对于像素边缘的水平分辨率的3倍。像素大小仍然相同但是偏移。这就是微软如何进行特殊字体渲染(真实类型)不幸的是,微软在使用这种方法方面有很多专利,所以我所能做的就是向你展示当你渲染忽略像素边界时的样子。

效果在水平分辨率中最为明显,并且不会提高垂直度(请注意这是我自己的画布渲染实现,并且它仍然在进行细化)此方法也不适用于透明图像

true type example

答案 2 :(得分:4)

像素矩阵上的斜线是你应该理解的。如果您需要绘制单个像素宽度的斜线,则无法阻止其上出现锯齿状边缘,因为倾斜是通过水平线的渐进垂直图案实现的。

解决方案是在线条周围产生一些模糊效果,使线条连接更平滑。

您需要使用画布上下文的shadowColorshadowBlurlineCaplineJoin属性来实现此目的。

进行以下设置并尝试绘制线条。

 for (i = 0; i < c.length; i++) {
    var canvas = c[i];
    var ctx = canvas.getContext("2d");

    ctx.shadowColor = "rgba(0,0,0,1)";
    ctx.shadowBlur = 2;
    ctx.lineCap = 'round';
    ctx.lineJoin = 'round';
    ctx.lineWidth = 1;
    ctx.strokeStyle = 'black';

    ctx.clearRect(0,0, canvas.width, canvas.height);

  }

结果如下

result

尝试使用shadowColor不透明度和模糊大小以及线宽和颜色进行播放。你可以获得非常惊人的结果。

在旁注中,您的项目比Canvas听起来更像SVG。可能你应该考虑转向SVG以获得更好的绘图支持和性能。

<强>更新

这是一个微调

ctx.shadowColor = "rgba(128,128,128,.2)";
ctx.shadowBlur = 1;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.lineWidth = 1;
ctx.strokeStyle = 'gray';

enter image description here

答案 3 :(得分:0)

对不起,我参加聚会很晚,但是这里的所有答案都使事情变得太复杂了。

实际上 您所见的是缺少伽玛校正。在此处查看Antialias 1&2示例:http://bourt.com/2014/(您首先需要为显示器校准伽玛值),以及以下简短说明:https://medium.com/@alexbourt/use-gamma-everywhere-da027d9dc82f

将向量绘制为好像在线性颜色空间中,而像素存在于经过伽马校正的空间中。就这么简单。不幸的是,Canvas不支持伽玛,所以您有点卡住。

有一种 可以解决此问题,但是您必须先绘制内容,然后直接访问像素并自己对伽玛值进行校正,就像我在那些示例中所做的那样。自然,使用简单的图形最容易做到这一点。对于更复杂的事情,您需要自己的渲染管道,其中要考虑伽玛。

(由于这个论点总是会出现,所以我现在要解决:最好还是在伽玛方面犯错。如果你说“好吧,我不知道用户监视器的伽玛值将是“”,并将其保持为1.0,几乎在所有情况下结果都将是错误的。但是,如果您进行有根据的猜测,例如1.8,那么对于很大一部分用户,您将猜到接近为他们的显示器正确。)

答案 4 :(得分:-1)

blury线的一个原因是在像素之间绘制。这个答案很好地概述了画布坐标系:

https://stackoverflow.com/a/3657831/4602079

保持整数坐标但仍能获得清晰线条的一种方法是将上下文转换为0.5像素:

context.translate(0.5,0.5);

看看下面的代码段。第二个画布由(0.5,0.5)平移,使得用整数坐标绘制的线看起来很清晰。

那应该让你的直线得到修复。曲线,对角线等将消除锯齿(笔划周围的灰色像素)。你无能为力。分辨率越高,它们越不可见,除了直线之外的所有线条看起来都更好地抗锯齿。

&#13;
&#13;
function draw(ctx){
    ctx.beginPath();
    ctx.moveTo(25, 30);
    ctx.lineTo(75, 30);
    ctx.stroke();

    ctx.beginPath();
    ctx.moveTo(25, 50.5);
    ctx.lineTo(75, 50.5);
    ctx.stroke();
}

draw(document.getElementById("c1").getContext("2d"))

var ctx = document.getElementById("c2").getContext("2d");
ctx.translate(0.5, 0.5);
draw(ctx);
&#13;
    <canvas id="c1" width="100" height="100" style="width: 100px; height: 100px"></canvas>
    <canvas id="c2" width="100" height="100" style="width: 100px; height: 100px"></canvas>
&#13;
&#13;
&#13;

答案 5 :(得分:-1)

抗锯齿有很大帮助。但是当你有一条接近水平或垂直或有轻微弯曲的弯角线时,抗锯齿将会更加引人注目。特别是对于宽度小于几个像素左右的细线。

正如Maciej指出的那样,如果你的线宽约为1px并直接在两个像素之间传递,那么抗锯齿会产生一条宽两个像素,半灰色的线。

你可能必须学会忍受它。反锯齿只能做到这么多。