SVG 在 image 標籤中的動態修改技巧

redmed發表於2019-01-30

tag: Web; JavaScript; SVG; DOM; 動畫 SVG

最近在專案中遇到了「帶動畫 SVG 圖示」與 「image」標籤結合使用的場景,使用過程中發現水還是有點深,因此整理出來,供有相似場景的童鞋以參考。

問題背景

我們這裡有一個的帶動畫 SVG 檔案

rings.gif

這是一個水波紋效果的 SVG,動畫時長是固定的,但是我們希望不同的標記動畫的播放時長可以略有不同,進而產生錯落交錯的感覺。期待效果如下:

rings_diff_ani.gif

其中控制動畫時長的屬性是寫死在 SVG 檔案中的(animate 標籤的 dur 屬性)。SVG 部分內容如下:

<circle cx="22" cy="22" r="6" stroke-opacity="0">
    <animate attributeName="r"
         begin="1.5s" dur="3s"
         values="6;22"
         calcMode="linear"
         repeatCount="indefinite" />
    <animate attributeName="stroke-opacity"
         begin="1.5s" dur="3s"
         values="1;0" calcMode="linear"
         repeatCount="indefinite" />
</circle>
複製程式碼

另外受限於元件要求,僅能使用 image標籤進行載入 SVG。因此只能從 image.src 屬性作為突破入口。

解題思路

先確定基本思路:在 SVG 被插入到 image 標籤前,修改 SVG 檔案中動畫 animate 標籤的屬性 dur,以達到修改動畫時間的目的。

那麼我們先來看下 image 載入 SVG 檔案幾種的方式。

1. image 的 src 直接指定檔案路徑

<img src="./assets/rings.svg">
複製程式碼

這種方式載入出來的 SVG 內容沒法被 JS 獲取到,更不要提修改屬性,因此該方案 放棄

2. 直接載入 base64 或者 Blob URL 字串

這種方式中,依靠的是 image 標籤支援 base64 和 Blob 型別 URL 的特性。

考慮可以先將 SVG 檔案轉換為對應的字串,然後通過正規表示式將動畫屬性修改,然後傳入 src 中來實現。不過這種方式需要編寫複雜的正規表示式,並且字串形式的 SVG 內容可讀性較差,因此這種方式也 不是最優的

如果可以先以 dom 形式修改 SVG 的動畫屬性,再將 dom 轉換為字串,最後再將字串轉換為 base64 或者 Blob 型別,就可以實現以上的需求了。

看起來這個思路靠譜,那麼就按照這個方向繼續探索。

處理過程

1. 獲取 dom

獲取 SVG 的方式有很多,但獲取方式不在本文重點,故只給出原生 JS 實現方式。

const xhr = new XMLHttpRequest();
xhr.addEventListener('load', () => {
  const resXML = xhr.responseXML;
  const svgDom = resXML.documentElement.cloneNode(true);
});
xhr.open('GET', './rings.svg');
xhr.send();
複製程式碼

2. 修改 dom

上文獲取的 svgDom 就是可操作的 dom 節點,接下來和操作 dom 一樣來操作它。

// 獲取 svg 中的 animate 標籤,使用 setAttribute 進行修改 dur、begin 等屬性,以下程式碼僅為示例
// 獲取 animation 節點
const ani = svgDom.children[0];
// 修改節點上的動畫時長 dur 屬性
ani.setAttribute('dur', Math.random() + 2 + 's');
複製程式碼

3. 轉換 domString

接來下便需要將 dom 轉換成字串:通過 XMLSerializer 將 XML 轉換成 String 型別。

const svgStr = new XMLSerializer().serializeToString(svgDom);
複製程式碼

4. 轉換 URL

但是上面的 svgStr 是沒法直接傳給 image.src 的,需要將其轉換為 image 可以解析的 base64 或者 Blob URL 型別。

方案 A:

這裡我選擇了 Blob 型別進行轉換,先用 new Blob([svgStr]) 轉換成 Blob 型別,再通過 URL.createObjectURL(blob) 方法將字串轉換成 Blob 型別 URL 傳入。

const blob = new Blob([svgStr], {
    type: 'image/svg+xml'
});
const blobStr = URL.createObjectURL(blob);
const template = `<img src="${blobStr}">`;
// 最後插入模板
複製程式碼
方案 B:

當然除了使用 Blob 資料型別外,我們也可以使用window.btoa()方法將 svgStr 轉換為 base64 型別傳入。

const base64 = window.btoa(svgStr);
const template = `<img src="data:image/svg+xml;base64,${base64}">`
// 最後插入模板
複製程式碼

結論

通過「讀入 SVG -> 修改屬性 -> 轉換 URL -> 傳入 image」 這樣一個流程,最終實現了我們的預期。

以上思路希望對有類似場景的同學們有所啟發。 最後強調的是使用環境為 Chrome 瀏覽器,其他瀏覽器未做測試。

參考資料

XMLHttpRequest
SVG Animate
XMLSerializer
Blob
URL.createObjectURL()
Using object URLs to display images
window.btoa()

相關文章