1. 程式人生 > >UWP 手繪視頻創作工具技術分享系列 - SVG 的解析和繪制

UWP 手繪視頻創作工具技術分享系列 - SVG 的解析和繪制

ima 默認 graphics 添加 hot 繼承關系 and opacity pat

本篇作為技術分享系列的第一篇,詳細講一下 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 格式的解析和繪制思路,屆時和大家分享,謝謝。

UWP 手繪視頻創作工具技術分享系列 - SVG 的解析和繪制