0

I am trying to make a (colliding) particle simulator right now and so far I am stuck on making multiple shapes without using an extra 40 lines of code for each shape. My best idea so far is to use an array and make one shape for each array item using a method that I learned from Python. My only goal right now is to make one shape for each array item or find a new method. For reference, I made a program a while ago that does what I want here but with only one shape, I have been copying over a lot of that code which can be found here.

(I have comments in the code to explain my issue more in case this was a bunch of gibberish)

Here is my JavaScript:

let c = document.getElementById('c').getContext('2d');

c.width = window.innerWidth;
c.height = window.innerHeight;

let x = 0;
let y = 0;

// Items that I want to take either the number of items or the names of the items
let objects = ['ball'];

function init(){
  setInterval(updateGameArea, 10);
}

function createObjects(){
  // Method I learned from Python here is 'for i in range(item):'
  // Not sure if just saying item[i] will cut in JS though because I'm getting an error

  for(let i = 0; i < objects.length; i++){
    // This is the main part that needs fixing, the 'new component' is where it should draw
    // the shape, but the name of the component has to be unique which is the main issue

    objects[i] = new component(20, 20, 'black', x, y);
  }
}

// function I'm trying to use to make each shape
function component(width, height, color, x, y, type) {
  this.type = type;
  this.width = width;
  this.height = height;
  this.x = x;
  this.y = y;
  this.update = function() {
    c = myGameArea.context;
    c.fillStyle = color;
    c.fillRect(this.x, this.y, this.width, this.height);
  }
}

function updateGameArea() {
  c.clear();
  objects[i].update();
}

The main issue that I have is that I know a method of assigning data to items in an array using python but (as far as I know) there isn't an alternative canvas for python (other than turtle which is way too slow) so I essentially need a new way of doing the whole for i in range(item): method but in JS.

Any help and or alternative ideas are appreciated.

Edit: W to the 43k rep guy who gave an incredible answer, although his answer is deeply appreciated, I now understand that I left out an important attribute of the particles that I am trying to make. I want to make colliding particles. So sorry to the guy who probably took 3 hours out of his day to make that absolute chad answer. Honestly I should be paying for stuff like that. Good job!

1 Answer 1

1

You could try to create your own particle physics engine.

Just set up a game loop and an event listener that listens for clicks. Store the point that was clicked. This will be the origin of the particles. Now, generate random particles and add them to the game object array.

The loop will trigger an update as well as a render. The particles below only live for 2 seconds. And only a max of 40 particles should be alive at any given time.

const TAU = Math.PI * 2;

const randomNumber = (min, max) =>
  Math.random() * (max - min) + min;

class Vector {
  constructor(x = 0, y = 0) {
    this.x = x;
    this.y = y;
  }
  add(v) {
    return new Vector(this.x + v.x, this.y + v.y);
  }
  multiply(v) {
    return new Vector(this.x * v.x, this.y * v.y);
  }
  multiplyScalar(s) {
    return new Vector(this.x * s, this.y * s);
  }
  floor() {
    return new Vector(Math.floor(this.x), Math.floor(this.y));
  }
  toString() {
    return `<${this.x}, ${this.y}>`;
  }
}

const rgbToHsl = (r, g, b) => {
  const vmax = max(r, g, b), vmin = min(r, g, b);
  let h, s, l = (vmax + vmin) / 2;
  if (vmax === vmin) {
    return [0, 0, l]; // achromatic
  }
  const d = vmax - vmin;
  s = l > 0.5 ? d / (2 - vmax - vmin) : d / (vmax + vmin);
  if (vmax === r) h = (g - b) / d + (g < b ? 6 : 0);
  if (vmax === g) h = (b - r) / d + 2;
  if (vmax === b) h = (r - g) / d + 4;
  h /= 6;
  return [h, s, l];
};

const hslToRgb = (h, s, l) => {
  let r, g, b;
  if (s === 0) {
    r = g = b = l; // achromatic
  } else {
    const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
    const p = 2 * l - q;
    r = hueToRgb(p, q, h + 1/3);
    g = hueToRgb(p, q, h);
    b = hueToRgb(p, q, h - 1/3);
  }
  return [r, g, b];
};

const hueToRgb = (p, q, t) => {
  if (t < 0) t += 1;
  if (t > 1) t -= 1;
  if (t < 1/6) return p + (q - p) * 6 * t;
  if (t < 1/2) return q;
  if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
  return p;
};

class RGBColor {
  constructor(r, g, b, a) {
    this.r = r;
    this.g = g;
    this.b = b;
    this.a = a;
  }
  toString() {
    const r = Math.floor(this.r * 255);
    const g = Math.floor(this.g * 255);
    const b = Math.floor(this.b * 255);
    return this.a == null
      ? `rgb(${r}, ${g}, ${b})`
      : `rgba(${r}, ${g}, ${b}, ${this.a})`;
  }
  toHsl() {
    const [h, s, l] = rgbToHsl(this.r, this.g, this.b);
    return new HSLColor(h, s, l, this.a);
  }
}

class HSLColor {
  constructor(h, s, l, a) {
    this.h = h;
    this.s = s;
    this.l = l;
    this.a = a;
  }
  toString() {
    const h = Math.floor(this.h * 360);
    const s = Math.floor(this.s * 100);
    const l = Math.floor(this.l * 100);
    return this.a == null 
      ? `hsl(${h}, ${s}%, ${l}%)`
      : `hsla(${h}, ${s}%, ${l}%, ${this.a})`;
  }
  toRgb() {
    const [r, g, b] = rgbToHsl(this.h, this.s, this.l);
    return new RGBColor(r, g, b, this.a);
  }
}

class Particle {
  constructor(x, y, radius, color, accelerationX = 0, accelerationY = 0, friction = 1, lifetime = 2000) {
    this.position = new Vector(x, y);
    this.velocity = new Vector(0, 0);
    this.acceleration = new Vector(accelerationX, accelerationY);
    this.friction = new Vector(friction, friction);
    this.radius = radius;
    this.color = color;
    this.lifetime = lifetime;
    this.expiration = performance.now() + lifetime;
  }
  update() {
    this.velocity = this.velocity.add(this.acceleration);
    this.velocity = this.velocity.multiply(this.friction);
    this.position = this.position.add(this.velocity);
    this.color.a = (this.expiration - performance.now()) / this.lifetime;
  }
  render(ctx) {
    const pos = this.position.floor();
    ctx.save();
    ctx.fillStyle = this.color.toString();
    ctx.beginPath();
    ctx.arc(pos.x, pos.y, this.radius, 0, TAU);
    ctx.fill();
    ctx.restore();
  }
  isDead() {
    return performance.now() - this.expiration > 0;
  }
}

const colorCount = 10;
const COLORS = Array.from({ length: colorCount }, (_, i) =>
  new HSLColor(i / colorCount, 1.0, 0.8, 1.0));

const randomColor = () =>
  COLORS[Math.floor(Math.random() * COLORS.length)];

const randInvert = (n) => Math.random() < 0.5 ? -n : n;

const randomParticle = () =>
  new Particle(
    click.x,
    click.y,
    Math.floor(randomNumber(2, 4)),
    randomColor(),
    randInvert(randomNumber(0.01, 0.03)),
    randInvert(randomNumber(0.01, 0.03)),
    0.1,
    4000
  );

let click;
let particleSpawnCount = 12;
let maxParticles = 24;
const gameObjects = [];

const canvas = document.querySelector('#particles');
const ctx = canvas.getContext('2d');
const origin = new Vector();

const centerPoint = (width, height) =>
  new Vector(width, height)
    .multiplyScalar(0.5)
    .floor();

const recalculateOrigin = () => {
  const c = centerPoint(ctx.canvas.width, ctx.canvas.height);
  origin.x = c.x;
  origin.y = c.y;
  ctx.translate(c.x, c.y);
}

const spawnParticles = (e) => {
  click = new Vector(
    e.clientX - origin.x - 5,
    e.clientY - origin.y - 5
  );
  const delta = gameObjects.length - maxParticles + particleSpawnCount;
  if (delta >= 0) {
     gameObjects.splice(0, delta);
  }
  for (let i = 0; i < particleSpawnCount; i++) {
    gameObjects.push(randomParticle());
  }
};

const update = (step) => {
  for (let i = gameObjects.length - 1; i >= 0; i--) {
    if (gameObjects[i].isDead()) {
      gameObjects.splice(i, 1);
    } else {
      gameObjects[i].update(step);
    }
  }
};

const render = () => {
  ctx.fillStyle = 'grey';
  ctx.fillRect(-origin.x, -origin.y, ctx.canvas.width, ctx.canvas.height);
  for (let obj of gameObjects) {
    obj.render(ctx);
  }
  ctx.textAlign = 'right';
  ctx.fillStyle = 'limegreen';
  ctx.font = 'bold 16px Consolas';
  ctx.fillText(
    `FPS: ${fps.toString().padStart(2, ' ')}`,
    origin.x - 8,
    origin.y - 8
  );
};

// Ref: https://stackoverflow.com/a/56507053/1762224
const timeStep = 1.0 / 30.0; // Cap at 30 FPS
let previousTime = 0.0, delta = 0.0, fps; // Actual FPS
const loop = time => {
  const dt = time - previousTime;
  delta = delta + dt;
  previousTime = time;
  while (delta > timeStep) {
    fps = Math.floor(1 / delta);
    update(timeStep);
    delta = delta - timeStep;
  }
  render();
  window.requestAnimationFrame(loop);
};

window.requestAnimationFrame((time) => {
  previousTime = time;
  window.requestAnimationFrame(loop);
  resizeCanvas();
});

const resizeCanvas = () => {
  canvas.width = window.innerWidth;
  canvas.height = window.innerHeight;
  recalculateOrigin();
};

window.addEventListener('resize', resizeCanvas);
canvas.addEventListener('click', spawnParticles);
*, *::before, *::after {
  box-sizing: border-box;
}

#particles {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 100;
}
<link href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css" rel="stylesheet"/>
<canvas id="particles"></canvas>

Sign up to request clarification or add additional context in comments.

1 Comment

This is actually incredible. Thank you so much for the fantastic answer. Although I will be using this code later, I was trying to make particles that can collide. sorry for the inconvenience and once more, fantastic code 👏👏👏👏👏

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.