body {
  margin: 0px;
  padding: 0px;
  background-color: black;
}
const WIDTH = window.innerWidth;
const HEIGHT = window.innerHeight;
const PIXEL_SIZE = 4;
const SCALE = WIDTH > 1000 ? WIDTH / 1000 : 1;
const UNIT_SIZE = Math.max(WIDTH, HEIGHT) * 400;
const THRESHOLD = 0.5;

const ROWS = Math.floor(HEIGHT / PIXEL_SIZE) + 1;
const COLS = Math.floor(WIDTH / PIXEL_SIZE) + 1;

/**
 * The base unit of charge
 * which depends on the screen size and pixel size and controls the power of force
 * */
const UNIT_CHARGE = PIXEL_SIZE * UNIT_SIZE;

class Circle {
  constructor(x, y, v, s = 4) {
    this.centre = [x, y];
    this.velocity = v;
    this.q = s;
  }

  // Outputs field values in range of [0, MAX_VALUE]
  // MAX_VALUE is as defined inside function.
  fieldPotential(x, y) {
    // Simulate infinity. MAX field value when on the charge itself. i.e. the center
    const MAX_VALUE = 100000;

    // r: distance from the center
    // We want the function to fall sharply so we don't sqrt
    const r = (this.centre[0] - x) ** 2 + (this.centre[1] - y) ** 2;

    // If r is 0, then return MAX_VALUE
    const potential = r > 0 ? (this.q * UNIT_CHARGE) / (r * r) : MAX_VALUE;
    return potential > THRESHOLD ? potential : 0;
  }
  move() {
    // If we hit the left or right of the screen
    if (this.centre[0] < 0 || this.centre[0] >= WIDTH) {
      // Horizontal velocity becomes opposite
      this.velocity[0] = -this.velocity[0];
    }
    this.centre[0] = this.centre[0] - this.velocity[0];

    // If we hit the top or bottom of the screen
    if (this.centre[1] < 0 || this.centre[1] >= HEIGHT) {
      // Vertical velocity becomes opposite
      this.velocity[1] = -this.velocity[1];
    }
    this.centre[1] = this.centre[1] - this.velocity[1];
  }
}

/**
 *
 * @param {number} x
 * @param {number} y
 * @param {Array<Circle>} circles
 * @returns {number} sum of force
 */
function totalPotential(x, y, circles) {
  return circles.reduce(
    (initialValue, circle) => initialValue + circle.fieldPotential(x, y),
    0
  );
}

function randomInRange(min, max) {
  return Math.random() * (max - min) + min;
}

const circles = [null, null, null].map(
  () =>
    new Circle(
      randomInRange(0, WIDTH),
      randomInRange(0, HEIGHT),
      [randomInRange(-SCALE, SCALE), randomInRange(-SCALE, SCALE)],
      randomInRange(UNIT_SIZE, UNIT_SIZE + PIXEL_SIZE * 100)
    )
);

// Add grid points from 0 to screen width and 0 to screen height
function setup() {
  createCanvas(WIDTH - 4, HEIGHT - 4);
}

function draw() {
  clear();
  circles.forEach((circle) => circle.move());

  for (let i = 0; i < COLS; i++) {
    for (let j = 0; j < ROWS; j++) {
      const fieldPotential = totalPotential(
        i * PIXEL_SIZE,
        j * PIXEL_SIZE,
        circles
      );

      let pointOpacity = fieldPotential;
      strokeWeight(10);
      //  We only want to get gradient by changing green from [100, 200]
      const gradient = 100 + j * (100 / ROWS);

      fill(225, gradient, 25, pointOpacity);
      strokeWeight(0);
      square(i * PIXEL_SIZE, j * PIXEL_SIZE, PIXEL_SIZE);
      strokeWeight(10);
    }
  }
}
View Compiled
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.0/p5.min.js