├── .DS_Store ├── README.md ├── index.html ├── main.css ├── main.js └── video.png /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivanseidel/BigBang-js/60d33c3794b7e20ae170a59c6fc2ff4e42bb5ba9/.DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BigBang JS 2 | 3 | ![Recriando o BigBang com JavaScript](https://raw.githubusercontent.com/ivanseidel/BigBang-js/master/video.png) 4 | 5 | A simple simulator that works with phisics to recreate gravitational forces. 6 | Watch the vídeo to understand better about the project 7 | 8 | **Watch** this video to see it in action, and learn how it works: [Recriando o BigBang com Javascript](https://www.youtube.com/watch?v=C5_7IV9XFd4) 9 | 10 | 11 | ## How does it work 12 | 13 | We have 3 different classes 14 | 15 | 1. Vector2 16 | 2. Planet 17 | 3. Simulator 18 | 19 | ## Phisics 20 | 21 | Each Planet is responsible for self-drawing and self-updating based on it's own intrinsics forces that are computed dynamically based on other planets. 22 | 23 | ## Implementation 24 | 25 | Help me write this! 🤓 26 | 27 | ## Credits 28 | 29 | - [Isaac Newton](https://pt.wikipedia.org/wiki/Isaac_Newton) 30 | - [Universe](https://en.wikipedia.org/wiki/Universe) 31 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | BigBang JS 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /main.css: -------------------------------------------------------------------------------- 1 | body, html { 2 | margin: 0; 3 | } -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | const MAX_ACCELERATION_MAGNITUDE = 10000 2 | const GRAVITATION_CONSTANT = 6.67e-11 * 10e10 3 | const BACKGROUND_COLOR = '#3E3E3E' 4 | 5 | const TRACE_LENGTH_PARTS = 10 6 | const TRACE_LENGTH_SKIP_STEPS = 8 7 | const EXISTING_RADIUS_MIN = 2 8 | const MASS_GIVEAWAY_FACTOR = 0.2 9 | 10 | const PLANETS_NUMBER = 200 11 | const PLANETS_POSITION_RANGE = 1000 12 | const PLANETS_VELOCITY_RANGE = 100 13 | const PLANETS_RADIUS_RANGE_MIN = 3 14 | const PLANETS_RADIUS_RANGE_MAX = 40 15 | const FIXED_DT = 0.016 16 | 17 | function start() { 18 | console.log('start') 19 | 20 | const canvas = initCanvas() 21 | const ctx = canvas.getContext('2d') 22 | 23 | // const p1 = new Planet(new Vector2(30, 30), 10) 24 | // const p2 = new Planet(new Vector2(40, 40), 5) 25 | const planets = [ 26 | // new Planet(new Vector2(200, 0), new Vector2(0, -290), 10), 27 | // new Planet(new Vector2(300, 0), new Vector2(0, -290), 20), 28 | // new Planet(new Vector2(-150, 0), new Vector2(0, -390), 30), 29 | // new Planet(new Vector2(-240, 0), new Vector2(10, -260), 15), 30 | // new Planet(new Vector2(0, 0), new Vector2(0, 0), 90), 31 | // new Planet(new Vector2(-600, -300), new Vector2(4, 2), 10), 32 | // new Planet(new Vector2(-300, 0), new Vector2(-100, -2), 50), 33 | // new Planet(new Vector2(300, 100), new Vector2(3, 0), 50), 34 | ] 35 | 36 | for (let k = 0; k < PLANETS_NUMBER; k ++ ) { 37 | let planet = new Planet( 38 | new Vector2(rand(-PLANETS_POSITION_RANGE, PLANETS_POSITION_RANGE), rand(-PLANETS_POSITION_RANGE, PLANETS_POSITION_RANGE)), 39 | new Vector2(rand(-PLANETS_VELOCITY_RANGE, PLANETS_VELOCITY_RANGE), rand(-PLANETS_VELOCITY_RANGE, PLANETS_VELOCITY_RANGE)), 40 | rand(PLANETS_RADIUS_RANGE_MIN, PLANETS_RADIUS_RANGE_MAX)) 41 | 42 | planets.push(planet) 43 | } 44 | 45 | planets.push(new Planet(new Vector2(0, 0), new Vector2(0, 0), 50)) 46 | 47 | const simulator = new Simulation(planets) 48 | 49 | setInterval(() => { 50 | // Update step 51 | simulator.update(FIXED_DT) 52 | 53 | // Clear canvas and Apply viewport 54 | updateCanvas(canvas) 55 | 56 | // Draw grid 57 | drawGrid(canvas) 58 | 59 | // Render step 60 | simulator.render(ctx) 61 | }, FIXED_DT * 1000); 62 | } 63 | 64 | class Simulation { 65 | constructor (planets) { 66 | this.planets = planets || [] 67 | 68 | // Seta a simulação no planeta 69 | this.planets.map(p => p.simulation = this) 70 | } 71 | 72 | update(dt = 0.016) { 73 | // Update all planets in simulation 74 | this.planets.map(planet => planet.update(dt)) 75 | } 76 | 77 | /** 78 | * 79 | * @param {CanvasRenderingContext2D} ctx 80 | */ 81 | render(ctx) { 82 | this.planets.map(p => p.render(ctx)) 83 | } 84 | 85 | removePlanet(planet) { 86 | this.planets = this.planets.filter(p => p != planet) 87 | } 88 | } 89 | 90 | class Vector2 { 91 | constructor(x = 0, y = 0) { 92 | this.x = x 93 | this.y = y 94 | } 95 | 96 | toString() { 97 | return `[${this.x.toFixed(2)}, ${this.y.toFixed(2)}]` 98 | } 99 | 100 | copy() { 101 | return new Vector2(this.x, this.y) 102 | } 103 | 104 | sub(vector) { 105 | this.x -= vector.x 106 | this.y -= vector.y 107 | return this 108 | } 109 | 110 | add(vector) { 111 | this.x += vector.x 112 | this.y += vector.y 113 | return this 114 | } 115 | 116 | scale(factorX, factorY = factorX) { 117 | this.x *= factorX 118 | this.y *= factorY 119 | return this 120 | } 121 | 122 | magnitude() { 123 | return Math.sqrt(this.x * this.x + this.y * this.y) 124 | } 125 | 126 | norm() { 127 | const mag = this.magnitude() 128 | this.x /= mag 129 | this.y /= mag 130 | return this 131 | } 132 | } 133 | 134 | class Planet { 135 | constructor(position = new Vector2(), velocity = new Vector2(), radius = 1, density = 1) { 136 | this.position = position 137 | this.velocity = velocity 138 | this.acceleration = new Vector2() 139 | this.forces = new Vector2() 140 | 141 | this.radius = radius 142 | this.density = density 143 | this.volume = 4 / 3 * Math.PI * Math.pow(radius, 3) 144 | this.mass = this.volume * this.density 145 | 146 | this.trace = [] 147 | } 148 | 149 | attractionTo(otherPlanet) { 150 | if (otherPlanet == this) { 151 | return new Vector2() 152 | } 153 | 154 | const distanceBetweenPlanets = otherPlanet.position.copy().sub(this.position) 155 | const distanceBetweenPlanetsScalar = distanceBetweenPlanets.magnitude() 156 | const forceScalar = newtonGravitationLaw(this.mass, otherPlanet.mass, distanceBetweenPlanetsScalar) 157 | const forceVector = distanceBetweenPlanets.norm().scale(forceScalar) 158 | 159 | return forceVector 160 | } 161 | 162 | computeTotalForces() { 163 | return this.simulation.planets 164 | .reduce((forces, planet) => forces.add(this.attractionTo(planet)), new Vector2()) 165 | } 166 | 167 | /** 168 | * 169 | * @param {Number} dt 170 | */ 171 | update(dt) { 172 | // Merge this planet to another if any 173 | let collidingPlanet = this.collidingPlanet() 174 | if (collidingPlanet) { 175 | this.mergeWith(collidingPlanet, dt) 176 | } 177 | 178 | this.forces = this.computeTotalForces() 179 | 180 | // Compute acceleration (Acc = Force / Mass) 181 | this.acceleration = this.forces.copy().scale(1 / this.mass) 182 | 183 | if (this.acceleration.magnitude() > MAX_ACCELERATION_MAGNITUDE) { 184 | this.exceeded_max_acceleration = true 185 | // this.acceleration.norm().scale(MAX_ACCELERATION_MAGNITUDE) 186 | this.acceleration.scale(0) 187 | } else { 188 | this.exceeded_max_acceleration = false 189 | } 190 | 191 | // Integrate to velocity (Vel = Vel + Acc * dt) 192 | this.velocity.add(this.acceleration.copy().scale(dt)) 193 | 194 | // Integrate to position 195 | this.position.add(this.velocity.copy().scale(dt)) 196 | 197 | // Add to trace 198 | let snapshot = { position: this.position.copy(), velocity: this.velocity.magnitude() } 199 | if (this.traceStep > TRACE_LENGTH_SKIP_STEPS) { 200 | this.trace.push(snapshot) 201 | this.trace = this.trace.slice(Math.max(0, this.trace.length - TRACE_LENGTH_PARTS)) 202 | this.traceStep = 0 203 | } else { 204 | this.traceStep = (this.traceStep || 0) + 1 205 | this.trace[this.trace.length - 1] = snapshot 206 | } 207 | } 208 | 209 | /** 210 | * 211 | * @param {CanvasRenderingContext2D} ctx 212 | */ 213 | render(ctx) { 214 | // Render the planet 215 | this.renderPlanet(ctx) 216 | 217 | // Render trace 218 | this.renderTrace(ctx) 219 | } 220 | 221 | renderPlanet(ctx) { 222 | ctx.beginPath() 223 | ctx.arc(this.position.x, this.position.y, this.radius, 0, 360) 224 | ctx.strokeStyle = this.exceeded_max_acceleration ? '#FF0000' : 'transparent' 225 | ctx.fillStyle = this.color() 226 | ctx.stroke() 227 | ctx.fill() 228 | } 229 | 230 | renderTrace(ctx) { 231 | if (this.trace.length > 1) { 232 | for (let i = 1; i < this.trace.length; i++) { 233 | ctx.beginPath() 234 | ctx.moveTo(this.trace[i - 1].position.x, this.trace[i - 1].position.y) 235 | ctx.lineTo(this.trace[i].position.x, this.trace[i].position.y) 236 | ctx.strokeStyle = colorForTrace(i, TRACE_LENGTH_PARTS) 237 | ctx.stroke() 238 | } 239 | } 240 | } 241 | 242 | color() { 243 | return interpolateColorStyleMapping(this.radius, 10, 100, 244 | [184, 233, 134, 0.8], 245 | [242, 100, 83, 0.8]) 246 | // [242, 174, 84, 0.8]) 247 | } 248 | 249 | 250 | addMass(mass, position) { 251 | let increase = (this.mass + mass) / this.mass 252 | let percent = mass / this.mass 253 | this.volume *= increase 254 | this.radius = Math.pow((3 / 4 * 1 / Math.PI * this.volume), (1 / 3)) 255 | this.mass += mass 256 | // this.position.add(this.position.copy().sub(position).scale(percent)) 257 | } 258 | 259 | mergeWith(planet, dt) { 260 | let giveMass = MASS_GIVEAWAY_FACTOR * this.mass * dt * 100 261 | 262 | if (this.radius < EXISTING_RADIUS_MIN) { 263 | giveMass = this.mass 264 | } 265 | 266 | planet.addMass(giveMass, this.position) 267 | this.addMass(-giveMass, planet.position) 268 | 269 | if (this.mass <= 0.1) { 270 | this.removed = true 271 | this.simulation.removePlanet(this) 272 | } 273 | } 274 | 275 | collidingPlanet() { 276 | return this.simulation.planets.find(p => this.collidingWith(p)) 277 | } 278 | 279 | collidingWith(planet) { 280 | if (planet == this || planet.mass < this.mass || this.removed) { 281 | return false 282 | } 283 | 284 | let distanceScalar = planet.position.copy().sub(this.position).magnitude() 285 | if (distanceScalar < planet.radius + this.radius) { 286 | return true 287 | } 288 | 289 | return false 290 | } 291 | } 292 | 293 | function newtonGravitationLaw(m1, m2, d) { 294 | const G = GRAVITATION_CONSTANT 295 | return G * (m1 * m2 / (d * d)) 296 | } 297 | 298 | 299 | function initCanvas() { 300 | const canvas = document.querySelector('#canvas') 301 | canvas.zoom = 1 302 | canvas.positionX = 0 303 | canvas.positionY = 0 304 | 305 | // resize the canvas to fill browser window dynamically 306 | window.addEventListener('resize', resizeCanvas, false); 307 | 308 | canvas.addEventListener('mousemove', function (evt) { 309 | if (!canvas.dragging) { 310 | return 311 | } 312 | canvas.positionX = (canvas.positionX || 0) + evt.movementX 313 | canvas.positionY = (canvas.positionY || 0) + evt.movementY 314 | }) 315 | 316 | canvas.addEventListener('mousedown', function drag() { 317 | canvas.dragging = true 318 | }) 319 | 320 | canvas.addEventListener('mouseup', function () { 321 | canvas.dragging = false 322 | }) 323 | 324 | canvas.addEventListener('wheel', function (evt) { 325 | canvas.zoom += canvas.zoom * (evt.deltaY / 100) 326 | }) 327 | 328 | function resizeCanvas() { 329 | canvas.width = window.innerWidth; 330 | canvas.height = window.innerHeight; 331 | } 332 | resizeCanvas(); 333 | 334 | return canvas 335 | } 336 | 337 | function updateCanvas(canvas) { 338 | const ctx = canvas.getContext('2d') 339 | 340 | const zoom = canvas.zoom 341 | const w = canvas.clientWidth 342 | const h = canvas.clientHeight 343 | const x = canvas.positionX || 0 344 | const y = canvas.positionY || 0 345 | 346 | ctx.resetTransform() 347 | ctx.fillStyle = BACKGROUND_COLOR 348 | ctx.fillRect(0, 0, canvas.clientWidth, canvas.clientHeight) 349 | ctx.translate(canvas.clientWidth / 2 + x, canvas.clientHeight / 2 + y) 350 | ctx.scale(zoom, zoom) 351 | } 352 | 353 | function drawGrid(canvas) { 354 | // const ctx = canvas.getContext('2d') 355 | // const w = canvas.clientWidth 356 | // const h = canvas.clientHeight 357 | 358 | // // Draw grid 359 | // ctx.beginPath() 360 | // ctx.strokeStyle = '#DDD' 361 | // for (let x = - w / 2; x <= w / 2; x += 100) { 362 | // for (let y = - w / 2; y <= h / 2; y += 100) { 363 | // ctx.moveTo(x, 0 - canvas.positionY); 364 | // ctx.lineTo(x, h - canvas.positionY); 365 | // ctx.stroke(); 366 | // ctx.moveTo(0, y); 367 | // ctx.lineTo(w, y); 368 | // ctx.stroke(); 369 | // } 370 | // } 371 | } 372 | 373 | function drawGridSVG(canvas) { 374 | if (!canvas.gridInitialized) { 375 | console.log('loading img') 376 | canvas.gridInitialized = true 377 | canvas.gridImg = null 378 | var data = ' \ 379 | \ 380 | \ 381 | \ 382 | \ 383 | \ 384 | \ 385 | \ 386 | \ 387 | \ 388 | \ 389 | '; 390 | 391 | var DOMURL = window.URL || window.webkitURL || window; 392 | 393 | var img = new Image(); 394 | var svg = new Blob([data], { type: 'image/svg+xml;charset=utf-8' }); 395 | var url = DOMURL.createObjectURL(svg); 396 | 397 | img.onload = function () { 398 | console.log('loaded img') 399 | canvas.gridImg = img 400 | DOMURL.revokeObjectURL(url); 401 | } 402 | } 403 | 404 | if (!canvas.gridImg) { 405 | return 406 | } 407 | 408 | const ctx = canvas.getContext('2d') 409 | ctx.drawImage(img, 0, 0); 410 | } 411 | 412 | function colorForTrace(mag, magE = 500) { 413 | const magS = 0 414 | const colorAtMax = [230, 255, 230, 0.9] 415 | const colorAtMin = [255, 255, 255, 0.05] 416 | 417 | return interpolateColorStyleMapping(mag, magS, magE, colorAtMin, colorAtMax) 418 | } 419 | 420 | function interpolateColorStyleMapping(mag, magS, magE, colorAtMin, colorAtMax) { 421 | let int = (mag - magS) / (magE - magS) 422 | return interpolateColorStyle(int, colorAtMin, colorAtMax) 423 | } 424 | 425 | function interpolateColorStyle(int, s, e) { 426 | int = Math.max(Math.min(int, 1), 0) 427 | intI = 1 - int 428 | return `rgba(${s[0] * intI + e[0] * int}, ${s[1] * intI + e[1] * int}, ${s[2] * intI + e[2] * int}, ${s[3] * intI + e[3] * int})` 429 | } 430 | 431 | function rand(min, max) { 432 | return Math.random() * (max-min) + min 433 | } 434 | 435 | window.onload = start 436 | 437 | -------------------------------------------------------------------------------- /video.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivanseidel/BigBang-js/60d33c3794b7e20ae170a59c6fc2ff4e42bb5ba9/video.png --------------------------------------------------------------------------------