JavaScript SVG分叉樹

使用 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)的任務,只有"任務隊列"通知主線程,某個異步任務可以執行了,該任務纔會進入主線程執行。

異步執行的運行機制

  1. 所有同步任務都在主線程上執行,形成一個執行棧(execution context stack)。
  2. 主線程之外,還存在一個"任務隊列"(task queue)。只要異步任務有了運行結果,就在"任務隊列"之中放置一個事件。
  3. 一旦"執行棧"中的所有同步任務執行完畢,系統就會讀取"任務隊列",看看裏面有哪些事件。那些對應的異步任務,於是結束等待狀態,進入執行棧,開始執行。
  4. 主線程不斷重複上面的第三步。主線程從"任務隊列"中讀取事件,這個過程是循環不斷的,所以整個的這種運行機制又稱爲Event Loop(事件循環)。只要主線程空了,就會去讀取"任務隊列",這就是JavaScript的運行機制。這個過程會循環反覆。

解決方案

因此就能解釋之前的問題了,setTimeout的函數,是等for循環執行完畢後,纔開始執行,此時迭代器已走到最後一個列表末端了。此時引用for循環中的局部變量都是最後一個的,所以就出現問題了。

解決方案就是,使用同步的匿名函數,將局部變量通過參數傳遞。由於匿名函數是同步執行的,因此setTimeout引用的也就成了匿名函數中的參數,也就是迭代器每走一步時的局部變量了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章