npm package: @lovetap/spray
Description: A canvas particle emitter
Author: Duncan Smith
Latest version: 1.1.2

# @lovetap/spray

[![CI](https://github.com/lovetap-fm/spray/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/lovetap-fm/spray/actions/workflows/ci.yml)

Spray is a lightweight JavaScript library for creating particle animations that render to an HTML `<canvas>` element. Its purpose is to present a minimal and simple-to-extend API, for both ease of use and efficiency.

## Installation

```sh
npm install @lovetap/spray
```

Or you can use a CDN:

```html
<script src="https://unpkg.com/@lovetap/spray"></script>
```

## Usage

To use Spray, create an instance of the `Particles` class and pass in a canvas element. Call the `start` method to begin the animation, and call `addParticles` any time to emit particles:

```ts
import Particles from "@lovetap/spray";

const canvas = document.getElementById("myCanvas");
const particles = new Particles({ canvas });
particles.start();
particles.addParticles(100); // this can come before start as well
```

The default particle behavior is to emit heart-emoji particles at random angles from the bottom center of the canvas, which will move up and fade out over time.

While this is a fine effect (the library is named after it), you may want particles that start from somewhere else, look and move differently, or are emitted at different rates (you may even want to render multiple particles for each one added). To do this, you can create (or customize) a "theme".

## Theming

The behavior of Spray particles is defined a `Theme` object, which is set as the `theme` property on a `Particles` instance (you can also pass in a theme to the `Particles` constructor, but know that you can always update it).

A theme is comprised of two methods, `generateParticles` and `updateParticles`, and a `delay` which is used to space out calls to `generateParticles` (allowing for gradual particle emission).

Here is how we define the default theme:

```ts
import { Particles, Particle, Theme } from "@lovetap/spray";

type CustomParticle = Particle & { vx: number };

const sprayTheme: Theme = {
  /**
   * Emit at most one particle every 80ms.
   */
  delay: 80,

  /**
   * Render one particle for each particle added, and give each a random
   * horizontal velocity. All particles start out opaque, and begin their
   * journey from the bottom-center of the canvas.
   */
  generateParticles: (numParticles, canvasEl) => {
    /* Return a `particles` array and the number of particles that remain
     * to be emitted.
     */
    return {
      /**
       * If you wanted to emit all particles at once, you could return an
       * array with multiple particles in it.
       */
      particles: [
        {
          x: canvasEl.clientWidth / 2,
          y: canvasEl.clientHeight,
          opacity: 1,
          size: 24,
          particle: "♥️",
          vx: Math.random() * 3.5 * (Math.random() > 0.5 ? 1 : -1),
        },
      ],

      /**
       * You also can omit the `remaining` property and Spray will assume
       * that you are emitting one physical particle per logical particle.
       */
      remaining: numParticles - 1,
    };
  },

  /**
   * Move each particle up and to the left or right, and fade it out.
   *
   * Note that this function modifies the particles in-place (it does not
   * return a new array) before they are re-drawn each frame.
   */
  updateParticles: (particles) => {
    for (const particle of particles) {
      particle.x += (particle as CustomParticle).vx;
      particle.y -= 1;
      particle.opacity -= 0.01;
    }
  },
};

const canvas = document.getElementById("myCanvas");
const particles = new Particles({ canvas, theme: sprayTheme });
particles.start();
```

The `generateParticles` method takes a number of particles to generate and and a reference to the canvas element. You don't have to generate the exact number of particles requested, but you must return an object with a `particles` array and a `remaining` property that indicates how many particles are left to emit. The `canvasEl` argument is useful for calculating particle coordinates.

The `updateParticles` method takes an array of particles and a reference to the canvas element, and overwrites the particle properties with the ones that should be drawn next.

### Customizing themes

Let's say you have a theme that you like, but you want to change something about the particles. Because particle behaviors are just functions, you can do this by creating a new theme object and overwriting the `generateParticles` method with a function that calls the original `generateParticles` and modifies the result:

```ts
import { Particles, Particle, Theme } from "@lovetap/spray";
import { sprayTheme } from "@lovetap/spray/themes";

const customSprayTheme: Theme = {
  ...sprayTheme,
  generateParticles: (numParticles, canvasEl) => {
    const { particles, remaining } = sprayTheme.generateParticles(
      numParticles,
      canvasEl
    );

    for (const particle of particles) {
      // The default theme uses hearts - let's try something else!
      particle.particle = "👍";
    }

    return { particles, remaining };
  },
};
```

Since it's just a function, you can also use a custom theme to add new particle behaviors. For example, let's say you want to emit particles that move in a circle. You can do this by creating a new theme that emits a single particle, and then updates it to move in a circle:

```ts
import { Particle, Theme } from "..";

type CustomParticle = Particle & { angle: number };

export const circleTheme: Theme = {
  delay: 80,
  generateParticles: (numParticles, canvasEl) => {
    return {
      particles: [
        {
          x: canvasEl.clientWidth / 2,
          y: canvasEl.clientHeight / 2,
          opacity: 1,
          size: 24,
          particle: "👍",
          angle: 0,
        },
      ],
      remaining: numParticles - 1,
    };
  },
  updateParticles: (particles, canvasEl) => {
    for (const particle of particles) {
      const { angle } = particle as CustomParticle;
      particle.x = Math.cos(angle) * 50 + canvasEl.clientWidth / 2;
      particle.y = Math.sin(angle) * 50 + canvasEl.clientHeight / 2;
      (particle as CustomParticle).angle += 0.1;
    }
  },
};
```

This example was generated by GitHub Copilot, and it worked first try (it's available in `@lovetap/spray/themes/circle`). In other words, it's pretty easy to create new particle behaviors!

Something to notice about this example is that it doesn't offer a way for particles to be destroyed. Spray will destroy particles once they become invisible: that is, when their `opacity` property is less than or equal to zero, or when their `size` property is less than or equal to zero, or when their `x` or `y` coordinates are outside the bounds of the canvas.

If we wanted to make sure that our particles were destroyed after they had completed their circle, we could add a check to the `updateParticles` method:

```ts
for (const particle of particles) {
  const { angle } = particle as CustomParticle;
  particle.x = Math.cos(angle) * 50 + canvasEl.clientWidth / 2;
  particle.y = Math.sin(angle) * 50 + canvasEl.clientHeight / 2;
  (particle as CustomParticle).angle += 0.1;

  if (angle > Math.PI * 2) {
    particle.opacity = 0;
  }
}
```

## API

### `Particles`

The `Particles` class is the main entry point for the library. It is responsible for rendering particles to a canvas element, and for updating the particles each frame.

#### `constructor(options: ParticlesOptions);`

The `Particles` constructor takes an options object with the following properties:

- `canvas`: A reference to the canvas element that should be used to render particles.
- `theme`: A theme object that defines how particles should be generated and updated.

#### `start(): void;`

Starts emitting particles.

#### `addParticles(numParticles: number): void;`

Emits additional particles.

## Entry Points

There are two entry points for the library:

- `@lovetap/spray`: The main entry point, which includes the `Particles` class and the `Theme` interface.

- `@lovetap/spray/themes`: A collection of themes that can be used with the `Particles` class.

## License

This library is licensed under the MIT license. See the `LICENSE` file for more details.

© 2023 Lovetap LLC