js实现一棵树的生长

2023-05-13,,

参考链接:https://blog.csdn.net/u010298576/article/details/76609244

HTML网页源码:

 1 <!DOCTYPE html>
2 <html>
3 <head lang="en">
4 <meta charset="UTF-8">
5 <title>生长的树</title>
6 <style>
7 html , body {
8 margin: 0;
9 padding: 0;
10 width: 100%;
11 height: 100%;
12 overflow: hidden;
13 background-color: #fff;
14 }
15 </style>
16 </head>
17 <body>
18 <canvas id="myCanvas">此浏览器不支持canvas</canvas>
19 <script src="tree.js"></script>
20 </body>
21 </html>

js代码:

  1 /**
2 * Created by 004928 on 2017/8/2.
3 */
4 (function (window) {
5
6 var w = window.innerWidth , h = window.innerHeight ; // innerheight 返回窗口的文档显示区的高度。 innerwidth 返回窗口的文档显示区的宽度。
7 var ctx = null ;
8 var treeNum = 5 ;
9 var initRadius = 25 ; // 树干的初始宽度
10 var maxGeneration = 5 ; // 最多分支的次数
11 var branchArray = null ; // 树干的集合
12 var flowers = []; // 花的集合
13
14 window.MyRequestAnimationFrame = window.requestAnimationFrame || //requestAnimationFrame的速度是由浏览器决定的,不同浏览器会自行决定最佳的帧效率。
15 window.mozRequestAnimationFrame || //解决了浏览器不知道javascript动画什么时候开始、不知道最佳循环间隔时间的问题。
16 window.webkitRequestAnimationFrame || //这个API是浏览器提供的js全局方法,针对动画效果。
17 window.msRequestAnimationFrame ;
18
19 window.MyCancelRequestAnimationFrame = window.cancelRequestAnimationFrame || //这是是把动画结束的
20 window.mozCancelRequestAnimationFrame ||
21 window.webkitCancelRequestAnimationFrame ||
22 window.msCancelRequestAnimationFrame ;
23
24 /**
25 * 初始化canvas
26 */
27 function initCanvas () {
28 var canvas = document.getElementById("myCanvas");
29 canvas.setAttribute('width' , w);
30 canvas.setAttribute('height' , h);
31 if(canvas.getContext) { //getContext() 方法返回一个用于在画布上绘图的环境。目前参数唯一的合法值是 "2d",它指定了二维绘图,并且导致这个方法返回一个环境对象,该对象导出一个二维绘图 API。
32 ctx = canvas.getContext('2d');
33 initTree();
34 loop();
35 }
36 }
37
38 /**
39 * 初始化树的数量
40 */
41 function initTree () {
42 branchArray = new BranchArray ();
43 for(var i = 0 ; i < treeNum ; i++) {
44 branchArray.add(new Branch(w / 2 , h));
45 }
46 }
47
48 function BranchArray () { //如果没有手动返回对象,则默认返回this指向的这个对象,也是隐性的
49 this.branchs = []; //具体见https://www.cnblogs.com/echolun/p/10903290.html
50 }
51
52 /**
53 * 树干
54 * @param x
55 * @param y
56 * @constructor
57 */
58 function Branch (x , y) {
59 this.x = x ;
60 this.y = y ;
61 this.radius = initRadius ;
62 this.angle = Math.PI / 2 ; // 树枝的初始角度
63 this.speed = 2.35 ; // 树生长的速度
64 this.generation = 1 ;
65 }
66
67 /**
68 * 生长
69 */
70 Branch.prototype.grow = function () {
71 this.draw();
72 this.update();
73 }
74
75 Branch.prototype.draw = function () {
76 ctx.fillStyle = '#55220F';
77 ctx.beginPath();
78 ctx.arc(this.x , this.y , this.radius , 0 , 2 * Math.PI);
79 ctx.fill();
80 }
81
82 /**
83 * 更改数的高度以及扭曲度
84 */
85 Branch.prototype.update = function () {
86
87 // 计算树干每次的扭曲角度,因为树一般不是笔直生长的,都会有不规则的扭曲
88 this.angle += random( -0.1 * this.generation , 0.1 * this.generation ); //因为树枝半径小的话那么它分支的角度可能会更大
89
90 var vx = this.speed * Math.cos(this.angle); //speed就表示这个树枝的生长长度
91 // 因为初始角度设置为Math.PI , 所以vy要取负数
92 var vy = - this.speed * Math.sin(this.angle);
93
94 if(this.radius < 0.99 || this.generation > maxGeneration) {
95 branchArray.remove(this);
96 }
97
98 this.x += vx ;
99 this.y += vy ;
100
101 this.radius *= 0.99 ;
102
103 if(this.radius >= 0.9) {
104 // 计算当前是第几代分支
105 var g = (maxGeneration - 1) * initRadius / (initRadius - 1) / this.radius + (initRadius - maxGeneration) / (initRadius - 1) ;
106 if( g > this.generation + 1) {
107 this.generation = Math.floor(g) ; //这个应该是把g向下取整
108 // 随机创建分支
109 for(var i = 0 ; i < random(1,3) ; i++) {
110 this.clone(this);
111 }
112 }
113 }
114
115 }
116
117 /**
118 * 创建分支
119 * @param b
120 */
121 Branch.prototype.clone = function (b) {
122 var obj = new Branch(b.x , b.y); //定义一个对象
123 obj.angle = b.angle ;
124 obj.radius = b.radius ;
125 obj.speed = b.speed;
126 obj.generation = b.generation;
127 branchArray.add(obj);
128 // 如果当前分支次数大于3则创建花,这样可以让花在树的顶端显示
129 if( b.generation > 3 ) {
130 flowers.push(new Flower(b.x , b.y));
131 }
132 }
133
134 /**
135 * 添加树干到集合中
136 * @param b
137 */
138 BranchArray.prototype.add = function (b) {
139 this.branchs.push(b);
140 }
141 /**
142 * 从集合中移除树干
143 * @param b
144 */
145 BranchArray.prototype.remove = function (b) {
146 if( this.branchs.length > 0) {
147 var index = this.branchs.findIndex(function (item) {
148 return b === item ;
149 })
150 if(index != -1) {
151 this.branchs.splice(index , 1);
152 }
153 }
154 }
155
156 /**
157 * 花
158 * @param x
159 * @param y
160 * @constructor
161 */
162 function Flower (x , y) {
163 this.x = x ;
164 this.y = y ;
165 this.r = 1 ; // 花瓣的半径
166 this.petals = 5 ; // 花瓣数量
167 this.speed = 1.0235 ;// 花的绽放速度
168 this.maxR = random(3 , 7); // 花的大小
169 }
170
171 /**
172 * 花朵开放(通过改变花的半径实现开放的效果)
173 * @param index
174 */
175 Flower.prototype.update = function (index) {
176 if(this.r == this.maxR) {
177 flowers.splice(index , 1);
178 return ;
179 }
180 this.r *= this.speed ;
181 if(this.r > this.maxR) this.r = this.maxR ;
182 }
183
184 /**
185 * 绘制花朵
186 */
187 Flower.prototype.draw = function () {
188 ctx.fillStyle = "#F3097B" ;
189 for(var i = 1 ; i <= this.petals ; i++) {
190 var x0 = this.x + this.r * Math.cos( Math.PI / 180 * (360 / this.petals) * i) ;
191 var y0 = this.y + this.r * Math.sin( Math.PI / 180 * (360 / this.petals) * i) ;
192 ctx.beginPath();
193 ctx.arc(x0 , y0 , this.r , 0 , 2 * Math.PI) ;
194 ctx.fill();
195 }
196 ctx.fillStyle = "#F56BC1";
197 ctx.beginPath();
198 ctx.arc(this.x , this.y , this.r / 2 , 0 , 2 * Math.PI) ;
199 ctx.fill();
200 }
201
202 function random (min , max) {
203 return Math.random() * (max - min) + min ;
204 }
205
206 /**
207 * 循环遍历所有树干和花,并调用更新和draw方法,实现动画效果
208 */
209 function loop () {
210 for(var i = 0 ; i < branchArray.branchs.length ; i ++) {
211 var b = branchArray.branchs[i];
212 b.grow();
213 }
214 var len = flowers.length ;
215 while (len --) {
216 flowers[len].draw();
217 flowers[len].update();
218 }
219 MyRequestAnimationFrame(loop);
220 }
221
222 window.onload = initCanvas;
223
224 })(window)

HTML 5 Canvas 参考手册:https://www.w3school.com.cn/tags/html_ref_canvas.asp

1、树是由一个一个实心圆来构成的

test网页HTML源码:

 1 <!DOCTYPE html>
2 <html>
3 <head lang="en">
4 <meta charset="UTF-8">
5 <title>生长的树</title>
6 <style>
7 html , body {
8 margin: 0;
9 padding: 0;
10 width: 100%;
11 height: 100%;
12 overflow: hidden;
13 background-color: #fff;
14 }
15 </style>
16 </head>
17 <body>
18 <canvas id="myCanvas">此浏览器不支持canvas</canvas>
19 <script>
20 (function (window) {
21 var ctx=null;
22 var w = window.innerWidth , h = window.innerHeight ;
23 var canvas = document.getElementById("myCanvas");
24 canvas.setAttribute('width' , w);
25 canvas.setAttribute('height' , h);
26 if(canvas.getContext) { //getContext() 方法返回一个用于在画布上绘图的环境。目前参数唯一的合法值是 "2d",它指定了二维绘图,并且导致这个方法返回一个环境对象,该对象导出一个二维绘图 API。
27 ctx = canvas.getContext('2d');
28 ctx.fillStyle = '#55220F';
29 ctx.beginPath();
30 ctx.arc(100 ,90 , 25 , 0 , 2 * Math.PI);
31 ctx.fill(); //用所选颜色将圆染色
32
33 ctx.fillStyle = '#55220F';
34 ctx.beginPath();
35 ctx.arc(100 ,80 , 25 , 0 , 2 * Math.PI);
36 ctx.fill();
37
38 ctx.fillStyle = '#55220F';
39 ctx.beginPath();
40 ctx.arc(100 ,70 , 25 , 0 , 2 * Math.PI);
41 ctx.fill();
42 }
43
44 })(window)
45 </script>
46 </body>
47 </html>

你会发现三个圆可以构成一个树的树干(前提是圆与圆之间的距离不能太大。很好理解,距离太大他们重合部分就会小,就看起来不像树干了)

2、树的扭曲

this.angle += random( -0.1 * this.generation  , 0.1 * this.generation  );  //因为树枝半径小的话那么它分支的角度可能会更大

树的生长方向是通过角度angle来控制的,在区间[-0.1,0.1]中随机产生角度,然后通过三角函数计算x,y轴偏移量,之后就在坐标(x,y)位置画圆

因为this.generation 的大小是和圆的半径相关(关系在下面),那么半径小的时候this.generation就会大,那么也就是

树枝半径小的话那么它分支的角度可能会更大

3、树的分支

对于在什么时候应该让树去产生分支,我之前的想法是规定一段树干的长度,然后每次计算当前位置
距离上一个分支点的距离是否大于我规定的长度,然后产生分支,但是这样就会看到每节分支之间的长度是一样的
看起来不美观,比较死板,最终使用双曲线方程 y=-1/x 去控制因为双曲线的走势是先快后慢的,
而树的生长也是越往后分支越多,可能你会奇怪曲线是先快后慢,树分叉是先慢后快的,不符合逻辑啊
别着急,下看下图:

从图中可以看见,X轴表示树干的粗细,Y轴表示分支的次数,当树干越来越细的时候,X轴变小
是不是Y轴就越来越大,且是先慢后快,这样就符合我们的需求了。

又因为我们不能让它无条件产生分支,所以我们需要加一个条件限制,因为如果无条件分支,那么这棵树的分支集合里面的分支对象会越来越多,永远也执行不完,就会死循环。

我们控制分支最大数量为maxGeneration

var g = (maxGeneration - 1) * initRadius / (initRadius - 1) / this.radius + (initRadius - maxGeneration) / (initRadius - 1) ;
            

4、花的开放

它是通过判断分支个数来决定什么时间开花,比如分支大于3就开花。花的属性具体看代码

5、程序整体运行

function loop () {
for(var i = 0 ; i < branchArray.branchs.length ; i ++) {
var b = branchArray.branchs[i];
b.grow();
}
var len = flowers.length ;
while (len --) {
flowers[len].draw();
flowers[len].update();
}
MyRequestAnimationFrame(loop);
}

由程序我们知道,每当一个branch执行grow函数的时候,它执行过后,里面的属性可能会发生改变,然后它有被放入branchArray里面以待下次执行

因为我们不停的往branchArray里面放树枝(即,branch),那么又因为只有分支个数大于某个值才会移除这个树枝。所以它是先把所有树枝长出来之后才开花

所以loop这个函数只执行了一次

js实现一棵树的生长的相关教程结束。

《js实现一棵树的生长.doc》

下载本文的Word格式文档,以方便收藏与打印。