Samuel Ogunleye Frontend Engineer, as well as a Technical Writer. A big fan of open-source software.

Build an animated cloud generator with SVG and CSS

8 min read 2286

Have you ever imagined an environment where the atmosphere appears both abstract and natural? Or have you ever wanted to change the mood of a scene by adding some soothing effects? If so, this article is for you.

Realistic clouds greatly enhance the realism of outdoor scenes. Cloud creation, hyperrealism, and cloud likeness are all part of the difficult task of creating realistic clouds.

In this article, you will learn how to create realistic clouds using CSS and SVG from scratch.

Prerequisites

To understand this tutorial, you will need:

  • Working knowledge of CSS
  • Working knowledge of SVGs
  • Working knowledge of React
  • A text editor

What are realistic clouds?

Realistic clouds mean constantly-changing clouds that appear lifelike onscreen. They should look and behave just like real clouds.

Scalable Vector Graphics

Have you ever had an issue with a logo’s image resolution, causing it to be distorted when displayed?

Because users will view your logo on various screen sizes, the issue is linked to responsive design; this should be optimized regardless of screen size. That’s where SVGs come into play.

SVG is a standard graphics file format for rendering two-dimensional images on the internet. For further information, go to this link.

SVGs have unique superpowers that allow you to automatically optimize any picture for any screen size without distortion. Using SVG properties simplifies the use of element rendering because it produces the same results on all screen sizes with no distortion.

SVGs are useful for making clouds due to their many elements. Certain elements (such as the <filter> element) can be manipulated to mimic the organic look of clouds.

How does it work?

SVGs work by defining multiple aspects of an image. Let’s create a simple HTML template that will have some adjustable SVG properties:

<svg width="100" height="100">
   <circle cx="50" cy="50" r="40" stroke="red" stroke-width="4" fill="green" />
</svg> 

As you can see from the example above, the SVG tag provides specific properties that aid in creating the desired shape.

cx works with a shape’s x-axis, so any movement along the x-axis is controlled by it. In SVG, the cx is used to set the horizontal position of a circle or ellipse. Similarly, cy is in charge of any movement along the y-axis.



The radius of a circle is defined by the r attribute. It is always used in connection with the SVG element <circle>.

The thickness of an object is determined by stroke. It demonstrates the shape’s rigidity.

stroke-width specifies the thickness of the stroke. It’s a line that’s drawn around components outside of the borders to make them “spark.” In CSS, it’s identical to border-outline.

The color of an SVG shape’s fill determines the color of the shape’s surface. fill is a fundamental SVG property that you may apply to any SVG shape.

The rendering area of the SVG viewport has a vertical length defined by height and a horizontal length defined by width.

Below is an example of our output. By manipulating these adjustable SVG properties, we can make something like the circle below, or a floating, fluffy cloud.

Green circle with red border made with SVG

Creating realistic clouds

To create a realistic cloud, we will use CSS box-shadow and the CSS and SVG filter properties. Let’s look at what these two terms mean.

What is box-shadow?

box-shadow is a CSS property that allows you to add one or more shadows to an HTML element. It accepts two to five values, depending on the user’s use case:

box-shadow: <offsetX> <offsetY> <blurRadius> <color> <spreadRadius>;

offsetX and offsetY specify the position of the shadow on the horizontal and vertical axes, respectively.

blurRadius specifies the amount of blur, and describes how sharp or blurry the shadow is.

spreadRadius deals with two values. The positive value increases the size of the shadow, while the negative value decreases it.

We will use the box-shadow property to create the shadowy, blurred edges of clouds.

What is the filter property and <filter> element?

The filter property adds visual or graphical effects to an HTML element. These are commonly used to adjust images, borders, or backgrounds.

To define filters, SVG employs the <filter> element. The <filter> element uses the id attribute to identify it uniquely. Filters are defined within <def> elements and then referenced by their ids by graphics elements. <filter> is used to add custom filter effects, but they are never rendered; instead, the properties are used by the element to which they are connected.

In this cloud generator application, we will use the CSS filter property in conjunction with the SVG <filter> element. This will give the clouds the correct texture and layers to make them look realistic.

Project setup

Because we will be using React, we need to generate a starter template using Create React App.

Open your terminal and type the following command:

yarn create react-app realistic-cloud-generator

This will create a folder called realistic-cloud-generator and generate a starter template.

After a successful installation, open the project in your preferred text editor. Your folder structure should look like this below.

Realistic cloud generator file structure

Let’s do some cleanup inside the project that was created above because there are some unnecessary files that we won’t be using.

Open the src folder and delete setupTests.js, reportWebVitals.js, logo.svg, App.css and App.test.js from the project. This is just to reduce the project file size since we won’t be using them.

Open App.js and replace the code with this:

function App() {
  return (
    <>
      Hello!!!!
    </>
  );
}
 
export default App;

The code above simply removes the previous content from the template so that we can work in a clean environment.

Another thing to note is that our application is broken because we deleted some files that are linked inside index.js. Let’s fix that by updating the file with the code below:

import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";

ReactDOM.render(
 <React.StrictMode>
   <App />
 </React.StrictMode>,

 document.getElementById("root")

);

Now that we’ve finished cleaning up, we can start planning out how we want our codebase to look. Create a file inside the parent src folder called Home.jsx, then copy and paste in the code below:

import React, { useState } from "react";
import Cloud from "./components/Cloud";

const Home = () => {
  const [range, setRange] = useState(50);

  return (
    <div>
      <Cloud range={range} />

      <div className="selector">
        <input
          type="range"
          min={1}
          max={100}
          value={range}
          onChange={(e) => setRange(e.target.value)}
        />

        <p className="rangeInput">{range}</p>
      </div>
    </div>
  );
};

export default Home;

In the code above, a useState hook was created to manage the selection of numbers within a certain range.

The Cloud.jsx component was called to render to the browser, and it received the state that was supplied as a prop called range in the Cloud.jsx component code.

It has an input tag with the range type and min and max properties. These values define the start and end of the input range.

Creating cloud components

Create another folder inside the parent src folder called components, then create a file inside the components folder called Cloud.jsx.

Inside the Cloud.jsx file, copy and paste the code below:

import React from "react";
import "./Cloud.css";

const Cloud = ({ range }) => {
  return (
    <>
      <div className="cloud" id="cloud-main"></div>
      <div className="cloud" id="cloud-before"></div>
      <div className="cloud" id="cloud-center"></div>
      <div className="cloud" id="cloud-after"></div>
      <svg width="0" height="0">
        {/* Top Layer */}
        <filter id="filter-main">
          <feTurbulence
            type="fractalNoise"
            baseFrequency="0.011"
            numOctaves="5"
            seed={100 + range * 6}
          />
          <feDisplacementMap in="SourceGraphic" scale={80 + range * 3} />
        </filter>
        <filter id="filter-before">
          <feTurbulence
            type="fractalNoise"
            baseFrequency="0.011"
            numOctaves="3"
            seed={100 + range * 6}
          />
          <feDisplacementMap in="SourceGraphic" scale={80 + range * 3} />
        </filter>
        <filter id="filter-center">
          <feTurbulence
            type="fractalNoise"
            baseFrequency="0.011"
            numOctaves="3"
            seed={100 + range * 6}
          />
          <feDisplacementMap in="SourceGraphic" scale={80 + range * 3} />
        </filter>
        <filter id="filter-after">
          <feTurbulence
            type="fractalNoise"
            baseFrequency="0.009"
            numOctaves="4"
            seed={100 + range * 6}
          />
          <feDisplacementMap in="SourceGraphic" scale={20 + range * 3} />
        </filter>
      </svg>
    </>
  );
};

export default Cloud;

The code above imports cloud.css to add the custom style to the component. Then, we call the range props that were passed as props from the parent component. We also have four div blocks that serve as cloud layers, and we have an SVG element that contains the filters for each div.

Making the clouds look realistic

Each filter includes FeTurbulence and FaDisplacementMap. FeTurbulence is an SVG filter that generates an image using the Perlin turbulence function. It allows artificial textures such as clouds or marble to be combined on the element associated with it. It primarily requires four properties: type, baseFrequency, numOctaves, and seeds.

The FadisplacementMap parameter is primarily used for scaling. It specifies how much you want to distort an image. The greater the scale, the greater the distortion effect.

NumOctaves determines the number of frequencies for the noise functions on FeTurbulence.

BaseFrequency determines the base frequency for the noise functions, and seed is the starting number of a random number generator for the FeTurbulence filter. It is linked with range props from the Home.jsx file.

The seed attribute has a lot going on under the hood, but for our purposes, we’ll just focus on one thing: each value given produces a different cloud shape. This is accomplished by employing the Perlin Noise function, which uses the value supplied as a starting point for its random generator; failure to include this attribute results in a value of zero.

FaDisplacementMap is used to dimensionally displace image content by using a displacement map. It takes two inputs to produce one result. The in parameter specifies the picture content to be displaced. The content to which displacement effect is applied is reserved in the in attribute.

Animating and adding cloud variants

Our application does not look nice because it has not been styled. Let’s add another file called Cloud.css to the components folder.

To make the value dynamic, which will allow us to create different cloud variants, we will pass the range value into the displacementMap scale value and the seed of the feTurbulence, which changes based on the number we get from the range.

The scale specifies the scale factor that will be applied to a displacementMap.

Inside the Cloud.css file, copy and paste the code below:

* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

body {
  width: 100vw;
  height: 100vh;
  background: linear-gradient(0deg, #62a0d8 0%, #2178d1 50%, #085cb3 100%);
  padding: 1%;
  overflow: hidden;
}

.cloud {
  width: 600px;
  height: 100px;
  border-radius: 50%;
  position: absolute;
  top: -100px;
  left: -100px;
  -webkit-animation: move ease-in 35s infinite;
          animation: move ease-in 35s infinite;
}

@-webkit-keyframes move {
  0% {
    left: -100px;
  }

  50% {
    left: 500px;
  }

  100% {
    left: -100px;
  }
}

@keyframes move {
  0% {
    left: -100px;
  }

  50% {
    left: 500px;
  }

  100% {
    left: -100px;
  }
}

#cloud-main {
  top: 90px;
  height: 100px;
  -webkit-filter: url(#filter-main);
          filter: url(#filter-main);
  box-shadow: 200px 170px 19px 40px rgba(255, 255, 255, 0.9);
}
#cloud-before {
  top: 90px;
  height: 30px;
  width: 500px;
  -webkit-filter: url(#filter-before);
          filter: url(#filter-before);
  box-shadow: 200px 200px 10px 40px rgba(215, 215, 215, 0.3);
}

#cloud-center {
  top: 80px;
  width: 580px;
  height: 35px;
  -webkit-filter: url(#filter-center);
          filter: url(#filter-center);
  box-shadow: 210px 250px 28px 30px rgba(66, 105, 146, 0.2);
}

#cloud-after {
  top: 70px;
  width: 550px;
  height: 35px;
  left: -60px;
  -webkit-filter: url(#filter-after);
          filter: url(#filter-after);
  box-shadow: 210px 272px 30px 0px rgba(0, 0, 0, 0.4);
}

.selector {
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  background: rgba(255, 255, 255, 0.1);
  padding: 2rem 6rem;
  display: flex;
  align-items: center;
  justify-content: center;
}

.selector input {
  width: 100%;
  height: 2rem;
  accent-color: #fff;
  outline: none;
  border: none;
}

.selector .rangeInput {
  background: #fff;
  padding: 0.4rem 0.5rem;
  border-radius: 0.2rem;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1rem;
  font-weight: 500;
  color: #000;
  margin-left: 1rem;
}

Because we have applied the essential styles, our application will now look more appealing.

To apply respective rules to each element, we retrieve the classNames and ID selectors that were given to it.

In the code above, The term keyframe refers to a frame that is used to animate an object.

The filter url() function is used to filter content. A graphical effect that changes the appearance of an input image is represented by the CSS data type, which is used by the filter and backdrop-filter attributes. It also accepts a reference to an SVG <filter> element, which will be applied to the element using the CSS filter property.

You can also use the CSS url() function to link to a resource like an image, a web font, or a filter.

Launching the app in a browser

Yay, we’re almost done! A reference to the App.js file that we created earlier in our project folder must be made for our application to be rendered in the browser.

In your App.js file, copy and paste the code below:

import Home from "./Home";

const App = () => {
  return (
    <div className="App">
      <Home />
    </div>
  );
};

export default App;

Let’s put it to the test now that we’ve generated all of the necessary components. Type this command below:

yarn start

This will launch a development server that will be rendered in the browser of your choice.

Yippee! Here’s what we came up with:

Gif of realistic cloud generator with multiple cloud types floating across the sky

Conclusion

In this article, we made a cloud generator application with minimal effort by exploring the realm of SVG using React.

We saw the effect of displacementMap, turbulence, scale, and seed, which can be used with the range props to change the input dynamically and create realistic moving clouds.

The source code of the cloud application built in this tutorial is available on GitHub for you to explore.

LogRocket: Full visibility into your production React apps

Debugging React applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket combines session replay, product analytics, and error tracking – empowering software teams to create the ideal web and mobile product experience. What does that mean for you?

Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay problems as if they happened in your own browser to quickly understand what went wrong.

No more noisy alerting. Smart error tracking lets you triage and categorize issues, then learns from this. Get notified of impactful user issues, not false positives. Less alerts, way more useful signal.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — .

Samuel Ogunleye Frontend Engineer, as well as a Technical Writer. A big fan of open-source software.

Leave a Reply