前阵子学习了一下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);{
}
本片博客摘抄自iderzheng