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

2022-01-17 00:00:00 javascript html html5-canvas

我一直在编写一个小的 javascript 插件,但在提高画布的整体渲染质量方面遇到了一些麻烦.我在网上到处搜索,但找不到任何有意义的东西.

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

了解其工作原理.

平滑基于点坐标转换为整数的剩余分数.由于平滑然后使用这个分数来确定 color 和 alpha 基于笔触主颜色和背景颜色来添加阴影像素,我们将很快遇到限制,因为用于平滑的每个像素本身都占用了整个像素而且由于这里没有空间,阴影会很粗糙,因此很容易暴露.

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

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

另外

虽然这总体上解释了原理 - 其他问题是(正如我在 :

ctx.lineWidth = 0.5;//亚像素所有点ctx.setTransform(1,0,0,1,0.5,0.5);//对曲线本身没那么有用

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

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

I have been writing a little javascript plugin, and i am having a little trouble with improving the canvas overall quality of the render. I have searched over the web here and there but can not find anything that makes sense.

The lines created from my curves are NOT smooth, if you look at the jsfiddle below you will understand what I mean. It kind of looks pixelated. Is there a way to improve the quality? Or is there a Canvas Framework that already uses some method to auto improve its quality that I can use in my project?

My Canvas Render

Not sure if this helps but i am using this code at the start of my script:

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

}

Thanks in advance

Example of my Curve Code:

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

解决方案

The Problem

This is one of those cases where it's almost impossible to get a smooth result without manually tweaking it.

The cause has to do with the minimal space to distribute smoothing pixels. In this case we only have a single pixel height between each section in the quadratic curve.

If we look at a curve with no smoothing we can more clearly see this limitation (without smoothing each pixel sits on an integer position):

The red line indicates a single section and we can see that the transition between the previous and next section has to be distributed over the height one pixel. See my answer here for how this works.

Smoothing is based on the remaining fraction for the point's coordinate conversion to integer. Since smoothing then uses this fraction to determine the color and alpha based on stroke main color and background color to add a shaded pixel, we will quickly run into limitations as each pixel used for smoothing occupies a whole pixel itself and due to the lack of space as here, the shades will be very rough and therefor revealing.

When a long line goes from y to y+/-1 (or x to x+/-1) there is not a single pixel between the end points that would land on a perfect bound which means every pixel between is instead a shade.

If we take a closer look at a couple of segments from the current line we can see the shades more clearly and how it affects the result :

Additionally

Though this explains the principle in general - Other problems are (as I barely hinted about in revision 1 (last paragraph) of this answer a few days ago, but removed and forgot about going deeper into it) is that lines drawn on top of each other in general, will contribute to contrast as the alpha pixels will blend and in some parts introduce higher contrast.

You will have to go over the code to remove unneeded strokes so you get a single stroke in each location. You have for instance some closePaths() that will connect end of path with the beginning and draw double lines and so forth.

A combination of these two should give a nice balance between smooth and sharp.

Smoothing test-bench

This demo allows you to see the effect for how smoothing is distributed based on available space.

The more bent the curve is, the shorter each section becomes and would require less smoothing. The result: smoother line.

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;

html, body {margin:0;font:12px sans-serif}; #c {margin-top:5px}

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

Solution

There is no good solution unless the resolution could have been increased. So we are stuck with tweaking the colors and geometry to give a more smooth result.

We can use a few of tricks to get around:

  1. We can reduce the line width to 0.5 - 0.75 so we get a less visible color gradient used for shading.
  2. We can dim the color to decrease the contrast
  3. We can translate half pixel. This will work in some cases, others not.
  4. If sharpness is not essential, increasing the line width instead may help balancing out shades. Example value could be 1.5 combined with a lighter color/gray.
  5. We could use shadow too but this is an performance hungry approach as it uses more memory as well as Gaussian blur, together with two extra composition steps and is relatively slow. I would recommend using 4) instead.

1) and 2) are somewhat related as using a line width < 1 will force sub-pixeling on the whole line which means no pixel is pure black. The goal of both techniques is to reduce the contrast to camouflage the shade gradients giving the illusion of being a sharper/thinner line.

Note that 3) will only improve pixels that as a result lands on a exact pixel bound. All other cases will still be blurry. In this case this trick will have little to no effect on the curve, but serves well for the rectangles and vertical and horizontal lines.

If we apply these tricks by using the test-bench above, we'll get some usable values:

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

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

We can fine-tune further by experimenting with line width and the stroke color/lightness.

An alternative is to produce a more accurate result for the curves using Photoshop or something similar which has better smoothing algorithms, and use that as image instead of using native curve.

相关文章