Separate example from core
This commit is contained in:
		
							parent
							
								
									fd32a3b1d0
								
							
						
					
					
						commit
						32411933cd
					
				
							
								
								
									
										295
									
								
								boids.mjs
								
								
								
								
							
							
						
						
									
										295
									
								
								boids.mjs
								
								
								
								
							|  | @ -1,79 +1,252 @@ | |||
| import Vector from './vector.mjs'; | ||||
| 
 | ||||
| const meanPos = new Vector(0, 0); | ||||
| const meanVel = new Vector(0, 0); | ||||
| const repelForce = new Vector(0, 0); | ||||
| 
 | ||||
| export const update = (boids, params, width, height, time) => | ||||
| class Boids | ||||
| { | ||||
|     const boidsSize = boids.length; | ||||
| 
 | ||||
|     for (let i = 0; i < boidsSize; ++i) | ||||
|     constructor(canvas, params = {}) | ||||
|     { | ||||
|         const me = boids[i]; | ||||
|         this.canvas = canvas; | ||||
|         this.ctx = canvas.getContext('2d'); | ||||
| 
 | ||||
|         meanPos.reset(); | ||||
|         meanVel.reset(); | ||||
|         repelForce.reset(); | ||||
|         this.params = Object.assign({ | ||||
|             centerAccel: 0.075, | ||||
|             repelAccel: 0.8, | ||||
|             matchAccel: 0.15, | ||||
|             boundsAccel: 0.01, | ||||
| 
 | ||||
|         let visibleSize = 0; | ||||
|             maxSpeed: 300, | ||||
| 
 | ||||
|         for (let j = 0; j < boidsSize; ++j) | ||||
|             closeDist: 20, | ||||
|             visibleDist: 60, | ||||
| 
 | ||||
|             radius: 10, | ||||
|             color: 'black', | ||||
|         }, params); | ||||
| 
 | ||||
|         // Current width and height of the canvas
 | ||||
|         this.width = canvas.width; | ||||
|         this.height = canvas.height; | ||||
| 
 | ||||
|         // Current center point of the canvas
 | ||||
|         this.center = new Vector(canvas.width / 2, canvas.height / 2); | ||||
| 
 | ||||
|         // Last time where the canvas was repainted
 | ||||
|         this.lastTime = null; | ||||
| 
 | ||||
|         // List of active simulated boids
 | ||||
|         this.boids = []; | ||||
| 
 | ||||
|         // Vector registers used for holding temporary values
 | ||||
|         this.registers = [ | ||||
|             new Vector(0, 0), | ||||
|             new Vector(0, 0), | ||||
|             new Vector(0, 0) | ||||
|         ]; | ||||
| 
 | ||||
|         // When the simulation is running, the ID of the next rAF request,
 | ||||
|         // otherwise, null
 | ||||
|         this.animationId = null; | ||||
| 
 | ||||
|         // Outline of the shape of a boid when angled at 0 rad
 | ||||
|         this.boidShape = [ | ||||
|             new Vector(1, 0), | ||||
|             new Vector(-.5, .5), | ||||
|             new Vector(-.5, -.5), | ||||
|             new Vector(1, 0), | ||||
|         ]; | ||||
|         this.boidShapeLength = this.boidShape.length; | ||||
|     } | ||||
| 
 | ||||
|     /** Change the canvas’ dimensions */ | ||||
|     resize(width, height) | ||||
|     { | ||||
|         this.canvas.width = width; | ||||
|         this.canvas.height = height; | ||||
| 
 | ||||
|         this.width = width; | ||||
|         this.height = height; | ||||
|         this.center = new Vector(width / 2, height / 2); | ||||
|     } | ||||
| 
 | ||||
|     /** Introduce a new boid in the simulation. */ | ||||
|     add(center) | ||||
|     { | ||||
|         this.boids.push({ | ||||
|             pos: center.clone(), | ||||
|             vel: new Vector(Math.random() - 0.5, Math.random() - 0.5), | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** Start the simulation. */ | ||||
|     start() | ||||
|     { | ||||
|         if (this.animationId !== null) | ||||
|         { | ||||
|             if (i != j) | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         this._step = this._step.bind(this); | ||||
|         this.animationId = requestAnimationFrame(this._step); | ||||
|     } | ||||
| 
 | ||||
|     /** Pause the simulation. */ | ||||
|     pause() | ||||
|     { | ||||
|         cancelAnimationFrame(this.animationId); | ||||
|         this.animationId = null; | ||||
|         this.lastTime = null; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @private | ||||
|      * Perform a time step of the simulation and update the canvas. | ||||
|      */ | ||||
|     _step(time) | ||||
|     { | ||||
|         if (!this.lastTime) | ||||
|         { | ||||
|             this.lastTime = time; | ||||
|         } | ||||
| 
 | ||||
|         const delta = (time - this.lastTime) / 1000; | ||||
|         this.lastTime = time; | ||||
| 
 | ||||
|         this._update(delta); | ||||
|         this._draw(); | ||||
| 
 | ||||
|         if (this.animationId !== null) | ||||
|         { | ||||
|             this.animationId = requestAnimationFrame(this._step); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @private | ||||
|      * Advance the simulation. | ||||
|      */ | ||||
|     _update(delta) | ||||
|     { | ||||
|         const length = this.boids.length; | ||||
| 
 | ||||
|         for (let i = 0; i < length; ++i) | ||||
|         { | ||||
|             const me = this.boids[i]; | ||||
| 
 | ||||
|             const meanPos = this.registers[0].reset(); | ||||
|             const meanVel = this.registers[1].reset(); | ||||
|             const repelForce = this.registers[2].reset(); | ||||
| 
 | ||||
|             let visibles = 0; | ||||
| 
 | ||||
|             for (let j = 0; j < length; ++j) | ||||
|             { | ||||
|                 const you = boids[j]; | ||||
|                 const dist = Vector.distSquared(me.pos, you.pos); | ||||
| 
 | ||||
|                 if (dist < params.visibleDist * params.visibleDist) | ||||
|                 if (i != j) | ||||
|                 { | ||||
|                     meanPos.add(you.pos); | ||||
|                     meanVel.add(you.vel); | ||||
|                     visibleSize += 1; | ||||
|                     const you = this.boids[j]; | ||||
|                     const dist = Vector.distSquared(me.pos, you.pos); | ||||
| 
 | ||||
|                     if (dist < params.closeDist * params.closeDist) | ||||
|                     if (dist < this.params.visibleDist | ||||
|                              * this.params.visibleDist) | ||||
|                     { | ||||
|                         repelForce.add(me.pos).sub(you.pos); | ||||
|                         meanPos.add(you.pos); | ||||
|                         meanVel.add(you.vel); | ||||
|                         visibles += 1; | ||||
| 
 | ||||
|                         if (dist < this.params.closeDist | ||||
|                                  * this.params.closeDist) | ||||
|                         { | ||||
|                             repelForce.add(me.pos).sub(you.pos); | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             // Attract towards center of visible flock
 | ||||
|             if (visibles >= 1) | ||||
|             { | ||||
|                 me.vel.add(meanPos.div(visibles) | ||||
|                                   .sub(me.pos) | ||||
|                                   .mul(this.params.centerAccel)); | ||||
|             } | ||||
| 
 | ||||
|             // Attract toward center of screen if out of bounds
 | ||||
|             if (me.pos.x < this.width * -.4 | ||||
|                     || me.pos.x > this.width * .4 | ||||
|                     || me.pos.y < this.height * -.4 | ||||
|                     || me.pos.y > this.height * .4) | ||||
|             { | ||||
|                 me.vel.addMul(me.pos, -this.params.boundsAccel); | ||||
|             } | ||||
| 
 | ||||
|             // Repel away from close boids
 | ||||
|             me.vel.add(repelForce.mul(this.params.repelAccel)); | ||||
| 
 | ||||
|             // Match other boids’ velocity
 | ||||
|             if (visibles >= 1) | ||||
|             { | ||||
|                 me.vel.add(meanVel.div(visibles) | ||||
|                                   .sub(me.vel) | ||||
|                                   .mul(this.params.matchAccel)); | ||||
|             } | ||||
| 
 | ||||
|             // Do not surpass maximum speed
 | ||||
|             const speed = me.vel.normSquared(); | ||||
| 
 | ||||
|             if (speed > this.params.maxSpeed * this.params.maxSpeed) | ||||
|             { | ||||
|                 me.vel.div(Math.sqrt(speed)).mul(this.params.maxSpeed); | ||||
|             } | ||||
| 
 | ||||
|             // Integrate speed
 | ||||
|             me.pos.addMul(me.vel, delta); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @private | ||||
|      * Redraw the boids. | ||||
|      */ | ||||
|     _draw() | ||||
|     { | ||||
|         const length = this.boids.length; | ||||
|         const transformed = this.registers[0]; | ||||
| 
 | ||||
|         this.ctx.clearRect(0, 0, this.width, this.height); | ||||
| 
 | ||||
|         // Draw each boid’s head following the angle of its course
 | ||||
|         this.ctx.beginPath(); | ||||
|         this.ctx.fillStyle = this.params.color; | ||||
| 
 | ||||
|         for (let i = 0; i < length; ++i) | ||||
|         { | ||||
|             const boid = this.boids[i]; | ||||
|             const angle = boid.vel.angle(); | ||||
|             let isFirst = true; | ||||
| 
 | ||||
|             for (let j = 0; j < this.boidShapeLength; ++j) | ||||
|             { | ||||
|                 transformed.x = this.boidShape[j].x; | ||||
|                 transformed.y = this.boidShape[j].y; | ||||
| 
 | ||||
|                 transformed | ||||
|                     .rotate(angle) | ||||
|                     .mul(this.params.radius) | ||||
|                     .add(this.center) | ||||
|                     .add(boid.pos); | ||||
| 
 | ||||
|                 if (isFirst) | ||||
|                 { | ||||
|                     this.ctx.moveTo(transformed.x, transformed.y); | ||||
|                     isFirst = false; | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     this.ctx.lineTo(transformed.x, transformed.y); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         // Attract towards center of visible flock
 | ||||
|         if (visibleSize >= 1) | ||||
|         { | ||||
|             me.vel.add(meanPos.div(visibleSize) | ||||
|                               .sub(me.pos) | ||||
|                               .mul(params.centerAccel)); | ||||
|         } | ||||
| 
 | ||||
|         // Attract toward center of screen if out of bounds
 | ||||
|         if (me.pos.x < width * -.4 || me.pos.x > width * .4 || | ||||
|                 me.pos.y < height * -.4 || me.pos.y > height * .4) | ||||
|         { | ||||
|             me.vel.addMul(me.pos, -params.boundsAccel); | ||||
|         } | ||||
| 
 | ||||
|         // Repel away from close boids
 | ||||
|         me.vel.add(repelForce.mul(params.repelAccel)); | ||||
| 
 | ||||
|         // Match other boids’ velocity
 | ||||
|         if (visibleSize >= 1) | ||||
|         { | ||||
|             me.vel.add(meanVel.div(visibleSize) | ||||
|                               .sub(me.vel) | ||||
|                               .mul(params.matchAccel)); | ||||
|         } | ||||
| 
 | ||||
|         // Do not surpass maximum speed
 | ||||
|         const speed = me.vel.normSquared(); | ||||
| 
 | ||||
|         if (speed > params.maxSpeed * params.maxSpeed) | ||||
|         { | ||||
|             me.vel.div(Math.sqrt(speed)).mul(params.maxSpeed); | ||||
|         } | ||||
| 
 | ||||
|         // Integrate speed
 | ||||
|         me.pos.addMul(me.vel, time); | ||||
|         this.ctx.fill(); | ||||
|     } | ||||
| }; | ||||
| } | ||||
| 
 | ||||
| export default Boids; | ||||
|  |  | |||
							
								
								
									
										53
									
								
								draw.mjs
								
								
								
								
							
							
						
						
									
										53
									
								
								draw.mjs
								
								
								
								
							|  | @ -1,53 +0,0 @@ | |||
| import Vector from './vector.mjs'; | ||||
| 
 | ||||
| const boidShape = [ | ||||
|     new Vector(1, 0), | ||||
|     new Vector(-.5, .5), | ||||
|     new Vector(-.5, -.5), | ||||
|     new Vector(1, 0), | ||||
| ]; | ||||
| 
 | ||||
| const boidShapeSize = boidShape.length; | ||||
| const transformed = new Vector(0, 0); | ||||
| 
 | ||||
| export const fill = (activeBoids, params, width, height, ctx, center) => | ||||
| { | ||||
|     const boidsSize = activeBoids.length; | ||||
| 
 | ||||
|     ctx.clearRect(0, 0, width, height); | ||||
| 
 | ||||
|     // Draw each boid’s head following the angle of its course
 | ||||
|     ctx.beginPath(); | ||||
|     ctx.fillStyle = params.color; | ||||
| 
 | ||||
|     for (let i = 0; i < boidsSize; ++i) | ||||
|     { | ||||
|         const boid = activeBoids[i]; | ||||
|         const angle = boid.vel.angle(); | ||||
|         let isFirst = true; | ||||
| 
 | ||||
|         for (let j = 0; j < boidShapeSize; ++j) | ||||
|         { | ||||
|             transformed.x = boidShape[j].x; | ||||
|             transformed.y = boidShape[j].y; | ||||
| 
 | ||||
|             transformed | ||||
|                 .rotate(angle) | ||||
|                 .mul(params.radius) | ||||
|                 .add(center) | ||||
|                 .add(boid.pos); | ||||
| 
 | ||||
|             if (isFirst) | ||||
|             { | ||||
|                 ctx.moveTo(transformed.x, transformed.y); | ||||
|                 isFirst = false; | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 ctx.lineTo(transformed.x, transformed.y); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     ctx.fill(); | ||||
| }; | ||||
							
								
								
									
										39
									
								
								index.html
								
								
								
								
							
							
						
						
									
										39
									
								
								index.html
								
								
								
								
							|  | @ -23,6 +23,43 @@ | |||
|     </head> | ||||
|     <body> | ||||
|         <canvas id="boids-canvas"></canvas> | ||||
|         <script src="index.mjs" type="module"></script> | ||||
|         <script type="module"> | ||||
|             import Boids from './boids.mjs'; | ||||
|             import Vector from './vector.mjs'; | ||||
| 
 | ||||
|             const canvas = document.querySelector('#boids-canvas'); | ||||
|             const boids = new Boids(canvas); | ||||
| 
 | ||||
|             canvas.onclick = ev => | ||||
|             { | ||||
|                 const pos = new Vector(ev.offsetX, ev.offsetY); | ||||
|                 pos.sub(boids.center); | ||||
| 
 | ||||
|                 for (let i = 0; i < 100; ++i) | ||||
|                 { | ||||
|                     boids.add(pos); | ||||
|                 } | ||||
|             }; | ||||
| 
 | ||||
|             window.onresize = () => | ||||
|             { | ||||
|                 boids.resize(window.innerWidth, window.innerHeight); | ||||
|             }; | ||||
| 
 | ||||
|             document.onvisibilitychange = () => | ||||
|             { | ||||
|                 if (document.visibilityState === 'visible') | ||||
|                 { | ||||
|                     boids.start(); | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     boids.pause(); | ||||
|                 } | ||||
|             }; | ||||
| 
 | ||||
|             boids.resize(window.innerWidth, window.innerHeight); | ||||
|             boids.start(); | ||||
|         </script> | ||||
|     </body> | ||||
| </html> | ||||
|  |  | |||
							
								
								
									
										106
									
								
								index.mjs
								
								
								
								
							
							
						
						
									
										106
									
								
								index.mjs
								
								
								
								
							|  | @ -1,106 +0,0 @@ | |||
| import Vector from './vector.mjs'; | ||||
| import * as boids from './boids.mjs'; | ||||
| import * as draw from './draw.mjs'; | ||||
| 
 | ||||
| const params = { | ||||
|     centerAccel: 0.075, | ||||
|     repelAccel: 0.8, | ||||
|     matchAccel: 0.15, | ||||
|     boundsAccel: 0.01, | ||||
| 
 | ||||
|     maxSpeed: 300, | ||||
| 
 | ||||
|     closeDist: 20, | ||||
|     visibleDist: 60, | ||||
| 
 | ||||
|     radius: 10, | ||||
|     color: '#675148', | ||||
| }; | ||||
| 
 | ||||
| const boidsCanvas = document.querySelector('#boids-canvas'); | ||||
| const boidsCtx = boidsCanvas.getContext('2d'); | ||||
| 
 | ||||
| let width = null; | ||||
| let height = null; | ||||
| let center = null; | ||||
| 
 | ||||
| const updateSize = () => | ||||
| { | ||||
|     width = window.innerWidth; | ||||
|     height = window.innerHeight; | ||||
|     center = new Vector(width / 2, height / 2); | ||||
| 
 | ||||
|     boidsCanvas.width = width; | ||||
|     boidsCanvas.height = height; | ||||
| }; | ||||
| 
 | ||||
| updateSize(); | ||||
| window.onresize = updateSize; | ||||
| 
 | ||||
| const activeBoids = []; | ||||
| 
 | ||||
| const addBoids = (pos, count) => | ||||
| { | ||||
|     for (let i = 0; i < count; ++i) | ||||
|     { | ||||
|         activeBoids.push({ | ||||
|             pos: pos.clone(), | ||||
|             vel: new Vector(Math.random(), Math.random()), | ||||
|         }); | ||||
|     } | ||||
| }; | ||||
| 
 | ||||
| boidsCanvas.onclick = ev => | ||||
| { | ||||
|     const pos = new Vector(ev.offsetX, ev.offsetY); | ||||
|     pos.sub(center); | ||||
|     addBoids(pos, 100); | ||||
| }; | ||||
| 
 | ||||
| let paused = false; | ||||
| let lastTime = null; | ||||
| 
 | ||||
| const loop = time => | ||||
| { | ||||
|     if (!lastTime) | ||||
|     { | ||||
|         lastTime = time; | ||||
|     } | ||||
| 
 | ||||
|     const delta = (time - lastTime) / 1000; | ||||
|     lastTime = time; | ||||
| 
 | ||||
|     boids.update(activeBoids, params, width, height, delta); | ||||
|     draw.fill(activeBoids, params, width, height, boidsCtx, center); | ||||
| 
 | ||||
|     if (!paused) | ||||
|     { | ||||
|         requestAnimationFrame(loop); | ||||
|     } | ||||
| }; | ||||
| 
 | ||||
| const start = () => | ||||
| { | ||||
|     lastTime = null; | ||||
|     paused = false; | ||||
|     requestAnimationFrame(loop); | ||||
| }; | ||||
| 
 | ||||
| const pause = () => | ||||
| { | ||||
|     paused = true; | ||||
| }; | ||||
| 
 | ||||
| document.onvisibilitychange = () => | ||||
| { | ||||
|     if (document.visibilityState === 'visible') | ||||
|     { | ||||
|         start(); | ||||
|     } | ||||
|     else | ||||
|     { | ||||
|         pause(); | ||||
|     } | ||||
| }; | ||||
| 
 | ||||
| start(); | ||||
		Loading…
	
		Reference in New Issue