简体   繁体   English

画布上的自画线

[英]Self drawing line on Canvas

I draw this 2D Path on a canvas context. 我在画布上下文上绘制此2D路径。

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

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! 谢谢!

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 我所做的是解析路径,然后返回一个对象,该对象的路径形式为可以获取长度为n的点

The function is parsePath(path) where path is the path string. 该函数是parsePath(path) ,其中path是路径字符串。 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}); 它返回具有属性totalLength的新路径对象,该属性为路径的大约长度(以像素为单位),并具有方法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. 它返回点{x:?,y:?}作为路径上位置pos的路径上的X和Y坐标。 pos = 0 start of path, pos = path.totalLength is end of path. pos = 0路径的开始, pos = path.totalLength是路径的结束。 Values outside the path return undefined The second argument is optional but best to supply a point to stop GC getting overworked. 路径外的值返回undefined第二个参数是可选的,但最好提供一个点来阻止GC过度工作。 parsePath() will throw a referenceError if anything goes wrong. 如果出现任何问题, parsePath()将抛出referenceError。

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. 当先前的路径段不是同一类型时S,s,T,t函数parsePath()不会执行水平和垂直线,也不会执行S,s,T,t路径命令。 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 您可以将该代码放入,因为我在代码中留下了存根并带有注释// 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. 在代码的底部,根据需要进行动画处理“我正在猜测”,因为将贝塞尔曲线切成部分并不容易,因此我仅每4个像素对路径进行采样,然后在它们之间绘制线条。 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. 该演示使用标准的Path2D对象绘制路径,然后使用解析的路径信息在其上绘制,从而为您提供一种判断质量是否可接受的方法。

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. 很抱歉,将需要更多代码来使bezier的速度保持恒定,但我现在没有时间解决此问题。 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 - px, 2) + Math.pow(p1.y - py, 2)); log(len) px = p1.x; py = 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 - px, 2) + Math.pow(p1.y - py, 2)); px = p1.x; py = 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 ZI 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> 

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, ... 所以基本上画了一部分线,超时了几毫秒,画了框架的另一部分,...

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM