├── .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 | 
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
--------------------------------------------------------------------------------