js实现一棵树的生长

参考链接: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> 
View Code

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> 
View Code

 

 

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

 

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这个函数只执行了一次

posted @ 2020-06-22 14:32  kongbursi  阅读(474)  评论(0编辑  收藏  举报