How to Create a Beautiful Particle Animation with HTML Canvas

Total
0
Shares



Live Demo / Download

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.

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 and Vue frameworks, enabling you to integrate this mesmerizing animation seamlessly into your web app.

Quick navigation




Create an HTML canvas animation with pure JavaScript

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.

  
   lang="en">

  
       charset="utf-8">
      </span>Particle Animation<span class="nt">
       name="viewport" content="width=device-width,initial-scale=1">
      
  

   class="font-inter antialiased">

       class="relative min-h-screen flex flex-col justify-center bg-slate-900 overflow-hidden">
           class="w-full max-w-6xl mx-auto px-4 md:px-6 py-24">

              
               class="absolute inset-0 pointer-events-none" aria-hidden="true">
                   data-particle-animation>
              
Enter fullscreen mode

Exit fullscreen mode

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:



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:

  new ParticleAnimation(canvas, options);
Enter fullscreen mode

Exit fullscreen mode

The class will accept two parameters:

Let’s create our class:

  // 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);
  });
Enter fullscreen mode

Exit fullscreen mode

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.

  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);
    }
  }
Enter fullscreen mode

Exit fullscreen mode

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:

   data-particle-animation width="2800" height="1600" style="width: 1400px; height: 800px;">
Enter fullscreen mode

Exit fullscreen mode



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:

  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();
  }
Enter fullscreen mode

Exit fullscreen mode

The options we have defined are as follows:

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

  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);
  });
Enter fullscreen mode

Exit fullscreen mode

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:

  <canvas data-particle-animation data-particle-quantity="5"></canvas>
Enter fullscreen mode

Exit fullscreen mode

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:

  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 };
    }
Enter fullscreen mode

Exit fullscreen mode

This function allows us to randomly define:

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:

  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);
    }
  }
Enter fullscreen mode

Exit fullscreen mode

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:

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

Exit fullscreen mode

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:

  // 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);
  });
Enter fullscreen mode

Exit fullscreen mode

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.

  animate() {
    // Things to be done ...
    window.requestAnimationFrame(this.animate);
  }
Enter fullscreen mode

Exit fullscreen mode

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():

  clearContext() {
    this.context.clearRect(0, 0, this.canvasSize.w, this.canvasSize.h);
  }
Enter fullscreen mode

Exit fullscreen mode

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

  animate() {
    this.clearContext();
    window.requestAnimationFrame(this.animate);
  }
Enter fullscreen mode

Exit fullscreen mode

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

  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);
  }
Enter fullscreen mode

Exit fullscreen mode

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):

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

Exit fullscreen mode

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:

  this.mouse = {
    x: 0,
    y: 0,
  };
Enter fullscreen mode

Exit fullscreen mode

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

  init() {
    this.initCanvas();
    this.animate();
    window.addEventListener('resize', this.initCanvas);
    window.addEventListener('mousemove', this.onMouseMove);
  }
Enter fullscreen mode

Exit fullscreen mode

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.

  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;
Enter fullscreen mode

Exit fullscreen mode



Reassembling the code

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

  // 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);
  });
Enter fullscreen mode

Exit fullscreen mode

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:

  <canvas data-particle-animation data-particle-quantity="5" data-particle-staticity="40" data-particle-ease="60"></canvas>
Enter fullscreen mode

Exit fullscreen mode

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

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 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.

  '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>
    )

  }
Enter fullscreen mode

Exit fullscreen mode

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



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:

  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;
  }
Enter fullscreen mode

Exit fullscreen mode

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.

  const mousePosition = MousePosition()

  useEffect(() => {
    onMouseMove()
  }, [mousePosition.x, mousePosition.y])
Enter fullscreen mode

Exit fullscreen mode



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:

  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)
  }
Enter fullscreen mode

Exit fullscreen mode

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

  useEffect(() => {    
    if (canvasRef.current) {
      context.current = canvasRef.current.getContext('2d')
    }
    initCanvas()
    animate()
    window.addEventListener('resize', initCanvas)

    return () => {
      window.removeEventListener('resize', initCanvas)
    }
  }, [])
Enter fullscreen mode

Exit fullscreen mode

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

  '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>
    )
  }
Enter fullscreen mode

Exit fullscreen mode

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

  <Particles className="absolute inset-0 pointer-events-none" quantity={50} />
Enter fullscreen mode

Exit fullscreen mode




Creating a similar component for Vue

Let’s now see how we can create a perfect clone of the Next.js component for Vue. First, let’s create a new Vue component.

The component we are about to create is available in our GitHub repository, 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:

  


  
Enter fullscreen mode

Exit fullscreen mode

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:

  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
  }
Enter fullscreen mode

Exit fullscreen mode

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

    import useMousePosition from './utils/MousePosition'

    const mousePosition = useMousePosition()

    watch(
      () => mousePosition.value,
      () => {
        onMouseMove()
      }
    )
Enter fullscreen mode

Exit fullscreen mode



Complete the Vue Component

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

  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)
  }
Enter fullscreen mode

Exit fullscreen mode

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

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

  onBeforeUnmount(() => {
    window.removeEventListener('resize', initCanvas)
  })
Enter fullscreen mode

Exit fullscreen mode

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

  <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>
Enter fullscreen mode

Exit fullscreen mode

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

   class="absolute inset-0 pointer-events-none" :quantity="50" />
Enter fullscreen mode

Exit fullscreen mode

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 on our website, and tag or mention us if you share our content on Twitter.

Total
0
Shares
Valentsea

Toxic Work environment

What is a toxic work environment? When hostile, bullying, or other unpleasant behavior is ingrained in the workplace’s…

You May Also Like