perspective: dónde está el ojo que mira
La diferencia entre la propiedad perspective y la función perspective(), qué significa su valor en píxeles, y cómo perspective-origin coloca el punto de fuga.
Sin perspectiva, una rotación en 3D es una proyección ortográfica: el elemento gira, pero los bordes que se alejan no se estrechan y el resultado parece un cartón que se dobla, no un objeto en el espacio. perspective introduce la división por profundidad que convierte esa proyección en algo que el ojo lee como volumen. Es una sola propiedad con un solo número, pero ese número es una distancia focal y colocarlo mal es la razón por la que la mayoría de los efectos 3D en CSS parecen efectos 3D en CSS.
- Distinguir la propiedad
perspectivede la funciónperspective()y saber cuándo usar cada una. - Interpretar el valor de perspectiva como una distancia entre el ojo y el plano z igual a cero.
- Colocar el punto de fuga con
perspective-origin. - Diagnosticar por qué un grupo de elementos 3D no comparte una escena coherente.
Un número que es una distancia
perspective: 800px significa: el observador está a 800 píxeles del plano de la pantalla. Cuanto menor sea el valor, más cerca está el ojo y más pronunciada es la deformación; cuanto mayor, más plana. Un valor muy grande tiende a la proyección ortográfica y un valor pequeño produce el efecto de gran angular.
La matemática es la de una cámara estenopeica. Un punto a profundidad z se proyecta escalado por un factor d / (d - z), donde d es el valor de perspective. Un punto con z igual a cero no se altera. Un punto que se acerca al observador —z positivo— crece. Un punto que se aleja se encoge. Y cuando z alcanza exactamente d, el punto está en el ojo: la especificación corta ahí, y por eso un elemento con translateZ() igual o mayor que la perspectiva simplemente desaparece.
.escena { perspective: 800px; }
.escena .cara { rotate: y 45deg; } /* ahora sí se estrecha por el lado que se aleja */
Elegir el valor no es estético sin más. Un perspective pequeño en una caja grande produce una distorsión que el ojo lee como error, porque implica que estás mirando un objeto de 600 píxeles desde 20 centímetros. La heurística que funciona es tomar como distancia focal algo del orden del tamaño del contenedor: para una tarjeta de 400 píxeles, valores entre 800 y 1500 dan un volumen creíble. Para efectos deliberadamente agresivos, por debajo de 400.
La propiedad y la función no son lo mismo
Existen dos formas de introducir perspectiva y la diferencia es estructural.
La propiedad perspective se declara en el padre y afecta a todos sus hijos, que comparten un mismo punto de vista. Es una cámara para una escena.
La función perspective() se escribe dentro de transform, en el propio elemento, y afecta solo a él. Cada elemento que la use tiene su propia cámara, colocada en su propio centro.
/* una cámara para toda la escena: los tres hijos comparten punto de fuga */
.escena { perspective: 1000px; }
.escena > * { rotate: y 30deg; }
/* tres cámaras independientes: cada hijo se proyecta desde su propio centro */
.escena > * { transform: perspective(1000px) rotateY(30deg); }
Visualmente la diferencia es evidente en cuanto hay más de un elemento. Con la propiedad, los hijos situados a la izquierda del punto de fuga se ven desde su derecha y los de la derecha desde su izquierda: es una escena. Con la función, todos se ven exactamente igual, como si cada uno estuviera centrado en la pantalla: es un catálogo de miniaturas idénticas.
La regla práctica: si el efecto es de un solo elemento aislado —una tarjeta que se voltea— la función basta y evita una propiedad más en el padre. Si hay varios elementos que deben parecer parte del mismo espacio, la propiedad en el ancestro común es la única forma de conseguirlo.
Un detalle de composición que cuesta descubrir: dentro de una lista de transform, perspective() tiene que ir primero. Si escribes rotateY(45deg) perspective(800px), la perspectiva se aplica después de la rotación y no hace nada útil, porque para entonces el punto ya está proyectado.
flowchart TB
A[Quieres un efecto tridimensional] --> B{Cuantos elementos participan}
B -->|Uno solo| C[Funcion perspective dentro de transform]
C --> D[Camara centrada en ese elemento]
B -->|Varios que comparten espacio| E[Propiedad perspective en el ancestro comun]
E --> F[Un unico punto de fuga para todos]
F --> G[perspective-origin mueve ese punto]
C --> H[Recuerda ponerla la primera de la lista]
style A fill:#89b4fa,color:#11111b
style D fill:#f9e2af,color:#11111b
style F fill:#a6e3a1,color:#11111b
style G fill:#cba6f7,color:#11111b
style H fill:#f9e2af,color:#11111bperspective-origin y el punto de fuga
perspective-origin coloca el ojo en el plano de la pantalla. Su valor inicial es 50% 50%: el observador está justo enfrente del centro del elemento que declara la perspectiva. Acepta la misma sintaxis de posición que transform-origin, con dos valores como máximo, porque la profundidad ya la da perspective.
.escena {
perspective: 1000px;
perspective-origin: 50% 0%; /* miramos la escena desde arriba */
}
Moverlo hacia arriba hace que veas las caras superiores de los objetos; moverlo a un lado revela el costado. Es la forma de dar a una escena un punto de vista que no sea el frontal aburrido, y es más barato que rotar todos los hijos.
Un uso menos obvio: ligar perspective-origin a la posición del puntero produce el efecto de paralaje que responde al ratón, con dos custom properties y sin tocar la geometría de ningún hijo.
.escena { perspective: 900px; perspective-origin: var(--px, 50%) var(--py, 50%); }
const escena = document.querySelector('.escena');
escena.addEventListener('pointermove', (e) => {
const r = escena.getBoundingClientRect();
escena.style.setProperty('--px', ((e.clientX - r.left) / r.width * 100) + '%');
escena.style.setProperty('--py', ((e.clientY - r.top) / r.height * 100) + '%');
});
El malentendido que más tiempo cuesta desenredar es creer que la perspectiva es una propiedad de escena que se hereda hacia abajo. No lo es. perspective afecta únicamente a los hijos directos del elemento que la declara, y solo mientras esos hijos formen parte del mismo contexto tridimensional. En cuanto un hijo intermedio aplana su subárbol —cosa que ocurre por defecto, porque el valor inicial de transform-style es flat— los nietos se proyectan sobre el plano del hijo y la cámara del abuelo deja de tener ningún efecto sobre ellos. El síntoma es característico: mueves perspective de sitio en el árbol y el efecto aparece o desaparece sin que cambie nada más. Y hay una segunda mitad de la trampa: cuando un descendiente sí propaga el 3D con transform-style: preserve-3d, la perspectiva del abuelo se aplica a la posición del hijo, que a su vez ya ha desplazado a sus propios hijos, con lo que la deformación se compone y el resultado se exagera de forma no lineal. Un carrusel 3D bien hecho tiene exactamente un elemento con perspectiva y un solo nivel de propagación; en cuanto hay dos cámaras encadenadas, la escena deja de ser predecible.
- Aplica
perspectivede 200, 800 y 3000 píxeles a la misma escena con un hijo rotado 45 grados en y, y compara. - Coloca cuatro tarjetas en fila con la propiedad
perspectiveen el padre y comprueba que las de los extremos se ven desde ángulos distintos. - Repite con la función
perspective()en cada tarjeta y observa que ahora las cuatro son idénticas. - Escribe
transform: rotateY(45deg) perspective(600px)y explica por qué no se ve volumen. - Ata
perspective-original puntero con el código de arriba y comprueba que ningún hijo necesita cambiar.