Я пытаюсь нарисовать кривую в холсте с линейным стилем градиента на кривой, как в этом изображении. На этой странице есть связанный файл svg, который дает инструкции о том, как выполнить эффект в svg. Может быть, подобный метод возможен в холсте?
Градиентный штрих вдоль кривой в холсте
Ответ 1
Демо: http://jsfiddle.net/m1erickson/4fX5D/
Довольно легко создать градиент, который изменяет вдоль пути:
Сложнее создать градиент, который изменяет по пути:
Чтобы создать градиент по пути, вы рисуете много линий градиента, касательных к пути:
Если вы нарисовываете достаточные касательные линии, тогда глаз видит кривую как градиент по пути.
Примечание. Яйца могут встречаться на внешних сторонах градиента пути. Это потому, что градиент действительно состоит из сотен тангенциальных линий. Но вы можете сгладить зубцы, вычерчивая линию по обе стороны градиента, используя соответствующие цвета (здесь анти-дряблые линии красны с верхней стороны и фиолетовые на нижней стороне).
Ниже приведены шаги по созданию градиента по пути:
-
Выделите сотни точек вдоль пути.
-
Рассчитайте угол пути в этих точках.
-
В каждой точке создайте линейный градиент и нарисуйте градиентную поглаженную линию по касательной этой точки. Да, вам нужно будет создать новый градиент для каждой точки, потому что линейный градиент должен соответствовать углу касательной к этой точке.
-
Чтобы уменьшить эффект нерезкости, вызванный рисованием многих отдельных линий, вы можете нарисовать гладкий путь вдоль верхней и нижней сторон пути градиента, чтобы перезаписать 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));
};