Display Floating Avatars for Event Attendees

Ifeoma Imoh

I recall registering for one of the previous editions of the Next.js conference and seeing animation on the landing page with participants' avatars joyously floating around.

In this article, we will recreate that animation using anime.js and React. A registration form will be displayed when the website loads. This form will contain four elements: an input field for the user's name, a button for uploading an avatar, a checkbox that the user selects to indicate whether or not the avatar should be displayed on the page, and a submit button.

Cloudinary will handle image uploads and display. You will need a Cloudinary account so create one here if you do not already have one. We will use antd for UI components in our application.

Here is a link to the demo CodeSandbox.

Project Setup

Create a new React app using the following command:

1npx create-react-app floating_avatars

Next, add the project dependencies using the following command:

1npm install animejs antd @ant-design/icons cloudinary-react axios

Next, we need to import the CSS for antd. To do this, add the following to the App.css file in your src directory:

1@import "~antd/dist/antd.css";

Next, we need to create some environment variables to hold our Cloudinary details. We will be sending images to Cloudinary via unsigned POST requests for this tutorial. To do this, we need our account cloud name and an unsinged upload preset. Create a new file called .env at the root of your project and add the following to it:

1REACT_APP_CLOUD_NAME = YOUR CLOUD NAME HERE
2 REACT_APP_UPLOAD_PRESET = YOUR UNSIGNED UPLOAD PRESET KEY HERE

This will be used as a default when the project is set up on another system. To update your local environment, create a copy of the .env file using the following command:

1cp .env .env.local

By default, this local file is added to .gitignore and mitigates the security risk of inadvertently exposing secret credentials to the public. You can update .env.local with your Cloudinary cloud name and generated upload preset.

Next, create a file named cloudinaryConfig.js in your src folder. This file will access the environment variables and prevent repeated process.env. calls throughout the project. Add the following to your cloudinaryConfig.js file:

1export const cloudName = process.env.REACT_APP_CLOUD_NAME;
2 export const uploadPreset = process.env.REACT_APP_UPLOAD_PRESET;
3 export const uploadTag = "floating_avatar_registration";

In addition, we declared a constant named uploadTag. This will be attached to the uploaded images with the user's permission to display in the floating avatars.

Create Helper Class for API Requests

To help with requests to the Cloudinary API, create a new file called api.js and add the following to it:

1import axios from "axios";
2 import { cloudName, uploadPreset, uploadTag } from "./cloudinaryConfig";
3 export const getAvatarResources = ({ successCallback }) => {
4 axios
5 .get(`https://res.cloudinary.com/${cloudName}/image/list/${uploadTag}.json`)
6 .then((response) => successCallback(response.data.resources));
7 };
8 export const uploadFile = ({ file, successCallback, addTag }) => {
9 const url = `https://api.cloudinary.com/v1_1/${cloudName}/image/upload`;
10 const data = new FormData();
11 data.append("file", file);
12 data.append("upload_preset", uploadPreset);
13 if (addTag) {
14 data.append("tags", uploadTag);
15 }
16 axios
17 .post(url, data, {
18 headers: {
19 "Content-Type": "multipart/form-data",
20 },
21 })
22 .then((response) => successCallback(response.data));
23 };

In the getAvatarResources function, we use the Client-side asset lists feature to retrieve the list of images with our set tag. To ensure that this feature is available on your Cloudinary account, you must ensure that the Resource list option is enabled. By default, the list delivery type is restricted.

The uploadFile function uploads the selected avatar to Cloudinary. With the selected file, a boolean flag named addTag and a callback function in the event of a successful upload, the uploadFile function makes a POST request to Cloudinary, providing the file and the upload preset we defined earlier. If the value of addTag is set to true, a tag is attached to the request to include the avatar in the animation(s) on the index page.

Create Avatar Selector component

Create a new folder called components in your src directory. In the src/components directory, create a new file called AvatarSelector.js and add the following to it:

1import { Button, Upload } from "antd";
2 import { UploadOutlined } from "@ant-design/icons";
3 const AvatarSelector = ({ setUploadedFile, isUploading }) => {
4 const props = {
5 name: "file",
6 onRemove: () => {
7 setUploadedFile(null);
8 },
9 beforeUpload: (file) => {
10 setUploadedFile(file);
11 return false;
12 },
13 showUploadList: false,
14 maxCount: 1,
15 };
16 return (
17 <Upload {...props}>
18 <Button icon={<UploadOutlined />} loading={isUploading}>
19 Click to Upload
20 </Button>
21 </Upload>
22 );
23 };
24 export default AvatarSelector;

The AvatarSelector component takes two props:

  1. setUploadedFile - this callback function is used to update the application state with the latest image uploaded by the user.
  2. isUploading - This flag is used to set the status of the upload button. If this value is true, it indicates that the currently selected image is being uploaded to Cloudinary.

By returning false in the beforeUpload method, we disable the Upload component's default behavior, which uploads the selected file to a provided URL. We also restrict the maximum number of files to 1.

Create Floating Avatar component

In your src/component folder, create a new file called FloatingAvatars.js and add the following to it:

1import { useEffect, useRef } from "react";
2 import anime from "animejs";
3 import { cloudName, uploadPreset } from "../util/cloudinaryConfig";
4 import { Image, Transformation } from "cloudinary-react";
5 const FloatingAvatars = ({ avatars }) => {
6 const refs = useRef([]);
7 const windowHeight = window.innerHeight;
8 const windowWidth = window.innerWidth;
9 const animation = () => {
10 anime({
11 targets: refs.current,
12 translateY: () => anime.random(0, windowHeight / 2),
13 translateX: () => anime.random(0, windowWidth / 2),
14 easing: "easeInOutSine",
15 duration: 5000,
16 loop: true,
17 direction: "alternate",
18 complete: animation,
19 });
20 };
21 useEffect(() => {
22 animation();
23 });
24 const getRandomNumber = (min, max) => Math.random() * (max - min) + min;
25 const randomTop = () => getRandomNumber(0, windowHeight / 2);
26 const randomLeft = () => getRandomNumber(0, windowWidth / 2);
27 return avatars.map((avatar, index) => (
28 <div
29 key={index}
30 ref={(el) => (refs.current[index] = el)}
31 style={{
32 position: "absolute",
33 top: `${randomTop()}px`,
34 left: `${randomLeft()}px`,
35 }}
36 >
37 <Image
38 publicId={`${avatar.public_id}.png`}
39 cloudName={cloudName}
40 upload_preset={uploadPreset}
41 secure={true}
42 >
43 <Transformation width={50} height={50} gravity="face" crop="thumb" />
44 <Transformation radius="max" />
45 <Transformation effect="trim" />
46 </Image>
47 </div>
48 ));
49 };
50 export default FloatingAvatars;

This component takes a list of Cloudinary image resources, renders them in random positions, and applies the specified animation. The Image and Transformation components from the Cloudinary-react package are used to dynamically generate a 50px by 50px rounded image focusing on the face. The image is also automatically converted to png format as specified, allowing the trimmed section's background to be transparent (instead of white).

The animation function targets the rendered images and animates them along a random course. This is done by generating random values for the translateX and translateY properties of the anime function.

Putting It All Together

Update your src/App.js with the following:

1import "./App.css";
2 import { Button, Card, Checkbox, Col, Form, Input, message } from "antd";
3 import AvatarSelector from "./components/AvatarSelector";
4 import { useEffect, useState } from "react";
5 import FloatingAvatars from "./components/FloatingAvatar";
6 import { getAvatarResources, uploadFile } from "./utils/api";
7 function App() {
8 const [avatars, setAvatars] = useState([]);
9 const [uploadedFile, setUploadedFile] = useState(null);
10 const [isUploading, setIsUploading] = useState(false);
11 useEffect(() => {
12 getAvatarResources({ successCallback: setAvatars });
13 }, []);
14 const onFinish = (values) => {
15 setIsUploading(true);
16 const showUploadedAvatar = values.showAvatar || false;
17 uploadFile({
18 file: uploadedFile,
19 successCallback: (data) => {
20 setIsUploading(false);
21 if (showUploadedAvatar) {
22 setAvatars((oldAvatars) => [...oldAvatars, data]);
23 }
24 message.success(`Welcome ${values.username}!!!`);
25 },
26 addTag: showUploadedAvatar,
27 });
28 };
29 const onFailedSubmission = (errorInfo) => {
30 console.log("Failed:", errorInfo);
31 };
32 return (
33 <div className="App" style={{ margin: "5%" }}>
34 <Card style={{ margin: "auto", width: "50%" }}>
35 <Form
36 onFinish={onFinish}
37 onFinishFailed={onFailedSubmission}
38 autoComplete="off"
39 >
40 <Form.Item
41 label="Username"
42 name="username"
43 rules={[
44 {
45 required: true,
46 message: "Please input your username",
47 },
48 ]}
49 >
50 <Input />
51 </Form.Item>
52 <Col span={16} offset={9}>
53 <AvatarSelector setUploadedFile={setUploadedFile} />
54 </Col>
55 <Form.Item
56 name="showAvatar"
57 valuePropName="checked"
58 wrapperCol={{ offset: 9, span: 16 }}
59 >
60 <Checkbox>Show my avatar</Checkbox>
61 </Form.Item>
62 <Form.Item wrapperCol={{ offset: 10, span: 16 }}>
63 <Button type="primary" htmlType="submit" loading={isUploading}>
64 Submit
65 </Button>
66 </Form.Item>
67 </Form>
68 </Card>
69 <FloatingAvatars avatars={avatars} />
70 </div>
71 );
72 }
73 export default App;

In this component, we create three state variables:

  • avatars holds the list of Cloudinary resources to be rendered by the FloatingAvatars component.
  • uploadedFile holds the file uploaded using the AvatarSelector component.
  • isUploading is used to show a loading animation on the AvatarSelector and submit button when the user completes the form.

We are calling the getAvatarResources function we declared in api.js in a useEffect hook and providing the setAvatars function as a callback if the API call is successful. This way, the avatars array is populated with the list of Cloudinary resources.

We also render a form with a single field for the username, a checkbox with which the user can indicate whether or not they would like their avatar to be displayed. The AvatarComponent is also rendered to allow the user to select an avatar. Finally, we rendered the FloatingAvatars component, passing in the necessary props.

The onFinish function is called when the Submit button is clicked. This function handles the upload of the selected file using the uploadFile function we declared in the api.js file. On a successful upload, if the user selected the Show my avatar checkbox, the returned resource is added to the array of Cloudinary resources, and the newly uploaded image is displayed on the screen.

Using the following command, start your application on http://localhost:3000/.

1npm run dev

The final result will look like the gif shown below.

Find the complete project here on GitHub.

Conclusion

In this article, we looked at how to animate floating avatars using the anime.js library. Additionally, we looked at how to use Cloudinary to handle the upload and storage of media, the dynamic transformation, and the rendering of images in a developer-friendly manner. Finally, using antd, we generated a clean and intuitive user interface with minimal code and styling.

Resources you may find helpful:

Ifeoma Imoh

Software Developer

Ifeoma is a software developer and technical content creator in love with all things JavaScript.