wandres.dev
TRANSFORMACIONES · El pipeline 2D y 3D

El orden importa: transform es multiplicación de matrices

Por qué translate seguido de rotate no es lo mismo que rotate seguido de translate, qué está multiplicando el motor por debajo, y cómo predecir el resultado sin dibujar.

⏱ 20 min

transform: rotate(45deg) translateX(100px) y transform: translateX(100px) rotate(45deg) producen posiciones distintas. No es un capricho de la implementación ni un bug: la lista de funciones se compone multiplicando matrices, y la multiplicación de matrices no es conmutativa. Entender esto convierte el ensayo y error —cambiar el orden hasta que se ve bien— en una predicción que puedes hacer de cabeza.

🎯 Al terminar esta lección sabrás
  • Traducir una lista de funciones de transform a una composición de matrices.
  • Predecir sin ejecutar el resultado de intercambiar dos funciones de la lista.
  • Interpretar los seis números de matrix() y saber cuándo aparecen en DevTools.
  • Elegir entre pensar en coordenadas locales o globales según el problema.

Qué hace el motor con la lista

Cada función de transformación es una matriz. En 2D son matrices de 3 por 3 en coordenadas homogéneas; en 3D, de 4 por 4. Una lista de funciones se convierte en una sola matriz, el producto de todas ellas en el orden en que aparecen escritas, de izquierda a derecha:

transform: A B C;
/* el motor calcula M = A x B x C */

Después, cada punto de la caja del elemento se multiplica por M para obtener su posición final. Esa es toda la mecánica. Lo que hace que el orden importe es que el producto de matrices no conmuta: A x B casi nunca es igual a B x A.

Ejemplo concreto con números pequeños. Un elemento en el origen, con transform-origin en su centro.

.a { transform: translateX(100px) rotate(45deg); }
.b { transform: rotate(45deg) translateX(100px); }

En .a el elemento se mueve 100 píxeles a la derecha y luego gira 45 grados sobre su propio centro, que ya está desplazado. Acaba a 100 píxeles a la derecha, inclinado.

En .b el elemento gira 45 grados y después se traslada 100 píxeles en su eje x, que tras el giro apunta en diagonal hacia abajo y a la derecha. Acaba a unos 70,7 píxeles a la derecha y 70,7 hacia abajo, con la misma inclinación.

La posición final difiere; la orientación final coincide. Ese patrón se repite: cuando mezclas una traslación con cualquier otra cosa, lo que cambia al reordenar es dónde acaba, no cómo de girado o estirado está.

Las dos formas de leer la lista

Hay dos interpretaciones correctas de la misma operación, y cada una es más cómoda para un tipo de problema. Conviene dominar las dos.

De derecha a izquierda, en el sistema de coordenadas global. Lees la última función primero y vas aplicando hacia la izquierda, siempre respecto a los ejes fijos de la pantalla. En rotate(45deg) translateX(100px), primero mueves el objeto 100 píxeles a la derecha en el eje x de la pantalla, y luego rotas todo el conjunto 45 grados alrededor del origen de transformación. El objeto describe un arco.

De izquierda a derecha, en el sistema de coordenadas local. Lees en el orden escrito, pero cada función se aplica sobre los ejes que dejó la anterior. En rotate(45deg) translateX(100px), primero giras los ejes locales 45 grados, y luego avanzas 100 píxeles a lo largo del nuevo eje x. Es la lectura de quien programa un brazo robótico o una tortuga: cada paso reorienta el mundo del siguiente.

Las dos dan el mismo resultado porque son la misma multiplicación mirada desde dos lados. La lectura local es la que más rápido te da la intuición cuando construyes algo articulado —una aguja de reloj, un satélite orbitando, un menú radial—; la global es la que necesitas cuando quieres razonar sobre el resultado en píxeles de pantalla.

flowchart TB
A[transform con lista de funciones] --> B[Cada funcion se convierte en una matriz]
B --> C[Se multiplican de izquierda a derecha]
C --> D[Matriz unica de 4x4]
D --> E[Cada vertice de la caja se multiplica por ella]
C --> F{Cambias el orden de dos funciones}
F --> G[Producto distinto porque no conmuta]
G --> H[Resultado visual distinto]
style A fill:#89b4fa,color:#11111b
style D fill:#cba6f7,color:#11111b
style E fill:#a6e3a1,color:#11111b
style G fill:#f9e2af,color:#11111b
style H fill:#f38ba8,color:#11111b

Un caso donde el orden da igual: dos rotaciones sobre el mismo eje, o dos traslaciones, o dos escalados uniformes. Las rotaciones sobre ejes distintos no conmutan, y es fácil comprobarlo con un libro en la mano: gíralo 90 grados sobre el eje vertical y luego 90 sobre el horizontal, y repite en el orden contrario. Acaba mirando a sitios distintos.

matrix, matrix3d y lo que ves en DevTools

Cuando inspeccionas un elemento animado con la API de animaciones o transformado por una librería, el valor computado que muestran las herramientas rara vez es la lista que escribiste: es una matrix() o una matrix3d(). Eso es el producto ya calculado.

matrix(a, b, c, d, e, f) corresponde a esta matriz 2D:

| a  c  e |
| b  d  f |
| 0  0  1 |

a y d son la escala en x e y; b y c son los términos de sesgo y rotación; e y f son la traslación en x e y en píxeles. Con eso puedes leer una matriz de un vistazo: si b y c valen 0, no hay rotación ni sesgo; si a y d valen 1 y b y c valen 0, es una traslación pura.

Una rotación de ángulo t es matrix(cos t, sin t, -sin t, cos t, 0, 0). Una rotación de 45 grados da matrix(0.7071, 0.7071, -0.7071, 0.7071, 0, 0).

matrix3d() toma 16 números, la matriz 4 por 4 completa en orden por columnas. Los tres últimos de la cuarta columna son la traslación. El elemento en la posición 11 —contando desde uno, el que ocupa la fila 4 columna 3— es el que introduce la perspectiva, y es lo que la función perspective() rellena.

Por qué la animación entre dos transform distintos se ve mal

Aquí es donde el álgebra se cobra su factura. Si dos fotogramas clave tienen listas de funciones con la misma estructura, la especificación interpola función a función: rotate(0deg) con rotate(90deg) da un giro limpio. Pero si las listas no coinciden —distinto número de funciones, o funciones distintas en la misma posición— el motor no puede emparejarlas, y aplica el procedimiento de último recurso: convierte cada extremo a matriz, descompone cada matriz en traslación, rotación, escalado y sesgo, interpola esas componentes y recompone. La descomposición no es única, y la que define la especificación elige una convención concreta que no siempre coincide con lo que tú tenías en la cabeza. El síntoma típico es un elemento que en mitad de la animación se aplasta, se invierte o pasa por un escalado negativo. La solución no es tocar la duración ni el easing: es hacer que las dos listas tengan exactamente la misma forma, aunque tengas que escribir rotate(0deg) o scale(1) como funciones identidad en el fotograma donde no hacen falta. Y si el caso lo permite, usar las propiedades individuales, que por construcción no tienen este problema porque cada una interpola su propio tipo simple.

⚔️ Predice antes de ejecutar
  1. Escribe en papel dónde acabará un cuadrado de 50 píxeles con transform: rotate(90deg) translateX(100px) y compruébalo en el navegador.
  2. Haz lo mismo con las funciones invertidas y explica la diferencia usando la lectura local.
  3. Construye una aguja de reloj: un contenedor que rota y una barra trasladada dentro de él. Consigue el mismo efecto con una sola lista de dos funciones.
  4. Lee en DevTools el matrix() computado de un elemento con rotate(30deg) scale(2) y verifica que a vale aproximadamente 1,732.
  5. Anima de transform: none a transform: rotate(180deg) y observa el resultado. Corrígelo declarando transform: rotate(0deg) en el estado inicial.