1. 程式人生 > >canvas繪製圖像輪廓效果

canvas繪製圖像輪廓效果

在2d圖形視覺化開發中,經常要繪製物件的選中效果。 一般來說,表達物件選中可以使用邊框,輪廓或者發光的效果。  發光的效果,可以使用canvas的陰影功能,比較容易實現,此處不在贅述。 # 繪製邊框 繪製邊框是最容易實現的效果,比如下面的圖片 ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f0ec2f32e0b84431ae3f8b499a8ca9bf~tplv-k3u1fbpfcp-zoom-1.image) 要繪製邊框,只需要使用strokeRect的方式即可。效果如下圖所示: ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/90820249311e4ad1a5ca99442c71a399~tplv-k3u1fbpfcp-zoom-1.image) 這個程式碼也很簡單,如下所示: ``` ctx1.strokeStyle = "red"; ctx1.lineWidth = 2; ctx1.drawImage(img, 1, 1,img.width ,img.height) ctx1.strokeRect(1,1,img.width,img.height); ``` # 繪製輪廓 問題是,簡單粗暴的加一個邊框,並不能滿足需求。很多時候,人們需要的是輪廓的效果,也就是圖片的有畫素和無畫素的邊緣處。如下圖的效果所示: ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/7b3638a679084dc5a3f1afc27f224b8b~tplv-k3u1fbpfcp-zoom-1.image) 要實現上述效果,最容易想到的思路就是通過畫素的計算來判斷邊緣,並對邊緣進行特定顏色的畫素填充。但是畫素的計算演算法並不容易,簡單的演算法又很難達到預期的效果,而且由於逐畫素操作,效率不高。 考慮到在三維webgl中,計算輪廓的演算法思路是這樣的: 1. 先繪製三維模型自身,並在繪製的時候啟動模板測試,把三維影象儲存到模板緩衝中。 1. 把模型適當放大,用純屬繪製模型,並在繪製的時候啟用模板測試,和之前的模板緩衝區中的畫素進行比較,如果對應的座標處在之前模板緩衝區中有畫素,就不繪製純色。 依據上述的原理,就可以繪製處三維物件的輪廓了。下面是一個示例效果,(參考[https://stemkoski.github.io/Three.js/Outline.html](https://stemkoski.github.io/Three.js/Outline.html)) ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/356894050ab24c99a1f4e1844c3521d6~tplv-k3u1fbpfcp-zoom-1.image) 在2d canvas裡面有類似的原理可以實現輪廓效果,就是使用globalCompositeOperation了。 大體思路是這樣的: 1. 首先繪製放大一些的圖片。 1. 然後開啟globalCompositeOperation = 'source-in', 並用純色填充整個canvas區域,由於source-in的效果,純色會填充放大圖片有畫素的區域。 1. 使用預設的globalCompositeOperation(source-over),用原始尺寸繪製圖片。 ## 繪製放大一些的圖片 通過drawImage的引數可以控制繪製圖片的大小,如下所示,drawImage有幾個形式: ``` 1 void ctx.drawImage(image, dx, dy); 2 void ctx.drawImage(image, dx, dy, dWidth, dHeight); 3 void ctx.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight); ``` 其中dx,dy 代表繪製的起始位置,一般繪製的時候使用第一個方法,代表繪製的大小就是原本圖片的大小。而使用第二個方法,我們可以指定繪製的尺寸,我們可以使用第二個方法繪製放大的圖片,程式碼如所示: ``` ctx.drawImage(img, p - s, p - s, w + 2 * s, h+ 2 * s); ``` 其中p代表圖片本身的繪製位置,s代表向左,向上的偏移量,同時圖片的寬和高都增加 2 * s ## 用純色填充放大圖片的區域 在上一步繪製的基礎上,開啟globalCompositeOperation = 'source-in', 並用純色填充整個canvas區域。 程式碼如下所示: ``` // fill with color ctx.globalCompositeOperation = "source-in"; ctx.fillStyle = "#FF0000"; ctx.fillRect(0, 0, cw, ch); ``` 最終的效果如下圖所示: ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/773404e7f144411d9f388afe28da9e2d~tplv-k3u1fbpfcp-zoom-1.image) 為什麼會出現這種效果是因為使用了globalCompositeOperation = 'source-in',具體原理可以參考本人的其他文章。 ## 繪製原始圖片 最後一步就是繪製原始圖片,程式碼如下所示: ``` ctx.globalCompositeOperation = "source-over"; ctx.drawImage(img, p, p, w, h); ``` 首先恢復globalCompositeOperation為預設值 "source-over",然後按照原本的大小繪製圖片。 經過以上步驟,最終的效果如下圖所示: ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/828bf29141304d669f0dc84866232366~tplv-k3u1fbpfcp-zoom-1.image) 可以看出最終獲得了我們要的效果。 ## 只顯示輪廓 如果我們只想得到圖片的輪廓,則可以在最後繪製的時候,globalCompositeOperation 設定為“destination-out”,程式碼如下: ``` ctx.globalCompositeOperation = "destination-out"; ctx.drawImage(img, p, p, w, h); ``` 效果圖如下: ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/41923b73d1094cd9a646a8e3f72d8cd0~tplv-k3u1fbpfcp-zoom-1.image) ## 輪廓粗細不一致的問題 上面的演算法實現,是在圖片的有畫素值區域中心和圖片本身的幾何中心基本一直,如果圖片的有畫素值的中心和圖片本身的幾何中心相差比較大,則會出現輪廓粗細不一致的情況,比如下面這張圖: ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8554d81ed23e4e15925644e4a5713c69~tplv-k3u1fbpfcp-zoom-1.image) 上半部分是透明的,下半部分是非透明的,畫素的中心在3/4出,而幾何中心在1/2處。使用上面的演算法,該圖片的輪廓如下: ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/286cd10773d949c595632f01d11e211c~tplv-k3u1fbpfcp-zoom-1.image) 可以發現上邊緣的輪廓寬度變成了0。 在比如下圖, ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/074b7e35bd454c2fa71b116dd6a62858~tplv-k3u1fbpfcp-zoom-1.image) 繪製後上邊緣的輪廓比其他邊緣的細。 ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ab17dbf9667545de8b25ddb206f90fd9~tplv-k3u1fbpfcp-zoom-1.image) 怎麼處理這種情況呢?可以在繪製放大圖片的時候,不直接使用縮放,而是在上下左右,上左,上右,下左,下右幾個方向進行偏移繪製,多次繪製,程式碼如下: ``` var dArr = [-1, -1, 0, -1, 1, -1, -1, 0, 1, 0, -1, 1, 0, 1, 1, 1], // offset array // draw images at offsets from the array scaled by s for (var i = 0; i < dArr.length; i += 2) { ctx.drawImage(img, p + dArr[i] * s, p + dArr[i + 1] * s, w, h); } ``` 再看上面圖片的輪廓效果,如下所示: ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/53e95443c89b4e8c95f7068ed3a90b64~tplv-k3u1fbpfcp-zoom-1.image) ## 半透明的情況 我在其他文章中說過,globalCompositeOperation為"source-in"的時候,source圖形的透明度,會影響到目標繪製圖形的透明度。所以會導致輪廓的畫素值會乘以透明度。比如,我們在繪製放大圖的時候,設定globalAlpha = 0.5進行模擬。 最後的繪製效果如下: ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d2429f3b67b549c4a83bf5788bb1a755~tplv-k3u1fbpfcp-zoom-1.image) 可以看到輪廓的顏色變淺了,解決辦法就是多繪製幾次放大圖。比如: ``` ctx.globalAlpha = 0.5; ctx.drawImage(img, p - s, p - s, w + 2 * s, h+ 2 * s); ctx.drawImage(img, p - s, p - s, w + 2 * s, h+ 2 * s); ``` 而上面通過偏移的方式繪製的時候,本身都繪製了好多遍,所以不存在這個問題。如下: ``` ctx.globalAlpha = 0.5; for (var i = 0; i < dArr.length; i += 2) { ctx.drawImage(img, p + dArr[i] * s, p + dArr[i + 1] * s, w, h); } ``` 如下圖所示: ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/cfe45a37a17f4ca6916d361ae767bfc9~tplv-k3u1fbpfcp-zoom-1.image) 當然,在透明度很低的情況下,使用繪製很多遍的方式,不是很好的解決方案。 # 使用演算法(marching-squares-algorithm) 上面的方法對於有些圖片效果就很不好,比如這張圖片: ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ffd60806344c4eed979be984f02d50d0~tplv-k3u1fbpfcp-zoom-1.image) 由於其有很多中空的效果,所以其最終效果如下圖所示: ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/01b225ee1e8045f88ac87d24426d88cd~tplv-k3u1fbpfcp-zoom-1.image) 但是想要的只是外部的輪廓,而不需要中空部分也繪製上輪廓效果。此時需要使用其他的演算法。 直接使用marching squares algorithm 可以獲取圖片的邊緣。這一塊的演算法具體實現本文不再講解,後續有機會單獨一篇文章進行講解。 此處直接使用開源的實現。比如可以使用  [https://github.com/sakri/MarchingSquaresJS](https://github.com/sakri/MarchingSquaresJS),程式碼如下: ``` function drawOuttline2(){ var canvas = document.createElement('canvas'); var ctx = canvas.getContext('2d'); var w = img.width; var h = img.height; canvas.width = w; canvas.height = h; ctx.drawImage(img, 0, 0, w, h); var pathPoints = MarchingSquares.getBlobOutlinePoints(canvas); var points = []; for(var i = 0;i < pathPoints.length;i += 2){ points.push({ x:pathPoints[i], y:pathPoints[i + 1], }) } // ctx.clearRect(0, 0, w, h); ctx.beginPath(); ctx.lineWidth = 2; ctx.strokeStyle = '#00CCFF'; ctx.moveTo(points[0].x, points[0].y); for (var i = 1; i < points.length; i += 1) { var point = points[i]; ctx.lineTo(point.x,point.y); } ctx.closePath(); ctx.stroke(); ctx1.drawImage(canvas,0,0); } ``` 首先使用呼叫MarchingSquaresJS的方法獲取img影象的輪廓點的集合,然後把所有的點連線起來。形成輪廓圖,最終效果如下: ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/87977f32342343908f5edec3310b7d16~tplv-k3u1fbpfcp-zoom-1.image) 不過可以看出,MarchingSquares 演算法獲得的輪廓效果鋸齒相對較多的。有光這塊演算法的優化,本文不講解。 # 總結 對於沒有中空效果的圖片,我們一般不採用MarchingSquares演算法,而採用前面的一種方式來實現,效率高,而且效果相對更好。 而對於有中空,就會使用MarchingSquares演算法,效果相對差,效率也相對低一些,實際應用中,可以通過快取來降低效能的損耗。 本文的起源來資源一個2.5D專案,上一張專案圖吧: ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/68630de38d9c4c7f9a24677a86f772a7~tplv-k3u1fbpfcp-zoom-1.image) # 參考文件 [https://www.emanueleferonato.com/2013/03/01/using-marching-squares-algorithm-to-trace-the-contour-of-an-image/](https://www.emanueleferonato.com/2013/03/01/using-marching-squares-algorithm-to-trace-the-contour-of-an-image/) [https://github.com/sakri/MarchingSquaresJS](https://github.com/sakri/MarchingSquaresJS) [https://github.com/OSUblake/msqr](https://github.com/OSUblake/msqr) [http://users.polytech.unice.fr/~lingrand/MarchingCubes/algo.html#squar](http://users.polytech.unice.fr/~lingrand/MarchingCubes/algo.html#squar) 如果對視覺化感興趣, 關注公號“ITMan彪叔” 可以及時收到更多有價值的文章。也可以加微信541002349進行交