Градиентный штрих вдоль кривой на холсте

Я пытаюсь нарисовать кривую на холсте с помощью стиля линейного градиента вдоль кривой, как в это изображение. На этой странице есть связанный файл svg, в котором даны инструкции о том, как добиться эффекта в svg. Может быть, подобный метод был бы возможен в холсте?


person user3705053    schedule 04.06.2014    source источник


Ответы (2)


Демонстрация: 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>
person markE    schedule 04.06.2014
comment
как насчет создания множества параллельных кривых разного цвета шириной 1 пиксель? - person Erik Kaplun; 28.09.2017
comment
@ErikAllik Создать кривую смещения кубической кривой Безье сложнее. Это будет включать в себя деление кубической кривой на квадратичные кривые. - person markE; 28.09.2017
comment
@markE, это отличное решение, но, конечно, сложно что-либо изменить :) Могу я спросить вас, как бы выглядел код, если бы у вас было только две точки вместо кривой (безье)? Как бы я рассчитал эти касательные вдоль этих двух точек? Заранее спасибо! - person Ben jamin; 21.05.2019
comment
@markE, не могли бы вы также объяснить, как вы создаете градиент вдоль пути (самый верхний пример)? - person wensveen; 16.04.2020

Я работаю над чем-то очень похожим, и я просто хотел добавить пару вещей. Ответ 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));
};
person derekmc    schedule 14.12.2018