Skip to main content
added 87 characters in body
Source Link
coderodde
  • 29.8k
  • 14
  • 77
  • 194

Since I am not a professional Javascript developer, it would be nice to hear the experts' opinions. Also, how should I go about making sure that the canvas occupies the entire view port?

Since I am not a professional Javascript developer, it would be nice to hear the experts' opinions.

Since I am not a professional Javascript developer, it would be nice to hear the experts' opinions. Also, how should I go about making sure that the canvas occupies the entire view port?

Source Link
coderodde
  • 29.8k
  • 14
  • 77
  • 194

Closed particle system simulation in vanilla Javascript

I have ported my simulation from Java to vanilla Javascript. The most important aim was to keep the sum of all energies constant. I have this:

simulation.js

var Configuration = {
    PIXELS_PER_UNIT_LENGTH: 10.0,
    FORCE_CONSTANT: 500.0,
    SLEEP_TIME: 10,
    TIME_STEP: 0.01,
    MINIMUM_PARTICLE_MASS: 10.0,
    MAXIMUM_PARTICLE_MASS: 30.0,
    MAXIMUM_INITIAL_VELOCITY: 40.0,
    DEFAULT_NUMBER_OF_PARTICLES: 10
};

function Particle(mass, radius) {
    this.mass = mass;
    this.radius = radius;
    this.x = 0.0;
    this.y = 0.0;
    this.velocityX = 0.0;
    this.velocityY = 0.0;
}

Particle.prototype.getMass = function() {
    return this.mass;
};

Particle.prototype.getX = function() {
    return this.x;
};

Particle.prototype.getY = function() {
    return this.y;
};

Particle.prototype.getVelocityX = function() {
    return this.velocityX;
};

Particle.prototype.getVelocityY = function() {
    return this.velocityY;
};

Particle.prototype.setX = function(x) {
    this.x = x;
};

Particle.prototype.setY = function(y) {
    this.y = y;
};

Particle.prototype.setVelocityX = function(velocityX) {
    this.velocityX = velocityX;
};

Particle.prototype.setVelocityY = function(velocityY) {
    this.velocityY = velocityY;
};

Particle.prototype.getSpeed = function() {
    var vxSquared = this.velocityX * this.velocityX;
    var vySquared = this.velocityY * this.velocityY;
    return Math.sqrt(vxSquared + vySquared);
};

Particle.prototype.getDistance = function(other) {
    var dx = this.x - other.x;
    var dy = this.y - other.y;
    return Math.sqrt(dx * dx + dy * dy);
};

Particle.prototype.getKineticEnergy = function() {
    var speed = this.getSpeed();
    return 0.5 * this.mass * speed * speed;
};

Particle.prototype.getRadius = function() {
    return this.radius;
};

function getForce(particle1, particle2) {
    var distance = particle1.getDistance(particle2);
    var mass1 = particle1.getMass();
    var mass2 = particle2.getMass();
    return Configuration.FORCE_CONSTANT * mass1 * mass2 / (distance * distance);
}

function getForceVector(particle1, particle2) {
    var vectorLength = getForce(particle1, particle2);
    var dx = particle1.getX() - particle2.getX();
    var dy = particle1.getY() - particle2.getY();
    var angle = Math.atan2(dy, dx);
    var xComponent = vectorLength * Math.cos(angle);
    var yComponent = vectorLength * Math.sin(angle);
    return new Vector(xComponent, yComponent);
}

function getPotentialEnergy(particle1, particle2) {
    var mass1 = particle1.getMass();
    var mass2 = particle2.getMass();
    var distance = particle1.getDistance(particle2);
    return Configuration.FORCE_CONSTANT * mass1 * mass2 / distance;
}

function ParticleRenderer(particle, color, canvasContext) {
    this.particle = particle;
    this.color = color;
    this.canvasContext = canvasContext;
}

ParticleRenderer.prototype.draw = function() {
    var effectiveX = this.particle.getX() * Configuration.PIXELS_PER_UNIT_LENGTH;
    var effectiveY = this.particle.getY() * Configuration.PIXELS_PER_UNIT_LENGTH;
    
    this.canvasContext.fillStyle = this.color;
    this.canvasContext.beginPath();
    this.canvasContext.arc(effectiveX, 
                           effectiveY, 
                           this.particle.getRadius() * 
                                   Configuration.PIXELS_PER_UNIT_LENGTH, 
                           0, 
                           2 * Math.PI, 
                           false);
    this.canvasContext.fill();
};

function Vector(x, y) {
    this.x = x;
    this.y = y;
}

Vector.prototype.getX = function() {
    return this.x;
};

Vector.prototype.getY = function() {
    return this.y;
};

Vector.prototype.plus = function(other) {
    return new Vector(this.x + other.x, this.y + other.y);
};

Vector.prototype.multiply = function(factor) {
    return new Vector(this.x * factor, this.y * factor);
};

function SimulationEngine(canvasContext, 
                          canvasElement,
                          particles,
                          renderers,
                          timeStep,
                          worldWidth,
                          worldHeight,
                          sleepTime) {
    this.canvasContext            = canvasContext;
    this.canvasElement            = canvasElement;
    this.particles                = particles;
    this.renderers                = renderers;
    this.timeStep                 = timeStep;
    this.totalEnergy              = null;
    this.worldWidth               = worldWidth;
    this.worldHeight              = worldHeight;
    this.sleepTime                = sleepTime;
    this.exit                     = false;
    this.pause                    = true;
    this.particleToForceVectorMap = {};
}

SimulationEngine.prototype.togglePause = function() {
    this.pause = !this.pause;
};

SimulationEngine.prototype.run = function() {
    this.redraw();
    this.totalEnergy = this.computeTotalEnergy();
    var self = this;
    
    setInterval(function() {
        if (!self.pause) {
            self.performStep();
            self.redraw();
        }
    }, this.sleepTime);
};

SimulationEngine.prototype.performStep = function() {
    this.computeForceVectors();
    this.updateParticleVelocities();
    this.moveParticles();
    this.resolveWorldBorderCollisions();
    this.normalizeVelocityVectors();
    this.particleToForceVectorMap = {};
};

SimulationEngine.prototype.redraw = function() {
    this.canvasContext.fillStyle = "#000";
    this.canvasContext.fillRect(0, 
                                0, 
                                this.canvasElement.width, 
                                this.canvasElement.height);
    
    for (var i = 0; i < this.renderers.length; ++i) {
        var renderer = this.renderers[i];
        renderer.draw();
    }
    
    this.canvasContext.fillStyle = "#fff";
    this.font = "20px Arial";
    this.canvasContext.fillText("Total energy: " + this.computeTotalEnergy(), 
                                0, 
                                30);
};

SimulationEngine.prototype.computeForceVectors = function() {
    this.particleToForceVectorMap = [];
    
    for (var i = 0; i < this.particles.length; ++i) {
        var particle = this.particles[i];
        var vector = new Vector(0.0, 0.0);
        
        for (var j = 0; j < this.particles.length; ++j) {
            var otherParticle = this.particles[j];
            
            if (i === j) {
                continue;
            }
            
            var aux = getForceVector(particle, otherParticle);
            vector = vector.plus(aux);
        }
        
        this.particleToForceVectorMap.push([particle, vector]);
    }
};

SimulationEngine.prototype.updateParticleVelocities = function() {
    for (var i = 0; i < this.particleToForceVectorMap.length; ++i) {
        var particle = this.particleToForceVectorMap[i][0];
        var vector   = this.particleToForceVectorMap[i][1];
            
        vector = vector.multiply(1.0 / particle.getMass());
        particle.setVelocityX(particle.getVelocityX() + vector.getX() * this.timeStep);
        particle.setVelocityY(particle.getVelocityY() + vector.getY() * this.timeStep);
    }
};

SimulationEngine.prototype.moveParticles = function() {
    for (var i = 0; i < this.particles.length; ++i) {
        var particle = this.particles[i];
        particle.setX(particle.getX() + particle.getVelocityX() * this.timeStep);
        particle.setY(particle.getY() + particle.getVelocityY() * this.timeStep);
    }
};

SimulationEngine.prototype.resolveWorldBorderCollisions = function() {
    for (var i = 0; i < this.particles.length; ++i) {
        var particle = this.particles[i];
        var radius = particle.getRadius();
        
        if (particle.getY() - radius <= 0.0) {
            particle.setVelocityY(-particle.getVelocityY());
        } else if (particle.getY() + radius >= this.worldHeight) {
            particle.setVelocityY(-particle.getVelocityY());
        }
        
        if (particle.getX() - radius <= 0.0) {
            particle.setVelocityX(-particle.getVelocityX());
        } else if (particle.getX() + radius >= this.worldWidth) {
            particle.setVelocityX(-particle.getVelocityX());
        }
    }
};

SimulationEngine.prototype.normalizeVelocityVectors = function() {
    var totalEnergyDelta = this.computeTotalEnergyDelta();
    var factor = this.getNormalizationFactor(totalEnergyDelta);
    
    for (var i = 0; i < this.particles.length; ++i) {
        var particle = this.particles[i];
        particle.setVelocityX(factor * particle.getVelocityX());
        particle.setVelocityY(factor * particle.getVelocityY());
    }
};

SimulationEngine.prototype.getNormalizationFactor = function(totalEnergyDelta) {
    var aux = totalEnergyDelta / this.computeTotalKineticEnergy() + 1.0;
    
    if (aux < 0.0) {
        console.log("aux: " + aux);
    }
    
    return Math.sqrt(aux);
};

SimulationEngine.prototype.computeTotalKineticEnergy = function() {
    var energy = 0.0;
    for (var i = 0; i < this.particles.length; ++i) {
        var particle = this.particles[i];
        energy += particle.getKineticEnergy();
    }
    
    return energy;
};

SimulationEngine.prototype.computeTotalEnergyDelta = function() {
    return this.totalEnergy - this.computeTotalEnergy();
};

SimulationEngine.prototype.computeTotalEnergy = function() {
    var totalEnergy = 0.0;
    
    for (var i = 0; i < this.particles.length; ++i) {
        totalEnergy += this.particles[i].getKineticEnergy();
    }
    
    for (var i = 0; i < this.particles.length; ++i) {
        var particle1 = this.particles[i];
        
        for (var j = i + 1; j < this.particles.length; ++j) {
            var particle2 = this.particles[j];
            totalEnergy += getPotentialEnergy(particle1, particle2);
        }
    }
    
    return totalEnergy;
};

function setCanvasDimensions(canvasElement) {
    canvasElement.width = window.innerWidth;
    canvasElement.height = window.innerHeight;
}

function createRandomParticles(number_of_particles, 
                               worldWidth, 
                               worldHeight,
                               canvasContext) {
    var particleData = [];
    
    for (var i = 0; i < number_of_particles; ++i) {
        particleData.push(createRandomParticleData(worldWidth, 
                                                   worldHeight, 
                                                   canvasContext));
    }
    
    return particleData;
}

function getRandomColor() {
    var letters = "0123456789abcdef";
    var color = "#";
    
    for (var i = 0; i < 6; ++i) {
        color += letters[Math.floor(Math.random() * 16)];
    }
    
    return color;
}

function createRandomParticleData(worldWidth, worldHeight, canvasContext) {
    var mass = Configuration.MINIMUM_PARTICLE_MASS + 
              (Configuration.MAXIMUM_PARTICLE_MASS - Configuration.MINIMUM_PARTICLE_MASS) 
             * Math.random();
    var radius = mass / Configuration.PIXELS_PER_UNIT_LENGTH;
    var particle = new Particle(mass, radius);
    particle.setX(worldWidth * Math.random());
    particle.setY(worldHeight * Math.random());
    particle.setVelocityX(Configuration.MAXIMUM_INITIAL_VELOCITY * Math.random());
    particle.setVelocityY(Configuration.MAXIMUM_INITIAL_VELOCITY * Math.random());
    var color = getRandomColor();
    var renderer = new ParticleRenderer(particle, color, canvasContext);
    return [particle, renderer];
}

function extractParticles(particleData) {
    particles = [];
    
    for (var i = 0; i < particleData.length; ++i) {
        particles.push(particleData[i][0]);
    }
    
    return particles;
}

function extractRenderers(particleData) {
    particles = [];
    
    for (var i = 0; i < particleData.length; ++i) {
        particles.push(particleData[i][1]);
    }
    
    return particles;
}

function main() {
    var canvasElement = document.getElementById("cnvs");
    setCanvasDimensions(canvasElement);
    var canvasContext = canvasElement.getContext("2d");
    
    var worldWidth  = canvasElement.width  / Configuration.PIXELS_PER_UNIT_LENGTH;
    var worldHeight = canvasElement.height / Configuration.PIXELS_PER_UNIT_LENGTH;
    
    var particleData = 
            createRandomParticles(Configuration.DEFAULT_NUMBER_OF_PARTICLES,
                                  worldWidth,
                                  worldHeight,
                                  canvasContext);
                                  
    var particles = extractParticles(particleData);
    var renderers = extractRenderers(particleData);
    
    var simulationEngine = new SimulationEngine(canvasContext,
                                                canvasElement,
                                                particles,
                                                renderers,
                                                Configuration.TIME_STEP,
                                                worldWidth,
                                                worldHeight,
                                                Configuration.SLEEP_TIME);
    window.onkeydown = function(e) {
        if (e.keyCode === 32) {
            simulationEngine.togglePause();
        }
    };                               
    
    simulationEngine.run();
}

main();

index.html

<!DOCTYPE html>
<html>
    <head>
        <title>simulating...</title>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
    </head>
    <body>
        <canvas id="cnvs" style="background-color: black; margin:0; padding:0;">Your browser does not support HTML5 canvases!</canvas>
        <script src="simulation.js"></script>
    </body>
</html>

You can view the simulation here. (Press Space bar for turning pause on/off.)

Critique request

Since I am not a professional Javascript developer, it would be nice to hear the experts' opinions.