/* * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. */ /* * Gravity Sim * @author Davis Yarbrough */ var GravitySim = (function(window, undefined) { /**************************************\ | Class Variables | \**************************************/ var frameRate; // rendering frame rate var frameLength; // length of a frame based on the frame rate var scale; // current scale of the canvas var suns; // array of all suns in the scene var satellites; // array of all satellites in the scene var canvas; // the canvas to render to var cw, ch; // canvas width and height var ctx; // the canvas rendering context var gui; // google gui var settings; // settings for this sim var creatingSun; // are we currently creating a sun? var newSunPos; // position of the new sun we are creating var panning, scaling; // are we currently panning or scaling? /**************************************\ | Game Logic | \**************************************/ /* * Initialize everything and start the game loop * @param {float} _frameRate Framerate to run the sim at * @param {Canvas} _canvas Canvas to render to */ function init(_frameRate, _canvas) { frameRate = _frameRate; frameLength = frameRate * 1000; scale = 1; suns = []; satellites = []; canvas = _canvas; initCanvasCtx(); initEventListeners(); initSettings(); initGUI(); initSuns(25); initSatellites(150); loop(); } /* * Initialize the canvas context and all related variables */ function initCanvasCtx() { ctx = canvas.getContext("2d"); ctx.webkitImageSmoothingEnabled = true; cw = canvas.width; ch = canvas.height; } /* * Initialize all event listeners */ function initEventListeners() { // key and mouse event handlers initialization KeyMouseEventHandlers.init(canvas); // disable the context menu canvas.oncontextmenu = function () { return false; }; // window event listeners window.onresize = function() { cw = canvas.width = window.innerWidth; ch = canvas.height = window.innerHeight; }; } /* * Initialize settings for this sim */ function initSettings() { settings = { zoomSpeed: 0.015, // how fast the camera zooms in and out panSpeed: 50, // how fast the camera pans minScale: 0.1, // minimum zoom of the camera maxScale: 3, // maximum zoom of the camera gravityStrength: 0.01 // strength of gravity }; } /* * Initialize the google GUI */ function initGUI() { gui = new dat.GUI(); // add variables we want visible on the GUI gui.add(settings, 'zoomSpeed', 0.01, 0.05); gui.add(settings, 'panSpeed', 15, 100); gui.add(settings, 'minScale', 0.01, 1); gui.add(settings, 'maxScale', 3, 10); gui.add(settings, 'gravityStrength', 0, 0.1); } /* * Initialize all suns in the scene * @param {int} num Number to initialize */ function initSuns(num) { for (var i = 0; i < num; i++) { var newSunDensity = Math.random() > 0.5 ? 1 : -1; var newSunColor = newSunDensity === 1 ? 'red' : 'blue'; var newSun = new GravityWell({ position: { x: (Math.random() * cw) - (cw / 2), y: (Math.random() * ch) - (ch / 2) }, density: newSunDensity, radius: Math.random() * 50, fillStyle: newSunColor, useGradient: true }); suns.push(newSun); } } /* * Initialize all satellites in the scene * @param {int} num Number to initialize */ function initSatellites(num) { for (var i = 0; i < num; i++) { var newSat = new GravityWell({ position: { x: (Math.random() * cw) - (cw / 2), y: (Math.random() * ch) - (ch / 2) }, density: 1, radius: Math.random() * 2.5, fillStyle: 'black' }); satellites.push(newSat); } } /* * The main game loop */ function loop() { updateScene(); renderScene(); setTimeout(loop, frameLength); } /* * Update all scene elements */ function updateScene() { updateWells(); createNewSuns(); createNewSatellites(); panScene(); scaleScene(); } /* * Update all gravity wells in the scene */ function updateWells() { // determine force to add for each well for (var i = 0; i < suns.length; i++) { for (var j = 0; j < satellites.length; j++) { suns[i].addForceFrom(satellites[j]); } } for (var i = 0; i < satellites.length; i++) { for (var j = 0; j < suns.length; j++) { satellites[i].addForceFrom(suns[j]); } } // update position of each gravity well based on determined force for (var i = 0; i < suns.length; i++) { suns[i].updatePosition(); } for (var i = 0; i < satellites.length; i++) { satellites[i].updatePosition(); } } /* * Create new suns upon user input */ function createNewSuns() { var eh = "KeyMouseEventHandlers"; // eval(eh).getMousePosition() === KeyMouseEventHandlers.getMousePosition() var mousepos = eval(eh).getMousePosition(); // get and react to user input if (eval(eh).mouseButtonDown(eval(eh).mouseCode.left)) { if (!creatingSun) { creatingSun = true; newSunPos = eval(eh).getMousePosition(); } } else if (creatingSun) { creatingSun = false; var newSunDensity = mousepos.x - newSunPos.x >= 0 ? 1 : -1; var newSunColor = newSunDensity === 1 ? 'red' : 'blue'; var newRadius = Misc.dist(mousepos, newSunPos) / scale; suns.push(new GravityWell({ position: { x: (newSunPos.x - (cw / 2)) / scale, y: (newSunPos.y - (ch / 2)) / scale }, density: newSunDensity, radius: newRadius, fillStyle: newSunColor, useGradient: true })); } // render the sun if (creatingSun) { ctx.beginPath(); var currRadius = Misc.dist(mousepos, newSunPos); ctx.arc(newSunPos.x, newSunPos.y, currRadius, 0, Math.PI * 2); ctx.fill(); } } /* * Create new satellites upon user input */ function createNewSatellites() { var eh = "KeyMouseEventHandlers"; // eval(eh).getMousePosition() = KeyMouseEventHandlers.getMousePosition() var mousepos = eval(eh).getMousePosition(); if (eval(eh).mouseButtonDown(eval(eh).mouseCode.right)) satellites.push(new GravityWell({ position: { x: (mousepos.x - (cw / 2)) / scale, y: (mousepos.y - (ch / 2)) / scale }, density: 1, radius: Math.random() * 3, fillStyle: 'black' })); } /* * Pan the scene upon user input */ function panScene() { var hdir, // horizontal direction vdir; // vertical direction var eh = "KeyMouseEventHandlers"; // eval(eh).isKeyDown() === KeyMouseEventHandlers.isKeyDown() panning = false; // determine if we should pan the scene and in what direction if (eval(eh).keyDown(eval(eh).keyCode.left)) hdir = 1; else if (eval(eh).keyDown(eval(eh).keyCode.right)) hdir = -1; else if (eval(eh).keyDown(eval(eh).keyCode.up)) vdir = 1; else if (eval(eh).keyDown(eval(eh).keyCode.down)) vdir = -1; // update horizontal position of each well if necessary if (hdir) { panning = true; suns.forEach(function(sun) { sun.position.x += (hdir * settings.panSpeed) / scale; }); satellites.forEach(function(satellite) { satellite.position.x += (hdir * settings.panSpeed) / scale; }); } // update vertical position of each well if necessary if (vdir) { panning = true; suns.forEach(function(sun) { sun.position.y += (vdir * settings.panSpeed) / scale; }); satellites.forEach(function(satellite) { satellite.position.y += (vdir * settings.panSpeed) / scale; }); } } /* * Scale the scene upon user input */ function scaleScene() { var eh = "KeyMouseEventHandlers"; // eval(eh).isKeyDown() === KeyMouseEventHandlers.isKeyDown() scaling = false; if (eval(eh).keyDown(eval(eh).keyCode.pageUp)) if (scale < settings.maxScale) { scaling = 1; scale += settings.zoomSpeed * scale; } else scale = settings.maxScale; else if (eval(eh).keyDown(eval(eh).keyCode.pageDown)) if (scale > settings.minScale) { scaling = 1; scale -= settings.zoomSpeed * scale; } else scale = settings.minScale; } /* * Render all scene elements */ function renderScene() { ctx.save(); ctx.translate(cw / 2, ch / 2); ctx.scale(scale, scale); //ctx.fillStyle = panning || scaling ? 'rgba(255, 255, 255, 1)' : 'rgba(255, 255, 255, 1)'; //ctx.fillRect((-cw / 2) / scale, (-ch / 2) / scale, cw / scale, ch / scale); ctx.clearRect((-cw / 2) / scale, (-ch / 2) / scale, cw / scale, ch / scale); suns.forEach(function(sun) { sun.render(ctx); }); satellites.forEach(function(satellite) { satellite.render(ctx); }); ctx.restore(); } /**************************************\ | Getters | \**************************************/ function getScale() { return scale; } function getFrameRate() { return frameRate; } function getGravityStrength() { return settings.gravityStrength; } /**************************************/ // Module elements we want visible publicly return { init : init, getScale : getScale, getGravityStrength : getGravityStrength, getFrameRate : getFrameRate }; })(window); /* * Class representing a gravity well */ function GravityWell(args) { this.position = args.position || {x: 0, y: 0}; // position of the well on the canvas this.force = {x: 0, y: 0}; // used for physics calculations this.velocity = args.velocity || {x: 0, y: 0}; // velocity of the well this.density = args.density || 1; // density of the well this.radius = args.radius || 5; // radius of the well this.mass = (4 / 3) * Math.PI * this.radius * this.radius * this.radius * this.density; // mass of the well this.fillStyle = args.fillStyle || 'black'; // color to use when rendering this well this.useGradient = args.useGradient || false; // whether or not to use a gradient when rendering this well } GravityWell.prototype = { /* * Determine the amount of force another gravity well is exerting on this one * @param {GravityWell} other The other gravity well */ addForceFrom: function(other) { // distance from other gravity well var xDist = this.position.x - other.position.x, yDist = this.position.y - other.position.y; var dist = this.distance(other); // angles var cos = xDist / dist; var sin = yDist / dist; var G = GravitySim.getGravityStrength(); // add force based on modified version of universal gravitation this.force.x += G * -cos * (this.mass * other.mass) / (dist); this.force.y += G * -sin * (this.mass * other.mass) / (dist); }, /* * Update the position of this well based on its current velocity and the * force it is experiencing */ updatePosition: function() { // calculate x and y acceleration var ax = this.force.x / this.mass, ay = this.force.y / this.mass; // calculate x and y velocity var frameRate = GravitySim.getFrameRate(); this.velocity.x += ax * frameRate; this.velocity.y += ay * frameRate; // calculate new x and y positions this.position.x += this.velocity.x * frameRate; this.position.y += this.velocity.y * frameRate; // reset forces this.force.x = 0; this.force.y = 0; }, /* * Draw the gravity well * @param {CanvasRenderingContext2D} ctx The context to draw to */ render: function(ctx) { var fillStyle = this.fillStyle; if (this.useGradient) { // create a gradient for rendering fillStyle = ctx.createRadialGradient(this.position.x, this.position.y, 0, this.position.x, this.position.y, this.radius); fillStyle.addColorStop(0, this.fillStyle); fillStyle.addColorStop(1, "white"); } // render the well ctx.beginPath(); ctx.fillStyle = fillStyle; ctx.arc(this.position.x, this.position.y, this.radius, 0, Math.PI * 2); ctx.fill(); ctx.closePath(); }, /* * Retrieve the distance in pixels from another gravity well * @param {GravityWell} other The gravity well to get the distance from */ distance: function(other) { return Misc.dist(this.position, other.position); }, }; /* * Contains miscellaneous helper functions */ var Misc = (function() { /* * Computes the distance in pixels between the two parameter points * @param {Pair} a The first point * @param {Pair} b The second point */ function dist(a, b) { if (!a.hasOwnProperty("x") || !a.hasOwnProperty("y") || !b.hasOwnProperty("x") || !b.hasOwnProperty("y")) throw TypeError("Parameters must be objects with the properties x and y."); var x_dist = b.x - a.x; var y_dist = b.y - a.y; return Math.sqrt(x_dist * x_dist + y_dist * y_dist); } return { dist : dist }; })(undefined); // start the simulator window.onload = function() { var canvas = document.createElement("canvas"); var center = document.createElement("center"); canvas.width = window.innerWidth; canvas.height = window.innerHeight; document.body.appendChild(center); center.appendChild(canvas); GravitySim.init(1 / 60, canvas); };