画布 - 有时使用lineTo()绘制的笔画会产生锯齿边缘。

4
我是一位有用的助手,可以为您进行文本翻译。以下是您需要翻译的内容:

我有一个简单的画布绘图应用程序。有时候lineTo()命令会产生少于预期坐标的线条,导致绘图有很多棱角:

enter image description here

我正在使用最新的火狐浏览器,是因为连接不好还是我的电脑太忙了?有什么解决方法吗? 以下是我的代码:JS FIDDLE
beginPath();
                moveTo(this.X, this.Y);
                lineTo(e.pageX , e.pageY );
                strokeStyle = "rgb(0,0,0)";
                ctx.lineWidth=3;
                stroke();

你可以尝试使用mousemove点(如上面截图中显示的顶点)来绘制曲线,而不是直线(不确定是否实用)。使用直线时,你要看浏览器重新渲染画布的速度(加上偶尔的浏览器或操作系统正在忙于其他事情的抽搐)。 - Matt Coughlin
是的,那是个好主意 - 你可以使用贝塞尔或卡特穆勒姆样条来平滑它,尽管有时会产生一些奇怪的效果。 - Pointy
是的,但是这样笔画就不会和我的鼠标移动类似了,它只是一种插值。而且要计算曲线会需要额外的时间。 - daisy
尝试在绘图时快速在Photoshop中擦动鼠标,看看会发生什么。 (提示:大致相同。) - Phrogz
2个回答

4

它正在尽可能快地响应。您的浏览器将尽其所能快速传递事件,但不能保证能够跟踪您移动鼠标。很多情况下取决于客户端机器的负载。

编辑 - 这里是一个修改过的演示版,展示了一些可以使它变得更好的方法。该版本保留了一个单独的“points”队列,每50毫秒绘制新的点。这样,“mousemove”处理程序只需记录事件中的点坐标,当鼠标快速移动时,绘图代码可以在一个画布更新中完成一堆点。但它仍然不完美。

 var canvas = document.getElementById('canvas');
        var ctx = canvas.getContext('2d');
        var width  = window.innerWidth;
        var height = window.innerHeight;
        canvas.height = height;
        canvas.width = width;
        canvas.addEventListener('mousedown', function(e) {
            this.down = true;
            points.setStart(e.pageX, e.pageY);
        }, 0);
        canvas.addEventListener('mouseup', function() {
            this.down = false;          
        }, 0);
        canvas.addEventListener('mousemove', function(e) {          
            if (this.down) {
                points.newPoint(e.pageX, e.pageY);
            }
        }, 0);

var points = function() {
    var queue = [], qi = 0;
    var ctx = canvas.getContext('2d');

    function clear() {
        queue = [];
        qi = 0;
    }

    function setStart(x, y) {
        clear();
        newPoint(x, y);
    }

    function newPoint(x, y) {
        queue.push([x, y]);
    }

    function tick() {

        var k = 20; // adjust to limit points drawn per cycle

        if (queue.length - qi > 1) {
            ctx.beginPath();
            if (qi === 0)
                ctx.moveTo(queue[0][0], queue[0][1]);
            else
                ctx.moveTo(queue[qi - 1][0], queue[qi - 1][1]);

            for (++qi; --k >= 0 && qi < queue.length; ++qi) {
                ctx.lineTo(queue[qi][0], queue[qi][1]);
            }

            ctx.strokeStyle = "rgb(0,0,0)";
            ctx.lineWidth = 3;
            ctx.stroke();
        }
    }

    setInterval(tick, 50); // adjust cycle time

    return {
        setStart: setStart,
        newPoint: newPoint
    };
}();

+1 创意使用点队列 + 定时绘制!在“k”油门上,您是否考虑绘制每个队列长度/k元素,而不仅仅是前k个元素。好答案! - markE
@markE 嗯,限制的想法是为了尝试避免阻塞鼠标响应,所以似乎有一个固定的最大绘图量会很好。我想也可以计算净线长度,但那需要更多的数学计算。 - Pointy

3

您可以使用基数样条来平滑类似这样的线条:

enter image description here

由于浏览器能够快速响应事件(mousemove),所以原因如@Pointy已经解释过了。有一个名为Pointer Lock API的API可能会在未来帮助解决这个问题,因为它更低级,但现在我们需要使用算法来平滑线条,以防止出现分段。
除了平滑外,还可以应用细节平滑、点减少、锥度和其他方法来改善结果。
但在这种特定情况下,您可以使用我制作的扩展到画布的以下函数。只需调用它即可:
ctx.curve(myPointArray, tension, segments);
ctx.stroke();

该数组包含您的x和y点,按如下顺序排序:[x1, y1, x2, y2, ... xn, yn]tension 的典型值为0.5。 segments(默认为16)是可选的。
张力越大,曲线看起来就越圆滑。分段是数组中每个点之间的分辨率。对于绘图应用程序,值为5可能效果良好(产生较少的结果点)。
要使其更好地工作,您可以在单独的画布上注册您的点,在那里绘制原始线条。在鼠标抬起时使用此函数处理该行,并将其绘制到主画布上,然后清除绘图画布。
此函数高度优化 - 它还返回处理后的点,因此您可以存储结果而不是每次重新处理。
/**
 *  curve() by Ken Fyrstenberg (c) 2013 Epistemex
 *  See Code Project for full source:
 *  http://www.codeproject.com/Tips/562175/Draw-Smooth-Lines-on-HTML5-Canvas
*/
CanvasRenderingContext2D.prototype.curve = function(pts, ts, nos) {

    nos = (typeof numOfSegments === 'undefined') ? 16 : nos;

    var _pts = [], res = [],        // clone array
        x, y,                       // our x,y coords
        t1x, t2x, t1y, t2y,         // tension vectors
        c1, c2, c3, c4,             // cardinal points
        st, st2, st3, st23, st32,   // steps
        t, i, l = pts.length,
        pt1, pt2, pt3, pt4;

    _pts.push(pts[0]);          //copy 1. point and insert at beginning
    _pts.push(pts[1]);

    _pts = _pts.concat(pts);

    _pts.push(pts[l - 2]);  //copy last point and append
    _pts.push(pts[l - 1]);

    this.moveTo(pts[0], pts[1])

    for (i = 2; i < l; i+=2) {

        pt1 = _pts[i];
        pt2 = _pts[i+1];
        pt3 = _pts[i+2];
        pt4 = _pts[i+3];

        // calc tension vectors
        t1x = (pt3 - _pts[i-2]) * ts;
        t2x = (_pts[i+4] - pt1) * ts;

        t1y = (pt4 - _pts[i-1]) * ts;
        t2y = (_pts[i+5] - pt2) * ts;

        for (t = 0; t <= nos; t++) {

            // pre-calc steps
            st = t / nos;
            st2 = st * st;
            st3 = st2 * st;
            st23 = st3 * 2;
            st32 = st2 * 3;

            // calc cardinals
            c1 = st23 - st32 + 1; 
            c2 = st32 - st23;
            c3 = st3 - 2 * st2 + st; 
            c4 = st3 - st2;

            res.push(c1 * pt1 + c2 * pt3 + c3 * t1x + c4 * t2x);
            res.push(c1 * pt2 + c2 * pt4 + c3 * t1y + c4 * t2y);

        } //for t
    } //for i

    l = res.length;
    for(i=0;i<l;i+=2) this.lineTo(res[i], res[i+1]);

    return res;

} //func ext

请参考此答案,了解基数样条的实现。


如果我想画一些有硬边缘的东西会发生什么? - daisy
通常情况下,这是通过计算膝值来处理的 - 在考虑“新”笔画之前容忍多少弯曲。膝值是前一条线和当前线(绘制当前线之前)之间的角度。如果超过阈值(例如50-60度),则会生成一个新的笔画,使您可以在不平滑所有内容的情况下进行急转弯。 - user1693593
@daisy,由于您还记录了笔画的点数,因此您还可以提供参数来调整中风后的这些阈值。 - user1693593

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接