在画布

时间:2017-08-21 04:09:50

标签: canvas pixijs

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

enter image description here

我想绘制一条线,如图所示,可能有不同的线宽和渐变。我尝试过createLinearGradient,但它并不像我预期的那样。我应该使用图像吗?或者我如何渲染上面的线?

我可能会与PixiJS合作。

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



$(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();

});
&#13;
<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>
&#13;
&#13;
&#13;

2 个答案:

答案 0 :(得分:6)

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

没有简单的方法来创建你想要的线型而不会牺牲很多质量。

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

下图将有助于解释我的意思。

enter image description here

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

将线分成相同的小部分,对于沿线所需的每个点,您需要找到线上的位置以及垂直于线上该点的矢量。然后,您可以在正确距离处找到点上方和下方的点,以使宽度成为该点的直线。

然后以正确的颜色绘制每个条带。

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

您可以通过勾勒相同颜色笔划的每个条带来解决这个问题,但这会破坏外边缘的质量,在线条外边缘的每个接缝处产生小凸起。

如果将剪辑区域设置为直线,则可以停止此操作。您可以通过描绘线的轮廓并将其设置为剪辑来完成此操作。

然后,您可以以可通过的质量呈现该行

在一个答案中解释的数学太多了。您需要在贝塞尔曲线上找到点和切线,您需要插入一个渐变,并且您需要一种方法来定义平滑宽度函数(另一个贝塞尔曲线)或者如示例中的复杂抛物线(函数{{ 1}})

实施例

以下示例将从单个bezier(第2和第3个订单)创建您所在行的类型。您可以使用多条曲线和线段来调整它。

这是关于你可以得到的最好的质量(虽然你可以渲染2或4倍的res和down样本以获得轻微的改进)

对于像素完美抗锯齿结果,您必须使用webGL渲染最终路径(但您仍需要生成路径,如示例所示)

curve
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; }

答案 1 :(得分:0)

我也在网上找到了类似的解决方案:)

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

相关问题