3D Airplane Demo with React and Three.js
Codrops Article
This is a cool demo I first saw on Codrops blogs many many years ago, thought it would be fun to implement it as a react nextjs component. The demo features a stylized airplane flying above a rotating sea, surrounded by drifting clouds in a dynamic sky environment.
components/PlaneDemo
├── classes
│ ├── Airplane.js
│ ├── Cloud.js
│ ├── Sea.js
│ └── Sky.js
├── constants
│ └── colors.js
├── index.jsx
└── utils
└── scene.js
Original Demo
The original Codrops demo creates a 3D scene using Three.js with several key components:
- A low-poly airplane that responds to mouse/touch input
- An animated sea with waves
- A sky populated with drifting clouds
- Atmospheric lighting with a hemisphere light for ambient illumination and directional light for shadows
The scene is rendered in a continuous loop that:
- Updates the sea's waves by rotating its mesh
- Moves clouds across the sky
- Adjusts the airplane's position and rotation based on user input
- Animates the propeller by continuously rotating it
React Implementation
While the core Three.js functionality remains the same, I've restructured the demo to work within React's component model:
- Each 3D object (Airplane, Cloud, Sea, Sky) is encapsulated in its own class under
/classes
- Scene setup and animation loop logic is abstracted into
/utils/scene.js
- Colors are maintained as constants in
/constants/colors.js
- The main component in
index.jsx
handles:- Component lifecycle (mounting/unmounting)
- Event listeners for window resize and mouse/touch input
- Canvas rendering and animation frame management
This modular structure makes the code more maintainable and reusable, while still preserving the original demo's visual appeal and interactivity.
Viewport and Resize Handling
The demo needs to be responsive and handle window resizing gracefully. This is managed through a combination of React hooks and Three.js viewport calculations:
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
const viewport = {
width: dimensions.width,
height: dimensions.height,
camera: {
fov: 60,
near: 1,
far: 10000,
position: { x: 0, y: 100, z: 200 }
}
};
// Update dimensions when container size changes
useEffect(() => {
if (!containerRef.current) return;
const updateDimensions = () => {
const container = containerRef.current;
const rect = container.getBoundingClientRect();
setDimensions({
width: rect.width,
height: rect.height
});
};
updateDimensions();
window.addEventListener('resize', updateDimensions);
return () => window.removeEventListener('resize', updateDimensions);
}, []);
// Update renderer and camera when viewport changes
useEffect(() => {
// ...scene setup code...
const handleResize = () => {
renderer.setSize(viewport.width, viewport.height);
camera.aspect = viewport.width / viewport.height;
camera.updateProjectionMatrix();
};
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
if (containerRef.current) {
containerRef.current.removeChild(renderer.domElement);
}
renderer.dispose();
};
}, [viewport]);
Key aspects of the viewport handling:
- Dimension State: Using React's
useState
to track container dimensions. - Viewport Config: A configuration object that combines dimensions with camera settings.
- Resize Observer: Using
getBoundingClientRect()
to accurately measure the container. - Cleanup: Proper cleanup of event listeners and Three.js resources.
This setup ensures the demo remains responsive and properly scaled across different screen sizes and window resizing events.