AntV Canvas 局部渲染總結

Canvas 局部渲染優化總結

簡介

G2(圖表引擎) 4.0 和 G6(圖分析引擎) 3.4版本已經替換了 G(2D 渲染引擎)4.0,這個版本最大的改進是支持了局部渲染,在一些場景下例如節點的狀態改變、圖形的個體動畫等方面性能提高巨大。G 4.0 從開始重構到如今穩定經歷了半年的不斷完善,遇到了各類各樣的問題,本文將對 Canvas 的局部渲染作一個總結,給後來者一些幫助。 javascript

問題分析

因爲 Canvas 的繪製方式是畫筆式的,在 Canvas 上繪圖時每調用一次 API 就會在畫布上進行繪製一次,一旦繪製圖形就成爲畫布的一部分。繪製圖形時並無對象保存下來,一旦圖形須要更新,須要清除整個畫布從新繪製。
java

image.png
image.png

爲何要把整個 Canvas 畫布都清除,而後總體重繪?咱們以上面的兩張圖爲例,以左圖爲例,1, 2 沒有同其餘的圖形重合,能夠清除掉從新繪製,可是 3,4 就沒法單獨清理掉重繪;右圖僅僅在左圖的基礎上增長一條折線,這時候咱們就沒法刷新單個圖形了。
而在咱們要實現局部渲染時,須要考慮的兩個因素是:

  • 單次刷新時影響的範圍最小
  • 刷新的圖形不會影響其餘圖形的正確繪製

僅僅縮小刷新時的範圍從而提高性能並不夠,以右圖爲例,若是咱們要刷新圖形 2 將圖形變成紅色。這時候若是僅僅清理掉圖形 2 ,從新繪製則:
git

image.png

折線就會部分消失,這與咱們的預期不一致。局部刷新不但要保證刷新的範圍足夠小,還要保證圖形繪製的正確性。

方案

咱們來思考 Canvas 局部渲染方案時,須要看 Canvas 的 API 給咱們提供了什麼樣的接口,這裏主要用到兩個方法:github

經過這兩個 API 咱們能夠獲得 Canvas 局部刷新的方案:canvas

  1. 清除指定區域的顏色,並設置 clip
  2. 全部同這個區域相交的圖形從新繪製

image.png
image.png

image.png
image.png


以上圖爲例,若是咱們想刷新圖形 3,使得圖形的顏色變成紅色

  1. 首先肯定圖形的矩形包圍盒
  2. 清除這個包圍盒內的顏色,設置這個區域爲 clip 區域
  3. 從新繪製全部跟這個區域相交的圖形
  4. 重繪圖形 3
  5. 重繪圖形 4

遇到的問題

真實的在 G 4.0 中實現局部渲染時遇到的問題比上面的案例複雜的多:瀏覽器

  • G 不只僅支持圖形渲染,也支持分組 Group,一旦分組發生變化也會觸發局部刷新
  • 除了圖形的屬性變化外,圖形的順序調整、添加、移除圖形以及顯示隱藏等也會致使刷新
  • 圖形和分組上會增長各類矩陣,圖形的包圍盒計算頻繁而又複雜

這些問題在 1-2 周內都解決了,可是在接入 G2 和 G6的過程當中遇到了一些徹底沒想過的問題持續了半年的時間,主要體如今兩個方面:安全

  • 包圍盒計算不精確,致使的殘影問題
  • 局部刷新致使的性能降低

殘影的問題

首先咱們來看畫布上的兩條線,一樣都是 1 像素顏色 #333 的線,有什麼差異?
markdown

image.png

很明顯上面的一條,兩像素寬,同時顏色變淡,兩條線的座標爲:

  • 線段1(粗): (10, 100) - (200, 100)
  • 線段2(細):(10, 149.5) - (200, 149.5)

因爲屏幕的分辨率只能在整數的點上繪製顏色,線段 1 一半繪製在 (10, 99)-(200, 99) 一半繪製在 (10, 101 )-(200 101)上,因此瀏覽器會自動的把落到半個像素上的點擴展成一個點,顏色變淡,就變成了下圖的示例(示例中畫布進行放大,每一個單元格表明一像素)。
app

image.png
image.png

因此在 Canvas 畫布上繪製圖形時,任意的點若是部分落到一個像素上,都會佔滿整個像素,這個問題在平時的總體刷新時不明顯,一旦咱們來實現局部刷新就會出現問題,下面的多個問題都與此相關。

浮點數計算的問題

咱們在繪製圖形時不少圖形屬性是自動計算出來的,例如:oop

  • 直線 (10.2, 44.3) -  (20.1, 10.5)
  • 圓,圓心(10.5, 8.8) 半徑 3.4

這時候圖形繪製的區域同數學計算出來的並不一致,這就會致使局部刷新時清空的區域不足,會留下一些殘影。

image.png

上圖中對圓進行幾何計算的包圍盒和實際的包圍盒有了差異,這時候局部渲染就出問題了。解決方案:

  • 將包圍盒的 minX, minY 向下取整 (10.2, 10.5) -> (10, 10)
  • 將包圍盒的 maxX,maxY 向上取整 (20.1, 44.3) -> (21, 45)

折線夾角的問題

因爲 Canvas 在實現折線時,在線段的交接處作了處理,會附加額外的像素,使得折線更美觀,咱們來看下單獨繪製兩條線段,和一條折線的差異:

image.png
image.png

紅框爲咱們經過數學計算出來的包圍盒,能夠看出折線的拐角處明顯超出一部分,這時候折線改變時的刷新就不許確。解決這個問題有兩個解決方案:

  • canvas 在繪製時提供了一個參數: lineJoin,能夠設置 context.lineJoin="bevel|round|miter";詳情參看 canvas lineJoin 能夠改爲 bevel 就再也不有尖角,可是同咱們的預期不一致。
  • 計算折線包圍盒時增長拐角的計算,若是折線線段的夾角小於 90 度,則計算超出的像素數。若是折線的線段比較多,能夠僅計算落到折線上下左右四個邊上拐點超出的像素數(有必定風險)

shadow 的問題

在 Canvas 上繪製圖形時能夠指定陰影,有四個參數關係到陰影的設置:

shadowColor 設置或返回用於陰影的顏色
shadowBlur 設置或返回用於陰影的模糊級別
shadowOffsetX 設置或返回陰影距形狀的水平距離
shadowOffsetY 設置或返回陰影距形狀的垂直距離

下面兩個圓,若是不考慮陰影進行局部刷新時會出現下面的狀況:

image.png
image.png

因此在局部渲染時,經過判斷是否有 shadowColor 來附加額外的包圍盒,計算出陰影影響的範圍,同原始的包圍盒相併便可:

// 若是存在 shadow 則計算 shadow
if (attrs.shadowColor) {
  const { shadowBlur = 0, shadowOffsetX = 0, shadowOffsetY = 0 } = attrs;
  const shadowLeft = minX - shadowBlur + shadowOffsetX;
  const shadowRight = maxX + shadowBlur + shadowOffsetX;
  const shadowTop = minY - shadowBlur + shadowOffsetY;
  const shadowBottom = maxY + shadowBlur + shadowOffsetY;
  minX = Math.min(minX, shadowLeft);
  maxX = Math.max(maxX, shadowRight);
  minY = Math.min(minY, shadowTop);
  maxY = Math.max(maxY, shadowBottom);
}
複製代碼

箭頭的問題

在線上增長箭頭是個常見需求,可是因爲箭頭是附加在線上的,計算包圍盒未計算在其中,這就致使刷新時箭頭未被清除,同時箭頭又有多種狀況,還要考慮箭頭的自定義:

image.png
image.png

所以 G 4.0 將箭頭實現成了一個新的 shape,線包圍盒計算時同時附加箭頭的包圍盒,進行相併處理。

文本渲染的問題

你能看清楚下面的文本發生了什麼嗎?若是仔細觀察會發現文本左側被裁剪掉了一像素,這種狀況在多個場景下都存在

image.png

經過一番痛苦的排查發現,在這個 demo 的頁面上有一個屬性,在不一樣的字體下致使文字的寬度不一致:
     
image.png

分析一下緣由發現,當前文本的寬度計算是經過離屏 Canvas ,也就是建立一個 canvas 標籤,可是沒有放入 document.body 下,致使離屏 Canvas (1*1 的畫布) 上的 font 相關的屬性同當前 Canvas 不一致致使的。有兩個方案能夠解決這個問題:

  • 將離屏的 cavas 添加到當前頁面文件流中
  • 在圖表中設置全部的 font 屬性,覆蓋掉 body 上的屬性

瀏覽器縮放的問題

G2 4.0 和 G6 3.4 發佈後,有用戶反饋在頁面上進行操做時,出現一些線的劃痕

image.png

一開始是懷疑線寬度計算時的浮點數問題,咱們在本地和虛擬機上進行了測試,始終沒有定位到問題。用戶反饋他們對瀏覽器進行了縮放,經過檢測他們瀏覽器頁面上的 window.devicePixelRatio 是非整數。這個參數是瀏覽器和屏幕的像素比,通常狀況下是 1,高精屏下多是 2 或者 3,爲了讓圖形的繪製更加清晰咱們在 G 上進行處理,屏蔽了這個參數,可是在用戶對瀏覽器進行了縮放後,這個參數會變成 1.二、1.3 、1.5 等非整數值。
在開始討論遇到的局部渲染問題時,咱們介紹了直線在屏幕上的繪製,繪製發生在部分像素時瀏覽器會將整個像素設置顏色,而且變淺,這就出現了線的淺色的劃痕。
解決方案:當 window.devicePixelRatio 是非整數時,給包圍盒四個方向各附加 0.5 像素,而後取整便可。

// 附加 0.5 像素,會解決1px 變成 2px 的問題,不管 pixelRatio 的值是多少
// 真實測試的環境下,發如今 1-2 之間時會出現 >2 和 <1 的狀況下未出現,可是爲了安全,統一附加 0.5
const appendPixel = 0.5;
if (region) {
  region.minX = Math.floor(region.minX - appendPixel);
  region.minY = Math.floor(region.minY - appendPixel);
  region.maxX = Math.ceil(region.maxX + appendPixel);
  region.maxY = Math.ceil(region.maxY + appendPixel);
}
複製代碼

性能問題

大量分散的圖形刷新

若是同時有多個圖形進行刷新,爲了減小包圍盒的計算,咱們會把全部刷新的圖形的包圍盒進行合併,可是會出現一些特殊狀況,致使局部渲染的性能降低,例如:

image.png

視窗外的圖形刷新

image.png

這些局部渲染的性能問題的解決方案牽扯的方面比較多,不在這裏展開,能夠參考 渲染裁剪優化

總結

經歷了半年的改造,G 4.0 的局部渲染方案已經經歷了 G2 和 G6 的考驗,在局部刷新的場景下在交互和性能等方面提高了 7-10 倍,可是依然存在一些特殊場景上的性能問題,還在持續優化中。渲染的一分提高,上層都會有十分的收穫。


AntV 官網:antv.vision/
2D 繪圖引擎 G:github.com/antvis/g

相關文章
相關標籤/搜索