Simulating A Cool Water Ripple Effect In JavaScript


Have you ever wanted to add a little extra magic to your website? Well, a water ripple effect can do just that! It is a simple effect, but it can be quite mesmerizing. You can use this effect to make your site feel alive and interactive. In this blog, we are going to look at how you can create a stunning water ripple distortion effect using HTML5 and JavaScript.

Prerequisites

Before we get started, make sure you have a basic understanding of HTML, CSS, and JavaScript.

  • Basic Understanding of HTML, CSS, and JavaScript: Familiarity with the fundamental concepts of HTML for structuring web pages, CSS for styling, and JavaScript for adding interactivity is essential. This tutorial assumes a basic knowledge of these languages.
  • Web Browser: Use a modern web browser like Google Chrome, Mozilla Firefox, or Microsoft Edge.

For the purpose of this tutorial, you can download the source files for the demo to help you follow along:

Note: As you test the source files, ensure that you load them within a suitable environment such that all the files are loaded. For offline use, we recommend that you use Live Server and vscode to load the files. Simply open the files in Vscode and run “index.html” using Live Server.

Demo

Here is what we are going to create. Move the mouse over the image to see the effect in action.

How it works

This project uses an HTML5 Canvas element to generate realistic ripple distortions that respond to user interactions. The effect works by simulating wave movements through pixel manipulation. When a user moves their mouse or clicks, ripples form and spread across the canvas, mimicking real water waves. The effect is achieved through dynamic pixel manipulation, where image data is shifted to create a refraction effect. To keep the animation smooth and performant, the ripples animate at a controlled frame rate of approximately 30 frames per second (fps).

1.Setting Up the Project

Let’s start by setting up the basic HTML structure for our project. Create an HTML called “index.html”, with a basic HTML web format, and add a canvas element.

HTML5’s <canvas> element allows you to draw graphics directly on a webpage using JavaScript. It’s essentially a blank space where we can paint anything we like 2D shapes, images, and even 3D projections!

Creating the Canvas Element

To get started, we need a <canvas> element in our HTML file:

<canvas id="canvas" width="500" height="500" style="background: black;"></canvas>

This creates a 500×500-pixel canvas with a black background. The next step is to access this canvas through JavaScript and set up a 2D rendering context for drawing.

Also, add a script tag for our javascript code, and you are good to go!

Here is our full code template.

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>3D Projection</title>
</head>

<body>

    <!-- Create a canvas element to draw on -->
    <canvas id="canvas" width="500" height="500" style="background: black;"></canvas>

    <script type="text/javascript">
      
    </script>

</body>

</html>

2. Defining Constants and Variables

Before we create the ripple effect, we need to set up some global variables to manage the canvas, its dimensions, and the ripple properties.

// Get the canvas element and its 2D rendering context
    const canvas = document.getElementById("canvas");
    const ctx = canvas.getContext("2d");

    // Store the canvas width and height for easier reference
    const width = canvas.width;
    const height = canvas.height;

    // Create a new image object for the background image
    const background = new Image();
    background.src = "image.jpg"; // Replace with your image path

    // Define a timeout to control the frame rate (~30fps)
    const timeout = 33; 

    // Array to store all the ripples
    let ripples = [];

    // === Global Ripple Settings ===
    // These settings define the behavior of all ripples on the canvas
    const GLOBAL_SETTINGS = {
        growthSpeed: 2,   // Speed at which the ripple grows (in pixels per frame)
        amplitude: 50,    // Maximum distortion of pixels in pixels
        wavelength: 500,  // Determines the frequency of the wave
        ringWidth: 10,    // Thickness of the ripple rings
        maxAge: 100,      // Maximum lifespan of a ripple (in frames)
        maxRipples: 50,   // Max number of ripples on the screen at once (for performance)
    };

These variables set up the canvas and store ripple effects dynamically

3. Creating the Ripple Class

The Ripple class is responsible for managing the properties and behavior of each ripple on the canvas. Each ripple grows, distorts the image, and then fades out smoothly. So the ripple class has functions to grow the ripple radius, fade it, and change its age.

// The Ripple class defines the properties and behavior of each ripple
    class Ripple {
        constructor(x, y, settings = GLOBAL_SETTINGS) {
            // Initialize ripple properties
            this.x = x;                      // X position of the ripple
            this.y = y;                      // Y position of the ripple
            this.age = 0;                    // Age of the ripple (used for lifespan and fading)
            this.maxAge = settings.maxAge;   // Max age of this ripple
            this.growthSpeed = settings.growthSpeed;  // How fast the ripple grows
            this.amplitude = settings.amplitude;      // Maximum distortion caused by the ripple
            this.wavelength = settings.wavelength;    // The wavelength of the wave effect
            this.ringWidth = settings.ringWidth;      // The width of the ripple ring
            this.initialRadius = settings.initialRadius || 0; // Optional initial radius, used for larger ripples on click
        }

        // Calculate the current radius of the ripple based on its age and growth speed
        get radius() {
            return this.initialRadius + this.age * this.growthSpeed;  // Adding initial radius for larger starting size
        }

        // Calculate the fade effect based on the age of the ripple
        get fade() {
            return 1 - this.age / this.maxAge;  // Fade the ripple over time
        }

        // Check if the ripple is still alive (i.e., not past its maximum age)
        isAlive() {
            return this.age < this.maxAge;
        }

        // Increment the age of the ripple
        incrementAge() {
            this.age++;
        }
    }

4. Handling User Interaction with Event Listeners

Now we need to make the ripples appear when the user moves their mouse or clicks on the canvas.

 // Event listener for mouse movement
    canvas.addEventListener("mousemove", function (e) {
        const rect = canvas.getBoundingClientRect();  // Get canvas bounds
        const x = e.clientX - rect.left;  // Calculate mouse X position on canvas
        const y = e.clientY - rect.top;   // Calculate mouse Y position on canvas

        // Create a ripple at the mouse position with faster growth and shorter lifespan
        const settings = {
            growthSpeed: 4,    // Faster growth speed for movement ripples
            maxAge: 100,       // Shorter lifespan for movement ripples
            amplitude: 30,     // Smaller distortion amplitude for movement ripples
            wavelength: 50,
            ringWidth: 6
        };

        ripples.push(new Ripple(x, y, settings));  // Add the new ripple to the array

        // If there are more than the maximum allowed ripples, remove the oldest one for performance
        if (ripples.length > GLOBAL_SETTINGS