commit 303a423a295f027e1c1a4f8b631355b1926fd989 Author: Lentil Hoffman Date: Sun Dec 21 08:13:43 2025 -0600 initial commit diff --git a/index.html b/index.html new file mode 100644 index 0000000..2f2ea32 --- /dev/null +++ b/index.html @@ -0,0 +1,32 @@ + + + + Gravity Simulator + + + + + +
+ + + diff --git a/simulator.js b/simulator.js new file mode 100644 index 0000000..8c7cdc7 --- /dev/null +++ b/simulator.js @@ -0,0 +1,192 @@ +export class MassObject { + mass = 0; + density = 1; + position = {x: undefined, y: undefined}; + color = {r: undefined, g: undefined, b: undefined}; + created = undefined; + + constructor(x, y) { + this.position.x = x; + this.position.y = y; + this.color.r = Math.random() * 256; + this.color.g = Math.random() * 256; + this.color.b = Math.random() * 256; + this.created = document.timeline.currentTime; + } + + get age() { + return document.timeline.currentTime - this.created; + } +} + +export class Sim { + info = {}; + objects = []; + frame = 0; + time = undefined; + + MASS_CREATION_RATE = 0.001; + DISPLAY_OBJECTS_INFO = false; + DISPLAY_CURSOR_INFO = false; + + fullscreen() { + this.canvas.width = document.documentElement.clientWidth; + this.canvas.height = document.documentElement.clientHeight; + // this.info['Canvas'] = `${this.canvas.width} x ${this.canvas.height}`; + this.fillCanvas(); + this.renderInfo(); + } + + fillCanvas() { + const ctx = this.ctx; + ctx.fillStyle = '#000'; + ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); + } + + renderInfo() { + this.infoBox.innerHTML = ''; + const table = document.createElement('table'); + for (let [k, v] of Object.entries(this.info)) { + let row = document.createElement('tr'); + let keyCell = document.createElement('td'); + keyCell.innerHTML = `${k}: `; + row.appendChild(keyCell); + let vs = Array.isArray(v) ? v : [v]; + for (let x of v) { + let valueCell = document.createElement('td'); + valueCell.innerHTML = x; + row.appendChild(valueCell); + } + table.appendChild(row); + } + this.infoBox.appendChild(table); + } + + init(divId) { + this.divId = divId; + const div = document.getElementById(this.divId); + this.div = div; + + // Add info text box + const infoBox = document.createElement('div'); + this.div.appendChild(infoBox); + this.infoBox = infoBox; + infoBox.style.position = 'absolute'; + infoBox.style.top = 0; + infoBox.style.left = 0; + infoBox.width = 'fit-content'; + infoBox.style.zIndex = 1; + + // Create canvas that fills the window + // If the window resizes, also resize the canvas + const canvas = document.createElement('canvas') + this.canvas = canvas; + this.ctx = canvas.getContext("2d"); + this.div.appendChild(canvas); + canvas.style.position = 'absolute'; + canvas.style.top = 0; + canvas.style.left = 0; + this.fullscreen(); + window.addEventListener('resize', () => this.fullscreen()); + + // Monitor mouse movements + const el = window; + el.addEventListener('mousemove', e => { + if (this.DISPLAY_CURSOR_INFO) { + this.info['Mouse move'] = [`${e.clientX}, `, `${e.clientY}`]; + this.renderInfo(); + } + }); + // Monitor touch events + el.addEventListener('touchmove', e => { + if (this.DISPLAY_CURSOR_INFO) { + this.info['Touch move'] = [`${e.touches[0].pageX}, `, `${e.touches[0].pageY}`]; + this.renderInfo(); + } + }); + el.addEventListener('pointerdown', e => { + if (this.DISPLAY_CURSOR_INFO) { + this.info['Pointer down'] = [`${e.clientX}, `, `${e.clientY}`]; + this.renderInfo(); + } + this.createObject(e.clientX, e.clientY); + }); + el.addEventListener('pointerup', e => { + if (this.DISPLAY_CURSOR_INFO) { + this.info['Pointer up'] = [`${e.clientX}, `, `${e.clientY}`]; + this.renderInfo(); + } + this.doneCreatingObject(); + }); + el.addEventListener('click', e => { + if (this.DISPLAY_CURSOR_INFO) { + this.info['Click'] = [`${e.clientX}, `, `${e.clientY}`]; + this.renderInfo(); + } + }); + + // Initiate main loop + this.time = document.timeline.currentTime; + requestAnimationFrame(t => this.loop(t)); + } + + // Create an object with mass that grows as pointer is held down + createObject(x, y) { + const obj = new MassObject(x, y); + const idx = this.objects.length; + this.creatingObject = idx; + this.objects.push(obj); + } + + // Done creating object + doneCreatingObject() { + this.creatingObject = undefined; + } + + // Main loop + loop(currentTime) { + const elapsedTime = currentTime - this.time; + this.time = currentTime; + + // If we're creating an object, increment its mass + // with the mass creation rate accelerating over time + if (this.creatingObject !== undefined) { + const obj = this.objects[this.creatingObject]; + const rate = this.MASS_CREATION_RATE * obj.age; + obj.mass += rate * elapsedTime; + } + + // Display objects info + if (this.DISPLAY_OBJECTS_INFO) { + for (let i = 0; i < this.objects.length; i++) { + const obj = this.objects[i]; + this.info[`Object ${i}`] = [`${obj.position.x}, `, `${obj.position.y}, `, `${obj.mass.toPrecision(6)} kg`]; + this.renderInfo(); + } + } + + this.fillCanvas(); + this.renderObjects(); + + requestAnimationFrame(t => this.loop(t)); + } + + renderObject(idx) { + const obj = this.objects[idx]; + const ctx = this.ctx; + // radius should be proportional to cube root of mass + const radius = Math.pow(obj.mass / obj.density, 1/3); + const {r, g, b} = obj.color; + const {x, y} = obj.position; + ctx.fillStyle = `rgb(${r}, ${g}, ${b})`; + ctx.beginPath(); + ctx.arc(x, y, radius, 0, 2*Math.PI); + ctx.fill(); + } + + renderObjects() { + for (let i = 0; i < this.objects.length; i++) { + this.renderObject(i); + } + } +}