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.maxRipples) {
            ripples.shift(); // Remove the oldest ripple from the array
        }
    });

    // Event listener for mouse clicks to create click-based ripples
    canvas.addEventListener("click", 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 click location with default settings
        const settings = {
            growthSpeed: 1.5,  // Slower growth speed for click ripples
            maxAge: 150,       // Longer lifespan for click ripples
            amplitude: 80,     // Larger distortion for click ripples
            wavelength: 50,
            ringWidth: 6,
            initialRadius: 30  // Initial radius set to 30 for a larger starting ripple
        };

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

        // Limit the number of ripples to improve performance by removing the oldest if necessary
        if (ripples.length > GLOBAL_SETTINGS.maxRipples) {
            ripples.shift(); // Remove the oldest ripple
        }
    });

5. Drawing Functions

To display the ripple effect, we need functions that draw both the background image and the ripples with distortion. The functions below draw the background image and apply ripple distortion dynamically.

 // this function draws the background image onto the canvas
    function drawBackground() {
        ctx.drawImage(background, 0, 0, width, height);  // Draw the background image scaled to canvas size
    }

    //this function is used to draw all ripples onto the canvas with distortion effects
    function drawRipples() {
        // Get the current image data from the canvas
        let imageData = ctx.getImageData(0, 0, width, height);
        let data = imageData.data;

        // Create a new image data object to hold the distorted pixel values
        let newImageData = ctx.createImageData(width, height);
        let newData = newImageData.data;

        // Loop through each pixel on the canvas to apply the ripple effect
        for (let y = 0; y < height; y++) {
            for (let x = 0; x < width; x++) {
                let offsetX = 0;
                let offsetY = 0;

                // Loop through each ripple to calculate its impact on the current pixel
                for (let ripple of ripples) {
                    let dx = x - ripple.x;
                    let dy = y - ripple.y;
                    let dist = Math.sqrt(dx * dx + dy * dy) + 0.01;  // Calculate the distance to the ripple center

                    let radius = ripple.radius;
                    let fade = ripple.fade;  // Apply fade effect based on age of the ripple

                    // Only apply distortion if the pixel is near the ripple's ring
                    if (dist > radius - ripple.ringWidth && dist < radius + ripple.ringWidth) {
                        let wave = Math.sin((dist - radius) / ripple.wavelength) * ripple.amplitude * fade;

                        // Calculate the distortion offsets based on the wave
                        offsetX += (dx / dist) * wave;
                        offsetY += (dy / dist) * wave;
                    }
                }

                // Apply the calculated distortion to the pixel's position
                let sx = Math.floor(x + offsetX);
                let sy = Math.floor(y + offsetY);

                if (sx >= 0 && sx < width && sy >= 0 && sy < height) {
                    let srcIndex = (sy * width + sx) * 4;  // Get the source pixel index
                    let dstIndex = (y * width + x) * 4;   // Get the destination pixel index

                    // Copy the pixel color from the source to the distorted position
                    newData[dstIndex] = data[srcIndex];
                    newData[dstIndex + 1] = data[srcIndex + 1];
                    newData[dstIndex + 2] = data[srcIndex + 2];
                    newData[dstIndex + 3] = data[srcIndex + 3];
                }
            }
        }

        // Apply the new image data (with distortion) to the canvas
        ctx.putImageData(newImageData, 0, 0);
    }

6. The Update Loop

The update() function ensures the animation keeps running smoothly by constantly redrawing the ripples.

// The main update function that continuously redraws the canvas
    function update() {
        drawBackground();  // Draw the background image
        drawRipples();     // Draw the ripples with distortion

        // Remove expired ripples and update the remaining ones
        ripples = ripples.filter(ripple => {
            ripple.incrementAge();  // Increase the age of each ripple
            return ripple.isAlive();  // Keep only alive ripples
        });

        // Request the next animation frame
        setTimeout(update, timeout);  // Call the update function after a delay to control frame rate
    }

    // Start the animation once the background image has loaded
    background.onload = function () {
        update();  // Begin the update loop
    };

Conclusion

With this tutorial, you should be able to make your own water ripple effects for your website. You can experiment with different properties of the ripples to see what you can create. We would love to see what you create. Feel free to leave comment below. Happy coding!

Leave a Reply

Your email address will not be published. Required fields are marked *