In this tutorial, we’ll explore how to use React Three Fiber to generate an attractive bulging effect in text.
For the past few weeks I’ve been experimenting with combining 3D and 2D to create some fascinating effects. Today I’ll show you how to recreate this bulge effect on text.
To streamline the process and maintain a structured approach to combining HTML and 3D, we use React Three Fiber.
Let’s dive in!
setting
First, let’s set up a 3D scene by creating:
- Plane (where the text bulge effect appears).
- HTML text element.
drei allows you to use HTML components to insert HTML elements directly within Scene components. This is useful in this case because we need to access the HTML within the 3D scene.
It’s also important to surround the title within a single div that spans the width and height of the entire viewport.Similarly for airplanes, for R3F useThree()
Hooks make it easy to get the viewport size.
For now, let’s set the plain opacity to 0 to see the HTML elements.
function Scene()
const viewport = useThree();
return (
<>
<Html zIndexRange=[-1, -10] prepend fullscreen>
<div className="dom-element">
WHEN <br />
WILL <br />
WE <br />
MEET ?<br />
</div>
</Html>
<mesh>
<planeGeometry args=[viewport.width, viewport.height, 254, 254] />
<meshBasicMaterial transparent opacity=hire graphic designer />
</mesh>
</>
);
HTML to texture conversion
Now, the main trick of this effect is to convert the div into a texture that will be applied to the plane. for that, html2 canvas Generate an image from a DOM element using a library and convert it to a texture.
To streamline this process for future projects, let’s create a custom hook named. useDomToCanvas
.
const useDomToCanvas = (domEl) =>
const [texture, setTexture] = useState();
useEffect(() =>
if (!domEl) return;
const convertDomToCanvas = async () =>
const canvas = await html2canvas(domEl, backgroundColor: null );
setTexture(new THREE.CanvasTexture(canvas));
;
convertDomToCanvas();
, [domEl]);
return texture;
;
You could also beef up your hooks to handle resizing, since the div may remain behind the canvas. Just call the function when the window is resized. Incorporate debouncing to prevent excessive draw calls.
const debouncedResize = debounce(() =>
convertDomToCanvas();
, 100);
window.addEventListener("resize", debouncedResize);
Implementation of bulge effect
Now, to achieve the bulge effect, use a shader program to access the vertices of the plane. Shader programming may seem difficult, but don’t worry. In this case, it’s a simple effect. I’ll break it down into three small steps so you can easily understand what’s going on.
Also see the Lewis Lepton YouTube series for an overview of shaders.
First, let’s use shaderMaterial
Create a fragment shader and a vertex shader using it as a plane material.
// Scene.jsx
...
<shaderMaterial
ref=materialRef
uniforms=uniforms
vertexShader=vertexShader
fragmentShader=fragmentShader
/>
step 1 : First, the idea is to draw a circle on a plane. To achieve this, we utilize UV coordinates and GLSL distance functions. For clarity, let’s encapsulate the code into a function.
// fragment.glsl
...
float circle(vec2 uv, vec2 circlePosition, float radius)
float dist = distance(circlePosition, uv);
return 1. - smoothstep(0.0, radius, dist);
void main()
float circleShape = circle(vUv, vec2(0.5), 0.5);
gl_FragColor = vec4(vec3(circleShape), 1.);
Step 2: Then dynamically adjust the origin position of the circle based on mouse movement. R3F provides easy access to normalized mouse position using useFrame(). Observe the movement of the circle by passing the mouse position uniformly to the fragment shader.
// Scene.jsx
...
useFrame((state, delta) =>
const mouse = state.mouse;
materialRef.current.uniforms.uMouse.value = mouse;
);
// fragment.glsl
...
void main()
vec2 mousePositions = uMouse * 0.5 + 0.5;
float circleShape = circle(vUv, mousePositions, 0.5);
gl_FragColor = vec4(vec3(circleShape), 1.);
Step 3: Now, just call the circle function in your vertex shader and adjust the Z position based on the circle. And…voila! There is a bulge effect!
(Also, don’t forget to replace the texture in the fragment shader.)
// vertex.glsl
void main()
vec3 newPosition = position;
// Elevation
vec2 mousePositions = uMouse * 0.5 + 0.5;
float circleShape = circle(uv, mousePositions, 0.2);
float intensity = 0.7;
newPosition.z += circleShape * intensity;
gl_Position = projectionMatrix * modelViewMatrix * vec4(newPosition, 1.0);
// fragment.glsl
uniform sampler2D uTexture;
varying vec2 vUv;
void main()
vec4 finalTexture = texture2D(uTexture, vUv);
gl_FragColor = vec4(finalTexture);
add lighting
Try incorporating lighting effects to enhance the three-dimensional effect. Coding custom lighting effects within a fragment shader can be complex, but you can leverage existing libraries such as: custom shader material. Custom shader materials seamlessly integrate standard materials and point lights for stunning shading effects.
// Scene.jsx
<CustomShaderMaterial
ref=materialRef
baseMaterial=THREE.MeshStandardMaterial
vertexShader=vertexShader
fragmentShader=fragmentShader
uniforms=uniforms
flatShading
/>
<pointLight position=[2, 4, 6] intensity=30 distance=12 decay=freelance designers />
Congratulations! The effect has been successfully implemented.
I’ve included a GUI in the repository so you can manipulate the position and light color. I’d love to see your work and how you build these demos.Please feel free to share your experiments with me twitter!
Moving the web forward: Addressing most of the limitations of web animation with one tool