Skip to content

Placing Images on Curved Surfaces Through Displacement Mapping

Say, you want to place a two-dimensional image on a curved surface, such as a mug or bottle, to create a product mockup or highlight printing capabilities. Thanks to Cloudinary’s displacement-mapping feature, that’s easy to do.

Here’s how displacement mapping works. By means of a map image, the algorithm displaces parts of the image based on the base image’s texture. A lighter red channel in the base causes more horizontal displacement; a darker green channel, more vertical displacement. Also, you can edit image angles for a natural look. Along with the transformation is a detailed map that combines the texture on both surfaces into one.

This tutorial shows displacement mapping in action. You’ll integrate Cloudinary into an application, upload images to Cloudinary, and then create displacement maps on various curved surfaces, such as coffee cups and T-shirts, with Cloudinary’s Transformation URL API. As for prerequisites, you must know the basics of JavaScript and have a working knowledge of Node.js and APIs.

First, sign up for a free Cloudinary account for an ample quota of thousands of transformations, storage, and bandwidth. Note the cloud name, API key, and API secret in your account’s dashboard  for connecting your server to Cloudinary later on.

Next, install Node.js. Afterwards, clone the starting code from this GitHub repo and check out the initial branch with two command lines:

git clone git@github.com:dillionmegida/cloudinary-snippet.git git checkout initial
Code language: PHP (php)

The application in this tutorial comprises a front end, which uploads images to the back-end server; and a back-end server, which uploads images to Cloudinary. The server fetches from Cloudinary the images along with the related transformation configurations, ultimately delivering the transformed media to the front end for display.

The front-end code will reside in a directory with two pages: the first one for image uploads, the second one for image display.

Build the front end, as follows:

1. Create an index.html file that contains the code below.

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>File Upload</title> <link href="./style.css" rel="stylesheet" /> </head> <body> <div class="container"> <h1>Image Upload</h1> <form id="form"> <div class="input-group"> <label for="image">Select image</label> <input id="image" type="file" /> </div> <button class="submit-btn" type="submit">Upload</button> </form> </div> <script src="./script.js"></script> </body> </html>
Code language: HTML, XML (xml)

2. Create a style.css file with the code below.

body { background-color: rgb(6, 26, 27); } * { box-sizing: border-box; } .container { max-width: 1200px; margin: 60px auto; } .container h1 { text-align: center; color: white; } form { background-color: white; padding: 30px; max-width: 500px; margin: 0 auto; } form .input-group { margin-bottom: 15px; } form label { display: block; margin-bottom: 10px; } form input { padding: 12px 20px; width: 100%; border: 1px solid #ccc; } .submit-btn { width: 100%; border: none; background: rgb(37, 83, 3); font-size: 18px; color: white; border-radius: 3px; padding: 20px; text-align: center; cursor: pointer; } .images { background-color: white; padding: 20px; display: grid; width: 100%; grid-template-columns: repeat(4, 1fr); grid-gap: 20px; } .images .image { width: 100%; } .images img { width: 100%; } .get-images-btn { background-color: rgb(37, 83, 3); color: white; padding: 20px; border-radius: 3px; text-align: center; cursor: pointer; border: none; margin: 0 auto 20px; display: block; }
Code language: CSS (css)

The above code generates this dialog box:

Dialog box

3. Create a script.js file with the code below for image uploads.

const form = document.getElementById("form"); if (form) form.addEventListener("submit", submitForm); function submitForm(e) { e.preventDefault(); const image = document.getElementById("image"); const formData = new FormData(); formData.append("image", image.files[0]); fetch("http://localhost:5000/upload_image", { method: "post", body: formData, }) .then((res) => { alert("successful upload"); image.value = ""; }) .catch((err) => ("Error occurred", err)); }
Code language: JavaScript (javascript)

The above code adds a submit event listener to the form and attaches the submitForm callback function to the listener. On a form submission, you append the selected file to the form data and make a POST request to localhost:5000/upload_files. The server will run on localhost:5000, and the /upload_files API will upload files.

Before building the back end, install the following software:

  • Express: a Node framework for creating APIs
  • cloudinary: Cloudinary’s Node.js SDK for uploading and retrieving images
  • multer: Node.js middleware for handling multipart form-data (images in forms)
  • nodemon: a tool that restarts Node.js applications in case of file changes
  • CORS: Express middleware for sharing of cross-origin resources
  • dotenv: a module for loading environment variables from an .env file

Now build the back end, as follows:

1. Run this command line to incorporate the above packages into the package.json file:

npm install express cloudinary multer nodemon cors

2. Update the scripts property:

"scripts": { "dev": "nodemon server.js" },
Code language: JavaScript (javascript)

This script starts the server and, in case of file changes, automatically triggers a restart.

3. Create a .env file and replace the asterisks below with the values from your account’s dashboard.

CLOUD_NAME=*** API_KEY=*** API_SECRET=***

4. Paste this starter code into the server.js file:

require("dotenv").config(); const multer = require("multer"); const express = require("express"); const cors = require("cors"); const cloudinary = require("cloudinary").v2; const upload = multer({ dest: "uploads/" }); cloudinary.config({ cloud_name: process.env.CLOUD_NAME, api_key: process.env.API_KEY, api_secret: process.env.API_SECRET, }); const app = express(); app.use(cors()); app.use(express.json()); app.post("/upload_image", upload.single("image"), uploadImage); app.get("/images", listImages); function uploadImage(req, res) { cloudinary.uploader.upload( req.file.path, { public_id: req.file.filename, folder: "cloudinary-test", }, () => { res.json({ message: "Successfully uploaded image" }); } ); } app.listen(5000, () => { console.log(`Server started...`); });
Code language: PHP (php)

Here, you have specified an upload directory for multer and created a POST API called upload_image. The uploadImage callback function uploads images to Cloudinary, placing them in the cloudinary-test folder. The file object on the request object is populated with multer middleware (upload.single('image')).

5. Run npm run dev in the terminal. When the  “Server started …” message is displayed, go back to the front end and upload an image.

6. Go to your Cloudinary account’s dashboard, click Browse Media Library, and open the cloudinary-test folder.

The image you uploaded is displayed, for example, this one:

Test upload

The front end calls an API in the back end, which then retrieves the uploaded images from Cloudinary.

Do the following:

1. Build the API by editing the code below the `listImages` function in the server.js file, like this:

app.get("/images", listImages); function listImages(req, res) { cloudinary.api.resources( { type: "upload", prefix: "cloudinary-test", }, (err, result) => { if (err) { res.status(500).send(err); } else { res.json(result.resources); } } ) }
Code language: PHP (php)

This code retrieves all the images in the cloudinary-test folder.

2. Update the front end by editing the `images.html` file with the code below:

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Cloudinary images</title> <link href="./style.css" rel="stylesheet" /> </head> <body> <div class="container"> <h1>Images</h1> <button class="get-images-btn" onclick="getImages()">Get Images</button> <div id="image-container" class="images"></div> </div> <script src="./script.js"></script> </body> </html>
Code language: HTML, XML (xml)

3. Edit the script.js file with the following code so that the image-container element will be populated by JavaScript.

function getImages() { fetch("http://localhost:5000/images") .then((res) => { return res.json(); }) .then((json) => { const images = json.map((image) => { return `<div class="image"> <img src="${image.url}" alt="${image.name}" /> </div>`; }); document.getElementById("image-container").innerHTML = images.join(""); }) .catch((err) => ("Error occurred", err)); }
Code language: JavaScript (javascript)

When the API returns the images in an array, the code above creates a modified array of those images containing a div and an img element, then appends the modified array to the DOM.


Now load the /images page and click Get Images. The uploaded image is displayed:

get images dialog box

You can apply various transformations with Cloudinary. The procedure below steps you through the process of applying displacement maps.

1. Download the image below, go to your Cloudinary account’s dashboard, create a folder called “gradients” at the root of the media library. Then, upload the image with the name `gradient` to that folder:

Gradient image
Bond Street

2. Upload the image above to the root of your media library and rename the image to logo.

3. Update the listImages function in the server.js file, as follows:

function listImages(req, res) { const imageUrl = cloudinary .image("logo", { overlay: "gradients:gradient", effect: "displace", x: 20, y: 20, use_root_path: true, }) .replace("<img src='", "") .replace("' />", ""); res.json({ imageUrl }); }
Code language: JavaScript (javascript)

The colon in gradients:gradient is a replacement for /, which specifies paths. So, gradients:gradient means the gradient file in the gradients directory.

The x and y values denote the amount of displacement on the horizontal and vertical axes, respectively. The higher the value, the greater the displacement.
With the use_root_path option, you can create shorter URLs for uploaded images. replace ensures that the output of cloudinary.image is in this format:

<img src="..." />
Code language: HTML, XML (xml)

You just need the URL, hence the replacements.

4. Edit the script.js file as follows so that the API responds with an array:

function getImages() { fetch("http://localhost:5000/images") .then((res) => { return res.json(); }) .then((json) => { const imageElem = ` <div class="image"> <img src="${json.imageUrl}" /> </div> `; document.getElementById("image-container").innerHTML = imageElem; }) .catch((err) => ("Error occurred", err)); }
Code language: JavaScript (javascript)

Clicking Get Images now results in the display of this image:

upload image with gradient

The procedure above displaces certain parts of the logo with a template map (gradient). The black regions distort the image upwards; the white regions, downwards. You can create various displacements with different gradient patterns.

Next, attach a modified image like the one you just created to another image—a curved surface generated by a gradient. Follow these steps:

1. Upload the first image below to the root of the media library and the second to the gradients folder in your account’s dashboard:

graffiti
Gradient image

2. Rename the top image `sticker` and the bottom image gradient2.

3. Update the server.js file to read like this:

function listImages(req, res) { const imageUrl = cloudinary .image("sticker", { overlay: "gradients:gradient2", effect: "displace", x: 30, y: 30, use_root_path: true, }) .replace("<img src='", "") .replace("' />", ""); res.json({ imageUrl }); }
Code language: JavaScript (javascript)

Clicking Get Images results in the display of this curved image:

upload graffiti image

You can now apply the new image as an overlay to a surface, e.g., a cup. Follow the steps below.

1. Upload an image of a plain paper cup, such as this one from Unsplash, to the cloudinary-test folder. Rename the image cup.

Empty product

2. Update the server.js file to read like this:

function listImages(req, res) { const imageUrl = cloudinary .image("cloudinary-test/cup", { use_root_path: true, transformation: [ { width: 500, crop: "scale" }, { overlay: "sticker", width: 100, height: 100, crop: "pad", y: 10, }, { overlay: "gradients:gradient2" }, { flags: "layer_apply", effect: "displace", y: 2 }, ], }) .replace("<img src='", "") .replace("' />", ""); res.json({ imageUrl }); }
Code language: JavaScript (javascript)

The above code groups the transformations appropriately with the transformation array property. Additionally, the code adds the image as an overlay; also the gradient, but with the displacement effect.

Clicking Get image results in this display:

Adding a graffiti image on a coffee cup

Neat, right? To enhance the aesthetics, move the image slightly to the left and to the top by adding the following object to the transformation array:

{ flags: "layer_apply", x: -7, y: -40, background: "transparent" }
Code language: CSS (css)

The image now looks like this:

Adjusted graffiti image on coffee cup

To place the image called logo on the surface of other products, do the following:

1. Download from Unsplash a few plain product images with a curved surface. For example:

image collage

2. Upload the images to Cloudinary from the front end of the application.

3. Apply the logo to all five images by editing the listImages function in the server.js file to read like this:

function listImages(req, res) { cloudinary.api.resources( { type: "upload", prefix: "cloudinary-test", }, (err, result) => { if (err) { res.status(500).send(err); } else { const transformed = result.resources.map((r) => { const transformedUrl = cloudinary .image(r.public_id, { use_root_path: true, transformation: [ { width: 500, crop: "scale" }, { overlay: "sticker", width: 100, height: 100, crop: "pad", y: 10, }, { overlay: "gradients:gradient2" }, { flags: "layer_apply", effect: "displace", y: 2 }, { flags: "layer_apply", x: 2, background: "transparent" }, ], }) .replace("<img src='", "") .replace("' />", ""); return { transformedUrl, ...r }; }); res.json(transformed); } } ); }
Code language: JavaScript (javascript)

4. Update the script.js file to receive an array:

function getImages() { fetch("http://localhost:5000/images") .then((res) => { return res.json(); }) .then((json) => { const images = json.map((image) => { return `<div class="image"> <img src="${image.transformedUrl}" alt="${image.name}" /> </div>`; }); document.getElementById("image-container").innerHTML = images.join(""); }) .catch((err) => ("Error occurred", err)); }
Code language: JavaScript (javascript)

Clicking Get Images results in this display:

get images collage

The logo looks fine on some objects but not on others. Why? Because, to simultaneously apply the logo to multiple product images, you must ensure that the displacement pattern is a fit for the latter. Otherwise, apply the displacement separately, one product image at a time.

The code for this tutorial is on GitHub.

With Cloudinary, you can transform images by leveraging just URL parameters—in this case, displacement maps, which enable texture objects to capitalize on the base image’s displacement strength. Hence the ease with which you can map displaced images to other pictures, rendering the final version lifelike.

To place images on curved surfaces, you’d usually need to merge images with photo-editing software. No need with Cloudinary. By following the simple procedures above, you can showcase how your products look on various objects, and vice versa.

Other image transformations you can perform with Cloudinary’s URL parameters include changing file extensions, editing backgrounds, as well as improving and optimizing quality. For details, see the related documentation. Let your creativity fly!

Back to top

Featured Post