使用 SVG 做一個樹的動畫
<!-- <!DOCTYPE> 聲明不是 HTML 標籤;它是指示 web 瀏覽器關於頁面使用哪個 HTML 版本進行編寫的指令。 -->
<!-- 下面這個是HTML5標誌 -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf8" />
<title>SVG Fractal Tree</title>
<style type="text/css">
body {
/* 背景是銀色,注意CSS中僅"多行註釋"這一種註釋有效 */
background-color: silver;
}
svg {
border: 1px dashed black;
}
svg line {
stroke: black;
stroke-width: .05;
}
</style>
</head>
<body>
<!-- SVG和Canvas一樣,width和height設置在style中是無效的 -->
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
id="svg" width="100" height="100" >
<defs id="defs">
<line id="stem" x1="0" y1="0" x2="0" y2="-1" />
</defs>
</svg>
<script type="text/javascript">
//SVG 名空間
var xmlns_svg = "http://www.w3.org/2000/svg";
//xlink 名空間
var xmlns_xlink = "http://www.w3.org/1999/xlink";
var svg = document.getElementById("svg");
var defs = document.getElementById("defs");
//莖ID
var stemId = "stem";
//樹ID
var treeId = "tree";
//樹的最大深度
var maxDepth = 9;
//繪製間隔(單位:毫秒)
var drawInterval = 500;
var dx = 400;
var dy = 350;
//創建SVG標籤
function createSvgElem(elemTag) {
return document.createElementNS(xmlns_svg, elemTag);
}
//組合成樹的transform屬性
function formTreeTransform(dx, dy) {
return "translate(" + dx + "," + dy + ") scale(100)";
}
//組合成莖的transform屬性
function formStemTransform(degree) {
return "translate(0,-1) rotate(" + degree + ") scale(.7)";
}
//樹
var Tree = function(elem, degree) {
//當前深度
this.depth = 1;
this.stemLeftTransform = formStemTransform(-1 * degree);
this.stemRightTransform = formStemTransform(degree);
console.log(degree +","+this.depth+" init");
//畫下一級的枝幹
this.grow = function() {
console.log(degree+","+this.depth);
//這裏用直接用諸如_this=tree是不合法的,因爲沒有使用var聲明的都是全局變量,會呼吸影響
var prevId = "#" + degree + "_" + (this.depth - 1) + "_" + stemId;
var id = degree + "_" + this.depth + "_" + stemId;
var g = createSvgElem("g");
var use1 = createSvgElem("use");
var use2 = createSvgElem("use");
var use3 = createSvgElem("use");
g.setAttribute("id", id);
//g.setAttributeNS(null, "id", id);
use1.setAttributeNS(xmlns_xlink, "xlink:href", prevId);
use1.setAttribute("transform", this.stemLeftTransform);
//use1.setAttributeNS(null, "transform", this.stemLeftTransform);
use2.setAttributeNS(xmlns_xlink, "xlink:href", prevId);
use2.setAttribute("transform", this.stemRightTransform);
//use2.setAttributeNS(null, "transform", this.stemRightTransform);
use3.setAttributeNS(xmlns_xlink, "xlink:href", "#" + stemId);
defs.appendChild(g);
g.appendChild(use1);
g.appendChild(use2);
g.appendChild(use3);
elem.setAttributeNS(xmlns_xlink, "xlink:href", "#" + id);
update(this);
}
//由於函數是全局的,因此內部沒有定義this對象
function update(tree) {
if(tree.depth < maxDepth) {
tree.depth++;
//setTimeout的參數必須是一個函數,而不能是函數變量,所以必須使用匿名函數做轉接
setTimeout(function(){
tree.grow();
}, drawInterval);
}
}
}
function init() {
var degrees = new Uint8Array([15, 25, 35, 45]);
//svg只能用setAttribute的方式設置寬高,不能像canvas一樣直接用width=value來設置
svg.setAttribute("width", dx * 2);
svg.setAttribute("height", dy * Math.floor(degrees.length / 2) + 20);
for(var i=0; i < degrees.length; i++) {
var id = degrees[i] + "_" + treeId;
var use = createSvgElem("use");
use.setAttributeNS(null, "transform", formTreeTransform((.5 + i % 2) * dx, Math.floor(i / 2 + 1) * dy));
svg.appendChild(use);
var tree = new Tree(use, degrees[i]);
tree.grow();
/*
這裏使用setTimeout,會導致只有循環的最後一個tree的grow方法有效,
通過全局變量和參數傳遞的方式也不能解決
setTimeout(function(){
tree.grow();
}, drawInterval);
隨着學習的深入知道,可以這麼解決
(function(tree){
setTimeout(function(){
tree.grow()
}, drawInterval);
})(tree);
*/
}
}
init();
</script>
</body>
</html>
答疑
1. 怎麼把 line 換成文字
有人在評論裏問我,我在這裏簡單的介紹一下
樹的原理,主要是對defs中定義的元素(我稱之爲本體)進行一個平移和旋轉,最後呈現出一個組合的圖形。
知道原理後,要替換就很簡單了,只要將其本體進行替換即可
但是有幾點要注意的
- svg 並沒有提供設置文字大小的屬性,文字大小需要通過 css 的 style 進行設置(文字最小 0.1)。
- css 中的 transform,使用百分比會相對整個svg圖進行換算,行不通。因此只能用寫死的數值平移,讓文字水平居中。
- 文字默認的位置是左下角基線的座標。
<svg width="100%" height="100%" version="1.1" xmlns="http://www.w3.org/2000/svg">
<text x="100" y="100" dx="20 40 60 80 100" fill="black" style="font-size:40px;">我是中國人</text>
<path d="M100,0 V200 M0,100 H200" stroke="red"/>
</svg>
要把 line 標籤,改爲 <text id="stem" x="0" y="0" style="font-size:0.8px;transform:translateX(-0.4px);">樹</text>
未加 translate 的效果
加了 translate 的效果
2. for 循環裏的 setTimeout 爲什麼只有最後一個索引生效
JS是單線程環境,也就是說代碼的執行是從上到下,依次執行。也就是同一個時間只能做一件事。
單線程就意味着,所有任務需要排隊,前一個任務結束,纔會執行後一個任務。如果前一個任務耗時很長,後一個任務就不得不一直等着。JavaScript將所有任務分成兩種,一種是同步任務,另一種是異步任務。同步任務指的是,在主線程上排隊執行的任務,只有前一個任務執行完畢,才能執行後一個任務;異步任務指的是,不進入主線程、而進入"任務隊列"(task queue)的任務,只有"任務隊列"通知主線程,某個異步任務可以執行了,該任務纔會進入主線程執行。
異步執行的運行機制
- 所有同步任務都在主線程上執行,形成一個執行棧(execution context stack)。
- 主線程之外,還存在一個"任務隊列"(task queue)。只要異步任務有了運行結果,就在"任務隊列"之中放置一個事件。
- 一旦"執行棧"中的所有同步任務執行完畢,系統就會讀取"任務隊列",看看裏面有哪些事件。那些對應的異步任務,於是結束等待狀態,進入執行棧,開始執行。
- 主線程不斷重複上面的第三步。主線程從"任務隊列"中讀取事件,這個過程是循環不斷的,所以整個的這種運行機制又稱爲Event Loop(事件循環)。只要主線程空了,就會去讀取"任務隊列",這就是JavaScript的運行機制。這個過程會循環反覆。
解決方案
因此就能解釋之前的問題了,setTimeout的函數,是等for循環執行完畢後,纔開始執行,此時迭代器已走到最後一個列表末端了。此時引用for循環中的局部變量都是最後一個的,所以就出現問題了。
解決方案就是,使用同步的匿名函數,將局部變量通過參數傳遞。由於匿名函數是同步執行的,因此setTimeout引用的也就成了匿名函數中的參數,也就是迭代器每走一步時的局部變量了。