原文: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);{
-
}

