JSFiddle - React, Tailwind, and code Playground
HTML
<html>
<body>
<canvas id="simulation" width="400" height="400"></canvas>
</body>
</html>
JavaScript
var canvas = document.getElementById("simulation");
var context = canvas.getContext("2d");
var width = canvas.width;
var height = canvas.height;
var points = [];
var grid = [];
var point_count = 20;
var grid_count = 20;
var gravity_force = 2000;
var falloff = 2;
context.fillStyle = "rgb(192,192,192)";
context.fillRect(0, 0, width, height);
for (var i = 0; i < point_count; i++)
points.push([Math.random()*width, Math.random()*height]);
function doCalculation() {
grid = [];
var yi, xi, y, x, r, a, f, dx, dy, i, ox, oy;
for (yi = 0; yi < grid_count - 1; yi++) {
grid.push([]);
for (xi = 0; xi < grid_count - 1; xi++) {
x = (width / grid_count) * (xi + 1);
y = (height / grid_count) * (yi + 1);
for (i = 0; i < points.length; i++) {
dx = points[i][0] - x;
dy = points[i][1] - y;
d = Math.sqrt(dx * dx + dy * dy);
a = Math.atan2(dy, dx);
f = gravity_force / Math.pow(d, falloff);
if (f > d)
f = d;
x += Math.cos(a) * f;
y += Math.sin(a) * f;
}
grid[yi].push([x, y]);
}
}
}
context.strokeStyle = 'black';
context.fillStyle = 'purple';
function render() {
var x, y, i;
var offset = width / grid_count;
context.beginPath();
for (x = 0; x < grid_count - 1; x++) {
context.moveTo((x + 1) * offset, 0);
for (y = 0; y < grid_count - 1; y++) {
context.lineTo(grid[y][x][0], grid[y][x][1]);
}
context.lineTo((x + 1) * offset, height);
}
for (y = 0; y < grid_count - 1; y++) {
context.moveTo(0, (y + 1) * offset);
for (x = 0; x < grid_count - 1; x++) {
context.lineTo(grid[y][x][0], grid[y][x][1]);
}
context.lineTo(width, (y + 1) * offset);
}
context.stroke();
for (i = 0; i < points.length; i++) {
context.beginPath();
context.arc(points[i][0], points[i][1], 5, 0, 2 * Math.PI);
context.fill();
}
}
doCalculation();
render();