Integración de escenas 3D con el DOM · click · scroll · mousemove · resize · Raycasting
Un evento es cualquier cosa que ocurre en el navegador:
un click, mover el mouse, hacer scroll, redimensionar la ventana, presionar
una tecla... JavaScript puede "escuchar" esos eventos con
addEventListener y ejecutar una función de respuesta.
// ── SINTAXIS BÁSICA ───────────────────────────────────────────
// elemento.addEventListener(tipo, callback, opciones?)
const boton = document.querySelector('#mi-boton');
boton.addEventListener('click', function(event) {
console.log('¡Clic!', event);
});
// Con arrow function (más conciso):
boton.addEventListener('click', (e) => {
e.preventDefault(); // cancela el comportamiento por defecto
e.stopPropagation(); // detiene la propagación hacia padres
});
// ── REMOVER UN LISTENER ──────────────────────────────────────
// IMPORTANTE: para poder quitar un listener, la función
// debe ser referenciada (no inline anónima).
const manejador = (e) => console.log(e);
boton.addEventListener('click', manejador);
boton.removeEventListener('click', manejador); // ← quita el listener
| Evento | Cuándo se dispara | Propiedades útiles del Event |
|---|---|---|
click | Al hacer click con el mouse | clientX, clientY, target, button |
mousemove | Al mover el cursor | clientX, clientY, movementX, movementY |
scroll | Al hacer scroll en la ventana o elemento | window.scrollY, scrollTop |
resize | Al cambiar el tamaño de la ventana | window.innerWidth, innerHeight |
keydown | Al presionar una tecla | key, code, ctrlKey, shiftKey |
keyup | Al soltar una tecla | key, code |
input | Al cambiar el valor de un input | target.value |
DOMContentLoaded | Al terminar de cargar el HTML | — (sin datos relevantes) |
clickmousemovescroll// ── MOUSEMOVE: coordenadas normalizadas ───────────────────────
// Para Three.js, necesitamos valores entre -1 y +1 (NDC)
const mouse = { x: 0, y: 0 };
window.addEventListener('mousemove', (e) => {
// normalizar: -1 (izquierda/abajo) a +1 (derecha/arriba)
mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
});
// ── SCROLL: porcentaje de la página ──────────────────────────
let scrollY = 0;
window.addEventListener('scroll', () => {
scrollY = window.scrollY;
// Porcentaje total de scroll (0 = inicio, 1 = final)
const maxScroll = document.body.scrollHeight - window.innerHeight;
const progreso = scrollY / maxScroll; // 0.0 a 1.0
});
// ── RESIZE: actualizar Three.js al cambiar la ventana ────────
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
<div>
En la semana 08 el canvas ocupaba toda la pantalla. Esta semana lo integramos
como cualquier otro elemento HTML: dentro de un <div> con
dimensiones definidas, conviviendo con texto y otros elementos.
// ── THREE.JS EN UN DIV ESPECÍFICO ────────────────────────────
const contenedor = document.getElementById('mi-contenedor-3d');
// Obtener dimensiones del contenedor (no de la ventana)
const W = contenedor.clientWidth;
const H = contenedor.clientHeight;
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(60, W / H, 0.1, 100);
camera.position.set(0, 0, 4);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(W, H); // tamaño del contenedor, NO de window
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
// Añadir el canvas generado al div contenedor
contenedor.appendChild(renderer.domElement); // ← key line
// CSS necesario en el div contenedor:
// #mi-contenedor-3d { width: 100%; height: 400px; position: relative; }
Un patrón muy usado en sitios modernos es poner el canvas 3D como fondo detrás del contenido HTML. Así el texto y los botones flotan sobre la escena 3D.
/* CSS: canvas fijo detrás de todo el contenido */
canvas {
position: fixed; /* fijo en el viewport */
top: 0; left: 0;
width: 100%; height: 100%;
z-index: -1; /* detrás de todo el HTML */
pointer-events: none; /* los clicks pasan al HTML */
}
Este texto está en el DOM normal. El canvas está en position: absolute detrás.
pointer-events: none en el canvas es esencial cuando el canvas está como fondo: permite que los clicks "atraviesen" el canvas y lleguen al HTML que está debajo en el z-index. Sin esto, los links y botones dejarían de funcionar.
El evento scroll nos da la posición vertical de la página.
Podemos mapear ese valor para mover la cámara, rotar objetos o cambiar
cualquier propiedad de la escena 3D.
// ── PATRÓN: scroll controla la cámara ────────────────────────
let scrollY = 0;
let currentSection = 0;
window.addEventListener('scroll', () => {
scrollY = window.scrollY;
// Calcular sección actual (asumiendo secciones de 100vh)
currentSection = Math.round(scrollY / window.innerHeight);
});
// En el loop de animación — movimiento suave con lerp
let cameraY = 0; // posición actual suavizada
function animate() {
requestAnimationFrame(animate);
// lerp = interpolación lineal. Fórmula: actual + (destino - actual) * velocidad
// Hace que la cámara se "arrastre" suavemente hacia la posición destino.
cameraY += (-scrollY * 0.002 - cameraY) * 0.08;
camera.position.y = cameraY;
// Alternativamente, rotar un objeto proporcional al scroll
mesh.rotation.y = scrollY * 0.001;
renderer.render(scene, camera);
}
mousemove → escena 3D
El evento mousemove nos da las coordenadas del cursor. Al
normalizar esas coordenadas (convertir de píxeles a valores
entre -1 y +1), podemos usarlas para inclinar la cámara o mover objetos
en respuesta al movimiento del mouse.
// ── COORDENADAS NORMALIZADAS (NDC) ───────────────────────────
// Three.js usa el sistema NDC: centro = (0,0), esquinas = (±1, ±1)
// Necesitamos convertir píxeles del DOM a ese sistema.
const mouse = new THREE.Vector2(); // x, y normalizados
window.addEventListener('mousemove', (e) => {
mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
// El eje Y se invierte porque DOM: Y crece hacia abajo
// Three.js: Y crece hacia arriba
});
// En el loop: mover la cámara o un objeto con lerp para suavidad
let targetX = 0, targetY = 0;
function animate() {
requestAnimationFrame(animate);
// La cámara "sigue" al mouse con inercia
targetX += (mouse.x * 0.5 - targetX) * 0.05; // lerp X
targetY += (mouse.y * 0.3 - targetY) * 0.05; // lerp Y
camera.position.x = targetX;
camera.position.y = targetY;
camera.lookAt(0, 0, 0); // siempre mirando al centro
renderer.render(scene, camera);
}
Raycasting es la técnica para detectar qué objeto 3D está bajo el cursor del mouse. Three.js proyecta un rayo invisible desde la cámara en la dirección del cursor y detecta con qué objetos intersecta.
// ── RAYCASTER: detectar clicks sobre objetos 3D ───────────────
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
const objetos = [mesh1, mesh2, mesh3]; // los objetos "clicables"
window.addEventListener('click', (e) => {
// 1. Normalizar coordenadas del click
mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
// 2. Lanzar el rayo desde la cámara hacia el punto del click
raycaster.setFromCamera(mouse, camera);
// 3. Calcular intersecciones con los objetos
const intersecciones = raycaster.intersectObjects(objetos);
if (intersecciones.length > 0) {
const objetoCliqueado = intersecciones[0].object; // el más cercano
const punto = intersecciones[0].point; // punto 3D exacto
console.log('Clicaste:', objetoCliqueado.name);
objetoCliqueado.material.color.setHex(0xff6b35); // cambiar color
}
});
// Para hover (mousemove en lugar de click):
window.addEventListener('mousemove', (e) => {
mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const hits = raycaster.intersectObjects(objetos);
document.body.style.cursor = hits.length > 0 ? 'pointer' : 'default';
});
Construye una página web completa donde una escena Three.js sea parte de la interfaz (no toda la pantalla), y que reaccione a al menos dos eventos del usuario: scroll, mousemove, click o teclas.
<div> con dimensiones definidas (no pantalla completa).scroll: la escena reacciona al desplazamiento (rotación, movimiento o cambio de color).mousemove: la cámara o un objeto sigue al cursor con suavizado (lerp).click con Raycaster: al hacer click sobre un objeto 3D ocurre algo visible.resize: la escena se adapta al cambiar el tamaño de ventana.addEventListener💡 Tip: Empieza por la estructura HTML y CSS, luego añade Three.js. Es más fácil integrar una escena 3D en una interfaz ya diseñada que diseñar la interfaz alrededor de una escena 3D ya existente. El lerp es tu mejor amigo para todo movimiento.