代码之家  ›  专栏  ›  技术社区  ›  HUNG

在画布中绘制渐变线

  •  4
  • HUNG  · 技术社区  · 7 年前

    我希望这篇文章不要重复。

    enter image description here

    我想画一条线,如图所示,可能有不同的线宽和梯度。我尝试了createLinearGradient,但它并不像我预期的那样。我应该用图像来代替吗?或者我如何渲染上面的线?

    更新: 我现在可以用渐变色生成线条,但如何创建动态宽度呢?

    $(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>
    2 回复  |  直到 5 年前
        1
  •  8
  •   Community CDub    4 年前

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

    没有简单的方法可以在不牺牲大量质量的情况下创建您想要的线条类型。

    为了获得最佳质量,需要将线渲染为一组垂直于线并沿线长度方向的小条带。对于每个部分,计算宽度和颜色,然后渲染该条带。

    下图将帮助解释我的意思。

    enter image description here

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

    如果将剪辑区域设置为直线,则可以停止此操作。您可以通过绘制线的轮廓并将其设置为剪辑来实现这一点。

    然后可以以可接受的质量渲染线

    有太多的数学问题无法用一个答案来解释。你需要在贝塞尔曲线上找到点和切线,你需要插值一个梯度,你需要一种定义平滑宽度函数(另一个贝塞尔)的方法,或者在示例中定义一个复杂的抛物线(函数 curve

    实例

    下面的示例将从单个贝塞尔曲线(第二阶和第三阶)创建您要查找的直线类型。可以使用多条曲线和线段进行调整。

    为了获得像素级的消除混叠效果,您必须使用webGL渲染最终路径(但仍需要像示例中那样生成路径)

    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>
        2
  •  0
  •   HUNG    7 年前

    (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>