Search code examples
htmlcanvasgradientcurvebezier

Gradient Stroke Along Curve in Canvas


I'm trying to draw a curve in canvas with a linear gradient stoke style along the curve, as in this image. On that page there is a linked svg file that gives instructions on how to accomplish the effect in svg. Maybe a similar method would be possible in canvas?


Solution

  • A Demo: http://jsfiddle.net/m1erickson/4fX5D/

    It's fairly easy to create a gradient that changes along the path:

    enter image description here

    It's more difficult to create a gradient that changes across the path:

    enter image description here

    To create a gradient across the path you draw many gradient lines tangent to the path:

    enter image description here

    If you draw enough tangent lines then the eye sees the curve as a gradient across the path.

    enter image description here

    Note: Jaggies can occur on the outsides of the path-gradient. That's because the gradient is really made up of hundreds of tangent lines. But you can smooth out the jaggies by drawing a line on either side of the gradient using the appropriate colors (here the anti-jaggy lines are red on the top side and purple on the bottom side).

    Here are the steps to creating a gradient across the path:

    • Plot hundreds of points along the path.

    • Calculate the angle of the path at those points.

    • At each point, create a linear gradient and draw a gradient stroked line across the tangent of that point. Yes, you will have to create a new gradient for each point because the linear gradient must match the angle of the line tangent to that point.

    • To reduce the jaggy effect caused by drawing many individual lines, you can draw a smooth path along the top and bottom side of the gradient path to overwrite the jaggies.

    Here is annotated code:

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