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