Implementing a 3D Floating Text Effect with React and Three.js
This article demonstrates how to recreate the Three.js Journey floating 3D text effect using a React and Three.js stack, covering resource imports, DOM structure, state management, CSS grid background, font loading, geometry creation, mouse interaction, fullscreen handling, post‑processing effects, and responsive scaling.
The tutorial explains how to build a 3D floating text demo originally shown in the Three.js Journey course, but using a React + Three.js stack. It lists the required knowledge areas such as CSS grid background, MeshNormalMaterial, FontLoader, TextGeometry, various BufferGeometries, post‑processing passes, and fullscreen APIs.
Resource import – The necessary modules are imported with ES6 syntax:
import * as THREE from "three";
import { FontLoader } from "three/examples/jsm/loaders/FontLoader";
import { TextGeometry } from "three/examples/jsm/geometries/TextGeometry";
import { EffectComposer } from "three/examples/jsm/postprocessing/EffectComposer.js";
import { RenderPass } from "three/examples/jsm/postprocessing/RenderPass.js";
import { GlitchPass } from "three/examples/jsm/postprocessing/GlitchPass.js";DOM structure – A simple container with an element #canvas for rendering, an .color_pick input for background colour, and a .pass_button to toggle the glitch effect:
<div className='floating_page' style={{backgroundColor:this.state.backgroundColor}}>
<div id="canvas"></div>
<input className='color_pick' type="color" onChange={this.handleInputChange} value={this.state.backgroundColor} />
<button className='pass_button' onClick={this.handleRenderChange}>特效<span className='highlight'>{this.state.renderGlithPass ? '开' : '关'}</span></button>
</div>State handling – The component stores backgroundColor and renderGlithPass in its state, disabling the glitch effect on iOS Safari to avoid rendering artifacts.
state = {
backgroundColor: '#164CCA',
renderGlithPass: !(window.navigator.userAgent.toLowerCase().indexOf('mobile') > 0)
}CSS grid background – A pure‑CSS linear‑gradient creates a subtle grid pattern:
background-image: linear-gradient(rgba(3,192,60,.3) 1px, transparent 1px),
linear-gradient(90deg, rgba(3,192,60,.3) 1px, transparent 1px);
background-size: 1em 1em;Scene initialization – The renderer is created with alpha:true and setClearAlpha(0) for a transparent background, followed by camera and scene setup.
canvas = document.getElementById('canvas');
renderer = new THREE.WebGLRenderer({ antialias:true, alpha:true });
renderer.setPixelRatio(Math.min(2, window.devicePixelRatio));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearAlpha(0);
canvas.appendChild(renderer.domElement);
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(70, window.innerWidth/window.innerHeight, .1, 10000);
camera.position.set(-2*10000, 0, 780);Material – All meshes share a single MeshNormalMaterial instance for colourful normal‑based shading.
const material = new THREE.MeshNormalMaterial();Font loading and text geometry – FontLoader loads a typeface JSON, then TextGeometry creates the 3D text mesh.
const loader = new FontLoader();
loader.load('./fonts/helvetiker_regular.typeface.json', font => {
textMesh.geometry = new TextGeometry('@dragonir\nfantastic\nthree.js\nart work', {
font: font,
size: 100,
height: 40,
curveSegments: 12,
bevelEnabled: true,
bevelThickness: 30,
bevelSize: 8,
bevelOffset: 1,
bevelSegments: 12
});
textMesh.material = material;
scene.add(textMesh);
});Additional geometries – Torus, Cone, and Octahedron buffer geometries are generated in bulk using a helper that randomises position, rotation and disables automatic matrix updates for performance.
generateRandomMesh = (geometry, material, count) => {
for (let i = 0; i < count; i++) {
let mesh = new THREE.Mesh(geometry, material);
// random position & rotation
mesh.position.x = Math.random()*distDouble - dist;
mesh.position.y = Math.random()*distDouble - dist;
mesh.position.z = Math.random()*distDouble - dist;
mesh.rotation.x = Math.random()*2*Math.PI;
mesh.rotation.y = Math.random()*2*Math.PI;
mesh.rotation.z = Math.random()*2*Math.PI;
mesh.matrixAutoUpdate = false;
mesh.updateMatrix();
group.add(mesh);
}
};
const octahedronGeometry = new THREE.OctahedronBufferGeometry(80);
generateRandomMesh(octahedronGeometry, material, 100);
const torusGeometry = new THREE.TorusBufferGeometry(40,25,16,40);
generateRandomMesh(torusGeometry, material, 200);
const coneGeometry = new THREE.ConeBufferGeometry(40,80,80);
generateRandomMesh(coneGeometry, material, 100);
scene.add(group);Mouse and touch interaction – Normalised mouse coordinates are computed on mousemove and touchmove events and stored in mouseX and mouseY for camera animation.
const mouseFX = {
windowHalfX: window.innerWidth/2,
windowHalfY: window.innerHeight/2,
coordinates: (coordX, coordY) => {
mouseX = (coordX - mouseFX.windowHalfX) * 5;
mouseY = (coordY - mouseFX.windowHalfY) * 5;
},
onMouseMove: e => mouseFX.coordinates(e.clientX, e.clientY),
onTouchMove: e => mouseFX.coordinates(e.changedTouches[0].clientX, e.changedTouches[0].clientY)
};
document.addEventListener('mousemove', mouseFX.onMouseMove, false);
document.addEventListener('touchmove', mouseFX.onTouchMove, false);Background colour switch – An input[type='color'] updates backgroundColor in state.
handleInputChange = e => {
this.setState({ backgroundColor: e.target.value });
};Post‑processing – An EffectComposer combines a RenderPass and a GlitchPass . The glitch effect can be toggled via a button.
composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene, camera));
glitchPass = new GlitchPass();
composer.addPass(glitchPass);
handleRenderChange = () => {
this.setState({ renderGlithPass: !this.state.renderGlithPass });
};Animation loop – requestAnimationFrame updates camera position, rotates the group and text mesh, renders the scene, and then renders the composer.
function animate() {
requestAnimationFrame(animate);
camera.position.x += (mouseX - camera.position.x) * 0.05;
camera.position.y += (mouseY * -1 - camera.position.y) * 0.05;
camera.lookAt(scene.position);
const t = Date.now() * 0.001;
const rx = Math.sin(t*0.7) * 0.5;
const ry = Math.sin(t*0.3) * 0.5;
const rz = Math.sin(t*0.2) * 0.5;
group.rotation.set(rx, ry, rz);
textMesh.rotation.set(rx, ry, rx);
renderer.render(scene, camera);
composer.render();
}
animate();Responsive resizing – The camera aspect, renderer size and composer size are updated on window.resize .
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
composer.setSize(window.innerWidth, window.innerHeight);
}, false);Fullscreen toggle – Double‑clicking the page calls Element.requestFullscreen or Document.exitFullscreen depending on the current state.
window.addEventListener('dblclick', () => {
let fullscreenElement = document.fullscreenElement || document.webkitFullscreenElement;
if (!fullscreenElement) {
if (canvas.requestFullscreen) canvas.requestFullscreen();
else if (canvas.webkitRequestFullscreen) canvas.webkitRequestFullscreen();
} else {
if (document.exitFullscreen) document.exitFullscreen();
else if (document.webkitExitFullscreen) document.webkitExitFullscreen();
}
});The article concludes with a summary of the key concepts covered, including CSS grid backgrounds, MeshNormalMaterial, FontLoader, TextGeometry, various BufferGeometries, Three.js post‑processing, GlitchPass, and fullscreen APIs, and points readers to related Three.js tutorials.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.