【问题标题】:Gradient Stroke Along Curve in Canvas画布中沿曲线的渐变描边
【发布时间】:2018-12-15 03:17:16
【问题描述】:

我正在尝试在画布中以线性渐变描边样式沿 曲线绘制曲线,如this image。在该页面上有一个链接的 svg 文件,它提供了有关如何在 svg 中实现效果的说明。也许在画布中可以使用类似的方法?

【问题讨论】:

    标签: html canvas gradient curve bezier


    【解决方案1】:

    一个演示:http://jsfiddle.net/m1erickson/4fX5D/

    创建一个沿路径变化的渐变是相当容易的:

    创建一个沿路径变化的渐变更加困难:

    在路径上创建渐变,您可以绘制许多与路径相切的渐变线:

    如果您绘制了足够多的切线,那么眼睛会将曲线视为路径上的渐变。

    注意:锯齿可能出现在路径梯度的外部。那是因为渐变实际上是由数百条切线组成的。但是您可以通过使用适当的颜色在渐变的任一侧绘制一条线来平滑锯齿(这里的反锯齿线在顶部为红色,在底部为紫色)。

    以下是创建路径上的渐变的步骤:

    • 沿路径绘制数百个点。

    • 计算这些点的路径角度。

    • 在每个点上,创建一个线性渐变并在该点的切线上绘制一条渐变描边线。是的,您必须为每个点创建一个新的渐变,因为线性渐变必须与与该点相切的线的角度相匹配。

    • 为减少因绘制多条单独的线而导致的锯齿效果,您可以沿着渐变路径的顶部和底部绘制一条平滑路径以覆盖锯齿。

    这里是注释代码:

    <!doctype html>
    <html>
    <head>
    <link rel="stylesheet" type="text/css" media="all" href="css/reset.css" /> <!-- reset css -->
    <script type="text/javascript" src="http://code.jquery.com/jquery.min.js"></script>
    <style>
        body{ background-color: ivory; }
        #canvas{border:1px solid red;}
    </style>       
    <script>
    $(function(){
    
        // canvas related variables
        var canvas=document.getElementById("canvas");
        var ctx=canvas.getContext("2d");
    
        // variables defining a cubic bezier curve
        var PI2=Math.PI*2;
        var s={x:20,y:30};
        var c1={x:200,y:40};
        var c2={x:40,y:200};
        var e={x:270,y:220};
    
        // an array of points plotted along the bezier curve
        var points=[];
    
        // we use PI often so put it in a variable
        var PI=Math.PI;
    
        // plot 400 points along the curve
        // and also calculate the angle of the curve at that point
        for(var t=0;t<=100;t+=0.25){
    
            var T=t/100;
    
            // plot a point on the curve
            var pos=getCubicBezierXYatT(s,c1,c2,e,T);
    
            // calculate the tangent angle of the curve at that point
            var tx = bezierTangent(s.x,c1.x,c2.x,e.x,T);
            var ty = bezierTangent(s.y,c1.y,c2.y,e.y,T);
            var a = Math.atan2(ty, tx)-PI/2;
    
            // save the x/y position of the point and the tangent angle
            // in the points array
            points.push({
                x:pos.x,
                y:pos.y,
                angle:a
            });
    
        }
    
    
        // Note: increase the lineWidth if 
        // the gradient has noticable gaps 
        ctx.lineWidth=2;
    
        // draw a gradient-stroked line tangent to each point on the curve
        for(var i=0;i<points.length;i++){
    
            // calc the topside and bottomside points of the tangent line
            var offX1=points[i].x+20*Math.cos(points[i].angle);
            var offY1=points[i].y+20*Math.sin(points[i].angle);
            var offX2=points[i].x+20*Math.cos(points[i].angle-PI);
            var offY2=points[i].y+20*Math.sin(points[i].angle-PI);
    
            // create a gradient stretching between 
            // the calculated top & bottom points
            var gradient=ctx.createLinearGradient(offX1,offY1,offX2,offY2);
            gradient.addColorStop(0.00, 'red'); 
            gradient.addColorStop(1/6, 'orange'); 
            gradient.addColorStop(2/6, 'yellow'); 
            gradient.addColorStop(3/6, 'green') 
            gradient.addColorStop(4/6, 'aqua'); 
            gradient.addColorStop(5/6, 'blue'); 
            gradient.addColorStop(1.00, 'purple'); 
    
            // draw the gradient-stroked line at this point
            ctx.strokeStyle=gradient;
            ctx.beginPath();
            ctx.moveTo(offX1,offY1);
            ctx.lineTo(offX2,offY2);
            ctx.stroke();
        }
    
    
        // draw a top stroke to cover jaggies
        // on the top of the gradient curve
        var offX1=points[0].x+20*Math.cos(points[0].angle);
        var offY1=points[0].y+20*Math.sin(points[0].angle);
        ctx.strokeStyle="red";
        // Note: increase the lineWidth if this outside of the
        //       gradient still has jaggies
        ctx.lineWidth=1.5;
        ctx.beginPath();
        ctx.moveTo(offX1,offY1);
        for(var i=1;i<points.length;i++){
            var offX1=points[i].x+20*Math.cos(points[i].angle);
            var offY1=points[i].y+20*Math.sin(points[i].angle);
            ctx.lineTo(offX1,offY1);
        }
        ctx.stroke();
    
    
        // draw a bottom stroke to cover jaggies
        // on the bottom of the gradient
        var offX2=points[0].x+20*Math.cos(points[0].angle+PI);
        var offY2=points[0].y+20*Math.sin(points[0].angle+PI);
        ctx.strokeStyle="purple";
        // Note: increase the lineWidth if this outside of the
        //       gradient still has jaggies
        ctx.lineWidth=1.5;
        ctx.beginPath();
        ctx.moveTo(offX2,offY2);
        for(var i=0;i<points.length;i++){
            var offX2=points[i].x+20*Math.cos(points[i].angle+PI);
            var offY2=points[i].y+20*Math.sin(points[i].angle+PI);
            ctx.lineTo(offX2,offY2);
        }
        ctx.stroke();
    
    
        //////////////////////////////////////////
        // helper functions
        //////////////////////////////////////////
    
        // calculate one XY point along Cubic Bezier at interval T
        // (where T==0.00 at the start of the curve and T==1.00 at the end)
        function getCubicBezierXYatT(startPt,controlPt1,controlPt2,endPt,T){
            var x=CubicN(T,startPt.x,controlPt1.x,controlPt2.x,endPt.x);
            var y=CubicN(T,startPt.y,controlPt1.y,controlPt2.y,endPt.y);
            return({x:x,y:y});
        }
    
        // cubic helper formula at T distance
        function CubicN(T, a,b,c,d) {
            var t2 = T * T;
            var t3 = t2 * T;
            return a + (-a * 3 + T * (3 * a - a * T)) * T
            + (3 * b + T * (-6 * b + b * 3 * T)) * T
            + (c * 3 - c * 3 * T) * t2
            + d * t3;
        }
    
        // calculate the tangent angle at interval T on the curve
        function bezierTangent(a, b, c, d, t) {
            return (3 * t * t * (-a + 3 * b - 3 * c + d) + 6 * t * (a - 2 * b + c) + 3 * (-a + b));
        };
    
    }); // end $(function(){});
    </script>
    </head>
    <body>
        <canvas id="canvas" width=300 height=300></canvas>
    </body>
    </html>
    

    【讨论】:

    • 如何创建许多不同颜色的“平行”曲线,宽度为 1px?
    • @ErikAllik 创建三次贝塞尔曲线的偏移曲线更加困难。它将涉及将三次曲线划分为二次曲线。
    • @markE 这是一个很好的解决方案,但更改任何东西肯定很复杂:) 如果你只有两个点而不是(贝塞尔曲线)曲线,我可以问你代码会是什么样子吗?我将如何沿着这两点计算这些切线?提前致谢!
    • @markE 您能否解释一下如何沿路径创建渐变(最上面的示例)?
    【解决方案2】:

    我正在做一些非常相似的事情,我只是想添加一些东西。 markE 的回答很好,但他称之为曲线的切线,实际上是法线或垂直于曲线的线。 (切线平行,法线垂直)

    对于我的特定应用程序,我在一条具有透明度的线上使用渐变。在这种情况下,接近像素完美的渐变区域很重要,因为重叠的透明度将被绘制两次,从而改变所需的颜色。因此,我没有画一堆垂直于曲线的线,而是将曲线分成四边形,并对每个四边形应用线性渐变。此外,使用这些四边形区域可以减少您必须进行的绘制调用次数,从而提高效率。你不需要大量的区域来获得相当平滑的效果,而且你使用的区域越少,渲染的速度就越快。

    我改编了 markE 的代码,因此感谢他的出色回答。这是小提琴:https://jsfiddle.net/hvyt58dz/

    这是我使用的改编代码:

    // canvas related variables
    var canvas = document.getElementById("canvas");
    var ctx = canvas.getContext("2d");
    
    // variables defining a cubic bezier curve
    var PI2 = Math.PI * 2;
    var s = {
        x: 20,
        y: 30
    };
    var c1 = {
        x: 200,
        y: 40
    };
    var c2 = {
        x: 40,
        y: 200
    };
    var e = {
        x: 270,
        y: 220
    };
    
    // an array of points plotted along the bezier curve
    var points = [];
    
    // we use PI often so put it in a variable
    var PI = Math.PI;
    
    // plot 400 points along the curve
    // and also calculate the angle of the curve at that point
    var step_size = 100/18;
    for (var t = 0; t <= 100 + 0.1; t += step_size) {
    
        var T = t / 100;
    
    
        // plot a point on the curve
        var pos = getCubicBezierXYatT(s, c1, c2, e, T);
    
        // calculate the tangent angle of the curve at that point
        var tx = bezierTangent(s.x, c1.x, c2.x, e.x, T);
        var ty = bezierTangent(s.y, c1.y, c2.y, e.y, T);
        var a = Math.atan2(ty, tx) - PI / 2;
    
        // save the x/y position of the point and the tangent angle
        // in the points array
        points.push({
            x: pos.x,
            y: pos.y,
            angle: a
        });
    
    }
    
    
    // Note: increase the lineWidth if 
    // the gradient has noticable gaps 
    ctx.lineWidth = 2;
    var overlap = 0.2;
    var outside_color = 'rgba(255,0,0,0.0)';
    var inside_color = 'rgba(255,0,0,0.7)';
    
    // draw a gradient-stroked line tangent to each point on the curve
    var line_width = 40;
    var half_width = line_width/2;
    for (var i = 0; i < points.length - 1; i++) {
    
        var x1 = points[i].x, y1 = points[i].y;
        var x2 = points[i+1].x, y2 = points[i+1].y;
        var angle1 = points[i].angle, angle2 = points[i+1].angle;
        var midangle = (angle1 + angle2)/ 2;
        // calc the topside and bottomside points of the tangent line
        var gradientOffsetX1 = x1 + half_width * Math.cos(midangle);
        var gradientOffsetY1 = y1 + half_width * Math.sin(midangle);
        var gradientOffsetX2 = x1 + half_width * Math.cos(midangle - PI);
        var gradientOffsetY2 = y1 + half_width * Math.sin(midangle - PI); 
        var offX1 = x1 + half_width * Math.cos(angle1);
        var offY1 = y1 + half_width * Math.sin(angle1);
        var offX2 = x1 + half_width * Math.cos(angle1 - PI);
        var offY2 = y1 + half_width * Math.sin(angle1 - PI);
    
        var offX3 = x2 + half_width * Math.cos(angle2)
                       - overlap * Math.cos(angle2-PI/2);
        var offY3 = y2 + half_width * Math.sin(angle2)
                       - overlap * Math.sin(angle2-PI/2);
        var offX4 = x2 + half_width * Math.cos(angle2 - PI)
                       + overlap * Math.cos(angle2-3*PI/2);
        var offY4 = y2 + half_width * Math.sin(angle2 - PI)
                       + overlap * Math.sin(angle2-3*PI/2);
    
        // create a gradient stretching between 
        // the calculated top & bottom points
        var gradient = ctx.createLinearGradient(gradientOffsetX1, gradientOffsetY1, gradientOffsetX2, gradientOffsetY2);
        gradient.addColorStop(0.0, outside_color);
        gradient.addColorStop(0.25, inside_color);
        gradient.addColorStop(0.75, inside_color);
        gradient.addColorStop(1.0, outside_color);
        //gradient.addColorStop(1 / 6, 'orange');
        //gradient.addColorStop(2 / 6, 'yellow');
        //gradient.addColorStop(3 / 6, 'green')
        //gradient.addColorStop(4 / 6, 'aqua');
        //gradient.addColorStop(5 / 6, 'blue');
        //gradient.addColorStop(1.00, 'purple');
    
        // line cap
        if(i == 0){
            var x = x1 - overlap * Math.cos(angle1-PI/2);
            var y = y1 - overlap * Math.sin(angle1-PI/2);
            var cap_gradient = ctx.createRadialGradient(x, y, 0, x, y, half_width);
            ctx.beginPath();
            ctx.arc(x, y, half_width, angle1 - PI, angle1);
            cap_gradient.addColorStop(0.5, inside_color);
            cap_gradient.addColorStop(1.0, outside_color);
            ctx.fillStyle = cap_gradient;
            ctx.fill();
        }
        if(i == points.length - 2){
            var x = x2 + overlap * Math.cos(angle2-PI/2);
            var y = y2 + overlap * Math.sin(angle2-PI/2);
            var cap_gradient = ctx.createRadialGradient(x, y, 0, x, y, half_width);
            ctx.beginPath();
            ctx.arc(x, y, half_width, angle2, angle2 + PI);
            cap_gradient.addColorStop(0.5, inside_color);
            cap_gradient.addColorStop(1.0, outside_color);
            ctx.fillStyle = cap_gradient;
            ctx.fill();
            console.log(x,y);
        }
        // draw the gradient-stroked line at this point
        ctx.fillStyle = gradient;
        ctx.beginPath();
        ctx.moveTo(offX1, offY1);
        ctx.lineTo(offX2, offY2);
        ctx.lineTo(offX4, offY4);
        ctx.lineTo(offX3, offY3);
        ctx.fill();
    }
    
    //////////////////////////////////////////
    // helper functions
    //////////////////////////////////////////
    
    // calculate one XY point along Cubic Bezier at interval T
    // (where T==0.00 at the start of the curve and T==1.00 at the end)
    function getCubicBezierXYatT(startPt, controlPt1, controlPt2, endPt, T) {
        var x = CubicN(T, startPt.x, controlPt1.x, controlPt2.x, endPt.x);
        var y = CubicN(T, startPt.y, controlPt1.y, controlPt2.y, endPt.y);
        return ({
            x: x,
            y: y
        });
    }
    
    // cubic helper formula at T distance
    function CubicN(T, a, b, c, d) {
        var t2 = T * T;
        var t3 = t2 * T;
        return a + (-a * 3 + T * (3 * a - a * T)) * T + (3 * b + T * (-6 * b + b * 3 * T)) * T + (c * 3 - c * 3 * T) * t2 + d * t3;
    }
    
    // calculate the tangent angle at interval T on the curve
    function bezierTangent(a, b, c, d, t) {
        return (3 * t * t * (-a + 3 * b - 3 * c + d) + 6 * t * (a - 2 * b + c) + 3 * (-a + b));
    };
    

    【讨论】:

      猜你喜欢
      • 2011-04-29
      • 2019-01-26
      • 1970-01-01
      • 1970-01-01
      • 2012-11-06
      • 1970-01-01
      • 2016-01-04
      • 2014-04-29
      • 2021-09-18
      相关资源
      最近更新 更多