JavaScript操作SVG的一些知識
網頁來源:http://blog.iderzheng.com/something-about-svg-with-javascript/
前陣子學習了一下SVG(Scalable Vector Graphics),希望能借此彌補自己在圖形藝術上的不足,當然最後也沒有得到什麼提高,不過也擴充了一些網頁前段技術知識。通過做了一些小的設計專案,也發現SVG可以彌補一些HTML元素的不足,比如傾斜、弧線、動畫、複用等等。
雖然SVG和HTML一樣都屬於XML的一種方言,一些基本的JavaScript對HTML的DOM操作都適用於SVG,但是在實際運用中還是因為這樣那樣的細微區別遇到了不大不小的麻煩。所以通過此篇文章記錄下遇到的問題和解決的方法。
獲取SVGDocument
當使用JavaScript在頁面上對HTML進行操作的使用,一個非常重要的物件就是document
了。無論是getElementById
、getElementsByTagName
,異或是createElement
,它們都是document物件上的方法。而且所有其它任何DOM物件都被包含在該物件之內。
一般而言,一個HTML檔案,或者說一個網頁都對應一個document
物件,所以如果SVG是直接巢狀在HTML的內容中的話,它們就會共用一個document
物件,因此可以直接通過該物件來獲取到SVG元素物件。
比如下邊的代表,在瀏覽器上開啟,就會看到一個藍色的圈而非綠色的圈,因為JavaScript通過document
circle
物件,並重新設定了其fill
屬性。
<html> <head> <title>Nested SVG</title> </head> <body> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="20" height="20"> <circle id="c" cx="10" cy="10" r="7" fill="green"/> </svg> <script type="text/javascript"> var c = document.getElementById('c'); c.setAttribute('fill','blue'); </script> </body> </html>
不過大多時候,SVG並不會直接巢狀在HTML之中重現出來,更多的會選擇將其作為圖片通過img標籤引進,或者當做背景圖片在CSS中通過url
引入。對於這種情況,SVG只是單純的當做圖片來使用而且一個XML型別的文件,所以也就無法使用JavaScript來操作它們。
還有一類方法則是通過object、embed或者iframe標籤將SVG檔案引入到HTML頁面上呈現出來。對於該種情況, 這些標籤實際上會把通過data
或src
屬性指定的內容相對獨立的引入到頁面上來,也就是其中的內容會有完全屬於自己的document
物件。所以使用原來的document
物件就無法取得通過上述標籤引入進來的SVG文件中元素,更不用說去修改上邊的屬性了。
好在當使用JavaScript獲取到這些元素物件的時候,它們都一個方法可以獲取所引用的SVG文件的document物件,那就是getSVGDocument()
。
當然這些都是需要瀏覽器支援才行的,對於目前主流最新瀏覽器來說這些都是標配的方法。如果使用object或iframe引入SVG文件,除了getSVGDocument()
,還可以使用contentDocument
屬性來獲取其引入文件對應的document物件。區別在於如果是引入的不是SVG檔案,而是XML或者HTML等等,contentDocuement
依然會返回物件,而getSVGDocument()
則返回null
。
獲取了SVG的document
之後,就可以像往常那樣獲取內部元素屬性、繫結事件等等。還可以定義一個document
為引數的方法形成區域性變數,要對某個引入SVG文件進行操作時就獲取該文件的document
物件傳入,想獲取主文件的物件時就使用window.document
即可。
function setup (document) {
// do something with svg docuemnt
}
setup(document.getElementById('svg-embed').getSVGDocument());
當然了使用上邊一系列屬性和方法都有一個大前提:要滿足同源策略(Same-origin policy)。若引入的SVG文件是來自於其它站點的,那麼瀏覽器就會禁止獲取document物件。
操作SVG的元素
SVG作為XML的方言,不同於HTML鬆散的標籤結構和格式,它嚴格遵循XML的語法格式,所以開始標籤都要有對應的結束標籤,所有標籤都要被svg標籤包含在內。另外在HTML經常被忽略的一個知識就是:XML是有名稱空間(namespace)的。名稱空間在通過JavaScript建立SVG元素物件的時候就引起了一些麻煩。
一般的在HTML中若想通過JavaScript建立一個元素物件的話,程式碼類似如下:
var inp = document.createElement('input');
inp.type = 'button';
inp.value = 'button';
inp.name = 'button';
var con = document.getElementById('container');
con.appendChild(inp);
但是使用相同的方法,建立SVG元素並新增到SVG文件中的話, 該元素並不會呈現出來。
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="20" height="20">
<script type="text/javascript">
var c = document.createElement('circle');
c.cx = 10;
c.cy = 10;
c.r = 7;
c.fill = 'green';
document.rootElement.appendChild(c);
</script>
</svg>
這是因為建立SVG元素需要指定名稱空間,就像需要在svg標籤上設定xmlns為http://www.w3.org/2000/svg。正確的構造方式是呼叫createElentNS()
方法,並將”http://www.w3.org/2000/svg”作為第一引數傳入。
此外,不同於HTML元素物件可以直接對一些屬性賦值,SVG元素物件都需要通過呼叫setAttribute()
方法來設定屬性值。因為大部分屬性都是SVGAnimatedLength型別,即使要通過屬性賦值也要寫成類似c.r.baseVal.value = 7,多層訪問其下屬性。不過像fill
、stroke
等預設都是undefined,所以使用setAttribute()
是更好的選擇。
下述程式碼就可以在頁面上呈現是一個半徑為7px的綠色的圓點。
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="20" height="20">
<script type="text/javascript">
var c = document.createElementNS('http://www.w3.org/2000/svg','circle');
c.setAttribute('cx', 10);
c.setAttribute('cy', 10);
c.r.baseVal.value = 7;
c.setAttribute('fill', 'green');
document.rootElement.appendChild(c);
</script>
</svg>
除了元素有名稱空間,有些屬性也有其特定的名稱空間。比如在HTML極為常用的a
標籤的,在SVG中又有存在,但是對於其href
屬性,在SVG之中就必須加上xmlns:
字首來指定其名稱空間了。對於設定這些屬性也需要用setAttributeNS()
方法並將”http://www.w3.org/1999/xlink“作為第一引數傳入。
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
version="1.1" width="20" height="20">
<script type="text/javascript">
var a = document.createElementNS('http://www.w3.org/2000/svg','a');
a.setAttributeNS('http://www.w3.org/1999/xlink','xlink:href','http://blog.iderzheng.com/');
var c = document.createElementNS('http://www.w3.org/2000/svg','circle');
c.setAttribute('cx', 10);
c.setAttribute('cy', 10);
c.r.baseVal.value = 7;
c.setAttribute('fill', 'green');
a.appendChild(c);
document.rootElement.appendChild(a);
</script>
</svg>
現在可以通過點選綠點進入到部落格主頁了。
不僅是a標籤,對於其它標籤,例如:use、image,若要設定xlink:href
屬性時都應使用名稱空間的方式,否則就不會起作用。對於其它該名稱空間下的屬性也是如此,例如xlink:title
、xlink:show
。這裡就不一一列舉,具體關於xlink的可參看此處。
如果你碰到了其它在JavaScript中因XML名稱空間引起的問題,也歡迎留言補充。
SVG與視窗座標系的轉換
SVG比HTML的一大優勢在於前者支援平移、縮放、切變等變換(Transform)。雖然現在CSS3也支援這些變換,但是畢竟SVG是向量圖,它在縮放的時候不會丟失畫素。而且SVG可以通過use標籤設定tranform屬性來進行快速複用。而HTML的標籤就沒有這樣的“複用性”,每個在頁面上呈現出來的內容都嚴格對應一個標籤元素。
不僅是transform
屬性可以變化座標,一些元素例如svg,也可以通過設定viewBox
來改變自身的座標系以影響呈現的內容。這就引發了一些問題座標系轉換的問題:比如滑鼠點選在頁面上的時候獲取到的是基於視窗座標系以畫素為單位的位置,如何轉變到SVG的座標系的點以確定被點選的物件或者進行其它操作呢?
一種方法可以是自己記錄下視窗和SVG圖的比例,然後根據比例進行轉換。這可能可以一定程度地解決平移和縮放帶來的變換問題,但是對於旋轉就無能為力了。而且當視窗進行了滾動或者拖拽,都需要重新計算這些比例。
其實在每個SVG元素物件上,都有一個getScreenCTM()
的方法,它會返回一個SVGMatrix
來表示元素的座標系所做過的變換。此外SVG還有一種SVGPoint
型別,它有x和y兩個屬性可以表示任一一個點,同時它還有一個matrixTransform()
方法可以將點跟某個SVGMatrix相乘得到相應矩陣變換後的點。通過這些再加上一些線性代數的知識,就可以輕鬆的進行座標系的變換了。
要注意的是SVGPoint只能通過svg元素物件的createSVGPoint()
來建立,不能用new SVGPoint()
這樣的方式。
function click(e) {
// rootElement is specific to SVG document
// documentElemnt is to any XML document include HTML
// they both can retrieve the root element of a document
var r = document.rootElement || document.documentElement,
pt = r.createSVGPoint(),
im = r.getScreenCTM().inverse(); // inverse of tranforma matrix
// set point with window coordination
pt.x = e.clientX;
pt.y = e.clientY;
// convert point to SVG coordination
var p = pt.matrixTransform(im);{
}