Подтвердить что ты не робот

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

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

4b9b3361

Ответ 1

Демо: http://jsfiddle.net/m1erickson/4fX5D/

Довольно легко создать градиент, который изменяет вдоль пути:

enter image description here

Сложнее создать градиент, который изменяет по пути:

enter image description here

Чтобы создать градиент по пути, вы рисуете много линий градиента, касательных к пути:

enter image description here

Если вы нарисовываете достаточные касательные линии, тогда глаз видит кривую как градиент по пути.

enter image description here

Примечание. Яйца могут встречаться на внешних сторонах градиента пути. Это потому, что градиент действительно состоит из сотен тангенциальных линий. Но вы можете сгладить зубцы, вычерчивая линию по обе стороны градиента, используя соответствующие цвета (здесь анти-дряблые линии красны с верхней стороны и фиолетовые на нижней стороне).

Ниже приведены шаги по созданию градиента по пути:

  • Выделите сотни точек вдоль пути.

  • Рассчитайте угол пути в этих точках.

  • В каждой точке создайте линейный градиент и нарисуйте градиентную поглаженную линию по касательной этой точки. Да, вам нужно будет создать новый градиент для каждой точки, потому что линейный градиент должен соответствовать углу касательной к этой точке.

  • Чтобы уменьшить эффект нерезкости, вызванный рисованием многих отдельных линий, вы можете нарисовать гладкий путь вдоль верхней и нижней сторон пути градиента, чтобы перезаписать jaggies.

Вот аннотированный код:

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

Ответ 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));
};