Create a Water Ripple Effect in Next.js

Eugene Musebe

Introduction

This article is to demonstrate how to build a water ripple effect using Nextjs framework and Pixi.js library

Codesandbox

The final version of this project can be viewed on Codesandbox.

You can find the full source code on my Github repo.

Prerequisites

Basic/entry-level knowledge and understanding of javascript and React/Nextjs.

Setting Up the Sample Project

In your respective directory, Create a new Next.js project by using: npx create-next-app videocall

Go to your project directory using: cd videocall

We will begin by setting up our backend with the Cloudinary feature.

Include Cloudinary in your dependencies: npm install cloudinary

Cloudinary Credentials Setup

Use Link to log in or create your Cloudinary account. You will be provided with the necessary environment variables for integration.

In your project root directory, create a new file named .env.local and use the following guide to fill your variables.

1"pages/api/upload.js"
2
3
4CLOUDINARY_CLOUD_NAME =
5
6CLOUDINARY_API_KEY =
7
8CLOUDINARY_API_SECRET=

Restart your project: npm run dev.

Create a directory pages/api/upload.js.

Configure the environment keys and libraries to avoid code duplication.

1"pages/api/upload.js"
2
3
4var cloudinary = require("cloudinary").v2;
5
6cloudinary.config({
7 cloud_name: process.env.CLOUDINARY_NAME,
8 api_key: process.env.CLOUDINARY_API_KEY,
9 api_secret: process.env.CLOUDINARY_API_SECRET,
10});

Finally, add a handler function to execute Nextjs post request:

1"pages/api/upload.js"
2
3
4export default async function handler(req, res) {
5 if (req.method === "POST") {
6 let url = ""
7 try {
8 let fileStr = req.body.data;
9 const uploadedResponse = await cloudinary.uploader.upload_large(
10 fileStr,
11 {
12 resource_type: "video",
13 chunk_size: 6000000,
14 }
15 );
16 } catch (error) {
17 res.status(500).json({ error: "Something wrong" });
18 }
19
20 res.status(200).json("backend complete");
21 }
22}

The above function will upload media files to Cloudinary and return the file's Cloudinary link as a response

Let us now build the ripple effect.

First, include Pixi.js which will allow us to create the effect using few lines of code: npm install pixi.js. The

Include all necessary imports in the Home component:

1import React, { useRef, useState } from "react";
2import * as PIXI from 'pixi.js';

Declare the following variables and state hooks. We will use them as we move on

1"pages/index.js"
2
3
4 let original, app, image, displacementSprite, displacementFilter, processedImage, animationID,finalCanvas;
5 const processedRef = useRef();
6 const [link, setLink] = useState();

Paste the following in your return statement. You can finc the css files in the github repository.

1"pages/index.js"
2
3
4return (
5 <div className="container">
6 <div className="navbar">
7 <h1>WaterRipple Effect in nextjs</h1>
8 <button onClick = {startAnimation}>Start water ripple</button>
9 <button onClick = {stopAnimation}>Stop water ripple</button>
10 {/* <button onClick={uploadHandler}>Upload Sample</button> */}
11 </div>
12 <div className="row">
13 <div className="column">
14 <img id="image" src="https://res.cloudinary.com/dogjmmett/image/upload/v1652412717/template_vowego.jpg" alt="fish" />
15 </div>
16 <div id="column2">
17 {link? <a href={link} className="link"><h3>Use Link</h3></a>:<h3 className="link">Link shows here</h3> }<br />
18
19 <div ref={processedRef} className="processed" />
20 </div>
21 </div>
22 </div>
23)

The above should be the same as below:

Create the following function named startAnimation and begin by refferencing the original image and the reffrenced div processedRef as follows:

1"pages/index.js"
2
3
4const startAnimation = () => {
5 processedImage = processedRef.current;
6 original = document.getElementById('image')
7}

We can now initialize Pixi.js by first creating an application object that will render your scene like renderer, root container and time tracking. We then add the HTML as a canvas to the referenced processedRef DOM element.

1"pages/index.js"
2
3
4 const startAnimation = () => {
5 processedImage = processedRef.current;
6 original = document.getElementById('image');
7 // console.log(original.width, original.height)
8 app = new PIXI.Application({ width: original.width, height: original.height, forceCanvas: true });
9 processedImage.appendChild(app.view);
10
11 }

Intoduce a sprite image.

1"pages/index.js"
2
3
4 const startAnimation = () => {
5 processedImage = processedRef.current;
6 original = document.getElementById('image');
7 // console.log(original.width, original.height)
8 app = new PIXI.Application({ width: original.width, height: original.height, forceCanvas: true });
9 processedImage.appendChild(app.view);
10 image = new PIXI.Sprite.from("https://res.cloudinary.com/dogjmmett/image/upload/v1652412717/template_vowego.jpg");
11 image.width = original.width;
12 image.height = original.height;
13 // console.log(app.view)
14 app.stage.addChild(image);
15
16 }

The trick to creating the displacement effect is to move the image pixels away from their original position. To achieve such displacement I will map the original image with a random black and white variation

Create sprite from this image and add a displacement filter from the sprite. Set wrapMode to repeat and displacement to cover the entire image

1"pages/index.js"
2
3
4 const startAnimation = () => {
5 processedImage = processedRef.current;
6 original = document.getElementById('image');
7 // console.log(original.width, original.height)
8 app = new PIXI.Application({ width: original.width, height: original.height, forceCanvas: true });
9 processedImage.appendChild(app.view);
10 image = new PIXI.Sprite.from("https://res.cloudinary.com/dogjmmett/image/upload/v1652412717/template_vowego.jpg");
11 image.width = original.width;
12 image.height = original.height;
13 // console.log(app.view)
14 app.stage.addChild(image);
15
16 displacementSprite = new PIXI.Sprite.from("https://res.cloudinary.com/dogjmmett/image/upload/v1652411299/cloud_eg89xa.png")
17 displacementFilter = new PIXI.filters.DisplacementFilter(displacementSprite);
18 displacementSprite.texture.baseTexture.wrapMode = PIXI.WRAP_MODES.REPEAT;
19 app.stage.addChild(displacementSprite);
20 app.stage.filters = [displacementFilter];
21
22}

Add an animation for a better view of the displacement effect and set it on a loop using requestAnimationFrame method.

1"pages/index.js"
2
3
4function animate() {
5 displacementSprite.x += 10;
6 displacementSprite.y += 4;
7 animationID = requestAnimationFrame(animate);
8 finalCanvas = app.view.toDataURL()
9
10}

A sample of captioned effect looks like below:

That's it! Ensure to go through this article to enjoy your experience.

Eugene Musebe

Software Developer

I’m a full-stack software developer, content creator, and tech community builder based in Nairobi, Kenya. I am addicted to learning new technologies and loves working with like-minded people.