Canvas學習:繪制箭頭

beyondqxch 7年前發布 | 30K 次閱讀 前端技術 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

 

 本文由用戶 beyondqxch 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!