画布上的自画线

时间:2016-01-06 14:19:57

标签: javascript html5 canvas

我在画布上下文中绘制这个2D Path。

http://jsbin.com/paroximebe/edit?js,output

var d = 'M0.464,59.322c0,0,35.468-88.67,101.478-48.276 s72.906,85.547,44.827,136.123s-70.443,67.817-101.97,81.118';
var p = new Path2D(d);

context.lineWidth = 2;
context.lineJoin = context.lineCap = 'round';
context.strokeStyle = '#000000';
context.translate(100, 100);
context.stroke(p);

我想要实现的是,当用户点击画布时,我希望该行是自绘的(动画的),类似于this效果。

有人能解释我如何实现这个目标吗?

谢谢!

2 个答案:

答案 0 :(得分:3)

没有简单的解决方案。我找了一个Path解析器,但没有找到任何东西,所以不得不写一个。很抱歉质量很差但是时间紧迫,无法提供完整的解决方案。

我所做的是解析路径,然后返回一个对象,该对象具有可以获得长度为n的形式的路径

函数是parsePath(path),其中path是路径字符串。它返回具有属性totalLength的新路径对象,该属性是路径的近似长度(以像素为单位),并且方法getPoint(pos,{x:0,y:0});返回点{x:?,y:?}作为X和Y坐标位置pos沿路径的路径。 pos = 0路径的起点,pos = path.totalLength是路径的终点。路径外的值返回undefined第二个参数是可选的,但最好提供一个点来阻止GC过度工作。如果出现任何问题,parsePath()将抛出referenceError。

使用

var path = "M0.464,59.322c0,0,35.468-88.67,101.478-48.276 s72.906,85.547,44.827,136.123s-70.443,67.817-101.97,81.118";

var pPath = parsePath(path);
console.log(pPath.totalLength);
// get a point half way along the path
var point = pPath.getPoint(pPath.totalLength/2);
console.log("Mid X coordinate:" + point.x)
console.log("Mid Y coordinate:" + point.y)

当前一个路径段不是同一类型时,函数parsePath()不执行水平和垂直线,也不执行S,s,T,t路径命令。在我应该发生的事情发生时,我无法解决问题。您可以使用注释// Missing CODE STUB

将该代码放入我的左侧存根中

我只在你的路上测试过它。

在代码的底部,我想要动画“我在猜测”因为将贝塞尔切成部分并不容易,我只需每隔4个像素对路径进行采样,然后在它们之间绘制线条。这只是一个近似值。 Demo使用标准的Path2D对象绘制路径,然后使用解析的路径信息绘制它,以便您判断质量是否可接受。

速度也不恒定。需要更多的代码才能让beziers的速度保持不变,对不起,我现在没有时间来解决这个问题。如果我有更多的时间,我会回来的。

对于糟糕的代码语法,最后一个道歉,将在我有机会时清理它。

这仅仅是作为如何解决问题的一个例子,并不是一个完整的解决方案,我没想到它会如此复杂并且会抛弃它但是有足够的空间来回答这个问题,所以浪费不想要。

// get canvas

var canvas = document.getElementById("can");
var ctx = canvas.getContext("2d");

// regexp for parsing path
var mark = /([MmLlQqSsHhVvCc])/g;
var spaces = /  /g;
var space2Comma = / /g;
var neg = /[0-9]-/g;
var noZ = /Z/gi;
const PRECISION = 0.1;  // smaller numbers make better fit. 
var path = "M0.464,59.322c0,0,35.468-88.67,101.478-48.276 s72.906,85.547,44.827,136.123s-70.443,67.817-101.97,81.118";

// Get point on cubic
var getPointOnBezierCurve = function(x1, y1, x2, y2, x3, y3, x4, y4, p, point){
    if(point === undefined){
        point = {x : null, y : null};
    }
    var xx1 = (x2 - x1) * p + x1;
    var yy1 = (y2 - y1) * p + y1;
    var xx2 = (x3 - x2) * p + x2;
    var yy2 = (y3 - y2) * p + y2;
    var xx3 = (x4 - x3) * p + x3;
    var yy3 = (y4 - y3) * p + y3;

    var xxA1 = (xx2 - xx1) * p + xx1;
    var yyA1 = (yy2 - yy1) * p + yy1;
    var xxA2 = (xx3 - xx2) * p + xx2;
    var yyA2 = (yy3 - yy2) * p + yy2;
        
    point.x = (xxA2 - xxA1) * p + xxA1;
    point.y = (yyA2 - yyA1) * p + yyA1;
    return point;
} 

// Get point on quad
var getPointOnBezier2Curve = function(x1, y1, x2, y2, x3, y3, p, point){
    if(point === undefined){
        point = {x : null, y : null};
    }
    var xx1 = (x2 - x1) * p + x1;
    var yy1 = (y2 - y1) * p + y1;
    var xx2 = (x3 - x2) * p + x2;
    var yy2 = (y3 - y2) * p + y2;
    point.x = (xx2 - xx1) * p + xx1;
    point.y = (yy2 - yy1) * p + yy1;
    return point
} 
// get length of a line
function getLineLength(){
    var n = this.nums;
    return Math.sqrt(Math.pow(n[0] - n[2], 2) + Math.pow(n[1] - n[3], 2));
}
// get length of a bezier quad
function getB2Length(){
    var n = this.nums, i, p, p1, len;
    p = {x : n[0], y : n[1]};
    p1 = {x : n[0], y : n[1]};
    len = 0;
    for(i = PRECISION; i <= 1; i += PRECISION){
        p1 =  getPointOnBezier2Curve(n[0], n[1], n[2], n[3], n[4], n[5], i ,p1);
        len += Math.sqrt(Math.pow(p1.x - p.x, 2) + Math.pow(p1.y - p.y, 2));
        log(len)
        p.x = p1.x;
        p.y = p1.y;
    }
    return len;
}

// get length of a cubic bezier
function getB3Length(){
    var n = this.nums, i, p, p1, len;
    p = {x : n[0], y : n[1]};
    p1 = {x : n[0], y : n[1]};
    len = 0;
    for(i = PRECISION; i <= 1; i += PRECISION){
        p1 =  getPointOnBezierCurve(n[0], n[1], n[2], n[3], n[4], n[5], n[6], n[7], i, p1);
        len += Math.sqrt(Math.pow(p1.x - p.x, 2) + Math.pow(p1.y - p.y, 2));
        p.x = p1.x;
        p.y = p1.y;
    }
    return len;
}

// get a point on a line
function pointOnLine(p, point){
    if(point === undefined){
        point = {x : null, y : null};
    }
    point.x = (this.nums[2] - this.nums[0]) * p + this.nums[0];
    point.y = (this.nums[3] - this.nums[1]) * p + this.nums[1];
    return point;
}

// get point on bezier cubic
function pointOnB2(p, point){
    var n = this.nums;
    return getPointOnBezier2Curve(n[0], n[1], n[2], n[3], n[4], n[5], p, point);
}
function pointOnB3(p, point){
    var n = this.nums;
    
    return getPointOnBezierCurve(n[0], n[1], n[2], n[3], n[4], n[5], n[6], n[7], p, point);
}

// not included V,H, and whatever arc is
var types = {
    "M":{numbers : 2},
    "L":{numbers : 2 , func : pointOnLine, lenFunc : getLineLength},
    "Q":{numbers : 4 , func : pointOnB2, lenFunc : getB2Length},
    "C":{numbers : 6 , func : pointOnB3, lenFunc : getB3Length},
    "S":{numbers : 4},
    "T":{numbers : 2},
}
function getPointOnPath(pos, point){
    var i = 0;
    while(i < this.length && !(this[i].startLength <= pos && this[i].startLength + this[i].length >= pos)){
        i += 1;
    }
    if(i < this.length){
        return this[i].getPoint((pos - this[i].startLength) / this[i].length, point);            
    }
    return undefined; 
}

// function to parse path string
function parsePath(path){
    var parts, newPath, i, seg, lseg, len;
    try{
        // Format path for easy parsing
        path = path.replace(noZ, ""); // remove the Z I am just ignoring it
        path = path.replace(spaces, " "); // remove any excess spaces
        path = path.replace(neg, ",-"); // insert commas if neg follows a number
        path = path.replace(space2Comma, ","); // convert spaces to commas
        
        // Split into segments
        parts = path.replace(mark, "#$1").substr(1).split("#");
        
        // parse each sement add to the new path
        newPath = [];
        parts.forEach(function(p){
            var i, nums, type, seg;
            // get the numbers
            nums = p.substr(1).split(",");
            // get the type as uppercase
            type = types[p[0].toUpperCase()];
            // create a segment
            seg = {
                type : p[0].toUpperCase(),
                nums : [],
                rel : false,
            }
            // check if relative
            if(p[0] === p[0].toLowerCase()){
                seg.rel = true;
            }
            // read the requiered numbers
            for(i = 0; i < type.numbers; i++){
                seg.nums.push(Number(nums[i]));
            }
            
            // add the new path segment
            newPath.push(seg);
        });
        
        
        // convert relative path coords to absolute
        newPath.forEach(function(seg, i){
            var j, x, y, xx, yy;
            if(i !== 0){
                xx = x = newPath[i-1].nums[newPath[i-1].nums.length-2];
                yy = y = newPath[i-1].nums[newPath[i-1].nums.length-1];
                if(seg.rel){
                    for(j = 0; j < seg.nums.length; j+= 2){
                        seg.nums[j] += x;
                        seg.nums[j + 1] += y;
                    }
                }
                // Add the start of the segment so that they can be handled
                // without the need to reference another seg
                if(seg.type !== "M"){
                    seg.nums.unshift(yy)
                    seg.nums.unshift(xx)
                }
            }
        });

        // Convert S an T path types to C and Q
        // Also remove M commands as they are not needed
        // also Calculate length of each seg NOTE bezier lengths are estimates only
        len = 0;
        for(i = 0; i < newPath.length; i++){
            seg = newPath[i]
            if(seg.type === "M"){
                newPath.splice(i, 1);
                i --;
            }else{
                if(seg.type === "S"){
                    seg.type = "C";
                    lseg = newPath[i - 1];
                    if(lseg.type === "C"){
                        seg.nums.splice(2, 0, seg.nums[0] - (lseg.nums[4] - lseg.nums[6]), seg.nums[1] - (lseg.nums[5] - lseg.nums[7]));
                    }else{
                        // Missing CODE STUB
                    }
                    
                }else
                if(newPath.type === "T"){
                    seg.type = "Q";
                    lseg = newPath[i - 1];
                    if(lseg.type === "Q"){
                        seg.nums.splice(2, 0,seg.nums[0] + (lseg.nums[2] - lseg.nums[4]), seg.nums[1] + (lseg.nums[3] - lseg.nums[5]));
                    }else{
                        // Missing CODE STUB                        
                    }
                }
                // add function to find point
                seg.getPoint = types[seg.type].func.bind(seg);
                // set start pos an calculate length
                seg.startLength = len;
                len += seg.length = (types[seg.type].lenFunc.bind(seg))();
            }
        }
        // set total calculated length
        newPath.totalLength = len;
        // add getPoint function binding to newPath
        newPath.getPoint = getPointOnPath.bind(newPath);
        return newPath;
    }catch(e){
       throw new ReferenceError("Something not so good parsing path.")
    }
}


// Path the path. Sorry code is real rush job from here
var p = parsePath(path);


ctx.lineJoin = "round";
ctx.lineCap = "round";

var pp = new Path2D(path); // use standard path to show that I am following correctly
var t = 0
var pt = {x : 0,y : 0};
function update1(){
    ctx.setTransform(1, 0, 0, 1, 0, 0);
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.setTransform(1, 0, 0, 1, 100, 100);
    ctx.lineWidth = 4;
    ctx.strokeStyle = "#FF8";
    ctx.stroke(pp);
    

    ctx.strokeStyle = "#000";
    t = (t + 1) % p.totalLength;
    ctx.beginPath();
    pt = p.getPoint(t % p.totalLength, pt);
    ctx.moveTo(pt.x, pt.y);
    for(var k = 0; k < 100; k += 4){
        var ppt = p.getPoint(t + k, pt);
        if(ppt !== undefined){
            ctx.lineTo(ppt.x, ppt.y);
        }
    }
    ctx.stroke();
    requestAnimationFrame(update1);
}
update1()
.canC {
  width:500px;
  height:500px;
}
<canvas class= "canC" id="can" width = 500 height = 500></canvas>

答案 1 :(得分:0)

我认为在这种情况下你必须逐帧绘制动画。 所以基本上绘制线的一部分,超时一些ms,画出帧的另一部分,......