Exploring a 3D Text Distortion Effect With React Three Fiber

A quick tutorial on how to create a beautiful distorted text ring effect in React Three Fiber.

Today we will take a look at how to create a beautiful animation in React Three Fiber. The idea is to show a 3D scene in the background that looks like a distorted glassy view of a text ring, constantly rotating around its own axis. It’s a beautiful design element that could be used as an animated background. The animation is not very complex, perfect for those interested in getting started with 3D animation and more.

This is a React specific tutorial so be sure to have an initial React app set up so that we can start.

First we will need to install React Three Fiber:

npm install three @react-three/fiber

After that we will prepare the canvas for rendering our 3D scene.

We first import the canvas from @react-three/fiber, style the main element to be 100% viewport width and height and use the imported Canvas component. We’ll also style it to have a black background.

import { Canvas } from "@react-three/fiber";

export default function App() {
  return (
    <main className="h-screen w-screen">
      <Canvas className="bg-black">

      </Canvas>
    </main>
  );
}

The next step is to prepare our 3D component that will be rendered inside the Canvas which we will call ring because we’ll be wrapping the text in the form of a ring. The way we can imagine this is as if we took our text and wrapped it around a bottle which would be forming this so called “TextRing”.

interface Props {
  radius: number;
  height: number;
  segments: number;
}

export default function Ring({ radius, height, segments }: Props) {
  return (
    <mesh>
      <cylinderGeometry args={[radius, radius, height, segments]} />
      <meshBasicMaterial />
    </mesh>
  );
}

After you have written the component the only remaining thing left to do is add it to the main scene in our Canvas:

import { Canvas } from "@react-three/fiber";
import Ring from "./ring";

export default function Scene() {
  return (
    <Canvas className="bg-black">
      <Ring radius={2} height={4} segments={32} />
    </Canvas>
  );
}

After this you should be seeing this on your screen:

In the next step we will have to include our text in the scene and in order to to that we will install another package called @react-three/drei that has a lots of awesome things. In this case we use the Text component.

npm install @react-three/drei

After we installed our package we have to adjust our component in order to show our text and wrap it around our cylinder:

import { Text } from "@react-three/drei";

interface Props {
  text: string;
  radius: number;
  height: number;
  segments: number;
}

export default function Ring({ text, radius, height, segments }: Props) {
  // Calculate positions for text
  const textPositions: { x: number; z: number }[] = [];
  const angleStep = (2 * Math.PI) / text.length;
  for (let i = 0; i < text.length; i++) {
    const angle = i * angleStep;
    const x = radius * Math.cos(angle);
    const z = radius * Math.sin(angle);
    textPositions.push({ x, z });
  }

  return (
    <group>
      <mesh>
        <cylinderGeometry args={[radius, radius, height, segments]} />
        <meshBasicMaterial />
      </mesh>
      {text.split("").map((char: string, index: number) => (
        <Text
          key={index}
          position={[textPositions[index].x, 0, textPositions[index].z]}
          rotation={[0, -angleStep * index + Math.PI / 2, 0]}
          fontSize={0.3}
          lineHeight={1}
          letterSpacing={0.02}
          color="white"
          textAlign="center"
        >
          {char}
        </Text>
      ))}
    </group>
  );
}

So, there is a lot of things going on here, but to explain it in basic terminology, we take each character and increasingly place it around in a circular position. We use the radius that we used for the cylinder to position all the letters.

We pass any text into the Ring component after this, and then it will be neatly wrapper around the cylinder like this:

export default function Scene() {
  return (
    <Canvas className="bg-black">
      <Ring
        radius={2}
        height={4}
        segments={32}
        text="X X X X X X X X X X X X X X X X X X X X X X X X X X X X "
      />
    </Canvas>
  );
}

To start animating things, we will simply take the group and rotate it along its x,y,z axis. For that we will use the useFrame hook from R3F and we will access the element via the useRef that we will link to the group element.

import { useRef } from "react";
import { useFrame } from "@react-three/fiber";
import { Text } from "@react-three/drei";

interface Props {
  text: string;
  radius: number;
  height: number;
  segments: number;
}

export default function Ring({ text, radius, height, segments }: Props) {
  const ref = useRef<any>();

  // Rotate the text
  useFrame(() => {
    ref.current.rotation.y += 0.01;
    ref.current.rotation.x += 0.01;
    ref.current.rotation.z += 0.01;
  });

  // Calculate positions for text
  const textPositions: { x: number; z: number }[] = [];
  const angleStep = (2 * Math.PI) / text.length;
  for (let i = 0; i < text.length; i++) {
    const angle = i * angleStep;
    const x = radius * Math.cos(angle);
    const z = radius * Math.sin(angle);
    textPositions.push({ x, z });
  }
  return (
    <group ref={ref}>
      <mesh>
        <cylinderGeometry args={[radius, radius, height, segments]} />
        <meshBasicMaterial />
      </mesh>
      {text.split("").map((char: string, index: number) => (
        <Text
          key={index}
          position={[textPositions[index].x, 0, textPositions[index].z]}
          rotation={[0, -angleStep * index + Math.PI / 2, 0]}
          fontSize={0.3}
          lineHeight={1}
          letterSpacing={0.02}
          color="white"
          textAlign="center"
        >
          {char}
        </Text>
      ))}
    </group>
  );
}

The only remaining step left to do is to slightly distort the element a bit and make it glass-like looking. Thankfully, drei has a material that helps us with that called MeshTransmissionMaterial:

import { useRef } from "react";
import { useFrame } from "@react-three/fiber";
import { MeshTransmissionMaterial, Text } from "@react-three/drei";

interface Props {
  text: string;
  radius: number;
  height: number;
  segments: number;
}

export default function Ring({ text, radius, height, segments }: Props) {
  const ref = useRef<any>();

  // Rotate the text
  useFrame(() => {
    ref.current.rotation.y += 0.01;
    ref.current.rotation.x += 0.01;
    ref.current.rotation.z += 0.01;
  });

  // Calculate positions for text
  const textPositions: { x: number; z: number }[] = [];
  const angleStep = (2 * Math.PI) / text.length;
  for (let i = 0; i < text.length; i++) {
    const angle = i * angleStep;
    const x = radius * Math.cos(angle);
    const z = radius * Math.sin(angle);
    textPositions.push({ x, z });
  }

  return (
    <group ref={ref}>
      <mesh>
        <cylinderGeometry args={[radius, radius, height, segments]} />
        <MeshTransmissionMaterial
          backside
          backsideThickness={5}
          thickness={2}
        />
      </mesh>
      {text.split("").map((char: string, index: number) => (
        <Text
          key={index}
          position={[textPositions[index].x, 0, textPositions[index].z]}
          rotation={[0, -angleStep * index + Math.PI / 2, 0]}
          fontSize={0.3}
          lineHeight={1}
          letterSpacing={0.02}
          color="white"
          textAlign="center"
        >
          {char}
        </Text>
      ))}
    </group>
  );
}

And with that we have our beautiful rotating glassy text ring:

I hope you enjoyed this tutorial!

I’m open for collaborations and projects, so drop me a line if you’d like to work with me!

Tagged with:

Stay in the loop: Get your dose of frontend twice a week

👾 Hey! Looking for the latest in frontend? Twice a week, we'll deliver the freshest frontend news, website inspo, cool code demos, videos and UI animations right to your inbox.

Zero fluff, all quality, to make your Mondays and Thursdays more creative!