純 CSS 實現大量小塊兒繪製
現在做的專案是公司內部全部組要用的 viewer 庫. Viewer 需要的功能非常的多,其中的一個就是需要提供一些常用的繪圖API功能, 比如使用者滑鼠移動畫箭頭,畫圈圈,高光選中文字等等。
挑戰
目前遇到的挑戰就是在 canvas, svg, dom +css之間如何選擇的問題,canvas 繪圖的方案已經有了成品,我們可以直接拿過來新增到現有的 Viewer 上面。但是基於以下考慮,主要用哪種技術遲遲不能下定論。
Canvas 糾結點:
1) 各部門合作問題,如果採用 canvas, 其他部門需要新增自定義圖形操作的時候就需要他們瞭解 Canvas 開發 重繪到 Viewer 已有的 Canvas 上面。
2) Canvas 會失去很多 Element 原生的屬性,比如文字選中,aria標籤, 事件等。
3) Viewer 的檔案可能會非常的大,並且可能會有成千上萬個頁面,每個頁面管理自己的繪圖。如果每個頁面一個Canvas, 效能會非常差(採用了 View Virtualization 思想,只渲染檢視需要的元素,這裡是做了個極限假設)。如果所有的圖形都繪製在一個Canvas上面,需要根據檢視區域的頁面屬性重繪圖形計算,開發成本會增加非常多。
4)Canvas 只能用於最底層,否則會覆蓋其他元素(Viewer上面的層非常多,需要繪圖層能管理事件,同時不丟失其他層的事件)。
於是老大就給幾天時間,讓我們盡情地先試著用其他的方式畫一下各種圖形,比較一下優劣(最後,我們應該會幾樣技術結合起來用)。其中一個案例就是高光選中文字,後端傳會傳一堆需要高光塊兒大小位置,前端畫出高光部分。 像這種非常多小面積的繪圖,Canvas是最合適不過的了,不過這幾天時間就是看看有沒有還有什麼其他的方案。大概就長這樣,高光是無數個小塊兒。
電腦刺繡繡花廠 http://www.szhdn.com 廣州品牌設計公司https://www.houdianzi.com
尋找方案
Canvas 畫高光的方案已經有了,我們首先想到的就是每一個塊兒給個 span 渲染,但因為需要高光的塊兒可能會非常多,並且可能會是動態的。哪麼這就涉及到 DOM 的頻繁操作,效能非常不好。後來採用createDocumentFragment,新增到DOM,效能明顯提升。
我當時有個非常大膽的想法,要是隻用一個span,然後所有的塊兒都新增成背景可不可以實現(考慮到現在的css3已經有了多背景重疊技術)。找路子的時候千萬次覺得不可能,結果實現之後再返回去看,其實原理非常的簡單。全部採用css, 用linear-gradient畫塊兒(採用linear-gradient, 是因為它可以背景重疊,如果有更好的方法歡迎指教),background-position, background-size分別給每個塊兒定位,結束!效能當然是驚人的好,因為根本沒有涉及到元素的增加刪除,只是css重繪(Viewer 有大量消耗效能的scroll監聽,操作等情況下都能無縫操作,Canvas 有非常微小地快閃)。這一試,彷彿大概了新世界,很多大量,簡單的圖形繪製完全可以嘗試純css實現,上程式碼。
Angular template
<spanclass="layer-content"[style.background]="_background"[style.backgroundSize]="_backgroundSize"[style.backgroundPosition]="_backgroundPosition"></span>
Angular ts file
this._subscription = this.highlights$.subscribe((highlights) => {
let background = ``;
let backgroundSize = ``;
let backgroundPosition = ``;
for (let i = 0; i < highlights.length; i++) {
const { x, y, width, height } = highlights[i];
// add connection comma when i is not the last one.
const comma = (i < highlights.length - 1) ? ', ' : '';
// 0px transparent to fill gradient syntax.
background += `linear-gradient(CurrentColor 100%, transparent 0px)${comma}`;
backgroundSize += `${width}px ${height}px${comma}`;
backgroundPosition += `${x}px ${y}px${comma}`;
}
this._background = this.sanitizer.bypassSecurityTrustStyle(background);
this._backgroundSize = backgroundSize;
this._backgroundPosition = backgroundPosition;
});
Css
span.layer-content {
z-index: 0;
display: inline-block;
width: 100%;
height: 100%;
background-repeat: no-repeat !important;
}