*,html,body{margin:0px;padding:0px;overflow:hidden;}
let msec = 0, prevInitMsec = 0, prevNow = 0;
let bodies = [];
let attraction = true;

// Cores do tema Dracula
const colors = {
  background: "#282a36",
  currentLine: "#44475a",
  foreground: "#f8f8f2",
  comment: "#6272a4",
  cyan: "#8be9fd",
  green: "#50fa7b",
  orange: "#ffb86c",
  pink: "#ff79c6",
  purple: "#bd93f9",
  red: "#ff5555",
  yellow: "#f1fa8c"
};

// Parâmetros da simulação
let bodyCount = 8;
let centralMass = 2500;
let minMass = 2;
let maxMass = 160;
let minDistance = 100;
let maxDistance = 200;
let minSpeed = 20;
let maxSpeed = 60;

function setup() {
  createCanvas(windowWidth, windowHeight);
  initBodies();
  createControls();
  prevNow = window.performance.now();
}

function draw() {
  clear();
  background(colors.background);
  ellapseTime();

  push();
  translate(width / 2, height / 2);
  stroke(colors.foreground);

  // Renderiza o rastro deixado pelo movimento do corpo
  bodies.forEach((b, i) => {
    noFill();
    let step = 4;
    for (let i = step; i < b.tail.length; i += step) {
      line(b.tail[i].x, b.tail[i].y, b.tail[i - 1].x, b.tail[i - 1].y);
    }
  });

  // Renderiza os corpos celestes
  bodies.forEach((b, i) => {
    if (i == 0) {
      fill(colors.orange);
    } else {
      fill(colors.cyan);
    }
    // O tamanho do corpo é proporcional à raiz cúbica da sua massa.
    let s = pow(b.mass, 1 / 3) * 2;
    ellipse(b.position.x, b.position.y, s, s);
  });

  pop();
}

function ellapseTime() {
  let ellapsedMsec = window.performance.now() - prevNow;
  prevNow = window.performance.now();
  updateSimulation(ellapsedMsec / 1000);
  msec += ellapsedMsec;

  // Recria os corpos a cada 10 segundos.
  if (msec > prevInitMsec + 10000) {
    initBodies();
    prevInitMsec += 10000;
  }
}

function updateSimulation(t) {
  // Calcula a força gravitacional entre todos os pares de corpos.
  for (let i = 0; i < bodies.length - 1; i++) {
    for (let j = i + 1; j < bodies.length; j++) {
      gravitate(bodies[i], bodies[j], t);
    }
  }

  // Atualiza a posição e a velocidade de cada corpo.
  for (let i = 0; i < bodies.length; i++) {
    bodies[i].update(t);
    if (bodies[i].prevPosition.dist(bodies[i].position) > 4) {
      bodies[i].tail.push(bodies[i].position.copy());
      bodies[i].prevPosition = bodies[i].position.copy();
    }
  }
}

function gravitate(o0, o1, t) {
  // A força gravitacional é inversamente proporcional ao quadrado da distância.
  let distance = max(1, o0.position.dist(o1.position) / 10);
  let force = o1.position.copy().sub(o0.position).normalize().mult(o0.mass * o1.mass / distance / distance);
  
  // Lógica de atração ou repulsão
  if (attraction) {
    o0.applyForce(force, t);
    o1.applyForce(force.mult(-1), t);
  } else {
    o0.applyForce(force, t);
    o1.applyForce(force, t);
  }
}

function initBodies() {
  bodies = [];
  for (let i = 0; i < bodyCount; i++) {
    let body;
    if (i == 0) {
      body = new Body(centralMass);
    } else {
      let angle = random(PI * 2);
      let distance = random(minDistance, maxDistance);
      let speed = random(minSpeed, maxSpeed);
      body = new Body(random(minMass, maxMass));
      body.position = createVector(cos(angle), sin(angle)).mult(distance);
      body.velocity = createVector(-sin(angle), cos(angle)).mult(speed);
    }
    body.tail = [];
    body.prevPosition = body.position.copy();
    bodies.push(body);
  }
}

class Body {
  constructor(m) {
    this.position = createVector(0, 0, 0);
    this.velocity = createVector(0, 0, 0);
    this.mass = m;
  }

  applyForce(f, t) {
    // Força é igual à massa vezes a aceleração (Segunda Lei de Newton).
    this.velocity.add(f.copy().mult(t / this.mass));
  }

  update(t) {
    // Move o corpo baseado na sua velocidade.
    this.position.add(this.velocity.copy().mult(t));

    // Reflete a velocidade se o corpo atingir as bordas da tela (simples colisão).
    const hw = width / 2;
    const hh = height / 2;
    if (this.position.x > hw || this.position.x < -hw) { this.velocity.x = -this.velocity.x; }
    if (this.position.y > hh || this.position.y < -hh) { this.velocity.y = -this.velocity.y; }
  }
}

function createControls() {
  let yOffset = 10; // Offset Y inicial para posicionar os controles
  let spacing = 50; // Espaço entre os sliders e controles
  
  // Cria sliders para os parâmetros da simulação
  createSliderWithLabel("Número de Corpos", 2, 16, bodyCount, 10, yOffset, (value) => {
    bodyCount = value;
    initBodies();
  });
  yOffset += spacing; // Aumenta o offset Y para o próximo controle

  createSliderWithLabel("Massa Central", 1000, 5000, centralMass, 10, yOffset, (value) => {
    centralMass = value;
    initBodies();
  });
  yOffset += spacing;

  createSliderWithLabel("Massa Mínima", 1, 100, minMass, 10, yOffset, (value) => {
    minMass = value;
    initBodies();
  });
  yOffset += spacing;

  createSliderWithLabel("Massa Máxima", 100, 200, maxMass, 10, yOffset, (value) => {
    maxMass = value;
    initBodies();
  });
  yOffset += spacing;

  // Cria um container para o switch de atração/repulsão
  let switchContainer = createDiv().position(10, yOffset); // Posiciona o container do switch
  let switchLabel = createP('Atração').parent(switchContainer).style('display', 'inline-block').style('color', colors.foreground).style('font-family', 'monospace');
  let switchButton = createCheckbox('', attraction).parent(switchContainer).style('display', 'inline-block').style('margin-left', '10px');
  
  switchButton.checked(attraction);
  switchButton.changed(() => {
    attraction = switchButton.checked();
    switchLabel.html(attraction ? 'Atração' : 'Repulsão');
  });
}

function createSliderWithLabel(labelText, min, max, initialValue, x, y, onChange) {
  let label = createP(labelText).position(x, y).style('color', colors.foreground).style('font-family', 'monospace');
  let slider = createSlider(min, max, initialValue).position(x, y + 20).style('width', '200px');
  slider.input(() => onChange(slider.value()));
}

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
}
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdn.jsdelivr.net/npm/p5@latest/lib/p5.min.js