Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Draw a line with gradient in canvas

Tags:

canvas

pixi.js

I hope this post is not duplicated.

enter image description here

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>
like image 873
HUNG Avatar asked Jan 30 '23 18:01

HUNG


2 Answers

Custom line rendering using 2D canvas API

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.

enter image description here

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)

Example

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>
like image 132
Blindman67 Avatar answered Feb 22 '23 23:02

Blindman67


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>
like image 30
HUNG Avatar answered Feb 23 '23 01:02

HUNG