I hope this post is not duplicated.
I would like to draw a line, as the image shown, that may have different line width and with gradient. I tried createLinearGradient but it is not as what I expected. Shall I use an image instead? Or how can I render the line above?
I may work with PixiJS.
Update: I can now generate the line with gradient color but how can I create a dynamic width ones?
$(function() {
var canvas = document.getElementById("canvas"),
ctx = canvas.getContext("2d"),
painting = false,
lastX = 0,
lastY = 0;
canvas.onmousedown = function (e) {
if (!painting) {
painting = true;
} else {
painting = false;
}
lastX = e.pageX - this.offsetLeft;
lastY = e.pageY - this.offsetTop;
ctx.lineJoin = ctx.lineCap = 'round';
};
var img = new Image();
img.src = "http://i.imgur.com/K6qXHJm.png";
canvas.onmousemove = function (e) {
if (painting) {
mouseX = e.pageX - this.offsetLeft;
mouseY = e.pageY - this.offsetTop;
// var grad= ctx.createLinearGradient(lastX, lastY, mouseX, mouseY);
// grad.addColorStop(0, "red");
// grad.addColorStop(1, "green");
//ctx.strokeStyle = grad;
ctx.lineWidth = 15;
//ctx.createPattern(img, 'repeat');
ctx.strokeStyle = ctx.createPattern(img, 'repeat');
ctx.beginPath();
ctx.moveTo(lastX, lastY);
ctx.lineTo(mouseX, mouseY);
ctx.stroke();
$('#output').html('current: '+mouseX+', '+mouseY+'<br/>last: '+lastX+', '+lastY+'<br/>mousedown: '+"mousedown");
lastX = mouseX;
lastY = mouseY;
}
}
function fadeOut() {
ctx.fillStyle = "rgba(255,255,255,0.3)";
ctx.fillRect(0, 0, canvas.width, canvas.height);
setTimeout(fadeOut,100);
}
fadeOut();
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<canvas id="canvas" width="800" height="500"></canvas>
<div id="output"></div>
There is no simple way to create the type of line you want without sacrificing a lot of quality.
For the best quality you need render the line as a set of small strips perpendicular to the line and all the way along the length of the line. For each part you calculate the width and the colour and then render that strip.
The following image will help explain what I mean.
The line in the middle is the defining curve. The outer lines show the changing width. The section marked A is a single strip (enlarged)
You divide the line into equally small parts, for every point along the line you need to find the position on the line and the vector perpendicular to that point on the line. You then find the point above and below the point at the correct distance to make the width the line for that point.
You then draw each strip at the correct colour.
The problem is that the 2D API is very bad at joining separate rendered paths, so this method will produce a pattern of perpendicular lines due to antialiasing between each strip.
You can combat this by outlining each strip with the same colours stroke, but this will destroy the quality of the outer edge, producing small bumps at each seam on the outer edge of the line.
This to can be stopped if you set the clip region to the line. You do this by tracing out the outline of the line and setting that as the clip.
You can then render the line at a passable quality
There is simply too much math to be explained in a single answer. You will need to find points and tangents on a bezier curve, you will need to interpolate a gradient, and you will need a way of defining a smooth width function (another bezier) or as in the example a complex parabola (the function curve
)
The following example will create the type of line you are after from a single bezier (2nd and 3rd order). You can adapt it use multiple curves and line segments.
This is about the best quality you can get (though you can render a 2 or 4 times res and down sample to get a slight improvement)
For a pixel perfect antialiased result you will have to use webGL to render the final path (but you will still need to generate the path as in the example)
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>
I do also found a solution online that do similarly :)
(function($) {
$.fn.ribbon = function(options) {
var opts = $.extend({}, $.fn.ribbon.defaults, options);
var cache = {},canvas,context,container,brush,painters,unpainters,timers,mouseX,mouseY;
return this.each(function() {
//start functionality
container = $(this).parent();
canvas = this;
context = this.getContext('2d');
canvas.style.cursor = 'crosshair';
$(this).attr("width",opts.screenWidth).attr("height",opts.screenHeight)
painters = [];
//hist = [];
unpainters = [];
timers = [];
brush = init(this.context);
start = false;
clearCanvasTimeout = null;
canvas.addEventListener('mousedown', onWindowMouseDown, false);
canvas.addEventListener('mouseup', onWindowMouseUp, false);
canvas.addEventListener('mousemove', onWindowMouseMove, false);
window.addEventListener('resize', onWindowResize, false);
//document.addEventListener('mouseout', onDocumentMouseOut, false);
//canvas.addEventListener('mouseover', onCanvasMouseOver, false);
onWindowResize(null);
});
function init() {
context = context;
mouseX = opts.screenWidth / 2;
mouseY = opts.screenHeight / 2;
// for(var i = 0; i < opts.strokes; i++) {
// var ease = Math.random() * 0.05 + opts.easing;
// painters.push({
// dx : opts.screenWidth / 2,
// dy : opts.screenHeight / 2,
// ax : 0,
// ay : 0,
// div : 0.1,
// ease : ease
// });
// }
this.interval = setInterval(update, opts.refreshRate);
function update() {
var i;
context.lineWidth = opts.brushSize;
//context.strokeStyle = "rgba(" + opts.color[0] + ", " + opts.color[1] + ", " + opts.color[2] + ", " + opts.brushPressure + ")";
context.lineCap = "round";
context.lineJoin = "round";
var img = new Image;
img.onload = function() {
context.strokeStyle = context.createPattern(img, 'repeat');;
};
img.src = "http://i.imgur.com/K6qXHJm.png";
if(start){
//if(clearCanvasTimeout!=null) clearTimeout(clearCanvasTimeout);
for( i = 0; i < painters.length; i++) {
context.beginPath();
var dx = painters[i].dx;
var dy = painters[i].dy;
context.moveTo(dx, dy);
var dx1 = painters[i].ax = (painters[i].ax + (painters[i].dx - mouseX) * painters[i].div) * painters[i].ease;
painters[i].dx -= dx1;
var dx2 = painters[i].dx;
var dy1 = painters[i].ay = (painters[i].ay + (painters[i].dy - mouseY) * painters[i].div) * painters[i].ease;
painters[i].dy -= dy1;
var dy2 = painters[i].dy;
context.lineTo(dx2, dy2);
context.stroke();
}
}else{
// if(clearCanvasTimeout==null){
// clearCanvasTimeout = setTimeout(function(){
context.clearRect(0, 0, opts.screenWidth, opts.screenWidth);
// clearCanvasTimeout = null;
// }, 3000);
// }else{
// }
//console.log(hist.length);
// for( i = hist.length/2; i < hist.length; i++) {
// context.beginPath();
// var dx = hist[i].dx;
// var dy = hist[i].dy;
// context.moveTo(dx, dy);
// var dx1 = hist[i].ax = (hist[i].ax + (hist[i].dx - mouseX) * hist[i].div) * hist[i].ease;
// hist[i].dx -= dx1;
// var dx2 = hist[i].dx;
// var dy1 = hist[i].ay = (hist[i].ay + (hist[i].dy - mouseY) * hist[i].div) * hist[i].ease;
// hist[i].dy -= dy1;
// var dy2 = hist[i].dy;
// context.lineTo(dx, dy);
// context.stroke();
// }
}
}
};
function destroy() {
clearInterval(this.interval);
};
function strokestart(mouseX, mouseY) {
mouseX = mouseX;
mouseY = mouseY
for(var i = 0; i < painters.length; i++) {
painters[i].dx = mouseX;
painters[i].dy = mouseY;
}
};
function stroke(mouseX, mouseY) {
mouseX = mouseX;
mouseY = mouseY;
};
function strokeEnd() {
//this.destroy()
}
function onWindowMouseMove(event) {
mouseX = event.clientX;
mouseY = event.clientY;
}
function onWindowMouseDown(event){
start = true;
for(var i = 0; i < opts.strokes; i++) {
var ease = Math.random() * 0.05 + opts.easing;
painters.push({
dx : event.clientX,
dy : event.clientY,
ax : 0,
ay : 0,
div : 0.1,
ease : ease
});
}
}
function onWindowMouseUp(){
start = false;
//hist = painters;
painters = [];
}
function onWindowResize() {
opts.screenWidth = window.innerWidth;
opts.screenHeight = window.innerHeight;
}
function onDocumentMouseOut(event) {
onCanvasMouseUp();
}
function onCanvasMouseOver(event) {
strokestart(event.clientX, event.clientY);
window.addEventListener('mousemove', onCanvasMouseMove, false);
window.addEventListener('mouseup', onCanvasMouseUp, false);
}
function onCanvasMouseMove(event) {
stroke(event.clientX, event.clientY);
}
function onCanvasMouseUp() {
strokeEnd();
}
}
$.fn.ribbon.defaults = {
canvas : null,
context : null,
container : null,
userAgent : $.browser,
screenWidth : $(window).width(),
screenHeight : $(window).height(),
duration : 6000, // how long to keep the line there
fadesteps : 10, // how many steps to fade the lines out by, reduce to optimize
strokes : 20, // how many strokes to draw
refreshRate : 30, // set this higher if performace is an issue directly affects easing
easing : .7, // kind of "how loopy" higher= bigger loops
brushSize : 2, // pixel width
brushPressure : 1, // 1 by default but originally variable setting from wacom and touch device sensitivity
color : [0, 0, 0], // color val RGB 0-255, 0-255, 0-255
backgroundColor : [255, 255, 255], // color val RGB 0-255, 0-255, 0-25
brush : null,
mouseX : 0,
mouseY : 0,
i : 0
}
})(jQuery);
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<canvas style="border: 1px solid black;" id="canvas" width="800" height="500"></canvas>
<script>
$(document).ready(function(){
var config = {
screenWidth : $("#canvas").width(),
screenHeight : $("#canvas").height(),
strokes: 150,
};
$("#canvas").ribbon(config);
});
</script>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With