Hiero
Hiero

Reputation: 2205

Self drawing line on Canvas

I draw this 2D Path on a canvas context.

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

The thing that I want to achieve is that when user click the canvas I want the line to be self drawn (animated), similar with this effect.

Can someone explain me how to achieve that?

Thanks!

Upvotes: 0

Views: 407

Answers (2)

Blindman67
Blindman67

Reputation: 54041

There is no easy solution. I looked for a Path parser but found nothing so had to write one. Sorry for the poor quality but I am pushed for time and can not provide a complete solution.

What I have done is parse the path and then returned an object that has the path in the form that you can get a point at length n

The function is parsePath(path) where path is the path string. It returns the new path object that has the property totalLength which is the approx length of the path in pixels and has the method getPoint(pos,{x:0,y:0}); which returns a point {x:?,y:?} as the X and Y coordinate on the path for position pos along the path. pos = 0 start of path, pos = path.totalLength is end of path. Values outside the path return undefined The second argument is optional but best to supply a point to stop GC getting overworked. parsePath() will throw a referenceError if anything goes wrong.

Use

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)

The function parsePath() does not do Horizontal and Vertical lines, nor does it do S,s,T,t path commands when the previous path segment is not of same type. I could not work out in the time I have what was supposed to happen. You can put that code in as I have left stubs for it with the comment // Missing CODE STUB

I have only tested it on your path.

At the bottom of the code I animate as you want "I am guessing" Because it is not easy to cut a bezier into parts I simply sample the path every 4 pixels and then draw lines between them. It is an approximation only. The Demo draws the path using the standard Path2D object and then draws over it with the parsed path info to give you a way to judge if the quality is acceptable.

Also the speed is not constant. Would take a lot more code to get the speed on beziers to be constant, I am sorry but I dont have the time right now to fix that. I will come back to it if I get more time.

And one last apology for the poor code syntax, will clean it up when I get a chance.

This is meant ONLY as an example of how to solve the problem and is NOT a complete solution, I did not expect it to be so complex and was going to dump it but there is enough there to answer the question, so waste not want not.

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

Upvotes: 3

benedict Benoit
benedict Benoit

Reputation: 1

I believe you will have to draw the animation frame by frame in this case. So basically draw part of the line, timeout some ms, draw another part of the frame, ...

Upvotes: 0

Related Questions