程式寫多了,什麼死人骨頭都可能遇到題材都有機會玩到。最近在寫電子表單流程圖模組,根本是在複習國中數學: sin、cos、兩點距離...  被幾何邏輯搞到昏頭,在草稿紙畫了一堆三角形示意圖還是似懂非懂 orz(數學老師站在我背後,他非常火)

其中有個需求: 用線條連接矩形中心與外部點,但線條需由矩形邊框開始畫起。換句話說,線條在矩形內部的部分隱形,只有與邊框交點到外部點間要畫線。原本已用一堆if else硬幹搞定,但想想還是該用幾何函數解決才會優雅。無奈數學天分不佳,只靠大腦模擬搞不出名堂,心一橫乾脆來寫個繪圖小程式即時顯示計算結果,再依此調整演算法,比全憑抽象思考簡單,適合我這種大腦不夠力的阿呆。測試程式決定用JavaScript寫,順便複習HTML5 Canvas,好個一石二鳥之計。

測試程式成品如下: Online Demo (果然,測試程式剛寫完,演算法也試出來了)

完整程式碼如下:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>計算矩形中心點連線與邊框交點</title>
</head>
<body>
  <canvas id="cSketchPad" width="640" height="480" style="border: 2px solid gray" />
  <script src="//code.jquery.com/jquery-1.11.0.min.js"></script>
  <script>
    var $canvas = $("#cSketchPad");
    var ctx = $canvas[0].getContext("2d");
    var box = { x: 100, y: 150, w: 140, h: 90 };
    var st = { x: box.x + box.w / 2, y: box.y + box.h / 2 };
    function init() {
      //清空背景
      ctx.fillStyle = "white";
      ctx.fillRect(0, 0, $canvas.width(), $canvas.height());
      //繪製矩形
      ctx.beginPath(); 
      ctx.rect(box.x, box.y, box.w, box.h);
      ctx.strokeStyle = "red"; 
      ctx.stroke();
      //繪製中心點
      ctx.fillStyle = "black";
      ctx.fillRect(st.x - 1, st.y - 1, 3, 3);
      ctx.save();
    }
    init();
    var ed, pos = $canvas.position(), mx = pos.left, my = pos.top;
    $canvas.mousedown(function(e) {
      init();
      ed = { x: e.pageX - mx, y: e.pageY - my };
      //繪製目標點
      ctx.fillStyle="black";
      ctx.fillRect(ed.x - 1, ed.y - 1, 3, 3);
      //計算與邊框相交點
      var pnt = findEdgePoint(st, ed, box.w, box.h);
      
      drawLine(st, pnt, "#ccc");
      drawCross(pnt, "black");
      drawLine(pnt, ed, "blue", true);
      
    });
 
    function drawCross(p, c) {
      var d = 3;
      drawLine({ x: p.x - d, y: p.y - d }, { x: p.x + d, y: p.y + d }, c);
      drawLine({ x: p.x + d, y: p.y - d }, { x: p.x - d, y: p.y + d }, c);
    }
    
    function drawLine(s, e, c, arrow) {
      ctx.beginPath();
      ctx.moveTo(s.x, s.y);
      ctx.lineTo(e.x, e.y);
      ctx.strokeStyle = c;
      ctx.stroke();
      //畫箭頭
      if (arrow) {
        ctx.save();
        ctx.fillStyle = c;
        ctx.translate(e.x, e.y);
        var ang = Math.atan2(e.y - s.y, e.x - s.x) + Math.PI / 2;
        ctx.rotate(ang);
        ctx.moveTo(0, 0);
        ctx.lineTo(-5, 10);
        ctx.lineTo(5, 10);
        ctx.closePath();
        ctx.fill();
        ctx.restore();
      }
    }
    
    Math.sign = function(n) { return n == 0 ? 0 : n / Math.abs(n); }
    function findEdgePoint(src, dst, w, h)
    {
      var dy = dst.y - src.y;
      var dx = dst.x - src.x;
      //計算斜率
      var ang = Math.atan2(dy, dx);
      //對角線斜率
      var a1 = Math.atan2(h, w), a2 = Math.PI - a1;
      //計算交點到中心的長度
      var l =
        (ang >= -a1 && ang <= a1 || ang >= a2 || ang <= -a2 ) ?
         Math.sign(dx) * w / Math.cos(ang): //交點在左右側時X軸長度要等於正負w
         Math.sign(dy) * h / Math.sin(ang); //交點在上下側時Y軸長度要等於正負h
      //顯示角度(Debug用)     
      ctx.fillText(ang * 180 / Math.PI, 12, 12);
      //計算交點座標
      var tx = src.x + l * Math.cos(ang) / 2;
      var ty = src.y + l * Math.sin(ang) / 2;
      return { x: tx, y: ty };
    }
  </script>
</body>
</html>

簡單補充:

  1. 在使用rect(), moveTo(), lineTo()時,記得一開始先beginPath()宣告為一段新Path,最後用stroke()繪線時,beginPath()之後的整段路徑會塗成同一顏色。
  2. 箭頭的畫法:
    先store()保留沒有位移、旋轉的狀態,用translate()將位置移至線條的末端,之後以(0, 0)為起點用moveTo()、lineTo()、closePath()圍出三角形,並以rotate()旋轉至與線條水平,最後以fill()塗色。完成後記得要restore()取消旋轉及位移,否則再畫其他元素時座標與角度會跑掉。
  3. 在計算與邊框交點時,需判斷線條觸及上、下、左、右的哪一邊而運算參數不同。我用的方法是找出矩形對角線的四個角度,如下圖中的-32.7、147.3、-147.3、-32.7。當中心與外部點連線的角度介於-32.7與32.7間、小於-147.3,或大於147.3時代表交點在左右側,此時要使 灰線長度 * cos(線條角度) == 矩形寬度的一半,所以灰線長度等於矩形半寬除以cos(線條角度);反之,若交點在上下側,灰線長度要等於矩形半高除以sin(線條角度)。透過這個演算法找出灰線長度,乘上cos及sin就能找出線條與邊框的交點。

Comments

# by 胡忠晞

有一個簡單的方式可以處理這個問題,先畫線再畫方框,用遮蓋的原理就可以做到了,自己算雖然是練功的好機會,但以後維護會很辛苦。

# by Jeffrey

to 胡忠晞,一開始有考慮用覆蓋法,不過因為後面還有進階需求--要沿著線條路徑跑動畫,該來的跑不掉,還是得乖乖把位置算出來~ 謝謝提醒。

# by 胡忠晞

需求沒想像中單純,你辛苦了!

# by CADCH

很利害,把位置算出來是王道~後面作甚麼都方便多了。

Post a comment