原文地址
我们一开始知道的有:
- 箭头可以在线段开头 也可以在结尾,也可以两端都有。
- 我们希望指定一个角度θ,见图,而且有缺省值
- 我们希望指定箭头的长度,而且也有缺省值
- 我们希望选择具有填充和未填充的头部,甚至是用户传递自定义功能以用于绘制头部的机会。
普通用户只需指定源点和目标点,其他所有内容都将默认。
函数形式为:
drawArrow(x1,y1,x2,y2,style,which,angle,length)
- x1,y1:开始点
- x2,y2:结束点
- style:箭头类型
-- 0:填充头部后面用arcTo绘制的曲线
-- 1:填充头部 用直线结束箭头
-- 2:不填充箭头 只描边
-- 3:用二次曲线绘制的曲线填充后面的曲线 quadraticCurveTo
-- 4:用bezierCurveTo绘制的曲线填充头部
-- function(ctx,x0,y0,x1,y1,x2,y2,style) :用户提供的绘制头部的功能。点(x1,y1)与线的末端相同,(x0,y0)和(x2,y2)是两个后角。 style参数是函数的this。在本文档的后面部分将显示在箭头的每个角上绘制小圆圈的示例。
默认值为3 - which:哪端结束
-- 0:哪端都不是
-- 1:x2,y2端
-- 2:x1,y1端
-- 3:两端
默认值为1 - 角度:从轴到箭头一侧的角度θ - 默认π/ 8弧度(22 1/2°,45°的一半)
-
length:从头部到箭头的背面的距离d(以像素为单位) - 默认为10px
用Math.atan2(y,x)来求角度。
(canvas的角度是从x正轴开始以逆时针方向计算全是负角度。)
对于象限I和II,它返回角度α作为负角度(-π<=α<= 0),对于象限III和IV,它返回角度α(0 <=α<=π)。
考虑从(x0,y0)到(x1,y1)的直线。
atan2(y1-y0,x2-x0)给出了它的角度,但是是逆时针的,我们的θ是顺时针的角。
为了弄清楚θ的角度,我们需要将θ加到α的相反位置。在弧度中,与α相反的是π+α。因此,箭头顶侧的角度为π+α+θ,箭头的底线角度为π+α - θ。
解读:
其实这段话的意思是:
我们已知θ角多少度了,但是不知道箭头在canvas坐标系中的角度。
当我们用atan2(y1-y0,x2-x0)计算出来了α角后
α+π,就是箭柄线段的角度了
那么
箭头上线段的角度就是 π+α+θ
箭头下线段的角度就是 π+α-θ
我们有箭头的每一边的角度,我们有d,但如果我们有h(斜边),我们可以很容易地计算出箭头两个角的x和y坐标。
由于cos(θ)= d / h,那么h = d / cos(θ)。
现在,d是一个长度,因此总是一个正数,余弦,取决于角度可以是正数或负数。
我们希望斜边也是一个长度,所以我们将取绝对值。h=Math.abs(d/Math.cos(angle)).。
解读:这是通过θ角度和d,求出了h,然后我们在通过canvas坐标系的π+α-θ的余弦值和h,可以得到该下线段的x和y坐标
从点(x2,y2)开始并以角度angle1向h距离,点(topx1,topy1)等于
(x2+Math.cos(angle1)h,y2+Math.sin(angle1)h)
类似地,给定箭头底部的角度(角度2),箭头底部的后角(botx,boty)的x和y值是
(x2+Math.cos(angle2)h,y2+Math.sin(angle2)h)
下面是部分源码
// 计算箭柄角度
var lineangle=Math.atan2(y2-y1,x2-x1);
// 箭头一端线段长度
var h=Math.abs(d/Math.cos(angle));
(计算线的角度,以便我们可以使用它来找到箭头顶部和底部线的角度,并使用它来计算两端的(x,y)位置并绘制它们。首先,如上所述,我们通过将Math.PI添加到线的角度来找到线的角度,以获得其相反的角度。然后我们将倒钩的传入角度添加到结果中。之后,我们可以通过基本trig轻松找到倒钩角的(x,y)坐标。底角的坐标以同样的方式找到,然后我们调用另一种方法来实际绘制头部,通过三个角并告诉它样式。)
if(which&1){ // 处理远端的箭头
var angle1=lineangle+Math.PI+angle;
var topx=x2+Math.cos(angle1)h;
var topy=y2+Math.sin(angle1)h;
var angle2=lineangle+Math.PI-angle;
var botx=x2+Math.cos(angle2)h;
var botx=y2+Math.sin(angle2)h;
toDrawHead(ctx,topx,topy,x2,y2,botx,boty,style);
}
if(which&2){ //处理近端的箭头
var angle1=lineangle+angle;
var topx=x1+Math.cos(angle1)h;
var topy=y1+Math.sin(angle1)h;
var angle2=lineangle-angle;
var botx=x1+Math.cos(angle2)h;
var boty=y1+Math.sin(angle2)h;
ctx.beginPath();
toDrawHead(ctx,topx,topy,x1,y1,botx,boty,style);
}
(类似地,我们处理箭头另一端的代码,计算点并将它们传递给头部绘图程序。主要区别在于我们不必将Math.PI添加到线条中,因为它已经在箭头的两侧的线条上以相同的方式。)
var drawArrow=function(ctx,x1,y1,x2,y2,style,which,angle,d)
{
//设置一些缺省值
'use strict';
if(typeof(x1)=='string') x1=parseInt(x1,10);
if(typeof(y1)=='string') y1=parseInt(y1,10);
if(typeof(x2)=='string') x2=parseInt(x2,10);
if(typeof(y2)=='string') y2=parseInt(y2,10);
which=typeof(which)!='undefined'? which:1; // 终点绘制箭头
angle=typeof(angle)!='undefined'? angle:Math.PI/8;
d =typeof(d) !='undefined'? d :10;
style=typeof(style)!='undefined'? style:3;
// 缺省用drawHead绘制头部 如果style参数是function,就用function代替
var toDrawHead=typeof(style)!='function'?drawHead:style;
(对于每个可以有默认值的参数,我们检查它们是否已设置,如果是,我们使用它们的值。如果没有,我们将它们设置为默认值。 另外,对于样式,我们检查它是否是一个函数。如果是这样,我们使用它来为我们的函数绘制头,否则我们使用我们的函数drawHead。我不打算谈论drawHead,因为它只是画布绘制例程的简单应用程序,但你可以自己查看它,它是在canvasutilities.js而是,我将向你展示如何编写自己的头部绘图常规传入。)
var headDrawer=function(ctx,x0,y0,x1,y1,x2,y2,style)
{
var radius=3;
var twoPI=2*Math.PI;
ctx.save();
ctx.beginPath();
ctx.arc(x0,y0,radius,0,twoPI,false);
ctx.stroke();
ctx.beginPath();
ctx.arc(x1,y1,radius,0,twoPI,false);
ctx.stroke();
ctx.beginPath();
ctx.arc(x2,y2,radius,0,twoPI,false);
ctx.stroke();
ctx.restore();
}
(关于这一点很少说,它只是在每个点画一个圆圈。您可以像drawArrow(x1,y1,x2,y2,headDrawer)一样使用它(假设您默认选择哪个结束,长度和角度)。您可以在下面的愚蠢移动图中看到它正在使用中。如果你看到大的黑暗的东西,那是因为随机值的那个头的大小随机变得非常大。头部侧面与轴之间的随机角度也可能大于90度。如果你等待它会随机变小,或者角度会随机变小,或者你可以刷新以获得较小的起始值。)
对弧进行相同的操作
我们已经解决了所有问题,只需要找出传递给头部绘制方法的参数。要指出正确的方法,我们需要知道弧的末端所产生的角度。那是该点曲线的瞬时斜率。如果你有第一学期的微积分,你知道你可以从圆的等式的一阶导数得到它。以(a,b)为中心的圆的每个点满足等式 (x-a)2 + (y-b)2 = r2(平方)
推导出 2(x-a)+2(y-b)dy/dx=0.
推导出 dy/dx=(a-x)/(y-b)
请注意,带有x的部分位于顶部,即使我们通常在一条线上预期斜率是y的变化除以x的变化。没关系。数学不是谎言。稍后我们将调用atan2来获取角度,并且我们将从这个微积分应用中得到的这些值传递给它。谁说没有人需要微积分!
lineangle=Math.atan2(x-sx,sy-y)
在这种情况下,(x,y)将是中心,(sx,sy)将是弧上的终点。 atan2返回与(sx,sy)处的弧相切的线的角度。 所以给定弧线,如果我们能够找出终点,我们应该很容易找出指向箭头的方向。 我们将得到这样一个弧:
drawArcedArrow(ctx,x,y,r,startangle,endangle,anticlockwise,style,which)
ctx - 第二个绘图上下文
x,y - 弧所属圆的中心
r - 圆的半径
startangle - 弧开始的角度
endangle - 弧端的角度
anticlockwise - 如果逆时针绘制弧,则为布尔值true
style - 箭头的样式,如上面的drawArrow
which - 哪一端获得箭头,如上面的drawArrow
要用箭头绘制弧线,我们将重用我们编写的代码来调用它来绘制箭头。我们要做的是找到与弧的末端相切的线的角度,从末端向后移动10个像素并绘制10像素的线,具有10个像素的头。为了确保线条不显示,我们设置strokeStyle用于绘制如下行:
strokeStyle='rgba(0,0,0,0)';
rgba让我们设置线的alpha或不透明度。前三个值r,g和b无关紧要,因为我们将第四个值设置为0,这提供了完全的透明度,该行是不可见的。在图中,我离开了线,这样你就可以看到它位于切线上。
drawArcedArrow()
var drawArcedArrow=
function(ctx,x,y,r,startangle,endangle,anticlockwise,style,which,angle,d)
{
'use strict';
style=typeof(style)!='undefined'? style:3;
which=typeof(which)!='undefined'? which:1; // end point gets arrow
angle=typeof(angle)!='undefined'? angle:Math.PI/8;
d =typeof(d) !='undefined'? d :10;
(设置缺省值)
ctx.save();
ctx.beginPath();
ctx.arc(x,y,r,startangle,endangle,anticlockwise);
ctx.stroke();
(画弧)
var sx,sy,lineangle,destx,desty;
ctx.strokeStyle='rgba(0,0,0,0)'; // don't show the shaft
var origwhich=which;
(使我们的箭头轴不可见,并记住我们将添加的弧的哪一端。我们记得的原因是,我们从这里传递给drawArrow()例程的总是相同的。我们总是沿着弧线末端的切线向后拉,所以我们希望源端是箭头的末端。)
if(origwhich&1){ // draw the destination end
sx=Math.cos(startangle)r+x;
sy=Math.sin(startangle)r+y;
lineangle=Math.atan2(x-sx,sy-y);
if(anticlockwise){
destx=sx+10Math.cos(lineangle);
desty=sy+10Math.sin(lineangle);
}else{
destx=sx-10Math.cos(lineangle);
desty=sy-10Math.sin(lineangle);
}
drawArrow(ctx,sx,sy,destx,desty,style,2,angle,d);
}
(正如上面所讨论的,我们找出了终点,(sx,sy),我们用它来计算切线,线角的角度,然后我们计算出一个10像素的点。 最后,我们绘制一条从弧线末端到切线上10个像素点的箭头线,确保告诉drawArrow()指向我们来自的末端的箭头。)
if(origwhich&2){ // draw the origination end
sx=Math.cos(endangle)r+x;
sy=Math.sin(endangle)r+y;
lineangle=Math.atan2(x-sx,sy-y);
if(anticlockwise){
destx=sx-10Math.cos(lineangle);
desty=sy-10Math.sin(lineangle);
}else{
destx=sx+10Math.cos(lineangle);
desty=sy+10Math.sin(lineangle);
}
drawArrow(ctx,sx,sy,destx,desty,style,2,angle,d);
}
ctx.restore();
}
(这与另一端的代码工作方式相同,唯一的区别是我们使用endangle而不是起始角来找到弧的终点,而我们去寻找切线上的点的方向是相反的。)
感谢Ceason指出了一个问题,其中drawArrow的参数可能是一个只看起来像数字的字符串。添加将改为连接,结果将被用作真正的大数字:)谢谢Ceason!感谢Ryan Cook指出x1 = parseInt(x1);应该是x1 = parseInt(x1,10),以便前导零不会将字符串解析为八进制。