Canvas 教程:如何绘制带箭头的曲线
這篇文章要解決一個問題,就是給定 HTML 中任意一個點(起點)和另一個點(終點),繪制一條帶箭頭的曲線。
廢話不多說,直奔主題。
我們只有兩個點的相對偏移量(offset),思路就是以這兩個點作為對角,創建一個絕對定位的 Canvas,然后在兩點中繪制一條曲線(Curve),最后在終點處繪制箭頭(Arrow)。
因此分為 3 步:
創建適當的 Canvas
先確定 Canvas 的絕對定位偏移量,因為是任意兩點,所以對角可能是左上加右下,也可能是左下加右上,不論是哪一種,它的左偏移量一定是兩個點的左偏移量的最小值,同理,上偏移量也是兩個點的上偏移量的最小值。
再確定 Canvas 的寬和高,寬等于兩點左偏移量之差的模,長等于兩點上偏移量之差的模。
// 隨機的起始點和終點,這里不考慮邊緣情況,實際生產環境下,相近的兩點應該很少會有加指向性箭頭的需求 const sp = { left: Math.floor(window.innerWidth * Math.random()), top: Math.floor(window.innerHeight * Math.random()) }; const ep = { left: Math.floor(window.innerWidth * Math.random()), top: Math.floor(window.innerHeight * Math.random()) };const canvas = document.createElement('canvas'); canvas.style.position = 'absolute'; // 設置絕對定位 canvas.style.left = Math.min(sp.left, ep.left) + 'px'; // 設置左偏移量 canvas.style.top = Math.min(sp.top, ep.top) + 'px'; // 設置右偏移量 canvas.width = Math.abs(sp.left - ep.left); // 設置寬度 canvas.height = Math.abs(sp.top - ep.top); // 設置高度// 順便為 Canvas 加個紅色的邊框,方便 debug canvas.style.border = '1px solid red';// 把 Canvas 放到 body 中 document.body.appendChild(canvas);繪制曲線
Canvas 中繪制曲線很簡單,API 中已經提供了貝塞爾曲線(Bezier Curve)的繪制方法。
而控制點的掌握…全靠經驗 😃
這里提供一個很簡單,很好算的控制點,繪制出的曲線效果也非常好。
const ctx = canvas.getContext('2d'); // 獲取 Canvas 上下文// 下面求各點在 Canvas 中的坐標 sp.x = sp.left - Math.min(sp.left, ep.left); sp.y = sp.top - Math.min(sp.top, ep.top); ep.x = ep.left - Math.min(sp.left, ep.left); ep.y = ep.top - Math.min(sp.top, ep.top);// 算貝塞爾曲線的控制點坐標,很簡單,只需要把起始點和終點的 x 相加除以 3, y 永遠和起始點的 y 一致 // 這樣向左和向右的箭頭不會是一樣的曲線,顯得不那么死板 const cp = {x: (sp.x + ep.x) / 3,y: sp.y };ctx.beginPath(); ctx.moveTo(sp.x, sp.y); ctx.quadraticCurveTo(cp.x, cp.y, ep.x, ep.y); ctx.strokeStyle = '#FB9845'; ctx.lineWidth = '3'; ctx.stroke(); ctx.closePath();// 繪制出控制點到終點的連線,方便 debug ctx.beginPath(); ctx.moveTo(cp.x, cp.y); ctx.lineTo(ep.x, ep.y); ctx.strokeStyle = 'red'; ctx.lineWidth = '1'; ctx.stroke(); ctx.closePath();繪制箭頭
繪制箭頭的步驟稍微復雜一點點,因為涉及到數學運算。
本人對貝塞爾曲線并沒有深入的研究,但是通過觀察發現控制點到終點的連線近似曲線在終點處的切線,可以作為箭頭的中線來使用。
所以問題被轉化為求控制點順時針和逆時針旋轉特定角度后的坐標。這個角度我們取 20,別問,問就是好看。
涉及到旋轉,就要理解參照系,為了簡化計算,我們把終點作為原點,那么終點在不同的角上,我們所使用的坐標系是不同的,因此需要有坐標轉換的方法。
// 把 Canvas 坐標轉換成旋轉計算所使用的坐標,接收 1 個參數,需要轉換的點 p function coordEx(p) {const result = {};if (ep.x < sp.x && ep.y < sp.y) {result.x = p.x;result.y = p.y;} else if (ep.x < sp.x && ep.y > sp.y) {result.x = p.x;result.y = Math.abs(sp.top - ep.top) - p.y;} else if (ep.x > sp.x && ep.y < sp.y) {result.x = Math.abs(sp.left - ep.left) - p.x;result.y = p.y;} else if (ep.x > sp.x && ep.y > sp.y) {result.x = Math.abs(sp.left - ep.left) - p.x;result.y = Math.abs(sp.top - ep.top) - p.y;}return result; }// 把旋轉計算用的坐標轉換回 Canvas 坐標,用于繪圖 function coordRe(p) {const result = {};if (ep.x < sp.x && ep.y < sp.y) {result.x = p.x;result.y = p.y;} else if (ep.x < sp.x && ep.y > sp.y) {result.x = p.x;result.y = Math.abs(sp.top - ep.top) - p.y;} else if (ep.x > sp.x && ep.y < sp.y) {result.x = Math.abs(sp.left - ep.left) - p.x;result.y = p.y;} else if (ep.x > sp.x && ep.y > sp.y) {result.x = Math.abs(sp.left - ep.left) - p.x;result.y = Math.abs(sp.top - ep.top) - p.y;}return result; }有了轉換后的坐標就可以開始計算了,向量關于原點的逆時針旋轉計算公式:
向量關于原點的順時針旋轉計算公式:
const CURVE_ARROW_ANGLE = 20; // 旋轉的角度 const CURVE_ARROW_LENGTH = 26; // 繪制箭頭線段的長度 const ncp = coordEx(cp); // 轉換控制點坐標// 計算逆時針旋轉后的坐標 const nlp = {x: ncp.x * Math.cos((CURVE_ARROW_ANGLE * Math.PI) / 180) - ncp.y * Math.sin((CURVE_ARROW_ANGLE * Math.PI) / 180),y: ncp.x * Math.sin((CURVE_ARROW_ANGLE * Math.PI) / 180) + ncp.y * Math.cos((CURVE_ARROW_ANGLE * Math.PI) / 180) };// 計算箭頭線段長度和 nlp 到原點距離的比值,用于計算繪制箭頭的坐標 const lRate = CURVE_ARROW_LENGTH / Math.sqrt(nlp.x * nlp.x + nlp.y * nlp.y);// 把 nlp 的坐標轉換為繪制箭頭的坐標 nlp.x = nlp.x * lRate; nlp.y = nlp.y * lRate;// 把繪制箭頭的坐標轉換回 Canvas 坐標 const lArrowPoint = coordRe(nlp); ctx.beginPath(); ctx.moveTo(lArrowPoint.x, lArrowPoint.y); ctx.lineTo(ep.x, ep.y); ctx.strokeStyle = '#FB9845'; ctx.lineWidth = '3'; ctx.stroke();以上是繪制逆時針箭頭的方法,同樣的方法可以繪制順時針箭頭,如果順利的話,現在你看到的圖像應該是這樣的:
也有可能不順利…
這是繪制超出了 canvas 的范圍,因此需要為 canvas 添加 padding。
添加 padding
可以通過增加 canvas 的長寬,同時調用 tranlate 方法來解決。
const PADDING = 20;// 修改上面設置 canvas 寬高的代碼 canvas.width = Math.abs(sp.left - ep.left) + PADDING * 2; // 設置寬度 canvas.height = Math.abs(sp.top - ep.top) + PADDING * 2; // 設置高度// 修改上面設置 canvas 偏移量的代碼 canvas.style.left = Math.min(sp.left, ep.left) - PADDING + 'px'; // 設置左偏移量 canvas.style.top = Math.min(sp.top, ep.top) - PADDING + 'px'; // 設置右偏移量// 并在獲取 canvas 上下文之后,設置 tranlate ctx.tranlate(PADDING, PADDING);最后
注釋掉輔助線代碼,即可獲得一條完美的帶箭頭的曲線。
源碼請在個人網站底部獲取
總結
以上是生活随笔為你收集整理的Canvas 教程:如何绘制带箭头的曲线的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: setup 命令
- 下一篇: java用虹软人脸识别SDK实现人脸识别