const ctx = canvas.getContext("2d");
canvas.height = canvas.width = 400;
// Minimum groover.geom library needed to use vecAt and tangentAsVec for bezier curves.
const geom = (()=>{
const v1 = new Vec();
const v2 = new Vec();
const v3 = new Vec();
const v4 = new Vec();
function Vec(x,y){
this.x = x;
this.y = y;
};
function Bezier(p1,p2,cp1,cp2){
this.p1 = p1;
this.p2 = p2;
this.cp1 = cp1;
this.cp2 = cp2;
}
Bezier.prototype = {
//======================================================================================
// single dimension polynomials for 2nd (a,b,c) and 3rd (a,b,c,d) order bezier
//======================================================================================
// for quadratic f(t) = a(1-t)^2+2b(1-t)t+ct^2
// = a+2(-a+b)t+(a-2b+c)t^2
// The derivative f'(t) = 2(1-t)(b-a)+2(c-b)t
//======================================================================================
// for cubic f(t) = a(1-t)^3 + 3bt(1-t)^2 + 3c(1-t)t^2 + dt^3
// = a+(-2a+3b)t+(2a-6b+3c)t^2+(-a+3b-3c+d)t^3
// The derivative f'(t) = -3a(1-t)^2+b(3(1-t)^2-6(1-t)t)+c(6(1-t)t-3t^2) +3dt^2
// The 2nd derivative f"(t) = 6(1-t)(c-2b+a)+6t(d-2c+b)
//======================================================================================
p1 : undefined,
p2 : undefined,
cp1 : undefined,
cp2 : undefined,
vecAt(position,vec){
var c;
if (vec === undefined) { vec = new Vec() }
if (position === 0) {
vec.x = this.p1.x;
vec.y = this.p1.y;
return vec;
}else if (position === 1) {
vec.x = this.p2.x;
vec.y = this.p2.y;
return vec;
}
v1.x = this.p1.x;
v1.y = this.p1.y;
c = position;
if (this.cp2 === undefined) {
v2.x = this.cp1.x;
v2.y = this.cp1.y;
v1.x += (v2.x - v1.x) * c;
v1.y += (v2.y - v1.y) * c;
v2.x += (this.p2.x - v2.x) * c;
v2.y += (this.p2.y - v2.y) * c;
vec.x = v1.x + (v2.x - v1.x) * c;
vec.y = v1.y + (v2.y - v1.y) * c;
return vec;
}
v2.x = this.cp1.x;
v2.y = this.cp1.y;
v3.x = this.cp2.x;
v3.y = this.cp2.y;
v1.x += (v2.x - v1.x) * c;
v1.y += (v2.y - v1.y) * c;
v2.x += (v3.x - v2.x) * c;
v2.y += (v3.y - v2.y) * c;
v3.x += (this.p2.x - v3.x) * c;
v3.y += (this.p2.y - v3.y) * c;
v1.x += (v2.x - v1.x) * c;
v1.y += (v2.y - v1.y) * c;
v2.x += (v3.x - v2.x) * c;
v2.y += (v3.y - v2.y) * c;
vec.x = v1.x + (v2.x - v1.x) * c;
vec.y = v1.y + (v2.y - v1.y) * c;
return vec;
},
tangentAsVec (position, vec ) {
var a, b, c, u;
if (vec === undefined) { vec = new Vec(); }
if (this.cp2 === undefined) {
a = (1-position) * 2;
b = position * 2;
vec.x = a * (this.cp1.x - this.p1.x) + b * (this.p2.x - this.cp1.x);
vec.y = a * (this.cp1.y - this.p1.y) + b * (this.p2.y - this.cp1.y);
}else{
a = (1-position)
b = 6 * a * position; // (6*(1-t)*t)
a *= 3 * a; // 3 * ( 1 - t) ^ 2
c = 3 * position * position; // 3 * t ^ 2
vec.x = -this.p1.x * a + this.cp1.x * (a - b) + this.cp2.x * (b - c) + this.p2.x * c;
vec.y = -this.p1.y * a + this.cp1.y * (a - b) + this.cp2.y * (b - c) + this.p2.y * c;
}
u = Math.sqrt(vec.x * vec.x + vec.y * vec.y);
vec.x /= u;
vec.y /= u;
return vec;
},
}
return { Vec, Bezier,}
})()
// this function is used to define the width of the curve
// It creates a smooth transition.
// power changes the rate of change
function curve(x,power){ // simple smooth curve x range 0-2 return value between 0 and 1
x = 1 - Math.abs(x - 1);
return Math.pow(x,power);
}
// this function returns a colour at a point in a gradient
// the pos is from 0 - 1
// the grad is an array of positions and colours with each
// an array [position, red, green, blue] Position is the position in the gradient
// A simple 2 colour gradient from black (start position = 0) to white (end position = 1)
// would be [[0,0,0,0],[1,255,255,255]]
// The bool isHSL if true will interpolate the values as HUE Saturation and luminiance
function getColFromGrad(pos,grad,isHSL){ // pos 0 - 1, grad array of [pos,r,g,b]
var i = 0;
while(i < grad.length -1 && grad[i][0] <= pos && grad[i+1][0] < pos){ i ++ }
var g1 = grad[i];
var g2 = grad[i + 1];
var p = (pos - g1[0]) / (g2[0] - g1[0]);
var r = (g2[1]-g1[1]) * p + g1[1];
var g = (g2[2]-g1[2]) * p + g1[2];
var b = (g2[3]-g1[3]) * p + g1[3];
if(isHSL){ return `hsl(${(r|0)%360},${g|0}%,${b|0}%)` }
return `rgb(${r|0},${g|0},${b|0})`
}
function drawLine(path,width,gradient){
var steps = 300;
var step = 1/steps;
var i = 0;
var pos = V(0,0);
var tangent = V(0,0);
var p = []; // holds the points
// i <= 1 + step/2 // this is to stop floating point error from missing the end value
for(i = 0; i <= 1 + step/2; i += step){
path.vecAt(i,pos); // get position along curve
path.tangentAsVec(i,tangent); // get tangent at that point]
var w = curve(i * 2,1/2) * width; // get the line width for this point
p.push(V(pos.x -tangent.y * w, pos.y + tangent.x * w)); // add the edge point above the line
p.push(V(pos.x +tangent.y * w, pos.y - tangent.x * w)); // add the edge point below
}
// save context and create the clip path
ctx.save();
ctx.beginPath();
// path alone the top edge
for(i = 0; i < p.length; i += 2){
ctx.lineTo(p[i].x,p[i].y);
}
// then back along the bottom
for(i = 1; i < p.length; i += 2){
ctx.lineTo(p[p.length - i].x,p[p.length - i].y);
}
// set this as the clip
ctx.clip();
// then for each strip
ctx.lineWidth = 1;
for(i = 0; i < p.length-4; i += 2){
ctx.beginPath();
// get the colour for this strip
ctx.strokeStyle = ctx.fillStyle = getColFromGrad(i / (p.length-4),gradient);
// define the path
ctx.lineTo(p[i].x,p[i].y);
ctx.lineTo(p[i+1].x,p[i+1].y);
ctx.lineTo(p[i+3].x,p[i+3].y);
ctx.lineTo(p[i+2].x,p[i+2].y);
// cover the seams
ctx.stroke();
// fill the strip
ctx.fill();
}
// remove the clip
ctx.restore();
}
// create quick shortcut to create a Vector object
var V = (x,y)=> new geom.Vec(x,y);
// create a quadratice bezier
var b = new geom.Bezier(V(50,50),V(50,390),V(500,10));
// create a gradient
var grad = [[0,0,0,0],[0.25,0,255,0],[0.5,255,0,255],[1,255,255,0]];
// draw the gradient line
drawLine(b,10,grad);
// and do a cubic bezier to make sure it all works.
var b = new geom.Bezier(V(350,50),V(390,390),V(300,10),V(10,0));
var grad = [[0,255,0,0],[0.25,0,255,0],[0.5,0,255,255],[1,0,0,255]];
drawLine(b,20,grad);
canvas { border : 2px solid black; }
<canvas id="canvas"></canvas>