本篇作為技術分享系列的第一篇,詳細講一下 SVG 的解析和繪製,這部分功能的研究和最終實現由團隊的 @黃超超 同學負責,感謝提供技術文檔和支持。 首先我們來看一下 SVG 的文件結構和組成 SVG (Scalable Vector Graphics) 是一種可縮放矢量圖形,使用 XML 格式來定義, ...
本篇作為技術分享系列的第一篇,詳細講一下 SVG 的解析和繪製,這部分功能的研究和最終實現由團隊的 @黃超超 同學負責,感謝提供技術文檔和支持。
首先我們來看一下 SVG 的文件結構和組成
SVG (Scalable Vector Graphics) 是一種可縮放矢量圖形,使用 XML 格式來定義,是一種 W3C 標準,圖像在放大或改變尺寸的情況下其圖形質量不會有所損失。
下麵是一個簡單的 SVG 的文件結構例子:
<?xml version="1.0" standalone="no"?> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="100%" height="100%" version="1.1" xmlns="http://www.w3.org/2000/svg"> <circle cx="100" cy="50" r="40" stroke="black" stroke-width="2" fill="red"/> </svg>
從文件結構來看,SVG 確實是一種標準的 XML 格式,而裡面的元素,從字面上來看,是一個坐標為(100,50),半徑為40,填充色為紅色,線條為黑色,線寬為2的圓形。下麵我們來看看 SVG 文件裡面的基本元素和屬性:
1. 結構元素
<defs>, <g>, <svg>, <symbol>, <use>
2. 圖形元素
<line>, <circle>, <ellipse>, <polygon>, <polyline>, <rect>, <path>
這些標簽相信大家都不陌生,幾乎每種界面語言都有類似的標記。在 SVG 里,最常用的還是<path>, 用它可以表示前面所有的標簽。
3. 特殊元素
<image> :圖片,源通常由 base64 string 或 url 表示。它通常出現在這種場景:通過 PhotoShop 編輯一張圖片後,導出為 SVG 格式,這時文件里就存在 <image> 標簽,之後再導入到 AI 中進行路徑編輯,導出為 SVG 格式,就有了一張可以描繪路徑,又包含 <image> 底圖的 SVG 文件了。
<text> :文本,設置文字內容和字體字型大小等信息後,就可以在 SVG 中顯示這些文字。 <text> 支持 transform 屬性,可以旋轉縮放文字,同時還支持 style, css 代碼可以直接添加進來。
完整的元素列表參考這裡:https://developer.mozilla.org/zh-CN/docs/Web/SVG/element
4. 元素的若幹屬性
opacity, fill, stroke, stroke-width, stroke-miterlimit, fill-opacity, stroke-opacity, fill-rule, stroke-dasharray, stroke-dashoffset, stroke-linecap, stroke-linejoin, transform
這些都不難理解,代表了元素的透明度,填充,線條,變換等,因為 SVG 是 W3C 標準,所以以上這些外觀屬性,在 CSS 中都有對應的屬性。另外,SVG 還支持其他的屬性類型,如動畫事件/動畫定時/關鍵幀動畫/圖形屬性/過濾器等,十分強大。
完整的屬性列表參考這裡:https://developer.mozilla.org/zh-CN/docs/Web/SVG/attribute
來看一個例子:自上而下,分別包含了 兩個矩形,一個圓形,一個橢圓,一條直線,一條折線,一個多邊形和一條自定義 path。
<?xml version="1.0" standalone="no"?> <svg width="200" height="250" version="1.1" viewBox="0 0 200 250" xmlns="http://www.w3.org/2000/svg"> <rect x="10" y="10" width="30" height="30" stroke="black" fill="transparent" stroke-width="5"/> <rect x="60" y="10" rx="10" ry="10" width="30" height="30" stroke="black" fill="transparent" stroke-width="5"/> <circle cx="25" cy="75" r="20" stroke="red" fill="transparent" stroke-width="5"/> <ellipse cx="75" cy="75" rx="20" ry="5" stroke="red" fill="transparent" stroke-width="5"/> <line x1="10" x2="50" y1="110" y2="150" stroke="orange" fill="transparent" stroke-width="5"/> <polyline points="60,110 65,120 70,115 75,130 80,125 85,140 90,135 95,150 100,145" stroke="orange" fill="transparent" stroke-width="5"/> <polygon points="50,160 55,180 70,180 60,190 65,205 50,195 35,205 40 190 30 180 45 180" stroke="green" fill="transparent" stroke-width="5"/> <path d="M20,230 Q40,205 50,230 T90,230" fill="none" stroke="blue" stroke-width="5"/> </svg>
這裡對上面的示例代碼做一些補充說明:
① 計量單位 width height x y 等沒有顯示指定單位,這時我們認為單位就是 px。也可以明確指定單位 in cm 等,這時會根據當前設備的環境來換算為 px 顯示。
② viewBox 定義了畫布上可以顯示的區域,格式為 “x y width height”,如上圖的 viewBox=“0 0 200 250”,從(0,0)點開始,寬高 200 * 250的區域,SVG 的 width=“200”,height=“250”,所以當前縮放比是1. 如果 SVG width=“400” height=“500”,則會有兩倍的放大效果。
③ path 和其他元素的對比 在 SVG 中 path 是最常用的元素,和 polyline 做對比,path 也可以通過 d 的設置完成一樣的折線或曲線,而且只需要很少的點就可以創建平滑的曲線,但 polyline 需要設置大量的點才能達到平滑的效果。所以從製作難度和縮放效果看,path 是更好的選擇。
接下來看一下 SVG 的繪製過程
首先說明繪製的兩個基本原則:
1. 解析順序和繪製順序一致,都要遵守 XML 中元素的位置排列。借用上面的例子,SVG 中元素在 XML 中有固定的排列順序,我們解析時會遵守這個順序,繪製時同樣也會遵守這個順序。也就是說先出現的元素,會出現在繪製的底層,而後出現的元素,會出現在繪製的頂層,如果元素間位置有重疊,則會出現頂層元素遮擋底層元素的情況。
2. 子節點會繼承父節點的一些屬性,如 opacity,transform 等。這點在繪製時需要特別註意,opacity 等靜態屬性需要繼承,而 transform 等屬性需要做矩陣變換才能得到子節點最終 transform。
來畫手繪視頻中對 SVG 的處理過程
處理中遇到的一些特殊情況和處理
1、解析SVG文檔時,忽略DTD驗證
雖然是 DTD 是 XML 解析的標準驗證方式,但是很多工具製作的 SVG,DTD 會缺失,所以解析時應該忽略 DTD 驗證,不然會直接造成解析錯誤
2、解析SVG文檔時,一些元素的屬性值可能有多種分隔/表明方式
多邊形的點集,元素的 transform,都是一個數字集合,集合的分割方式可能是 “空格”,“,” 也可能是其他符號,所以在解析時需要相容多種分割方式。
顏色的表示,長度單位等,也可能會出現多種形式,如顏色有已知顏色和顏色值等形式,都需要做相容。
3、元素的某些屬性會繼承父級元素
transform,透明度等屬性,都需要考慮父級元素的繼承關係。transform 會複雜一些,transform [3*2] 的 矩陣,會包括縮放/平移/旋轉 等信息,子元素的平移信息,需要和父級元素做縮放相乘後,再做平移。
4、元素屬性的預設值
很多工具導出的 SVG,是會忽略一些屬性的,而這些屬性如果沒有值,我們是沒辦法正確顯示的。所以我們需要針對它們設置預設值。例如 fill 預設應該是 none,stroke 預設是 black,stroke-width 預設為1px,fill-rule 預設為 nonzero。這裡重點說一下 fill-rule,它分為 evenodd 和 nonzero 兩種方式:
EvenOdd:確定一個點是否位於填充區域內的規則,具體方法是從該點沿任意方向畫一條無限長的射線,然後計算該射線在給定形狀中因交叉而形成的路徑段數。 如果該數為奇數,則點在內部;如果為偶數,則點在外部。
Nonzero:確定一個點是否位於路徑填充區域內的規則,具體方法是從該點沿任意方向畫一條無限長的射線,然後檢查形狀段與該射線的交點。 從零開始計數,每當線段從左向右穿過該射線時加1,而每當路徑段從右向左穿過該射線時減 1。 計算交點的數目後,如果結果為零,則說明該點位於路徑外部。 否則,它位於路徑內部。
5、解析順序與渲染順序,描邊與填色的順序
解析順序和渲染順序必須一致,並且和 XML 中的順序一致,否則會出現錯誤的遮擋現象和繪製順序倒轉。描邊和填色的順序,基本原則是,單個元素的描邊完成後,操作填色,然後再操作下一個元素。當然這裡的填色可以靈活控制,比如保存所有填色,等所有描邊完成後,一次性填色。
6、包含<image>標簽的繪製
包含 <image> 標簽的 SVG,處理起來會有些特殊的地方。這種 SVG 的存在,一般是畫師通過 PS 編輯圖片後,再導入 AI 中生成的 SVG。處理這種 SVG 的繪製時,基本思路是:解析 <image> 標簽,當做 SVG 的底圖,用一個透明遮罩擋住;然後解析後面的 <path> 標簽,這是只需要解析 path 和 stroke,不需要 fill,用這裡的 path 去塗抹底圖,塗抹過的地方,透明遮罩失效,底圖露出,就達到了塗抹出底圖線條的目的。按照這個思路把底圖塗抹出來,類似刮刮卡的感覺。
到這裡,SVG 的基本知識、解析和繪製原理就介紹完了,當然這隻是很基礎的過程,在後面我們會整理出一些很特殊的 SVG 格式的解析和繪製思路,屆時和大家分享,謝謝。