Canvas 教程:如何绘制带箭头的曲线
這篇文章要解決一個(gè)問(wèn)題,就是給定 HTML 中任意一個(gè)點(diǎn)(起點(diǎn))和另一個(gè)點(diǎn)(終點(diǎn)),繪制一條帶箭頭的曲線。
廢話不多說(shuō),直奔主題。
我們只有兩個(gè)點(diǎn)的相對(duì)偏移量(offset),思路就是以這兩個(gè)點(diǎn)作為對(duì)角,創(chuàng)建一個(gè)絕對(duì)定位的 Canvas,然后在兩點(diǎn)中繪制一條曲線(Curve),最后在終點(diǎn)處繪制箭頭(Arrow)。
因此分為 3 步:
創(chuàng)建適當(dāng)?shù)?Canvas
先確定 Canvas 的絕對(duì)定位偏移量,因?yàn)槭侨我鈨牲c(diǎn),所以對(duì)角可能是左上加右下,也可能是左下加右上,不論是哪一種,它的左偏移量一定是兩個(gè)點(diǎn)的左偏移量的最小值,同理,上偏移量也是兩個(gè)點(diǎn)的上偏移量的最小值。
再確定 Canvas 的寬和高,寬等于兩點(diǎn)左偏移量之差的模,長(zhǎng)等于兩點(diǎn)上偏移量之差的模。
// 隨機(jī)的起始點(diǎn)和終點(diǎn),這里不考慮邊緣情況,實(shí)際生產(chǎn)環(huán)境下,相近的兩點(diǎn)應(yīng)該很少會(huì)有加指向性箭頭的需求 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'; // 設(shè)置絕對(duì)定位 canvas.style.left = Math.min(sp.left, ep.left) + 'px'; // 設(shè)置左偏移量 canvas.style.top = Math.min(sp.top, ep.top) + 'px'; // 設(shè)置右偏移量 canvas.width = Math.abs(sp.left - ep.left); // 設(shè)置寬度 canvas.height = Math.abs(sp.top - ep.top); // 設(shè)置高度// 順便為 Canvas 加個(gè)紅色的邊框,方便 debug canvas.style.border = '1px solid red';// 把 Canvas 放到 body 中 document.body.appendChild(canvas);繪制曲線
Canvas 中繪制曲線很簡(jiǎn)單,API 中已經(jīng)提供了貝塞爾曲線(Bezier Curve)的繪制方法。
而控制點(diǎn)的掌握…全靠經(jīng)驗(yàn) 😃
這里提供一個(gè)很簡(jiǎn)單,很好算的控制點(diǎn),繪制出的曲線效果也非常好。
const ctx = canvas.getContext('2d'); // 獲取 Canvas 上下文// 下面求各點(diǎn)在 Canvas 中的坐標(biāo) 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);// 算貝塞爾曲線的控制點(diǎn)坐標(biāo),很簡(jiǎn)單,只需要把起始點(diǎn)和終點(diǎn)的 x 相加除以 3, y 永遠(yuǎn)和起始點(diǎn)的 y 一致 // 這樣向左和向右的箭頭不會(huì)是一樣的曲線,顯得不那么死板 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();// 繪制出控制點(diǎn)到終點(diǎn)的連線,方便 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();繪制箭頭
繪制箭頭的步驟稍微復(fù)雜一點(diǎn)點(diǎn),因?yàn)樯婕暗綌?shù)學(xué)運(yùn)算。
本人對(duì)貝塞爾曲線并沒(méi)有深入的研究,但是通過(guò)觀察發(fā)現(xiàn)控制點(diǎn)到終點(diǎn)的連線近似曲線在終點(diǎn)處的切線,可以作為箭頭的中線來(lái)使用。
所以問(wèn)題被轉(zhuǎn)化為求控制點(diǎn)順時(shí)針和逆時(shí)針旋轉(zhuǎn)特定角度后的坐標(biāo)。這個(gè)角度我們?nèi)?20,別問(wèn),問(wèn)就是好看。
涉及到旋轉(zhuǎn),就要理解參照系,為了簡(jiǎn)化計(jì)算,我們把終點(diǎn)作為原點(diǎn),那么終點(diǎn)在不同的角上,我們所使用的坐標(biāo)系是不同的,因此需要有坐標(biāo)轉(zhuǎn)換的方法。
// 把 Canvas 坐標(biāo)轉(zhuǎn)換成旋轉(zhuǎn)計(jì)算所使用的坐標(biāo),接收 1 個(gè)參數(shù),需要轉(zhuǎn)換的點(diǎn) 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; }// 把旋轉(zhuǎn)計(jì)算用的坐標(biāo)轉(zhuǎn)換回 Canvas 坐標(biāo),用于繪圖 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; }有了轉(zhuǎn)換后的坐標(biāo)就可以開始計(jì)算了,向量關(guān)于原點(diǎn)的逆時(shí)針旋轉(zhuǎn)計(jì)算公式:
向量關(guān)于原點(diǎn)的順時(shí)針旋轉(zhuǎn)計(jì)算公式:
const CURVE_ARROW_ANGLE = 20; // 旋轉(zhuǎn)的角度 const CURVE_ARROW_LENGTH = 26; // 繪制箭頭線段的長(zhǎng)度 const ncp = coordEx(cp); // 轉(zhuǎn)換控制點(diǎn)坐標(biāo)// 計(jì)算逆時(shí)針旋轉(zhuǎn)后的坐標(biāo) 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) };// 計(jì)算箭頭線段長(zhǎng)度和 nlp 到原點(diǎn)距離的比值,用于計(jì)算繪制箭頭的坐標(biāo) const lRate = CURVE_ARROW_LENGTH / Math.sqrt(nlp.x * nlp.x + nlp.y * nlp.y);// 把 nlp 的坐標(biāo)轉(zhuǎn)換為繪制箭頭的坐標(biāo) nlp.x = nlp.x * lRate; nlp.y = nlp.y * lRate;// 把繪制箭頭的坐標(biāo)轉(zhuǎn)換回 Canvas 坐標(biāo) 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();以上是繪制逆時(shí)針箭頭的方法,同樣的方法可以繪制順時(shí)針箭頭,如果順利的話,現(xiàn)在你看到的圖像應(yīng)該是這樣的:
也有可能不順利…
這是繪制超出了 canvas 的范圍,因此需要為 canvas 添加 padding。
添加 padding
可以通過(guò)增加 canvas 的長(zhǎng)寬,同時(shí)調(diào)用 tranlate 方法來(lái)解決。
const PADDING = 20;// 修改上面設(shè)置 canvas 寬高的代碼 canvas.width = Math.abs(sp.left - ep.left) + PADDING * 2; // 設(shè)置寬度 canvas.height = Math.abs(sp.top - ep.top) + PADDING * 2; // 設(shè)置高度// 修改上面設(shè)置 canvas 偏移量的代碼 canvas.style.left = Math.min(sp.left, ep.left) - PADDING + 'px'; // 設(shè)置左偏移量 canvas.style.top = Math.min(sp.top, ep.top) - PADDING + 'px'; // 設(shè)置右偏移量// 并在獲取 canvas 上下文之后,設(shè)置 tranlate ctx.tranlate(PADDING, PADDING);最后
注釋掉輔助線代碼,即可獲得一條完美的帶箭頭的曲線。
源碼請(qǐng)?jiān)趥€(gè)人網(wǎng)站底部獲取
總結(jié)
以上是生活随笔為你收集整理的Canvas 教程:如何绘制带箭头的曲线的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: setup 命令
- 下一篇: 小程序电商直播有什么优势?