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!