在画布上绘制渐变线 [英] Draw a line with gradient in canvas

查看:89
本文介绍了在画布上绘制渐变线的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我希望这篇帖子不会重复。

I hope this post is not duplicated.

我想画一条线,如图所示,可能具有不同的线宽和渐变。我尝试了createLinearGradient,但它没有达到我的预期。我应该改用图片吗?或者如何渲染上面的线?

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?

我可以使用PixiJS。

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>

推荐答案

使用2D canvas API自定义线条渲染


在不牺牲很多质量的情况下,没有简单的方法来创建所需的线条类型。

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.

中间的线是定义曲线。外线显示宽度的变化。标记为A的部分是单个带(放大)

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.

问题在于2D API在连接单独的渲染路径时非常不好,因此由于每个条带之间的抗锯齿,该方法将产生垂直线条的图案。

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.

这是您可以获得的最佳质量(尽管您可以渲染2或4倍的分辨率和下采样以获得轻微的改善) )

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)

要获得完美的像素抗锯齿效果,您将不得不使用webGL渲染最终路径(但仍然需要像示例中那样生成路径)

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>

这篇关于在画布上绘制渐变线的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆