此系列文章
在這一系列文章中我們把 SVG 整個元素視為一個畫布(Canvas),而不是去 SVG 討論裡面的各個圖形,另外,只討論當 viewport 和 viewBox 兩者比例相等的情況。
在上一篇文章中([教學] 理解 SVG 中的 Viewport 和 ViewBox-拖曳與縮放功能實做(上)),說明到了在 viewport 座標系統中,1px 的大小就會是實際 1px 的大小;但在 viewBox 的 SVG 座標系統中,則可以透過 ViewBox 的設定,去進行縮放和移動,進而使的圖案本身有位移(translate)和縮放(scale)的效果。
為了要讓我們能夠實做出拖拉和縮放的效果,有幾點是我們需要進一步說明的。
實做效果和原始碼
這是我們這一篇文章希望做出來的效果,我們能夠取的 viewport 的座標點,並轉換成 SVG 座標系統中的座標值。另外我另外加了幾個按鈕,讓你可以直接設定 viewBox 並觀察其中圓心的座標會有如何的變化,建議可以在 jsfiddle 中比較方便操作和感受。
詳細的作法會在下面說明:
複習SVG 座標系統和 viewport 座標系統
從上篇文章我們知道 viewport 的座標系統可以視為是相對於瀏覽器不會變動的座標系統(1px 就是 1px),但是 SVG 座標系統則是會變動的,隨著給定不同的 viewBox 值,SVG 座標系統中的 1 個單位以有可能等於、大於或小於 1px。
這三張圖對於這個觀念的理解真的很重要很重要,所以讓我們快速地再看一次(灰色的是 viewport 座標系統;藍色的是 SVG 座標系統):
當 viewBox 等同於 viewport 的情況
當 viewport 等同於 viewBox 時可以看到鸚鵡右下角的藍點不論在 viewport 座標系統中的 offset 座標點或 SVG 座標系統中的座標點,座標值都是(200, 300),你可以對應到上方側和左方側的指標:viewBox 和 viewport 尺寸大小相同時,offset 座標值等於 svg |
當 viewBox 小於 viewport 的情況
當 viewBox 小於 viewport 時(這裡 viewBox = “400 300 0 0”),因為 viewBox 會先被裁切然後填滿整個 viewport ,所以在 SVG 座標系統藍點雖然還是(200, 300),可視實際上 viewport 座標系統中的 offset 座標值對應回去是(400, 600),也就是說在這個情況下 SVG 座標系統中的 1 單位,會是 viewport 座標系統的 2 單位,也就是 2px。此時 SVG 座標系統中的 1 單位,會是 viewport 座標系統的 2 單位,也就是 2px |
當 viewBox 大於 viewport 的情況
當 viewBox 大於 viewport 時(這裡 viewBox = “1600 1200 0 0”),因為 viewBox 會先被裁切然後填滿整個 viewport ,所以在 SVG 座標系統藍點雖然還是(200, 300),可視實際上 viewport 座標系統中對應回去是(100, 150),也就是說在這個情況下 SVG 座標系統中的 1 單位,會是 viewport 座標系統的 0.5 單位,也就是 0.5px。此時 SVG 座標系統中的 1 單位,會是 viewport 座標系統的 0.5 單位,也就是 0.5px |
SVG 座標系統和 viewport 座標系統的轉換:觀念
如果你對於上面的觀念還不清楚的話,建議可以再把第一篇文章看過一次,或在想清楚。接下來我們要說明如何在 SVG 座標系統和 viewport 座標系統中做轉換。
另外,在上一篇文章中,我們有提到 viewBox = "x-min y-min width height" 這四個屬性值,而在上面的範例中,敏銳的讀者應該會發現, 不論是放大或縮小,都是從最左上方的點開始(橘點),這點非常很重要,之後在實做縮放的時候會用到這個觀念。
不論是放大或縮小,都是從最左上方的點開始(橘點) |
client 座標和 offset 座標一樣都是屬於 viewport 座標系統,只是前者是以瀏覽器視窗左上角為原點(0, 0);後者則是以容器左上角為原點。
client 座標和 offset 座標一樣都是屬於 viewport 座標系統,只是前者是以瀏覽器視窗左上角為原點(0, 0);後者則是以容器左上角為原點 |
SVG 座標系統和 viewport 座標系統間的轉換需要一些數學的運算(電腦會幫你算),但為了避免
作法其實很簡單,想像有一個叫做 CTM 的魔術棒,透過這個魔術棒,我們就可以將 viewport 座標系統中的 client(x, y) 轉換成 SVG 座標系統中的 svg(x, y);相似地,我們可以透過 CTM 這個魔術幫棒將 SVG 座標系統中的 svg(x, y) 轉換為 viewport 座標系統中的 client(x, y):
更精確的說,當我們要從 SVG 座標系統 → viewport(client) 座標系統時,是利用 CTM X SVG 座標系統中的座標,可以得到 viewport 座標系統中的座標值:
可是如果要從 viewport(client) 座標系統 → SVG 座標系統,那麼我們的 CTM 也要反轉 一下才能得會 SVG 座標。為什麼要把 CTM 反轉過來才能用呢?你可以想像你要用鑰匙開鎖和解鎖,開鎖的時候要順時鐘轉,解鎖的時候要用逆時針轉,CTM 的反轉也可以用這樣的概念來想。
這裡有一點要留意的是兩個座標系統間的轉換時,我們用的是 viewport 座標系統中的 client(x, y) 而不是 offset(x, y),這點要非常留意。
CTM 的真面貌
好吧!一下魔術棒,一下要鑰匙的,你可能覺得很煩,我就直接說了,其實 CTM 就是個 3 x 3 的矩陣,全名是
current transform matrix
。什麼…!!矩陣…!!那我需要把高中的數學課本拿出來嗎??但是如果你想要更深入瞭解 SVG 的各種變化,像是選旋轉(rotate)、歪斜(skewed)或者應用到 SVG 當中的各個元素的話(例如,<circle></circle>、<path></path>),那你可能就需要更深入的瞭解 CTM 矩陣。
至於為什麼魔術棒需要像鑰匙一樣向左轉、向右轉,其實就是反矩陣的概念。
為了閱讀上的舒適度,我會把實際上矩陣轉換的公式寫在文中最後,如果不排斥的話,可以最後再看,或者就先直接當成它是一個魔術棒吧!
至於要如何取得 CTM 這個魔術棒,就讓我們實際看一下 code 吧!
SVG 座標系統和 viewport 座標系統的轉換:實做
接下來我們會看到許多 JavaScrip Web API 來協助我們進行 SVG 的操作,但是要注意的是,在這裡我們都是針對 SVG 這個元素(也就是 ,
<svg></svg>
個 tag 本身),而不是針對 svg 裡面的圖案(例如 `<circle></circle>, <line></line> 等等)進行操作。在 JavaScript Web API 中可以直接透過
SVGGraphicsElement.getScreenCTM()
這個方法,只要針對某個 svg 元素就可以讓我們直接取得 CTM,那麼到底要如何實際易應用呢?取得 viewport 座標系統的座標值(clientXY, offsetXY)
我們先在 HTML 建立 SVG 元素:
<svg id="svg" width="600" height="300" viewBox="0 0 600 300">
<rect x="0" y="0" fill="none" stroke="#EB7B2D" stroke-width="2" width="600" height="300" />
<circle cx="300" cy="150" r="5" fill="#EB7B2D" />
</svg>
<!-- 取得滑鼠座標資訊 -->
<div id="info">
<p id="offset">offset</p>
<p id="client">client</p>
<p id="showSvg">svg</p>
</div>
透過 <svg width="600" height="300">
,我們定義了 viewport 為 600 x 300(px)。利用 <circle></circle>
,我們在 SVG 中畫了一個圓心在(300, 150)、半徑(r)為 5 的圓,要注意的是,這裡的(300, 150)指的是 SVG 座標系統。另外利用
<rect x="0" y="0" fill="none" stroke="#EB7B2D" stroke-width="2" width="600" height="300" />
也在 SVG 中幫我們畫一個邊框,這能夠幫助我們看之後 SVG 座標系統的位移和縮放。接著,我們利用 JS 來取得 viewport 座標系統中的 offset 和 client 座標值,並且希望滑鼠滑動的時候可以馬上取得該滑鼠所在的 viewport 座標值,因此我們在 svg 這個元素上把上 mousemove 事件
svg.addEventListener('mousemove', reportCurrentPoint)
,當滑鼠一滑動,就觸發去執行 reportCurrentPoint
這個函式:
function reportCurrentPoint (e) {
// 選取 HTML 各元素
const info = document.getElementById('info')
const offset = document.getElementById('offset')
const client = document.getElementById('client')
const showSvg = document.getElementById('showSvg')
// 取得 viewport 座標系統中的 offset 和 client 座標值
offset.textContent = `offset (${e.offsetX}, ${e.offsetY})`
client.textContent = `client (${e.clientX}, ${e.clientY})`
}
const svg = document.getElementById('svg')
svg.addEventListener('mousemove', reportCurrentPoint)
利用則是利用 ES6 中模版字串符的方式代入 offset 和 client 座標值。
在我們的範例中,你會發現 offset 和 client 的值相差 9px ,當我們把滑鼠移到這個 container 的最左上角時,你會發現 offset(0, 0),client(9, 9),也就是從 container 左上角和 window 左上角的差值。
(註:一般來說 client 的值會以瀏覽器視窗左上角的圓點,但在示範所用的 fiddle 中,因為它是分割視窗的緣故,所以是以Result 視窗的左上角為原點)
取的 SVG 座標系統中的座標值
接著我們要寫 JS ,要在座標系統中的座標可以彼此轉換,我們要先在 SVG 座標系統中建立一個點,可以直接使用SVGElement.createSVGPoint()
這個方法,這個點建立起來後會是一個 SVGPoint 物件,包含屬性 x 和屬性 y ,初始值會是(0, 0)。然後,我們可以利用
SVGGraphicsElement.getScreenCTM()
來取得魔術棒 CTM。
const svg = document.getElementById('svg') // 選取 SVG 元素
let SVGPoint = svg.createSVGPoint() // 建立一個 SVG 座標,初始值為(0,0)
let CTM = svg.getScreenCTM() // 取的該元素的 CTM
/*
** svgPoint(SVG 座標系統) * CTM ---> client (viewport 座標系統)
*/
// 把 SVG 的座標點給進去
SVGPoint.x = 300
SVGPoint.y = 150
clientPoint = SVGPoint.matrixTransform(CTM) // client(309, 159)
/*
** client (viewport 座標系統) * CTM^(-1) ---> svgPoint(SVG 座標系統)
*/
SVGPoint = clientPoint.matrixTransform(CTM.inverse()) // svg(300, 150)
SVG 座標系統轉換成 viewport 座標系統的 client 座標點,可以直接使用 CTM。
因此假設我想要知道圓心的 svg(300, 150) 轉換之後在 viewport 的 client 座標是多少,我們可以利用
SVGPoint.x = 300; SVGPoint.y = 150
把座標點給進去,接著讓這個 svg 座標點透過魔術棒 CTM (實際上做的是矩陣相乘,CTM * svg)轉換為 client 座標點,於是寫 clientPoint = SVGPoint.matrixTransform(CTM)
,我們就能得到 client(309, 159) 這個屬於 viewport 座標系統的座標值。viewport 座標系統的 client 座標點要轉換為 SVG 座標系統的 SVGPoint時,則使用反轉後的 CTM(CTM的反矩陣)。
我們也可以把剛剛得到的 client(309, 159) 代入反轉過的 CTM 中轉回 SVG 座標系統的座標點(一樣是矩陣相乘,只是被乘的是 CTM 的反矩陣, CTM^(-1) x client),寫法是
SVGPoint = clientPoint.matrixTransform(CTM.inverse())
,這樣我就會得回 SVG 座標系統中的座標值。把 SVG 的轉換也綁到滑鼠移動的事件上
一般來說,因為 viewport 座標系統的尺寸是確定的(1px 就是 1px),所以我們比較常利用 client 座標來回推 SVG 座標系統的座標值,也就是 CTM^(-1) x client。現在,就讓我們把 client 座標轉換為 SVG 座標的寫法綁在 mousemove 這個事件上,讓滑鼠移動的時候,都能夠馬上取得 SVG 座標系統中的座標值。
function reportCurrentPoint (e) {
// 選取 HTML 各元素
const info = document.getElementById('info')
const offset = document.getElementById('offset')
const client = document.getElementById('client')
const showSvg = document.getElementById('showSvg')
const svgElement = document.getElementById('svgElement')
// 取得 viewport 座標系統中的 offset 和 client 座標值,並顯示於視窗上
offset.textContent = `offset (${e.offsetX}, ${e.offsetY})`
client.textContent = `client (${e.clientX}, ${e.clientY})`
// 建立 SVG 座標點(0, 0)
const clientPoint = svg.createSVGPoint()
// 取得 CTM
const CTM = svg.getScreenCTM()
// 將 SVG 座標點的 x, y 設成 client(x, y)
clientPoint.x = e.clientX
clientPoint.y = e.clientY
// 將 client 的座標點轉成 SVG 座標點
SVGPoint = clientPoint.matrixTransform(CTM.inverse())
// 將資訊顯示於視窗上
showSvg.textContent = `svg (${SVGPoint.x.toFixed(0)}, ${SVGPoint.y.toFixed(0)})`
}
svg.addEventListener('mousemove', reportCurrentPoint)
在這裡我把最後顯示的 SVG 座標加上 SVGPoint.x.toFixed(0)
加上 toFixed(0)
,因為矩陣轉換之後得到的會是很多為的小數,不方便觀看,因此我就四捨五入把小數點去除。改變 viewBox 看看座標系統會有什麼樣的影響
接著,你可以改變設定在 SVG 中的 viewBox 值,看看對於 SVG 的座標系統會有什麼樣的影響,在 viewBox 中前兩項(
min-x
和 min-y
)控制了位移;後兩項控制了縮放(width
和 height
),為了方便瞭解,會建議你在後兩項的設定和 viewport 是等比例的縮放,也就是我們原本的 viewport 是 600 x 300 ,你可以把後面這兩項等比例放大設為 1200 x 600 或縮小 300 x 150,來看效果。例如,一開始 viewport 和 viewBox 的尺寸相等,且沒有位移時(
viewBox = "0 0 600 300"
),圓心的點座標會如下圖:但是當我把 viewBox width 和 height 做等比例的改變後(
viewBox = "0 0 450 225"
),你會發現圓點的座標,在 SVG 座標系統仍然是 svg(300, 150),但是在 viewport 的座標系統中 offset(400, 200),client(409, 209)。這是因為當我的 viewBox 比例縮小為原本的 0.75 倍時,SVG 座標系統則會被放大(從左上角的地方放大),進而把圓點從中心的位置往右下推動,而圓也跟著放大了。是從左上方當中心開始做縮放 |
你可以再把 viewBox 設定成不同的值測試看看,這將有助於你更能夠掌握 viewport 和 ViewBox 之間的關係。
進一步瞭解 CTM
在這裡我們會簡單說明 CTM 矩陣,如果你只是想要瞭解如何用 viewBox 做到縮放和拖移的效果,沒有瞭解 CTM 矩陣並不會有太大的影響。但是如何你想要對於座標點是如何在兩個不同的座標系統間轉換,瞭解一下矩陣的運算是很有幫助了。另外,在這裡我們是針對 SVG 這個 tag 本身去取得 CTM ,如果你是想要針對 SVG 裡的圖形(例如,<circle></circle>, <path></path>)去做變化,其背後的原理是相似的,但這就不在我們這篇討論的範圍內了。
CTM 概念說明
CTM 它的本質是一個 3 X 3 的矩陣,長的像這樣子:CTM 矩陣的樣子 |
更深入的話,透過 a, b, c, d, e, f 的改變,我們就能達到各種縮放、位移、選轉、變形等等的效果。
在矩陣的概念中,我們則是將座標點用這樣的方式表示:
以矩陣的方式表示座標點 |
所以從 SVG 座標系統的座標點透過 CTM 轉到 viewport 座標系統的座標點,公式就像這樣:
SVG 座標系統的座標點透過 CTM 轉到 viewport 座標系統的座標點 |
如果想從 viewport 座標系統轉回 SVG 座標系統就需要利用到 CTM 的反矩陣(以-1表示),公式像這樣:
viewport 座標系統轉回 SVG 座標系統就需要利用到 CTM 的反矩陣(以-1表示) |
讓我們來實做看看
這裡推薦一個很方便的網站-矩陣計算器,我們就用這個網站來驗證我們上面程式的結果。現在我把上面範例中的
viewBox = "0 0 450 225"
的設定,透過上面所教得方式,我們將滑鼠游標移到圓心,可以發現在 viewport 座標系統中的 client(409, 209),接著,利用 svg.getScreenCTM()
我們可以取得當前 SVG 元素的 CTM ,算出來是的 matrix(a, b, c, d, e, f) = matrix(1.3333, 0, 0, 1.3333, 9, 9),現在我們就可以把這些值代入矩陣計算機中,按下等號:計算的結果就像這樣,你可以看到最後的結果就是(300, 150),而這也就是我們上面利用程式所顯示的 SVG 座標值:
計算方式對應回我們的公式 |
重點總結
- 利用魔術棒(CTM)可以讓 viewport 座標系統中的 client 座標值和 SVG 座標系統中的座標值做轉換。
- 要記得轉換回去的是 viewport 的 clientXY 而不是 offsetXY
參考資料
- SVG 座標轉換 @ MSDN
- Understanding SVG Coordinate Systems and Transformations
- [中譯]理解 SVG 座標系統和 Transformation @ Andyyou
- SVG 研究之路 (20) - transform Matrix @ OXXO
- SVGPoint @ MDN
- 首頁圖片來源 @ TeamTreehouse
- Grid Icon Created By Josh @ NounProject
0 意見:
張貼留言