preface

Hi, here is CSS and WebGL magic – Alphardex.

In practice before writing The particle effects found some specific implementation has a lot to do with physical knowledge (such as fireworks effects), and then happened to find a book of god – The Nature of Code, it teaches you how to use physics and mathematics knowledge to simulate realize some of The effects of Nature, after reading my soul was completely inspired, The result is this article, which will take you through implementing a physics driven particle effect from scratch.

Here’s a look at the final implementation

Let’s get started!

The preparatory work

Click on the lower right corner of the author’s P5.js template to fork a copy

Creation began

Particles in the class

Draw a dot somewhere, and that’s it

class Particle {
  s: p5;
  position: p5.Vector; / / position
  constructor(s: p5, position = s.createVector(0.0)) {
    this.s = s;
    this.position = position.copy();
  }
  / / show
  display() {
    this.s.circle(this.position.x, this.position.y, 6);
  }
  / / run
  run() {
    this.display(); }}Copy the code

Particle system class

You draw a few more points, you store them in an array, and you get what’s called a particle system

class ParticleSystem {
  s: p5;
  particles: Particle[]; // All the particles under the system
  origin: p5.Vector; // System origin
  constructor(s: p5, origin = s.createVector(0.0)) {
    this.s = s;
    this.particles = [];
    this.origin = origin;
  }
  // Add a single particle
  addParticle() {
    const particle = new Particle(this.s, this.origin);
    this.particles.push(particle);
  }
  // Add multiple particles
  addParticles(count = 1) {
    for (let i = 0; i < count; i++) {
      this.addParticle(); }}/ / run
  run() {
    for (const particle of this.particles) { particle.run(); }}}Copy the code

Create a particle

Draw 100 particles on the picture

const sketch = (s: p5) = > {
  let ps: ParticleSystem;

  const setup = () = > {
    s.createCanvas(s.windowWidth, s.windowHeight);

    ps = new ParticleSystem(s, s.createVector(s.width / 2, s.height / 2));

    ps.addParticles(100);
  };

  const draw = () = > {
    s.background(0);
    s.blendMode(s.ADD);

    ps.run();

    s.blendMode(s.BLEND);
  };

  s.setup = setup;
  s.draw = draw;
};
Copy the code

All 100 particles are overlapping in the center of the picture. They should be shuffled

Dislocate particles

Just assign random coordinates to all the particles

class ParticleSystem {...// Scramble particle positions
  shuffle() {
    this.particles.forEach((p) = > {
      const x = this.s.random(0.this.s.width);
      const y = this.s.random(0.this.s.height);
      const randPos = this.s.createVector(x, y); p.position = randPos; }); }}const sketch = (s: p5) = >{...const setup = () = >{... ps.shuffle(); }; };Copy the code

100 particles are scattered all over the picture

Particles roaming

It’s not going to work if they’re all stationary, but if you give them a random velocity, they’re going to move

class Particle {...velocity: p5.Vector; / / speed
  constructor(s: p5, position = s.createVector(0.0)){...this.velocity = this.s.createVector(0.0);
  }
  // Update the status
  update() {
    this.position.add(this.velocity);
  }
  / / run
  run() {
    this.update();
    this.display(); }}class ParticleSystem {...// Let the particles wander randomly
  wander() {
    this.particles.forEach((p) = > {
      const x = this.s.random(-1.1);
      const y = this.s.random(-1.1);
      const randVel = this.s.createVector(x, y); p.velocity = randVel; }); }}const sketch = (s: p5) = >{...const setup = () = >{... ps.wander(); }; };Copy the code

The particles then float around the screen

Apply a force to the particle

In physics class, the teacher taught us that force can change the motion state of the object. According to niu’s Law, we can apply a force to the object, increase the acceleration of the object, and thus increase the speed of the object

Try to make the particles fall under the influence of gravity

class Particle {...acceleration: p5.Vector; / / acceleration
  topSpeed: number; // Speed limit
  constructor(s: p5, position = s.createVector(0.0)){...this.acceleration = this.s.createVector(0.0);
    this.topSpeed = 12;
  }
  // Update the status
  update() {
    this.velocity.add(this.acceleration);
    this.velocity.limit(this.topSpeed);
    this.position.add(this.velocity);
    this.acceleration.mult(0);
  }
  / / force f
  applyForce(force: p5.Vector) {
    // Newton's second law: F = ma
    const mass = 1; // Set the mass unit to 1
    const acceleration = p5.Vector.div(force, mass);
    this.acceleration.add(acceleration); }}class ParticleSystem {...// Apply force to each particle
  applyForce(force: p5.Vector) {
    this.particles.forEach((p) = >p.applyForce(force)); }}const sketch = (s: p5) = >{...const draw = () = >{... ps.run();const gravity = s.createVector(0.0.05); ps.applyForce(gravity); . }; };Copy the code

Attractor class

Add a New Attractor class that attracts the particles so they all run toward it

class Attractor extends Particle {
  attractForceMag: number; // The size of the attraction
  radius: number; / / radius
  id: number; / / id identification
  constructor(s: p5, position = s.createVector(0.0), radius = 16, id = 0) {
    super(s, position);
    this.attractForceMag = 0.05;
    this.radius = radius;
    this.id = id;
  }
  / / show
  display() {
    this.s.circle(this.position.x, this.position.y, this.radius * 2);
  }
  // Apply gravity
  applyAttractForce(p: Particle) {
    const attractForce = p5.Vector.sub(this.position, p.position);
    attractForce.setMag(this.attractForceMag); p.applyForce(attractForce); }}class ParticleSystem {...// Apply attraction to each particle
  applyAttractor(attractor: Attractor) {
    this.particles.forEach((p) = >attractor.applyAttractForce(p)); }}const sketch = (s: p5) = >{...let attractor: Attractor;

  const setup = () = >{... attractor =new Attractor(s, s.createVector(s.width / 2, s.height / 2));
  };

  const draw = () = >{...// const gravity = s.gravity (0, 0.05);
    // ps.applyForce(gravity);attractor.run(); ps.applyAttractor(attractor); . }; };Copy the code

Click Add Attractor

By clicking the mouse, we can dynamically add attractors to the screen

const sketch = (s: p5) = >{...// let attractor: Attractor;
  let attractors: Attractor[] = [];
  let currentAttractorId = 0;
  let mousePos: p5.Vector;

  const setup = () = >{...// attractor = new Attractor(s, s.createVector(s.width / 2, s.height / 2));
  };

  const draw = () = >{ mousePos = s.createVector(s.mouseX, s.mouseY); .// const gravity = s.gravity (0, 0.05);
    // ps.applyForce(gravity);
    // attractor.run();
    // ps.applyAttractor(attractor);

    // The attractor attracts particles
    attractors.forEach((attractor) = >{ attractor.run(); ps.applyAttractor(attractor); }); . };// Add one attractor for each click
  const mousePressed = () = > {
    const attractor = new Attractor(s, mousePos, 16, currentAttractorId);
    attractors.push(attractor);
    currentAttractorId += 1; }; . s.mousePressed = mousePressed; };Copy the code

Attractors attract each other

It would be even more interesting if the attractors could also attract each other

const sketch = (s: p5) = >{...const draw = () = >{...// The action of the attractors towards each other
    for (let i = 0; i < attractors.length; i++) {
      for (let j = 0; j < attractors.length; j++) {
        if(i ! == j) {const attractorA = attractors[j];
          const attractorB = attractors[i];

          // Attractors attract each otherattractorA.applyAttractForce(attractorB); }}}... }; };Copy the code

The attractor merges as it approaches

But attraction alone is surely not enough. How about trying to get them to merge like a fish eating a fish?

class Attractor extends Particle {
  // Are you close to another person
  isNearAnother(attractor: Attractor) {
    const distAB = p5.Vector.dist(this.position, attractor.position);
    const distMin = (this.radius + attractor.radius) * 0.8;
    const isNear = distAB < distMin;
    return isNear;
  }
  / / absorption
  absorb(attractor: Attractor) {
    this.attractForceMag += attractor.attractForceMag;
    this.radius += attractor.radius;
    this.velocity = this.s.createVector(0.0); }}const sketch = (s: p5) = >{...const draw = () = >{...// The action of the attractors towards each other
    for (let i = 0; i < attractors.length; i++) {
      for (let j = 0; j < attractors.length; j++) {
        if(i ! == j) {const attractorA = attractors[j];
          const attractorB = attractors[i];

          // Attractors attract each other
          attractorA.applyAttractForce(attractorB);

          // If the two attractors are too close together, attractor A absorbs attractor B
          if (attractorA.isNearAnother(attractorB)) {
            attractorA.absorb(attractorB);
            attractors = attractors.filter(
              (attractor) = >attractor.id ! == attractorB.id ); }}}}... }; };Copy the code

Notice that the RADIUS on the Attractor was turned down

The suction body collapses after the radius exceeds the limit

What if I accidentally get too much? Let it collapse on the spot. Boom Shakalaka!

class Attractor extends Particle {...isCollasping: boolean; // Is it collapsing
  isDead: boolean; // Whether the collapse is complete
  static RADIUS_LIMIT = 100; // Radius upper limit.constructor(s: p5, position = s.createVector(0.0), radius = 16, id = 0){...this.isCollasping = false;
    this.isDead = false;
  }
  / / collapse
  collapse() {
    this.isCollasping = true;
    this.radius *= 0.75;
    if (this.radius < 1) {
      this.isDead = true; }}}const sketch = (s: p5) = >{...const draw = () = >{...// The attractor attracts particles
    attractors.forEach((attractor) = >{...// When an attractor absorbs too many particles, its radius exceeds the limit and it collapses
      if (
        attractor.radius >= Attractor.RADIUS_LIMIT ||
        attractor.isCollasping
      ) {
        attractor.collapse();
        if (attractor.isDead) {
          attractors = attractors.filter((item) = >item.id ! == attractor.id); }}}); . }; . };Copy the code

Set the gradient background color

The black background is too drab, make it a beautiful gradient

const sketch = (s: p5) = > {
  // https://p5js.org/examples/color-linear-gradient.html
  // Create gradients
  const setGradient = (
    x: number,
    y: number,
    w: number,
    h: number,
    c1: p5.Color,
    c2: p5.Color,
    axis: number
  ) = > {
    s.strokeWeight(1);
    if (axis === 1) {
      // Top to bottom gradient
      for (let i = y; i <= y + h; i++) {
        let inter = s.map(i, y, y + h, 0.1);
        letc = s.lerpColor(c1, c2, inter); s.stroke(c); s.line(x, i, x + w, i); }}else if (axis === 2) {
      // Left to right gradient
      for (let i = x; i <= x + w; i++) {
        let inter = s.map(i, x, x + w, 0.1);
        letc = s.lerpColor(c1, c2, inter); s.stroke(c); s.line(i, y, i, y + h); }}};const draw = () = >{... s.background(0);
    s.blendMode(s.ADD);

    setGradient(
      0.0,
      s.width,
      s.height,
      s.color("#2b5876"),
      s.color("#4e4376"),
      1); ps.run(); . }; };Copy the code

Line trailing effect

A trailing track can be obtained by recording the last position of a particle’s motion and connecting it to the current position

class Particle {...lastPosition: p5.Vector; // The previous position (used to create delayed trailing effects)
  constructor(s: p5, position = s.createVector(0.0)){...this.lastPosition = this.s.createVector(0.0);
  }
  / / show
  display() {
    // this.s.circle(this.position.x, this.position.y, 6);
    this.s.fill(255);
    this.s.stroke(255);
    this.s.strokeWeight(2);
    this.s.strokeJoin(this.s.ROUND);
    this.s.strokeCap(this.s.ROUND);
    this.s.line(
      this.position.x,
      this.position.y,
      this.lastPosition.x,
      this.lastPosition.y
    );
  }
  // Update the status
  update() {
    this.lastPosition = this.position.copy();
    this.velocity.add(this.acceleration);
    this.velocity.limit(this.topSpeed);
    this.position.add(this.velocity);
    this.acceleration.mult(0); }}const sketch = (s: p5) = >{...const setup = () = > {
    const canvas = s.createCanvas(s.windowWidth, s.windowHeight);

    // The scene is semi-transparent to make the trailing lines clearer
    const ctx = (canvas as any).drawingContext as CanvasRenderingContext2D;
    ctx.globalAlpha = 0.5; . }; };Copy the code

Attractor oscillation effect

By using the sine sine function, we can make the size of the attractor oscillate regularly

class Attractor extends Particle {...oscillatingRadius: number; // Wave radius
  constructor(s: p5, position = s.createVector(0.0), radius = 16, id = 0){...this.oscillatingRadius = 0;
  }
  / / show
  display() {
    this.s.blendMode(this.s.BLEND);
    this.s.stroke(0);
    this.s.fill("# 000000");
    this.oscillate();
    this.s.circle(this.position.x, this.position.y, this.oscillatingRadius * 2);
  }
  / / oscillation
  oscillate() {
    const oscillatingRadius = this.s.map(
      this.s.sin(this.s.frameCount / 6) * 2,
      -1.1.this.radius * 0.8.this.radius
    );
    this.oscillatingRadius = oscillatingRadius; }}Copy the code

The project address

Attractor Particle System