<canvas id="canvas"></canvas>
<!--
Click adds a new gravity point.  
Click and drag a gravity point to move it.  
Ctrl + Click on a gravity point to remove it.  
Shift + Click anywhere to spawn a few dead satellites back.  

    

The gravity points gain mass / size as they consume satellites. 
-->
html, body {
    height: 100%;
    width: 100%;
    margin: 0px;
    padding: 0px;
}
canvas {
    position: absolute;
}
/*
	requestAnimationFrame shim
*/
window.requestAnimationFrame = (function () {
    return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || function (callback) {
        window.setTimeout(callback, 1000 / 60);
    };
})();
function Particle(x, y, s, c, m) {
    // positions
    this.x = x;
    this.y = y;
    // velocities
    this.vx = 0;
    this.vy = 0;
    // size
    this.s = s;
    // color
    this.c = c; // as of right now, unused. 
    // mass
    this.m = m;
    // alive
    this.alive = true;
}
Particle.prototype = {
    constructor: Particle,
    gravitate: function (host) {
        var dx = host.x - this.x,
            dy = host.y - this.y,
            d = Math.sqrt(Math.abs(dx * dx + dy * dy)),
            a = Math.atan2(dx, dy) * (180 / Math.PI),
            ac = host.m / Math.pow(d, 2);

        if (d < host.s / 2 + this.s / 2) {
            this.alive = false;
            host.m += this.m * .033;
            host.s += this.s * .033;
        }

        var accel = (host.m / Math.pow(d, 2));
        this.vx += Math.sin(a * (Math.PI / 180)) * accel;
        this.vy += Math.cos(a * (Math.PI / 180)) * accel;
    },
    update: function () {
        this.x += this.vx;
        this.y += this.vy;
        // if it gets away from us then kill it. 
        if (this.x < -width || this.x > width * 2 || this.y < -height || this.y > height * 2) {
            this.alive = false;
        }
    },
    reset: function () {
        var p = start_position(0, width, 0, height),
            a = Math.atan2(p.x - width / 2, p.y - height / 2) * (180 / Math.PI);
        this.x = p.x;
        this.y = p.y;
        this.vx = -Math.sin((a+Math.random()*5) * (Math.PI / 180)) * .14;
        this.vy = -Math.cos((a+Math.random()*5) * (Math.PI / 180)) * .14;
        this.alive = true;
    },
    render: function (context) {
        context.beginPath();
        context.arc(this.x, this.y, this.s / 2, 0, Math.PI * 2, true);
        context.fillStyle = this.c;
        context.fill();
        context.closePath();
    }
};
var canvas = document.getElementById('canvas'),
    context = canvas.getContext('2d'),    
    width,height,particles,hosts,moving;
var stats = new Stats();
setTimeout(init, 100);

function init() {
    height = canvas.height = document.body.offsetHeight;
    width = canvas.width = document.body.offsetWidth;
    stats.setMode(0); // 0: fps, 1: ms

    // Align top-left
    stats.domElement.style.position = 'absolute';
    stats.domElement.style.left = '0px';
    stats.domElement.style.top = '0px';

    document.body.appendChild(stats.domElement);
    particles = [];
    hosts = [];
    
    for (var i = 0; i < 1024; i++) {
        var x = Math.random() * width,
            y = Math.random() * height;
        var p = new Particle(x, y, Math.random() * 3 + 1, 'black', 3),
            a = Math.atan2(p.x - width / 2, p.y - height / 2) * (180 / Math.PI);
        p.vx = Math.random() * 1.5 - .75;
        p.vy = Math.random() * 1.5 - .75;
        particles.push(p);
    }
    make_host();
    for (var i = 0; i < 2; i++) {
        var x = Math.random() * (width / 2) + (width / 4),
            y = Math.random() * (height / 2) + (height / 4);
        make_host(x, y);
    }
    canvas.onmousedown = handle_mousedown;
    canvas.onmouseup = handle_mouseup;
    update();
    render();
}

function handle_mousedown(e) {
    var x = e.clientX,
        y = e.clientY;
    if (e.shiftKey) {
        var limit = 12;
        for (var i = 0; i < particles.length; i++) {
            if (particles[i].alive) {
                continue;
            }
            if (--limit <= 0) {
                return;
            }
            particles[i].reset();
        }
    }
    for (var i = 0; i < hosts.length; i++) {
        var host = hosts[i];
        if (host.x > x - host.s/2 && host.x < x + host.s/2 && host.y > y - host.s/2 && host.y < y + host.s/2) {
            if (e.ctrlKey) {
                hosts.splice(i, 1);
            } else {
                moving = i;
                canvas.addEventListener('mousemove', handle_mousemove, false);
            }
            return;
        }
    }
    make_host(e.clientX, e.clientY);
}

function handle_mouseup(e) {
    if (typeof moving === 'undefined') {
        return;
    }
    moving = undefined;
    canvas.removeEventListener('mousemove', handle_mousemove, false);

}

function handle_mousemove(e) {
    hosts[moving].x = e.clientX;
    hosts[moving].y = e.clientY;
}

function make_host(x, y) {
    var mass = Math.random() * 50 + 50,
        size = mass / 5;
    hosts.push({
        x: x || width / 2,
        y: y || height / 2,
        m: mass,
        s: size,
        c: 'red'
    });
}

function update() {
    for (var i = 0; i < particles.length; i++) {
        if (!particles[i].alive) {
            continue;
        }
        for (var j = 0; j < hosts.length; j++) {
            particles[i].gravitate(hosts[j]);
        }
        particles[i].update();
    }
    setTimeout(update, 1000 / 60);
}

function start_position(x1, x2, y1, y2, buffer) {
    buffer = Math.random() * (buffer || 100);
    if (Math.random() > .5) {
        return {
            x: (Math.random() * (x2 - x1)) + x1,
            y: Math.random() > .5 ? -buffer : height + buffer
        }
    }
    return {
        x: Math.random() > .5 ? -buffer : width + buffer,
        y: (Math.random() * (y2 - y1)) + y1
    }
}

function render() {
    
    stats.begin();

    context.fillStyle = 'rgba(255, 255, 255, .1)';
    context.fillRect(0, 0, width, height);

    for (var j = 0; j < hosts.length; j++) {
        context.beginPath();
        context.arc(hosts[j].x, hosts[j].y, hosts[j].s / 2, 0, Math.PI * 2, true);
        context.fillStyle = hosts[j].c;
        context.fill();
        context.closePath();
    }
    
    for (var i = 0; i < particles.length; i++) {
        if (!particles[i].alive) {
            continue;
        }
        particles[i].render(context);
    }
    requestAnimationFrame(render);
    
    stats.end();
}

setInterval(function() {
    if( particles.filter(function(a){ return a.alive }).length === 0 ) {
        handle_mousedown({shiftKey: true});
        // init();
    }
}, 60000);

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/stats.js/r11/Stats.js