Canvas學習:繪制箭頭
在這篇文章中主要來聊在Canvas中怎么繪制箭頭。在Canvas的 CanvasRenderingContext2D 對象中是沒有提供繪制箭頭的方法,言外之意,在Canvas中要繪制箭頭是需要自己封裝函數來處理。那今天的主題就是來看怎么封裝繪制箭頭的函數。
了解一些基礎知識
平常我們常常看到的一些箭頭樣式如下圖所示:
在繪制箭頭最關鍵之處就是處理箭頭:
其包括幾個部分:
- 一條直線,從起點 P1 到終點 P2
- 終點 P2 向這條直線兩側擴展,將會產生一個 P3 和 P4 點
- 另外 P2P3 或者 P2P4 構成箭頭斜線率
- 箭頭斜線和直線有一個夾角 theta ( θ )
- 指定箭頭的長度 d
從上圖上我們可以看出,控制一個箭頭,可以通過這幾個參數來控制:
- 起點 P1 ( (fromX, fromY) )
- 終點 P2 ( (toX, toY) )
- 斜線率 headlen
- 夾角 theta ( θ )
對于箭頭的 P3 和 P4 點,我們就需要通過相應的 三角函數 計算得來。
那么 P3 的坐標可以輕易計算出來:
p3[0] = P2[0] - Math.cos(θ * Math.PI / 180); // P3對應的X坐標 p3[1] = p2[1] - Math.sin(θ * Math.PI / 180); // P3對應的Y坐標
用同樣的方法可以計算出 P4 坐標:
p4[0] = P2[0] - Math.cos(θ * Math.PI / 180); // P4對應的X坐標 p3[1] = p2[1] + Math.sin(θ * Math.PI / 180); // P4對應的Y坐標
除此之外,還有一個關鍵,就是箭頭的角度。獲取箭頭的角度,可以直接通過 atan2(y,x) 來獲取。這也就涉及到三角函數中的 反正切函數 。
在三角函數中,兩個參數的函數 atan2 是正切函數 tan 的一個變種。對于任意不同時等于 0 的實參數 x 和 y , atan2(y,x) 所表達的意思是坐標原點為起點,指向 (x,y) 的射線在坐標平面上與x軸正方向之間的角的角度。當 y>0 時,射線與 x 軸正方向的所得的角的角度指的是 x 軸正方向繞逆時針方向到達射線旋轉的角的角度;而當 y<0 時,射線與 x 軸正方向所得的角的角度指的是 x 軸正方向繞順時針方向達到射線旋轉的角的角度。
在幾何意義上, atan2(y, x) 等價于 atan(y/x) ,但 atan2 的最大優勢是可以正確處理 x=0 而 y≠0 的情況,而不必進行會引發除零異常的 y/x 操作。
簡單的用下圖來闡述:
在一個單位圓內 atan2 函數在各點的取值。圓內標注代表各點的取值的幅度表示。圖片中,從最左端開始,角度的大小隨著逆時針方向逐漸從 -π 增大到 +π ,并且角度大小在點位于最右端時,取值為 0 。另外要注意的是,函數 atan2(y,x) 中參數的順序是倒置的, atan2(y,x) 計算的值相當于點 (x,y) 的角度值。
簡單的了解了反正切函數,我們回到我們的主題中。
先來看一張圖:
通過 Math.atan2() 函數計算出 angle :
angle = Math.atan2(toY - fromY, toX - fromX)
為了和 θ 的單位值相匹配,將上面的公式進行一下轉換:
angle = Math.atan2(toY - fromY, toX - fromX) * 180 / Math.PI
除此之外,還需要計算出箭頭兩條側邊線的夾角:
angle1 = (angle + theta) * Math.PI / 180; angle2 = (angle - theta) * Math.PI / 180;
感覺有點零亂,其實我自己也瞎折騰了好幾天。
封裝繪制箭頭函數
通過前面的內容,可能對繪制箭頭有一定的理論基礎,接下來,我們看如何封裝箭頭函數。
drawArrow(ctx, fromX, fromY, toX, toY, theta, headlen, width, color)
這里我們傳了九個參數:
- ctx :Canvas繪圖環境
- fromX, fromY :起點坐標(也可以換成 p1 ,只不過它是一個數組)
- toX, toY :終點坐標 (也可以換成 p2 ,只不過它是一個數組)
- theta :三角斜邊一直線夾角
- headlen :三角斜邊長度
- width :箭頭線寬度
- color :箭頭顏色
根據前面的內容,我們可以這樣來寫這個函數:
function drawArrow(ctx, fromX, fromY, toX, toY,theta,headlen,width,color) { theta = typeof(theta) != 'undefined' ? theta : 30; headlen = typeof(theta) != 'undefined' ? headlen : 10; width = typeof(width) != 'undefined' ? width : 1; color = typeof(color) != 'color' ? color : '#000'; // 計算各角度和對應的P2,P3坐標 var angle = Math.atan2(fromY - toY, fromX - toX) * 180 / Math.PI, angle1 = (angle + theta) * Math.PI / 180, angle2 = (angle - theta) * Math.PI / 180, topX = headlen * Math.cos(angle1), topY = headlen * Math.sin(angle1), botX = headlen * Math.cos(angle2), botY = headlen * Math.sin(angle2); ctx.save(); ctx.beginPath(); var arrowX = fromX - topX, arrowY = fromY - topY; ctx.moveTo(arrowX, arrowY); ctx.moveTo(fromX, fromY); ctx.lineTo(toX, toY); arrowX = toX + topX; arrowY = toY + topY; ctx.moveTo(arrowX, arrowY); ctx.lineTo(toX, toY); arrowX = toX + botX; arrowY = toY + botY; ctx.lineTo(arrowX, arrowY); ctx.strokeStyle = color; ctx.lineWidth = width; ctx.stroke(); ctx.restore(); }
這個時候,只需要調用這個封裝好的函數,我們就可以輕松的繪制一條向右的箭頭:
drawArrow(ctx, 150, 100, 400,100,30,30,10,'#f36');
改變不同的坐標,可以得到不同方向的箭頭:
// 向右箭頭 drawArrow(ctx, myCanvas.width / 2, myCanvas.height / 2, myCanvas.width / 2 + 150, myCanvas.height / 2,30,30,4,'#f36'); // 向下箭頭 drawArrow(ctx, myCanvas.width / 2, myCanvas.height / 2, myCanvas.width / 2, myCanvas.height / 2 + 150,30,30,4,'#f66'); // 向左箭頭 drawArrow(ctx, myCanvas.width / 2, myCanvas.height / 2, myCanvas.width / 2 - 150, myCanvas.height / 2,30,30,4,'#0f6'); // 向上箭頭 drawArrow(ctx, myCanvas.width / 2, myCanvas.height / 2, myCanvas.width / 2, myCanvas.height / 2 - 150,30,30,4,'#d6f');
有的時候,我們需要線的兩頭都要有箭頭形狀,在上面的基礎上,稍加修改,增加一個反項的代碼即可:
function drawArrow(ctx, fromX, fromY, toX, toY, theta, headlen, width, color) { theta = typeof(theta) != 'undefined' ? theta : 30; headlen = typeof(theta) != 'undefined' ? headlen : 10; width = typeof(width) != 'undefined' ? width : 1; color = typeof(color) != 'color' ? color : '#000'; var angle = Math.atan2(fromY - toY, fromX - toX) * 180 / Math.PI, angle1 = (angle + theta) * Math.PI / 180, angle2 = (angle - theta) * Math.PI / 180, topX = headlen * Math.cos(angle1), topY = headlen * Math.sin(angle1), botX = headlen * Math.cos(angle2), botY = headlen * Math.sin(angle2); ctx.save(); ctx.beginPath(); var arrowX = fromX - topX, arrowY = fromY - topY; ctx.moveTo(arrowX, arrowY); ctx.lineTo(fromX, fromY); arrowX = fromX - botX; arrowY = fromY - botY; ctx.lineTo(arrowX, arrowY); ctx.moveTo(fromX, fromY); ctx.lineTo(toX, toY); // Reverse length on the other side arrowX = toX + topX; arrowY = toY + topY; ctx.moveTo(arrowX, arrowY); ctx.lineTo(toX, toY); arrowX = toX + botX; arrowY = toY + botY; ctx.lineTo(arrowX, arrowY); ctx.strokeStyle = color; ctx.lineWidth = width; ctx.stroke(); ctx.restore(); }
調用函數:
drawArrow(ctx, myCanvas.width / 2 - 200, myCanvas.height / 2, myCanvas.width / 2 + 200,myCanvas.height / 2,30,30,5,'#f36');
看到的效果如下:
上面我們看到的僅是一種箭頭方式,文章開頭,提到箭頭方式有多種方式。那么我們可以將 drawArrow 功強變得更為強大一些。比如@Patrick Horgan在他的文章中提到的方法:
- drawHead :封裝一個專門繪制箭頭頭部的函數,而且提供了四種樣式做為選擇
- drawArrow : 封裝直線箭頭,并且提供兩個方向
- drawArcedArrow :函數一個曲線箭頭
由于代碼較多,這里就不展示出來了,不過可以在對應的 CodePen示例 中查看到代碼:
總結
這篇文章主要介紹了通過三角函數的一些知識,封裝了一個箭頭函數,用來幫助我們在Canvas中更輕易的繪制出箭頭。因為在Canvas中沒有直接提供繪制箭頭的函數或者說方法。那么三角在實際中有什么哪些運用呢?大家可以發揮想象力,思考一下,寫寫實例。在最后一個方法中,我們其實還運用到了Canvas中的貝塞爾曲線繪制的方法。在下一節中,我們就來學習在Canvas中怎么繪制貝塞爾曲線。
來自:http://www.w3cplus.com/canvas/drawing-arrow.html