I have a React project using three.js, and I want to provide a seamless experience for users without page breaks. To achieve this, I avoided using page routing because it would require re-entering VR mode each time a page is switched. Instead, I’ve included three animations in a single file, with animations transitioning automatically based on a timer.
import React, { useEffect, useRef, useState } from 'react';
import * as THREE from 'three';
import { RoomEnvironment } from 'three/examples/jsm/environments/RoomEnvironment';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
import { VRButton } from 'three/examples/jsm/webxr/VRButton';
// Função para configurar a cena, câmera e renderizador
const initializeScene = (container) => {
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0000ff);
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 1.5, 3);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.xr.enabled = true;
container.appendChild(renderer.domElement);
document.body.appendChild(VRButton.createButton(renderer));
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.25;
controls.enableZoom = true;
const environment = new RoomEnvironment();
const pmremGenerator = new THREE.PMREMGenerator(renderer);
scene.environment = pmremGenerator.fromScene(environment).texture;
return { scene, camera, renderer, controls };
};
// Funções auxiliares para animações de página
const createCubePage1 = (scene, renderer, camera) => {
const cubeGeometry1 = new THREE.BoxGeometry();
const textureLoader = new THREE.TextureLoader();
const texture = textureLoader.load('utils/moovz.jpg');
const cubeMaterial1 = new THREE.MeshBasicMaterial({ map: texture });
const cube1 = new THREE.Mesh(cubeGeometry1, cubeMaterial1);
cube1.position.set(0, -5, -5);
scene.add(cube1);
let startTime;
let emergingDuration = 3000;
let floatAmplitude = 0.2;
let isEmergingComplete = false;
const startAnimation = () => {
startTime = Date.now();
const animateCube1 = () => {
const elapsedTime = Date.now() - startTime;
if (elapsedTime < emergingDuration) {
const progress = elapsedTime / emergingDuration;
cube1.position.y = -5 + (7 * progress);
} else {
if (!isEmergingComplete) {
cube1.position.y = 1.9;
isEmergingComplete = true;
}
cube1.position.y = 2 + Math.sin(Date.now() * 0.001) * floatAmplitude;
}
cube1.rotation.x += 0.001;
cube1.rotation.y += 0.001;
renderer.render(scene, camera);
requestAnimationFrame(animateCube1);
};
requestAnimationFrame(animateCube1);
};
setTimeout(startAnimation, 10000);
return 20000; // Tempo de redirecionamento
};
const createModelPage2 = (scene, renderer, camera) => {
const gltfLoader = new GLTFLoader();
gltfLoader.load('utils/marca 3d eixo x.glb', (gltf) => {
const model = gltf.scene;
scene.add(model);
model.position.set(-20, 0, -15);
const targetPosition = new THREE.Vector3(-8, 0, -15);
const duration = 3000;
const startTime = Date.now();
const animateModel = () => {
const elapsedTime = Date.now() - startTime;
const progress = Math.min(elapsedTime / duration, 1);
model.position.lerpVectors(new THREE.Vector3(-20, 0, -15), targetPosition, progress);
renderer.render(scene, camera);
if (progress < 1) {
requestAnimationFrame(animateModel);
}
};
requestAnimationFrame(animateModel);
}, undefined, (error) => {
console.error('Erro ao carregar o modelo GLB:', error);
});
return 10000; // Tempo de redirecionamento
};
const createCubePage3 = (scene, renderer, camera) => {
camera.position.set(0, 1.5, 5);
const cubeGeometry3 = new THREE.BoxGeometry();
const cubeMaterial3 = new THREE.MeshStandardMaterial({ color: 0x0000ff });
const cube3 = new THREE.Mesh(cubeGeometry3, cubeMaterial3);
cube3.position.set(0, 1, -2);
scene.add(cube3);
const animateCube3 = () => {
cube3.rotation.x += 0.01;
cube3.rotation.y += 0.01;
renderer.render(scene, camera);
requestAnimationFrame(animateCube3);
};
requestAnimationFrame(animateCube3);
return null; // Sem redirecionamento
};
// Componente principal
const Page = () => {
const containerRef = useRef(null);
const sceneRef = useRef(null);
const cameraRef = useRef(null);
const rendererRef = useRef(null);
const [currentPage, setCurrentPage] = useState(1);
const redirectTimeoutRef = useRef(null);
useEffect(() => {
if (containerRef.current) {
const { scene, camera, renderer, controls } = initializeScene(containerRef.current);
sceneRef.current = scene;
cameraRef.current = camera;
rendererRef.current = renderer;
const onWindowResize = () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
};
window.addEventListener('resize', onWindowResize);
const animate = () => {
if (renderer.xr.isPresenting) {
camera.position.set(0, 1.5, 3);
controls.enabled = false;
} else {
controls.update();
}
renderer.render(scene, camera);
};
renderer.setAnimationLoop(animate);
return () => {
window.removeEventListener('resize', onWindowResize);
clearTimeout(redirectTimeoutRef.current);
if (renderer.domElement.parentNode) {
renderer.domElement.parentNode.removeChild(renderer.domElement);
}
};
}
}, []);
useEffect(() => {
const scene = sceneRef.current;
const renderer = rendererRef.current;
const camera = cameraRef.current;
if (scene && camera && renderer) {
// Limpa o conteúdo anterior
while (scene.children.length > 0) {
scene.remove(scene.children[0]);
}
let redirectTime;
switch (currentPage) {
case 1:
redirectTime = createCubePage1(scene, renderer, camera);
break;
case 2:
redirectTime = createModelPage2(scene, renderer, camera);
break;
case 3:
redirectTime = createCubePage3(scene, renderer, camera);
break;
default:
break;
}
if (redirectTime) {
redirectTimeoutRef.current = setTimeout(() => {
setCurrentPage((prevPage) => prevPage + 1); // Muda para a próxima página
}, redirectTime);
}
}
}, [currentPage]);
return <div ref={containerRef} style={{ width: '100vw', height: '100vh' }}></div>;
};
export default Page;
I tested this setup using Mozilla Firefox with the WebXR extension, and it worked perfectly. However, when I tested it on a Meta Quest 2, the animation doesn’t work when entering VR mode in the browser.