How to Create a Beautiful Particle Animation with HTML Canvas
Tags: tailwindcss, html, nextjs, vue
Published: 2023-06-06 09:34:38

####**[Live Demo](https://cruip-tutorials.vercel.app/particle-animation/) / [Download](https://github.com/cruip/cruip-tutorials/blob/main/particle-animation/particle-animation.js)**

--

In this tutorial, we will utilize the power of **HTML canvas** to create a **mesmerizing particle animation** resembling a magnificent galaxy of stars. We will delve into the development process behind **Stellar**, our latest [landing page template built with Tailwind CSS](https://cruip.com/stellar/).

What sets this animation apart is its interactivity, as **the stars will gracefully respond to the movement of your mouse cursor**. The interaction will come alive through the magic of JavaScript, allowing us to manipulate the canvas and bring our stellar vision to reality.

We will also demonstrate how to create components with TypeScript support for both [Next.js](#create-a-nextjs-particle-animation-component) and [Vue](#creating-a-similar-component-for-vue) frameworks, enabling you to integrate this mesmerizing animation seamlessly into your web app.

Quick navigation

- [HTML Version](#HTML Version)
- [Nextjs Version](#Nextjs Version)
- [Vue Version](#Vue Version)

* * *

##Create an HTML canvas animation with pure JavaScript<a name="HTML Version"></a>

To make things easy and easily understandable, we will start with a simple HTML document where we will reference an external JS file that will contain the code we’re going to write.

```html
  <!DOCTYPE html>
  <html lang="en">
  
  <head>
      <meta charset="utf-8">
      <title>Particle Animation</title>
      <meta name="viewport" content="width=device-width,initial-scale=1">
      <script src="https://cdn.tailwindcss.com"></script>
  </head>
  
  <body class="font-inter antialiased">
  
      <main class="relative min-h-screen flex flex-col justify-center bg-slate-900 overflow-hidden">
          <div class="w-full max-w-6xl mx-auto px-4 md:px-6 py-24">
      
              <!-- Particles animation -->
              <div class="absolute inset-0 pointer-events-none" aria-hidden="true">
                  <canvas data-particle-animation></canvas>
              </div>
                  
          </div>
      </main>
  
      <script src="./particle-animation.js"></script>
  </body>
  
  </html>
```

We have created a very simple structure that you can modify according to your preferences and needs.

Here are the important points to know in this step:

*   We have inserted a `canvas` tag within our HTML document, which will be our reference element for creating the animation. We will add the `data-particle-animation` attribute to the element, which will be used to invoke the animation from our JS file
*   It is not necessary to define the `width` and `height` of the canvas, as the dimensions will be inherited – via JS – from the parent container, which in our case is the `div` tag with the class `absolute inset-0 pointer-events-none`
*   We have inserted a `script` tag within our HTML document, which references the JS file we will write
*   We have used the `aria-hidden="true"` attribute on the canvas container to hide the animation from screen readers, as it is not relevant for accessibility purposes

### Create a JavaScript class for the animation

Now that we have created the HTML structure, we can proceed to write the JS code. First, let’s create a new JavaScript file named `particle-animation.js` and place it in the same folder as our HTML document.

For our animation, we want to create a JavaScript class called `ParticleAnimation`, which we will initialize as follows:

```javascript
  new ParticleAnimation(canvas, options);
```

The class will accept two parameters:

*   `canvas`, which is our HTML canvas element created in the HTML document
*   `options`, which we will add in a later phase, is an object that will contain the options for our animation

Let’s create our class:

```javascript
  // Particle animation
  class ParticleAnimation {
    constructor(el) {
      this.canvas = el;
      if (!this.canvas) return;
      this.init();
    }
  
    init() {
      // Initialize canvas
    }
  }
  
  // Init ParticleAnimation
  const canvasElements = document.querySelectorAll('[data-particle-animation]');
  canvasElements.forEach(canvas => {
    new ParticleAnimation(canvas);
  });
```

We have created an `init()` function that will be called inside the constructor of the class. In this function, we will define all the functionalities we need to initialize the animation.

Also, we have created a `forEach` loop that allows us to initialize the class for each canvas element found in our HTML document.

### Set the canvas size

Firstly, we need to define the dimensions of the canvas, which will be inherited from its container. To achieve this, let’s create a `resizeCanvas()` method inside our class that will be invoked on page load and whenever the user resizes the browser window.

```javascript
  class ParticleAnimation {
    constructor(el) {
      this.canvas = el;
      if (!this.canvas) return;
      this.canvasContainer = this.canvas.parentElement;
      this.context = this.canvas.getContext('2d');
      this.dpr = window.devicePixelRatio || 1;
      this.canvasSize = {
        w: 0,
        h: 0,
      };
      this.initCanvas = this.initCanvas.bind(this);
      this.resizeCanvas = this.resizeCanvas.bind(this);
      this.init();
    }
  
    init() {
      this.initCanvas();
      window.addEventListener('resize', this.initCanvas);
    }
  
    initCanvas() {
      this.resizeCanvas();
    }
  
    resizeCanvas() {
      this.canvasSize.w = this.canvasContainer.offsetWidth;
      this.canvasSize.h = this.canvasContainer.offsetHeight;
      this.canvas.width = this.canvasSize.w * this.dpr;
      this.canvas.height = this.canvasSize.h * this.dpr;
      this.canvas.style.width = this.canvasSize.w + 'px';
      this.canvas.style.height = this.canvasSize.h + 'px';
      this.context.scale(this.dpr, this.dpr);
    }
  }
```

The `resizeCanvas()` method calculates the dimensions of the canvas and assigns them to the canvas itself and its container. Additionally, it defines the canvas scale factor, which is necessary to make it sharp on high-resolution screens.

In other words, assuming that the browser window has dimensions of 1400x800px and we are viewing the document on a retina display, this function will transform our canvas element as follows:

```html
  <canvas data-particle-animation width="2800" height="1600" style="width: 1400px; height: 800px;"></canvas>
```

### Define the animation options

For our canvas, we want to define some options that allow us to customize the animation. To do this, let’s create a `settings` object within our constructor and define the default options:

```javascript
  constructor(el, { quantity = 30, staticity = 50, ease = 50 } = {}) {
    this.canvas = el;
    if (!this.canvas) return;
    this.canvasContainer = this.canvas.parentElement;
    this.context = this.canvas.getContext('2d');
    this.dpr = window.devicePixelRatio || 1;
    this.settings = {
      quantity: quantity,
      staticity: staticity,
      ease: ease,
    };
    this.canvasSize = {
      w: 0,
      h: 0,
    };
    this.initCanvas = this.initCanvas.bind(this);
    this.resizeCanvas = this.resizeCanvas.bind(this); 
    this.init();
  }
```

The options we have defined are as follows:

*   `quantity` the number of particles we want to display
*   `staticity` the speed of particle movement when the user moves the mouse (a higher value means slower movement)
*   `ease` the smooth effect we want to apply to the particle movement (a higher value means smoother movement)

Additionally, let’s adjust how we invoke the class by adding the options:

```javascript
  const canvasElements = document.querySelectorAll('[data-particle-animation]');
  canvasElements.forEach(canvas => {
    const options = {
      quantity: canvas.dataset.particleQuantity,
      staticity: canvas.dataset.particleStaticity,
      ease: canvas.dataset.particleEase,
    };
    new ParticleAnimation(canvas, options);
  });
```

This way, we can define the options directly in our HTML document using the `data-*` attributes. For example, if we want to modify the number of particles, we can add the `data-particle-quantity` attribute to our canvas element:

```javascript
  <canvas data-particle-animation data-particle-quantity="5"></canvas>
```

In the example above, we have set the number of particles to 5. Similarly, we can modify the `staticity` and `ease` options using the `data-particle-staticity` and `data-particle-ease` attributes, respectively.

### Draw the particles

It’s time to create the particles in our canvas. To do this, let’s start by creating an empty array `this.circles = []` inside our constructor. This array will hold all the particles we create.

Next, let’s create a method called `circleParams()`, which will allow us to define the properties of each particle:

```javascript
  circleParams() {
    const x = Math.floor(Math.random() * this.canvasSize.w);
    const y = Math.floor(Math.random() * this.canvasSize.h);
    const translateX = 0;
    const translateY = 0;
    const size = Math.floor(Math.random() * 2) + 1;
    const alpha = parseFloat((Math.random() * 0.6 + 0.1).toFixed(1));
    const dx = (Math.random() - 0.5) * 0.2;
    const dy = (Math.random() - 0.5) * 0.2;
    const magnetism = 0.1 + Math.random() * 4;
    return { x, y, translateX, translateY, size, alpha, dx, dy, magnetism };
    }
```

This function allows us to randomly define:

*   The size of the particles (between 1 and 3 pixels in diameter)
*   The position of the particles (between 0 and the width/height of the canvas)
*   The opacity of the particles (between 0.1 and 0.7)
*   The movement speed of the particles (between -0.1 and 0.1)
*   The attraction force of the particles (between 0.1 and 4)

These values, defined as `{ x, y, translateX, translateY, size, alpha, dx, dy, magnetism }`, will then be passed to another function responsible for drawing the particles on the canvas.

This function is called `drawCircle()` and is defined as follows:

```javascript
  drawCircle(circle, update = false) {
    const { x, y, translateX, translateY, size, alpha } = circle;
    this.context.translate(translateX, translateY);
    this.context.beginPath();
    this.context.arc(x, y, size, 0, 2 * Math.PI);
    this.context.fillStyle = `rgba(255, 255, 255, ${alpha})`;
    this.context.fill();
    this.context.setTransform(this.dpr, 0, 0, this.dpr, 0, 0);
    if (!update) {
      this.circles.push(circle);
    }
  }
```

We won’t go into detail about everything this function does. For now, it’s enough to know that this function draws a circle on the canvas using the properties we defined earlier.

The last step is to create a method called `drawParticles()`, which will be responsible for invoking the `drawCircle()` function for each particle we have defined:

```javascript
  drawParticles() {
    const particleCount = this.settings.quantity;
    for (let i = 0; i < particleCount; i++) {
      const circle = this.circleParams();
      this.drawCircle(circle);
    }
  }
```

At this point in the tutorial, we have created a canvas and drawn **static** particles inside it. Here’s the complete code we have written so far:

```javascript
  // Particle animation
  class ParticleAnimation {
    constructor(el, { quantity = 30, staticity = 50, ease = 50 } = {}) {
      this.canvas = el;
      if (!this.canvas) return;
      this.canvasContainer = this.canvas.parentElement;
      this.context = this.canvas.getContext('2d');
      this.dpr = window.devicePixelRatio || 1;
      this.settings = {
        quantity: quantity,
        staticity: staticity,
        ease: ease,
      };
      this.circles = [];    
      this.canvasSize = {
        w: 0,
        h: 0,
      };
      this.initCanvas = this.initCanvas.bind(this);
      this.resizeCanvas = this.resizeCanvas.bind(this);
      this.drawCircle = this.drawCircle.bind(this);
      this.drawParticles = this.drawParticles.bind(this);    
      this.init();
    }
  
    init() {
      this.initCanvas();
      window.addEventListener('resize', this.initCanvas);
    }
  
    initCanvas() {
      this.resizeCanvas();
      this.drawParticles();
    }
  
    resizeCanvas() {
      this.circles.length = 0;
      this.canvasSize.w = this.canvasContainer.offsetWidth;
      this.canvasSize.h = this.canvasContainer.offsetHeight;
      this.canvas.width = this.canvasSize.w * this.dpr;
      this.canvas.height = this.canvasSize.h * this.dpr;
      this.canvas.style.width = this.canvasSize.w + 'px';
      this.canvas.style.height = this.canvasSize.h + 'px';
      this.context.scale(this.dpr, this.dpr);
    }
  
    circleParams() {
      const x = Math.floor(Math.random() * this.canvasSize.w);
      const y = Math.floor(Math.random() * this.canvasSize.h);
      const translateX = 0;
      const translateY = 0;
      const size = Math.floor(Math.random() * 2) + 1;
      const alpha = parseFloat((Math.random() * 0.6 + 0.1).toFixed(1));
      const dx = (Math.random() - 0.5) * 0.2;
      const dy = (Math.random() - 0.5) * 0.2;
      const magnetism = 0.1 + Math.random() * 4;
      return { x, y, translateX, translateY, size, alpha, dx, dy, magnetism };
    }
  
    drawCircle(circle, update = false) {
      const { x, y, translateX, translateY, size, alpha } = circle;
      this.context.translate(translateX, translateY);
      this.context.beginPath();
      this.context.arc(x, y, size, 0, 2 * Math.PI);
      this.context.fillStyle = `rgba(255, 255, 255, ${alpha})`;
      this.context.fill();
      this.context.setTransform(this.dpr, 0, 0, this.dpr, 0, 0);
      if (!update) {
        this.circles.push(circle);
      }
    }
  
    drawParticles() {
      const particleCount = this.settings.quantity;
      for (let i = 0; i < particleCount; i++) {
        const circle = this.circleParams();
        this.drawCircle(circle);
      }
    }  
  }
  
  // Init ParticleAnimation
  const canvasElements = document.querySelectorAll('[data-particle-animation]');
  canvasElements.forEach(canvas => {
    const options = {
      quantity: canvas.dataset.particleQuantity,
      staticity: canvas.dataset.particleStaticity,
      ease: canvas.dataset.particleEase,
    };
    new ParticleAnimation(canvas, options);
  });
```

Note that `this.circles.length = 0;` has been added inside the `resizeCanvas()` method. This allows us to empty the `circles` array each time the canvas is resized, preventing the particle count from multiplying with each resize.

As we previously said, the particles are currently static, meaning they don’t move in space, and there’s no interaction effect with the mouse. To achieve this, we need to add some more code.

### Animate the particles

Now things get a bit more complicated, but don’t worry, we’ll explain step by step what needs to be done. For the animation, we will create a method called `animate()`, and we will use `requestAnimationFrame()` inside it to execute the function at each animation frame.

```javascript
  animate() {
    // Things to be done ...
    window.requestAnimationFrame(this.animate);
  }
```

This way, the function will run in a loop, and we can update the properties of the particles on each frame. For each frame, we need to clear the canvas first and then redraw the particles. To do this, let’s create a new method called `clearContext()`:

```javascript
  clearContext() {
    this.context.clearRect(0, 0, this.canvasSize.w, this.canvasSize.h);
  }
```

And let’s call it inside the `animate()` method:

```javascript
  animate() {
    this.clearContext();
    window.requestAnimationFrame(this.animate);
  }
```

Then, let’s loop through the `circles` array and redraw each particle on every frame:

```javascript
  animate() {
    this.clearContext();
    this.circles.forEach((circle, i) => {
      // Handle the alpha value
      const edge = [
        circle.x + circle.translateX - circle.size, // distance from left edge
        this.canvasSize.w - circle.x - circle.translateX - circle.size, // distance from right edge
        circle.y + circle.translateY - circle.size, // distance from top edge
        this.canvasSize.h - circle.y - circle.translateY - circle.size, // distance from bottom edge
      ];
      const closestEdge = edge.reduce((a, b) => Math.min(a, b));
      const remapClosestEdge = this.remapValue(closestEdge, 0, 20, 0, 1).toFixed(2);
      if (remapClosestEdge > 1) {
        circle.alpha += 0.02;
        if (circle.alpha > circle.targetAlpha) circle.alpha = circle.targetAlpha;
      } else {
        circle.alpha = circle.targetAlpha * remapClosestEdge;
      }
      circle.x += circle.dx;
      circle.y += circle.dy;
      // circle gets out of the canvas
      if (circle.x < -circle.size || circle.x > this.canvasSize.w + circle.size || circle.y < -circle.size || circle.y > this.canvasSize.h + circle.size) {
        // remove the circle from the array
        this.circles.splice(i, 1);
        // create a new circle
        const circle = this.circleParams();
        this.drawCircle(circle);
      // update the circle position
      } else {
        this.drawCircle({ ...circle, x: circle.x, y: circle.y, translateX: circle.translateX, translateY: circle.translateY, alpha: circle.alpha }, true);
      }
    });
    window.requestAnimationFrame(this.animate);
  }
```

The first thing we’re doing in the `animate()` function is handling the opacity of each particle. By default, particles have an opacity of `0` on page load, and on each frame, it increases by `0.02` until it reaches the `targetAlpha` value.

Additionally, we want the opacity of the particles to decrease as they get closer to the edges of the canvas. So when the distance from the edge is less than `20px`, the opacity of the particle gradually starts to decrease using the `remapValue()` function, which remaps the `closestEdge` value from `0` to `20` (distance from the edge) to `0` to `1` (particle opacity):

```javascript
  remapValue(value, start1, end1, start2, end2) {
    const remapped = (value - start1) * (end2 - start2) / (end1 - start1) + start2;
    return remapped > 0 ? remapped : 0;
  }
```

In the last part of the `animate()` method, we update the position of each particle, and if a particle goes outside the canvas, we remove it from the `circles` array and create a new one. If the particle is still within the canvas, we redraw it using the `drawCircle()` method.

Fantastic! We have created an animation of particles moving in space on their own. Now, we want these particles to be attracted to the mouse cursor.

### Add mouse interaction

To add mouse interaction, we first need to know its coordinates relative to the canvas. Let’s start by adding a `mouse` property to the constructor, which will hold the coordinates:

```javascript
  this.mouse = {
    x: 0,
    y: 0,
  };
```

Next, let’s create an `onMouseMove()` method that updates the coordinates when the mouse moves:

```javascript
  init() {
    this.initCanvas();
    this.animate();
    window.addEventListener('resize', this.initCanvas);
    window.addEventListener('mousemove', this.onMouseMove);
  }
```

Great! At this point, we want the particles to be attracted to the mouse. While we used the `x` and `y` properties for the natural movement of the particles, we’ll use the `translateX` and `translateY` properties for the mouse attraction. This way, the particles will move independently of their natural movement and won’t lose their trajectory.

Let’s update the `animate()` method to ensure that we update the position of each particle based on its distance from the mouse. The value added (or subtracted) to the `translateX` and `translateY` properties will be the distance from the mouse: particles closer to the mouse will move faster than those farther away. Particles movement will be affected by `staticity`, `magnetism` and `ease` parameters too.

```javascript
  circle.translateX += ((this.mouse.x / (this.settings.staticity / circle.magnetism)) - circle.translateX) / this.settings.ease;
  circle.translateY += ((this.mouse.y / (this.settings.staticity / circle.magnetism)) - circle.translateY) / this.settings.ease;
```

### Reassembling the code

And here is the final JS code for our canvas animation:

```javascript
  // Particle animation
  class ParticleAnimation {
    constructor(el, { quantity = 30, staticity = 50, ease = 50 } = {}) {
      this.canvas = el;
      if (!this.canvas) return;
      this.canvasContainer = this.canvas.parentElement;
      this.context = this.canvas.getContext('2d');
      this.dpr = window.devicePixelRatio || 1;
      this.settings = {
        quantity: quantity,
        staticity: staticity,
        ease: ease,
      };
      this.circles = [];
      this.mouse = {
        x: 0,
        y: 0,
      };
      this.canvasSize = {
        w: 0,
        h: 0,
      };
      this.onMouseMove = this.onMouseMove.bind(this);
      this.initCanvas = this.initCanvas.bind(this);
      this.resizeCanvas = this.resizeCanvas.bind(this);
      this.drawCircle = this.drawCircle.bind(this);
      this.drawParticles = this.drawParticles.bind(this);
      this.remapValue = this.remapValue.bind(this);
      this.animate = this.animate.bind(this);
      this.init();
    }
  
    init() {
      this.initCanvas();
      this.animate();
      window.addEventListener('resize', this.initCanvas);
      window.addEventListener('mousemove', this.onMouseMove);
    }
  
    initCanvas() {
      this.resizeCanvas();
      this.drawParticles();
    }
  
    onMouseMove(event) {
      const { clientX, clientY } = event;
      const rect = this.canvas.getBoundingClientRect();
      const { w, h } = this.canvasSize;
      const x = clientX - rect.left - (w / 2);
      const y = clientY - rect.top - (h / 2);
      const inside = x < (w / 2) && x > -(w / 2) && y < (h / 2) && y > -(h / 2);
      if (inside) {
        this.mouse.x = x;
        this.mouse.y = y;
      }
    }
  
    resizeCanvas() {
      this.circles.length = 0;
      this.canvasSize.w = this.canvasContainer.offsetWidth;
      this.canvasSize.h = this.canvasContainer.offsetHeight;
      this.canvas.width = this.canvasSize.w * this.dpr;
      this.canvas.height = this.canvasSize.h * this.dpr;
      this.canvas.style.width = this.canvasSize.w + 'px';
      this.canvas.style.height = this.canvasSize.h + 'px';
      this.context.scale(this.dpr, this.dpr);
    }
  
    circleParams() {
      const x = Math.floor(Math.random() * this.canvasSize.w);
      const y = Math.floor(Math.random() * this.canvasSize.h);
      const translateX = 0;
      const translateY = 0;
      const size = Math.floor(Math.random() * 2) + 1;
      const alpha = 0;
      const targetAlpha = parseFloat((Math.random() * 0.6 + 0.1).toFixed(1));
      const dx = (Math.random() - 0.5) * 0.2;
      const dy = (Math.random() - 0.5) * 0.2;
      const magnetism = 0.1 + Math.random() * 4;
      return { x, y, translateX, translateY, size, alpha, targetAlpha, dx, dy, magnetism };
    }
  
    drawCircle(circle, update = false) {
      const { x, y, translateX, translateY, size, alpha } = circle;
      this.context.translate(translateX, translateY);
      this.context.beginPath();
      this.context.arc(x, y, size, 0, 2 * Math.PI);
      this.context.fillStyle = `rgba(255, 255, 255, ${alpha})`;
      this.context.fill();
      this.context.setTransform(this.dpr, 0, 0, this.dpr, 0, 0);
      if (!update) {
        this.circles.push(circle);
      }
    }
  
    clearContext() {
      this.context.clearRect(0, 0, this.canvasSize.w, this.canvasSize.h);
    }
  
    drawParticles() {
      this.clearContext();
      const particleCount = this.settings.quantity;
      for (let i = 0; i < particleCount; i++) {
        const circle = this.circleParams();
        this.drawCircle(circle);
      }
    }
  
    // This function remaps a value from one range to another range
    remapValue(value, start1, end1, start2, end2) {
      const remapped = (value - start1) * (end2 - start2) / (end1 - start1) + start2;
      return remapped > 0 ? remapped : 0;
    }
  
    animate() {
      this.clearContext();
      this.circles.forEach((circle, i) => {
        // Handle the alpha value
        const edge = [
          circle.x + circle.translateX - circle.size, // distance from left edge
          this.canvasSize.w - circle.x - circle.translateX - circle.size, // distance from right edge
          circle.y + circle.translateY - circle.size, // distance from top edge
          this.canvasSize.h - circle.y - circle.translateY - circle.size, // distance from bottom edge
        ];
        const closestEdge = edge.reduce((a, b) => Math.min(a, b));
        const remapClosestEdge = this.remapValue(closestEdge, 0, 20, 0, 1).toFixed(2);
        if (remapClosestEdge > 1) {
          circle.alpha += 0.02;
          if (circle.alpha > circle.targetAlpha) circle.alpha = circle.targetAlpha;
        } else {
          circle.alpha = circle.targetAlpha * remapClosestEdge;
        }
        circle.x += circle.dx;
        circle.y += circle.dy;
        circle.translateX += ((this.mouse.x / (this.settings.staticity / circle.magnetism)) - circle.translateX) / this.settings.ease;
        circle.translateY += ((this.mouse.y / (this.settings.staticity / circle.magnetism)) - circle.translateY) / this.settings.ease;
        // circle gets out of the canvas
        if (circle.x < -circle.size || circle.x > this.canvasSize.w + circle.size || circle.y < -circle.size || circle.y > this.canvasSize.h + circle.size) {
          // remove the circle from the array
          this.circles.splice(i, 1);
          // create a new circle
          const circle = this.circleParams();
          this.drawCircle(circle);
          // update the circle position
        } else {
          this.drawCircle({ ...circle, x: circle.x, y: circle.y, translateX: circle.translateX, translateY: circle.translateY, alpha: circle.alpha }, true);
        }
      });
      window.requestAnimationFrame(this.animate);
    }
  }
  
  // Init ParticleAnimation
  const canvasElements = document.querySelectorAll('[data-particle-animation]');
  canvasElements.forEach(canvas => {
    const options = {
      quantity: canvas.dataset.particleQuantity,
      staticity: canvas.dataset.particleStaticity,
      ease: canvas.dataset.particleEase,
    };
    new ParticleAnimation(canvas, options);
  });
```

As previously mentioned, you just need to include the above code in a JavaScript file and include it in your project. Then, you can use particle animations anywhere on the HTML page by simply adding a `data-particle-animation` attribute to the canvas tag, and optionally specifying other desired parameters. For example:

```javascript
  <canvas data-particle-animation data-particle-quantity="5" data-particle-staticity="40" data-particle-ease="60"></canvas>
```

The above code will create a canvas element with a particle animation that will have `5` particles, a staticity of `40`, and an ease of `60`.

* * *

##Create a Next.js particle animation component<a name="Nextjs Version"></a>

Now let’s see how to create a React component for this particle animation that can be used in a Next.js app. We want to create a component that integrates the JavaScript functionality (but with TypeScript support) and is reusable, allowing optional parameters to be passed as props.

The component we are about to build is available in our [GitHub repository](https://github.com/cruip/cruip-tutorials-next/blob/main/components/particles.tsx) that collects all Next.js tutorials.

### Create the component structure

Create a new file called `particles.tsx` in the `components` folder of our Next.js project. Let’s set up the structure where we will later insert the code created in the previous sections.

```tsx
  'use client'
  
  import React, { useRef, useEffect } from 'react'
  
  interface ParticlesProps {
    className?: string
    quantity?: number
    staticity?: number
    ease?: number,
  }
  
  export default function Particles({
    className = '',
    quantity = 30,
    staticity = 50,
    ease = 50,
  }: ParticlesProps) {
    const canvasRef = useRef<HTMLCanvasElement>(null)
    const canvasContainerRef = useRef<HTMLDivElement>(null)
    const context = useRef<CanvasRenderingContext2D | null>(null)
    const circles = useRef<any[]>([])
    const mouse = useRef<{ x: number; y: number }>({ x: 0, y: 0 })
    const canvasSize = useRef<{ w: number; h: number }>({ w: 0, h: 0 })
    const dpr = typeof window !== 'undefined' ? window.devicePixelRatio : 1
  
    return (
      <div className={className} ref={canvasContainerRef} aria-hidden="true">
        <canvas ref={canvasRef} />
      </div>
    )
  
  }
```

The important things to note in this first step are as follows:

*   We have defined the props that the component accepts (`className`, `quantity`, `staticity`, and `ease`) with default values
*   We have defined the same `const` variables that we used in the JavaScript code, with `useRef`
*   Since we are using TypeScript, we have defined an interface for the props to provide type checking
*   The component returns not only the `canvas` but also the wrapping `<div>` element, from which it inherits the dimensions and to which we can pass desired CSS classes

### Create a new component for mouse position tracking

To obtain the mouse position within the client, we will create a new component specifically designed to return the mouse coordinates. This component will enhance modularity and reusability. Let’s create a new file called `mouse-position.tsx` and insert the following code:

```tsx
  import { useState, useEffect } from 'react';
  
  interface MousePosition {
    x: number;
    y: number;
  }
  
  export default function useMousePosition(): MousePosition {
    const [mousePosition, setMousePosition] = useState<MousePosition>({ x: 0, y: 0 });
  
    useEffect(() => {
      const handleMouseMove = (event: MouseEvent) => {
        setMousePosition({ x: event.clientX, y: event.clientY });
      }
  
      window.addEventListener('mousemove', handleMouseMove);
  
      return () => {
        window.removeEventListener('mousemove', handleMouseMove);
      }
    }, []);
  
    return mousePosition;
  }
```

This component is very simple but quite useful. Essentially, whenever the mouse moves within the client, the component returns the mouse coordinates. Additionally, the component takes care of removing the event listener when unmounted.

Once we import the `MousePosition` component into our `Particles` component using `import MousePosition from './utils/mouse-position'`, we can define a new constant variable that will hold the mouse coordinates. We can then use this variable as a dependency in the `watch` function that executes the `onMouseMove` function.

In simpler terms, the `onMouseMove` function will be executed every time the mouse moves.

```tsx
  const mousePosition = MousePosition()
  
  useEffect(() => {
    onMouseMove()
  }, [mousePosition.x, mousePosition.y])
```

### Add the component logic

Let’s proceed with adding the previously defined methods from the JavaScript class to the component. It’s a simple copy and paste, but with some modifications. For example, in the React component, each function is defined as a `const` variable instead of a class method. We have also added TypeScript annotations:

```ts
  const initCanvas = () => {
    resizeCanvas()
    drawParticles()
  }
  
  const onMouseMove = () => {
    if (canvasRef.current) {
      const rect = canvasRef.current.getBoundingClientRect()
      const { w, h } = canvasSize.current
      const x = mousePosition.x - rect.left - w / 2
      const y = mousePosition.y - rect.top - h / 2
      const inside = x < w / 2 && x > -w / 2 && y < h / 2 && y > -h / 2
      if (inside) {
        mouse.current.x = x
        mouse.current.y = y
      }
    }
  }
  
  const resizeCanvas = () => {
    if (canvasContainerRef.current && canvasRef.current && context.current) {
      circles.current.length = 0
      canvasSize.current.w = canvasContainerRef.current.offsetWidth
      canvasSize.current.h = canvasContainerRef.current.offsetHeight
      canvasRef.current.width = canvasSize.current.w * dpr
      canvasRef.current.height = canvasSize.current.h * dpr
      canvasRef.current.style.width = canvasSize.current.w + 'px'
      canvasRef.current.style.height = canvasSize.current.h + 'px'
      context.current.scale(dpr, dpr)
    }
  }
  
  type Circle = {
    x: number
    y: number
    translateX: number
    translateY: number
    size: number
    alpha: number
    targetAlpha: number
    dx: number
    dy: number
    magnetism: number
  }  
  
  const circleParams = (): Circle => {
    const x = Math.floor(Math.random() * canvasSize.current.w)
    const y = Math.floor(Math.random() * canvasSize.current.h)
    const translateX = 0
    const translateY = 0
    const size = Math.floor(Math.random() * 2) + 1
    const alpha = 0
    const targetAlpha = parseFloat((Math.random() * 0.6 + 0.1).toFixed(1))
    const dx = (Math.random() - 0.5) * 0.2
    const dy = (Math.random() - 0.5) * 0.2
    const magnetism = 0.1 + Math.random() * 4
    return { x, y, translateX, translateY, size, alpha, targetAlpha, dx, dy, magnetism }
  }
  
  const drawCircle = (circle: Circle, update = false) => {
    if (context.current) {
      const { x, y, translateX, translateY, size, alpha } = circle
      context.current.translate(translateX, translateY)
      context.current.beginPath()
      context.current.arc(x, y, size, 0, 2 * Math.PI)
      context.current.fillStyle = `rgba(255, 255, 255, ${alpha})`
      context.current.fill()
      context.current.setTransform(dpr, 0, 0, dpr, 0, 0)
  
      if (!update) {
        circles.current.push(circle)
      }
    }
  }
  
  const clearContext = () => {
    if (context.current) {
      context.current.clearRect(0, 0, canvasSize.current.w, canvasSize.current.h)
    }
  }
  
  const drawParticles = () => {
    clearContext()
    const particleCount = quantity
    for (let i = 0; i < particleCount; i++) {
      const circle = circleParams()
      drawCircle(circle)
    }
  }
  
  const remapValue = (
    value: number,
    start1: number,
    end1: number,
    start2: number,
    end2: number
  ): number => {
    const remapped = ((value - start1) * (end2 - start2)) / (end1 - start1) + start2
    return remapped > 0 ? remapped : 0
  }
  
  const animate = () => {
    clearContext()
    circles.current.forEach((circle: Circle, i: number) => {
      // Handle the alpha value
      const edge = [
        circle.x + circle.translateX - circle.size, // distance from left edge
        canvasSize.current.w - circle.x - circle.translateX - circle.size, // distance from right edge
        circle.y + circle.translateY - circle.size, // distance from top edge
        canvasSize.current.h - circle.y - circle.translateY - circle.size, // distance from bottom edge
      ]
      const closestEdge = edge.reduce((a, b) => Math.min(a, b))
      const remapClosestEdge = parseFloat(remapValue(closestEdge, 0, 20, 0, 1).toFixed(2))
      if (remapClosestEdge > 1) {
        circle.alpha += 0.02
        if (circle.alpha > circle.targetAlpha) circle.alpha = circle.targetAlpha
      } else {
        circle.alpha = circle.targetAlpha * remapClosestEdge
      }
      circle.x += circle.dx
      circle.y += circle.dy
      circle.translateX += ((mouse.current.x / (staticity / circle.magnetism)) - circle.translateX) / ease
      circle.translateY += ((mouse.current.y / (staticity / circle.magnetism)) - circle.translateY) / ease
      // circle gets out of the canvas
      if (
        circle.x < -circle.size ||
        circle.x > canvasSize.current.w + circle.size ||
        circle.y < -circle.size ||
        circle.y > canvasSize.current.h + circle.size
      ) {
        // remove the circle from the array
        circles.current.splice(i, 1)
        // create a new circle
        const newCircle = circleParams()
        drawCircle(newCircle)
        // update the circle position
      } else {
        drawCircle(
          {
            ...circle,
            x: circle.x,
            y: circle.y,
            translateX: circle.translateX,
            translateY: circle.translateY,
            alpha: circle.alpha,
          },
          true
        )
      }
    })
    window.requestAnimationFrame(animate)
  }
```

Finally, we use the `useEffect` hook to initialize the canvas, animate it, and update it whenever the window size changes.

```tsx
  useEffect(() => {    
    if (canvasRef.current) {
      context.current = canvasRef.current.getContext('2d')
    }
    initCanvas()
    animate()
    window.addEventListener('resize', initCanvas)
  
    return () => {
      window.removeEventListener('resize', initCanvas)
    }
  }, [])
```

Great, the Next.js component is complete! Here’s how it looks:

```tsx
  'use client'
  
  import React, { useRef, useEffect } from 'react'
  import MousePosition from './utils/mouse-position'
  
  interface ParticlesProps {
    className?: string
    quantity?: number
    staticity?: number
    ease?: number
  }
  
  export default function Particles({
    className = '',
    quantity = 30,
    staticity = 50,
    ease = 50,
  }: ParticlesProps) {
    const canvasRef = useRef<HTMLCanvasElement>(null)
    const canvasContainerRef = useRef<HTMLDivElement>(null)
    const context = useRef<CanvasRenderingContext2D | null>(null)
    const circles = useRef<any[]>([])
    const mousePosition = MousePosition()
    const mouse = useRef<{ x: number; y: number }>({ x: 0, y: 0 })
    const canvasSize = useRef<{ w: number; h: number }>({ w: 0, h: 0 })
    const dpr = typeof window !== 'undefined' ? window.devicePixelRatio : 1
  
    useEffect(() => {    
      if (canvasRef.current) {
        context.current = canvasRef.current.getContext('2d')
      }
      initCanvas()
      animate()
      window.addEventListener('resize', initCanvas)
  
      return () => {
        window.removeEventListener('resize', initCanvas)
      }
    }, [])
  
    useEffect(() => {
      onMouseMove()
    }, [mousePosition.x, mousePosition.y])
  
    const initCanvas = () => {
      resizeCanvas()
      drawParticles()
    }
  
    const onMouseMove = () => {
      if (canvasRef.current) {
        const rect = canvasRef.current.getBoundingClientRect()
        const { w, h } = canvasSize.current
        const x = mousePosition.x - rect.left - w / 2
        const y = mousePosition.y - rect.top - h / 2
        const inside = x < w / 2 && x > -w / 2 && y < h / 2 && y > -h / 2
        if (inside) {
          mouse.current.x = x
          mouse.current.y = y
        }
      }
    }
  
    const resizeCanvas = () => {
      if (canvasContainerRef.current && canvasRef.current && context.current) {
        circles.current.length = 0
        canvasSize.current.w = canvasContainerRef.current.offsetWidth
        canvasSize.current.h = canvasContainerRef.current.offsetHeight
        canvasRef.current.width = canvasSize.current.w * dpr
        canvasRef.current.height = canvasSize.current.h * dpr
        canvasRef.current.style.width = canvasSize.current.w + 'px'
        canvasRef.current.style.height = canvasSize.current.h + 'px'
        context.current.scale(dpr, dpr)
      }
    }
  
    type Circle = {
      x: number
      y: number
      translateX: number
      translateY: number
      size: number
      alpha: number
      targetAlpha: number
      dx: number
      dy: number
      magnetism: number
    }  
  
    const circleParams = (): Circle => {
      const x = Math.floor(Math.random() * canvasSize.current.w)
      const y = Math.floor(Math.random() * canvasSize.current.h)
      const translateX = 0
      const translateY = 0
      const size = Math.floor(Math.random() * 2) + 1
      const alpha = 0
      const targetAlpha = parseFloat((Math.random() * 0.6 + 0.1).toFixed(1))
      const dx = (Math.random() - 0.5) * 0.2
      const dy = (Math.random() - 0.5) * 0.2
      const magnetism = 0.1 + Math.random() * 4
      return { x, y, translateX, translateY, size, alpha, targetAlpha, dx, dy, magnetism }
    }
  
    const drawCircle = (circle: Circle, update = false) => {
      if (context.current) {
        const { x, y, translateX, translateY, size, alpha } = circle
        context.current.translate(translateX, translateY)
        context.current.beginPath()
        context.current.arc(x, y, size, 0, 2 * Math.PI)
        context.current.fillStyle = `rgba(255, 255, 255, ${alpha})`
        context.current.fill()
        context.current.setTransform(dpr, 0, 0, dpr, 0, 0)
  
        if (!update) {
          circles.current.push(circle)
        }
      }
    }
  
    const clearContext = () => {
      if (context.current) {
        context.current.clearRect(0, 0, canvasSize.current.w, canvasSize.current.h)
      }
    }
  
    const drawParticles = () => {
      clearContext()
      const particleCount = quantity
      for (let i = 0; i < particleCount; i++) {
        const circle = circleParams()
        drawCircle(circle)
      }
    }
  
    const remapValue = (
      value: number,
      start1: number,
      end1: number,
      start2: number,
      end2: number
    ): number => {
      const remapped = ((value - start1) * (end2 - start2)) / (end1 - start1) + start2
      return remapped > 0 ? remapped : 0
    }
  
    const animate = () => {
      clearContext()
      circles.current.forEach((circle: Circle, i: number) => {
        // Handle the alpha value
        const edge = [
          circle.x + circle.translateX - circle.size, // distance from left edge
          canvasSize.current.w - circle.x - circle.translateX - circle.size, // distance from right edge
          circle.y + circle.translateY - circle.size, // distance from top edge
          canvasSize.current.h - circle.y - circle.translateY - circle.size, // distance from bottom edge
        ]
        const closestEdge = edge.reduce((a, b) => Math.min(a, b))
        const remapClosestEdge = parseFloat(remapValue(closestEdge, 0, 20, 0, 1).toFixed(2))
        if (remapClosestEdge > 1) {
          circle.alpha += 0.02
          if (circle.alpha > circle.targetAlpha) circle.alpha = circle.targetAlpha
        } else {
          circle.alpha = circle.targetAlpha * remapClosestEdge
        }
        circle.x += circle.dx
        circle.y += circle.dy
        circle.translateX += ((mouse.current.x / (staticity / circle.magnetism)) - circle.translateX) / ease
        circle.translateY += ((mouse.current.y / (staticity / circle.magnetism)) - circle.translateY) / ease
        // circle gets out of the canvas
        if (
          circle.x < -circle.size ||
          circle.x > canvasSize.current.w + circle.size ||
          circle.y < -circle.size ||
          circle.y > canvasSize.current.h + circle.size
        ) {
          // remove the circle from the array
          circles.current.splice(i, 1)
          // create a new circle
          const newCircle = circleParams()
          drawCircle(newCircle)
          // update the circle position
        } else {
          drawCircle(
            {
              ...circle,
              x: circle.x,
              y: circle.y,
              translateX: circle.translateX,
              translateY: circle.translateY,
              alpha: circle.alpha,
            },
            true
          )
        }
      })
      window.requestAnimationFrame(animate)
    }
  
    return (
      <div className={className} ref={canvasContainerRef} aria-hidden="true">
        <canvas ref={canvasRef} />
      </div>
    )
  }
```

You can import and use it like a regular React component. For example:

```tsx
  <Particles className="absolute inset-0 pointer-events-none" quantity={50} />
```

* * *

##Creating a similar component for Vue<a name="Vue Version"></a>

Let’s now see how we can create a perfect clone of the [Next.js component](#create-a-nextjs-particle-animation-component) for Vue. First, let’s create a new Vue component.

The component we are about to create is available in our [GitHub repository](https://github.com/cruip/cruip-tutorials-vue/blob/main/src/components/Particles.vue), which includes all the examples from the Cruip tutorials.

### Vue component setup

We will be using the Composition API, script setup syntax, and TypeScript. So, create a file called `Particles.vue` and set up the component structure as follows:

```html
  <script setup lang="ts">
  import { ref, onMounted, onBeforeUnmount, reactive, watch } from 'vue'
  
  const canvasRef = ref<HTMLCanvasElement | null>(null)
  const canvasContainerRef = ref<HTMLDivElement | null>(null)
  const context = ref<CanvasRenderingContext2D | null>(null)
  const circles = ref<any[]>([])
  const mouse = reactive<{ x: number; y: number }>({ x: 0, y: 0 })
  const canvasSize = reactive<{ w: number; h: number }>({ w: 0, h: 0 })
  const dpr = window.devicePixelRatio || 1
  
  interface Props {
    quantity?: number
    staticity?: number
    ease?: number
  }
  
  const props = withDefaults(defineProps<Props>(), {
    quantity: 30,
    staticity: 50,
    ease: 50,
  })
  </script>
  
  
  <template>
    <div ref="canvasContainerRef" aria-hidden="true">
      <canvas ref="canvasRef"></canvas>
    </div>
  </template>
```

As in the React version, we have created a functional component that accepts the props `quantity`, `staticity`, and `ease`. We have also defined the same `const` variables used in the JavaScript code, but in this case, we have defined them as `ref` variables so that we can use them within the Vue component.

### Create a helper function for mouse tracking

We don’t have to come up with anything new. We can simply adapt the code from the React component to create a Vue component that allows us to track the mouse movement. Let’s create a new file called `MousePosition.ts` and copy the following code into it:

```ts
  import { ref, onMounted, onBeforeUnmount } from 'vue'
  
  export default function useMousePosition() {
    const mousePosition = ref < { x: number; y: number } > ({ x: 0, y: 0 })
  
    const handleMouseMove = (event: MouseEvent) => {
      mousePosition.value = { x: event.clientX, y: event.clientY }
    }
  
    onMounted(() => {
      window.addEventListener('mousemove', handleMouseMove)
    })
  
    onBeforeUnmount(() => {
      window.removeEventListener('mousemove', handleMouseMove)
    })
  
    return mousePosition
  }
```

This component can be imported in `Particles.vue` and used to trigger other functions whenever the mouse moves, using a watcher. For example:

```ts
    import useMousePosition from './utils/MousePosition'
    
    const mousePosition = useMousePosition()
    
    watch(
      () => mousePosition.value,
      () => {
        onMouseMove()
      }
    )
```

### Complete the Vue Component

Now we just need to complete the Vue component by adding the functions with TypeScript annotations:

```ts
  const initCanvas = () => {
    resizeCanvas()
    drawParticles()
  }
  
  const onMouseMove = () => {
    if (canvasRef.value) {
      const rect = canvasRef.value.getBoundingClientRect()
      const { w, h } = canvasSize
      const x = mousePosition.value.x - rect.left - w / 2
      const y = mousePosition.value.y - rect.top - h / 2
      const inside = x < w / 2 && x > -w / 2 && y < h / 2 && y > -h / 2
      if (inside) {
        mouse.x = x
        mouse.y = y
      }
    }
  }
  
  const resizeCanvas = () => {
    if (canvasContainerRef.value && canvasRef.value && context.value) {
      circles.value.length = 0
      canvasSize.w = canvasContainerRef.value.offsetWidth
      canvasSize.h = canvasContainerRef.value.offsetHeight
      canvasRef.value.width = canvasSize.w * dpr
      canvasRef.value.height = canvasSize.h * dpr
      canvasRef.value.style.width = canvasSize.w + 'px'
      canvasRef.value.style.height = canvasSize.h + 'px'
      context.value.scale(dpr, dpr)
    }
  }
  
  type Circle = {
    x: number
    y: number
    translateX: number
    translateY: number
    size: number
    alpha: number
    targetAlpha: number
    dx: number
    dy: number
    magnetism: number
  }
  
  const circleParams = (): Circle => {
    const x = Math.floor(Math.random() * canvasSize.w)
    const y = Math.floor(Math.random() * canvasSize.h)
    const translateX = 0
    const translateY = 0
    const size = Math.floor(Math.random() * 2) + 1
    const alpha = 0
    const targetAlpha = parseFloat((Math.random() * 0.6 + 0.1).toFixed(1))
    const dx = (Math.random() - 0.5) * 0.2
    const dy = (Math.random() - 0.5) * 0.2
    const magnetism = 0.1 + Math.random() * 4
    return { x, y, translateX, translateY, size, alpha, targetAlpha, dx, dy, magnetism }
  }
  
  const drawCircle = (circle: Circle, update = false) => {
    if (context.value) {
      const { x, y, translateX, translateY, size, alpha } = circle
      context.value.translate(translateX, translateY)
      context.value.beginPath()
      context.value.arc(x, y, size, 0, 2 * Math.PI)
      context.value.fillStyle = `rgba(255, 255, 255, ${alpha})`
      context.value.fill()
      context.value.setTransform(dpr, 0, 0, dpr, 0, 0)
  
      if (!update) {
        circles.value.push(circle)
      }
    }
  }
  
  const clearContext = () => {
    if (context.value) {
      context.value.clearRect(0, 0, canvasSize.w, canvasSize.h)
    }
  }
  
  const drawParticles = () => {
    clearContext()
    const particleCount = props.quantity
    for (let i = 0; i < particleCount; i++) {
      const circle = circleParams()
      drawCircle(circle)
    }
  }
  
  const remapValue = (
    value: number,
    start1: number,
    end1: number,
    start2: number,
    end2: number
  ): number => {
    const remapped = ((value - start1) * (end2 - start2)) / (end1 - start1) + start2
    return remapped > 0 ? remapped : 0
  }
  
  const animate = () => {
    clearContext()
    circles.value.forEach((circle, i) => {
      // Handle the alpha value
      const edge = [
        circle.x + circle.translateX - circle.size, // distance from left edge
        canvasSize.w - circle.x - circle.translateX - circle.size, // distance from right edge
        circle.y + circle.translateY - circle.size, // distance from top edge
        canvasSize.h - circle.y - circle.translateY - circle.size, // distance from bottom edge
      ]
      const closestEdge = edge.reduce((a, b) => Math.min(a, b))
      const remapClosestEdge = parseFloat(remapValue(closestEdge, 0, 20, 0, 1).toFixed(2))
      if (remapClosestEdge > 1) {
        circle.alpha += 0.02
        if (circle.alpha > circle.targetAlpha) circle.alpha = circle.targetAlpha
      } else {
        circle.alpha = circle.targetAlpha * remapClosestEdge
      }
      circle.x += circle.dx
      circle.y += circle.dy
      circle.translateX += ((mouse.x / (props.staticity / circle.magnetism)) - circle.translateX) / props.ease
      circle.translateY += ((mouse.y / (props.staticity / circle.magnetism)) - circle.translateY) / props.ease
      // circle gets out of the canvas
      if (
        circle.x < -circle.size ||
        circle.x > canvasSize.w + circle.size ||
        circle.y < -circle.size ||
        circle.y > canvasSize.h + circle.size
      ) {
        // remove the circle from the array
        circles.value.splice(i, 1)
        // create a new circle
        const newCircle = circleParams()
        drawCircle(newCircle)
        // update the circle position
      } else {
        drawCircle(
          {
            ...circle,
            x: circle.x,
            y: circle.y,
            translateX: circle.translateX,
            translateY: circle.translateY,
            alpha: circle.alpha,
          },
          true
        )
      }
    })
    window.requestAnimationFrame(animate)
  }
```

And finally, the `onMounted` hook to initialize the canvas and the `animate()` function to start the animation:

```ts
  onMounted(() => {
    if (canvasRef.value) {
      context.value = canvasRef.value.getContext('2d')
    }
    initCanvas()
    animate()
    window.addEventListener('resize', initCanvas)
  })
  
  onBeforeUnmount(() => {
    window.removeEventListener('resize', initCanvas)
  })
```

The Vue component is also ready, and here is the complete code:

```ts
  <script setup lang="ts">
  import { ref, onMounted, onBeforeUnmount, reactive, watch } from 'vue'
  import useMousePosition from './utils/MousePosition'
  
  const canvasRef = ref<HTMLCanvasElement | null>(null)
  const canvasContainerRef = ref<HTMLDivElement | null>(null)
  const context = ref<CanvasRenderingContext2D | null>(null)
  const circles = ref<any[]>([])
  const mousePosition = useMousePosition()
  const mouse = reactive<{ x: number; y: number }>({ x: 0, y: 0 })
  const canvasSize = reactive<{ w: number; h: number }>({ w: 0, h: 0 })
  const dpr = window.devicePixelRatio || 1
  
  onMounted(() => {
    if (canvasRef.value) {
      context.value = canvasRef.value.getContext('2d')
    }
    initCanvas()
    animate()
    window.addEventListener('resize', initCanvas)
  })
  
  onBeforeUnmount(() => {
    window.removeEventListener('resize', initCanvas)
  })
  
  watch(
    () => mousePosition.value,
    () => {
      onMouseMove()
    }
  )
  
  const initCanvas = () => {
    resizeCanvas()
    drawParticles()
  }
  
  const onMouseMove = () => {
    if (canvasRef.value) {
      const rect = canvasRef.value.getBoundingClientRect()
      const { w, h } = canvasSize
      const x = mousePosition.value.x - rect.left - w / 2
      const y = mousePosition.value.y - rect.top - h / 2
      const inside = x < w / 2 && x > -w / 2 && y < h / 2 && y > -h / 2
      if (inside) {
        mouse.x = x
        mouse.y = y
      }
    }
  }
  
  const resizeCanvas = () => {
    if (canvasContainerRef.value && canvasRef.value && context.value) {
      circles.value.length = 0
      canvasSize.w = canvasContainerRef.value.offsetWidth
      canvasSize.h = canvasContainerRef.value.offsetHeight
      canvasRef.value.width = canvasSize.w * dpr
      canvasRef.value.height = canvasSize.h * dpr
      canvasRef.value.style.width = canvasSize.w + 'px'
      canvasRef.value.style.height = canvasSize.h + 'px'
      context.value.scale(dpr, dpr)
    }
  }
  
  type Circle = {
    x: number
    y: number
    translateX: number
    translateY: number
    size: number
    alpha: number
    targetAlpha: number
    dx: number
    dy: number
    magnetism: number
  }
  
  const circleParams = (): Circle => {
    const x = Math.floor(Math.random() * canvasSize.w)
    const y = Math.floor(Math.random() * canvasSize.h)
    const translateX = 0
    const translateY = 0
    const size = Math.floor(Math.random() * 2) + 1
    const alpha = 0
    const targetAlpha = parseFloat((Math.random() * 0.6 + 0.1).toFixed(1))
    const dx = (Math.random() - 0.5) * 0.2
    const dy = (Math.random() - 0.5) * 0.2
    const magnetism = 0.1 + Math.random() * 4
    return { x, y, translateX, translateY, size, alpha, targetAlpha, dx, dy, magnetism }
  }
  
  const drawCircle = (circle: Circle, update = false) => {
    if (context.value) {
      const { x, y, translateX, translateY, size, alpha } = circle
      context.value.translate(translateX, translateY)
      context.value.beginPath()
      context.value.arc(x, y, size, 0, 2 * Math.PI)
      context.value.fillStyle = `rgba(255, 255, 255, ${alpha})`
      context.value.fill()
      context.value.setTransform(dpr, 0, 0, dpr, 0, 0)
  
      if (!update) {
        circles.value.push(circle)
      }
    }
  }
  
  const clearContext = () => {
    if (context.value) {
      context.value.clearRect(0, 0, canvasSize.w, canvasSize.h)
    }
  }
  
  const drawParticles = () => {
    clearContext()
    const particleCount = props.quantity
    for (let i = 0; i < particleCount; i++) {
      const circle = circleParams()
      drawCircle(circle)
    }
  }
  
  const remapValue = (
    value: number,
    start1: number,
    end1: number,
    start2: number,
    end2: number
  ): number => {
    const remapped = ((value - start1) * (end2 - start2)) / (end1 - start1) + start2
    return remapped > 0 ? remapped : 0
  }
  
  const animate = () => {
    clearContext()
    circles.value.forEach((circle, i) => {
      // Handle the alpha value
      const edge = [
        circle.x + circle.translateX - circle.size, // distance from left edge
        canvasSize.w - circle.x - circle.translateX - circle.size, // distance from right edge
        circle.y + circle.translateY - circle.size, // distance from top edge
        canvasSize.h - circle.y - circle.translateY - circle.size, // distance from bottom edge
      ]
      const closestEdge = edge.reduce((a, b) => Math.min(a, b))
      const remapClosestEdge = parseFloat(remapValue(closestEdge, 0, 20, 0, 1).toFixed(2))
      if (remapClosestEdge > 1) {
        circle.alpha += 0.02
        if (circle.alpha > circle.targetAlpha) circle.alpha = circle.targetAlpha
      } else {
        circle.alpha = circle.targetAlpha * remapClosestEdge
      }
      circle.x += circle.dx
      circle.y += circle.dy
      circle.translateX += ((mouse.x / (props.staticity / circle.magnetism)) - circle.translateX) / props.ease
      circle.translateY += ((mouse.y / (props.staticity / circle.magnetism)) - circle.translateY) / props.ease
      // circle gets out of the canvas
      if (
        circle.x < -circle.size ||
        circle.x > canvasSize.w + circle.size ||
        circle.y < -circle.size ||
        circle.y > canvasSize.h + circle.size
      ) {
        // remove the circle from the array
        circles.value.splice(i, 1)
        // create a new circle
        const newCircle = circleParams()
        drawCircle(newCircle)
        // update the circle position
      } else {
        drawCircle(
          {
            ...circle,
            x: circle.x,
            y: circle.y,
            translateX: circle.translateX,
            translateY: circle.translateY,
            alpha: circle.alpha,
          },
          true
        )
      }
    })
    window.requestAnimationFrame(animate)
  }
  
  interface Props {
    quantity?: number
    staticity?: number
    ease?: number
  }
  
  const props = withDefaults(defineProps<Props>(), {
    quantity: 30,
    staticity: 50,
    ease: 50,
  })
  </script>
  
  
  <template>
    <div ref="canvasContainerRef" aria-hidden="true">
      <canvas ref="canvasRef"></canvas>
    </div>
  </template>
```

You can import and use it as a regular Vue component. For example:

```html
  <Particles class="absolute inset-0 pointer-events-none" :quantity="50" />
```

We have reached the end of this tutorial. We hope you’ve found it helpful and have got a clear idea of how to apply this fantastic effect to your layouts to impress your visitors and add dynamism to your designs.

Remember that in our tutorial, we applied this effect to a hero section, but feel free to get creative and replicate it anywhere else in your apps, websites, and landing pages.

If you enjoyed the tutorial, don’t forget to check out the other [Tailwind CSS tutorials](https://cruip.com/tutorials/) on our website, and [tag or mention us](https://twitter.com/Cruip_com) if you share our content on Twitter.