css - ¿Por qué importa el orden de las transformaciones? SVG rotar/escalar no da el mismo resultado que escalar/rotar
css3 css-transforms (2)
Después de analizar la especificación SVG y guías como this y this , todavía me cuesta entender exactamente cómo funcionan las transformaciones de encadenamiento.
Cotizaciones relevantes seleccionadas
Cuando aplica el atributo transform a un elemento SVG, ese elemento obtiene una "copia" del sistema de coordenadas de usuario actual en uso.
Y:
Cuando las transformaciones están encadenadas, lo más importante a tener en cuenta es que, al igual que con las transformaciones de elementos HTML, cada transformación se aplica al sistema de coordenadas después de que ese sistema se transforma por las transformaciones anteriores.
Y:
Por ejemplo, si va a aplicar una rotación a un elemento, seguido de una traducción, la traducción se realiza de acuerdo con el nuevo sistema de coordenadas, no el inicial no rotado.
Y:
La secuencia de transformaciones importa. La secuencia que las funciones de transformación se especifican dentro del atributo de transformación es la secuencia que se aplican a la forma.
Código
El sistema de coordenadas actual del primer rectángulo se escala, luego se gira (observe el orden). El sistema de coordenadas actual del segundo rectángulo se gira y luego se escala.
svg {
border: 1px solid green;
}
<svg xmlns="http://www.w3.org/2000/svg">
<style>
rect#s1 {
fill: red;
transform: scale(2, 1) rotate(10deg);
}
</style>
<rect id="s1" x="" y="" width="100" height="100" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg">
<style>
rect#s2 {
fill: blue;
transform: rotate(10deg) scale(2, 1);
}
</style>
<rect id="s2" x="" y="" width="100" height="100" />
</svg>
Pregunta
Sabemos que cuando encadenamos transformaciones, se hace una copia del sistema de coordenadas actual en uso para ese elemento, luego las transformaciones se aplican en el orden en que se especifican.
Cuando tenemos un sistema de coordenadas de usuario que ya está escalado, y le aplicamos una rotación, el rectángulo está (como se ve) efectivamente sesgado (observe los ángulos cambiados). Esto no sucede si hacemos las dos transformaciones al revés (rotar, luego escalar).
Se agradecería profundamente la ayuda de expertos sobre cómo gira exactamente el sistema de coordenadas actual escalado. Estoy tratando de entender, desde un ángulo técnico (funcionamiento interno), exactamente por qué ocurre el sesgo en el primer rectángulo.
Gracias.
La forma en que Temani Afif lo explicó sigue los sistemas de coordenadas que abarca cada transformación. Comienza con la ventana gráfica, y cada sistema de coordenadas consecutivo se deriva y se ubica en un lugar diferente en el lienzo. Es posible que estos sistemas de coordenadas no sean cartesianos (un "universo extendido"). Se construyen en el árbol DOM desde afuera hacia adentro, y cuando se encadenan en un atributo, de izquierda a derecha.
Pero puede imaginar la misma transformación también en la dirección opuesta, de adentro hacia afuera: primero dibuja un rectángulo en su sistema de coordenadas cartesianas del espacio de usuario, y luego lo transforma mediante una cadena de escalas, rotaciones, etc., hasta dibujarlo. en el sistema de coordenadas de la ventana gráfica, está distorsionado a otra cosa.
Pero si lo miras de esta manera, las transformaciones encadenadas en un atributo deben procesarse de derecha a izquierda:
transform: scale(2, 1) rotate(10deg)
significa tomar un rectángulo,
primero
rotarlo en 10 grados y
luego
escale el rectángulo girado en la dirección horizontal.
En resumen, estos dos son equivalentes:
- Si dibuja un gráfico en un sistema de coordenadas transformado , construya el sistema de coordenadas aplicando transformaciones a estos sistemas de coordenadas de izquierda a derecha .
- Si dibuja un gráfico transformado en el sistema de coordenadas original, construya el gráfico aplicando transformaciones al gráfico de derecha a izquierda .
Para ilustrar cómo funciona, consideremos una animación para mostrar cómo el efecto de escala cambia la rotación.
.red {
width:80px;
height:20px;
background:red;
margin:80px;
transform-origin:left center;
animation: rotate 2s linear infinite;
}
@keyframes rotate {
from{transform:rotate(0)}
to{transform:rotate(360deg)}
}
<div class="container">
<div class="red">
</div>
</div>
Como puede ver arriba, la rotación está creando una forma de círculo perfecta.
Ahora escalemos el contenedor y veamos la diferencia:
.red {
width:80px;
height:20px;
background:red;
margin:80px;
transform-origin:left center;
animation: rotate 5s linear infinite;
}
@keyframes rotate {
from{transform:rotate(0)}
to{transform:rotate(360deg)}
}
.container {
display:inline-block;
transform:scale(3,1);
transform-origin:left center;
}
<div class="container">
<div class="red">
</div>
</div>
Observe cómo ya no tenemos un círculo, pero ahora es una elipse. Es como si tomáramos el círculo y lo abriéramos, lo que está creando el efecto de inclinación dentro de nuestro rectángulo.
Si hacemos el efecto contrario y comenzamos por tener un efecto de escala y luego aplicamos una rotación, no tendremos ningún sesgo.
.red {
width:80px;
height:20px;
background:red;
margin:80px;
animation: rotate 2s linear infinite;
}
@keyframes rotate {
from{transform:scale(1,1)}
to{transform:scale(3,1)}
}
.container {
display:inline-block;
transform:rotate(30deg);
transform-origin:left center;
}
<div class="container">
<div class="red">
</div>
</div>
Para explicarlo de manera diferente: la aplicación de una rotación mantendrá la misma relación entre los ejes X e Y, por lo que no verá ningún efecto negativo al escalar más tarde, pero al escalar solo un eje se romperá la relación, por lo que nuestra forma se verá mal cuando lo intentemos para aplicar una rotación.
Puede consultar este enlace si desea más detalles sobre cómo se encadenan las transformaciones y cómo se clasifica la matriz: https://www.w3.org/TR/css-transforms-1/#transform-rendering . Se trata del elemento HTML, pero como se dice en la especificación SVG, es lo mismo.
Aquí están las partes relevantes:
Las transformaciones son acumulativas. Es decir, los elementos establecen su sistema de coordenadas local dentro del sistema de coordenadas de su padre.
Desde la perspectiva del usuario, un elemento acumula efectivamente todas las propiedades de transformación de sus antepasados, así como cualquier transformación local aplicada a él.
Hagamos un poco de matemática para ver la diferencia entre ambas transformaciones. Consideremos la multiplicación de matrices y, dado que estamos tratando con una transformación lineal 2D, haremos esto en ℝ² por simplicidad 1 .
Para la
scale(2, 1) rotate(10deg)
tendremos
|2 0| |cos(10deg) -sin(10deg)| |2*cos(10deg) -2*sin(10deg) |
|0 1| x |sin(10deg) cos(10deg) | = |1*sin(10deg) 1*cos(10deg) |
Ahora si aplicamos esta matriz a un
(Xi,Yi)
obtendremos
(Xf,Yf)
como a continuación:
Xf = 2* (Xi*cos(10deg) - Yi*sin(10deg))
Yf = Xi*sin(10deg) + Yi*cos(10deg)
Observe cómo
Xf
está teniendo un multiplicador adicional que es el culpable de crear el efecto sesgado.
Es como si cambiamos el comportamiento o
Xf
y mantuvimos el
Yf
Ahora consideremos la
rotate(10deg) scale(2, 1)
:
|cos(10deg) -sin(10deg)| |2 0| |2*cos(10deg) -1*sin(10deg) |
|sin(10deg) cos(10deg) | x |0 1| = |2*sin(10deg) 1*cos(10deg) |
Y luego tendremos
Xf = 2*Xi*cos(10deg) - Yi*sin(10deg)
Yf = 2*Xi*sin(10deg) + Yi*cos(10deg)
Podemos considerar el
2*Xi
como un
Xt
y podemos decir que rotamos el elemento (
Xt,Yi
) y este elemento fue inicialmente escalado considerando el eje X.
1 CSS también utiliza la transformación afín (como traducir), por lo que usar ℝ² (coordenadas cartesianas) no es suficiente para realizar nuestro cálculo, por lo que debemos considerar ℝℙ² (coordenadas homogéneas). Nuestro cálculo anterior será:
|2 0 0| |cos(10deg) -sin(10deg) 0| |2*cos(10deg) -2*sin(10deg) 0|
|0 1 0| x |sin(10deg) cos(10deg) 0| = |1*sin(10deg) 1*cos(10deg) 0|
|0 0 1| |0 0 1| |0 0 1|
Nada cambiará en este caso porque la parte afín es
nula,
pero si tenemos una traducción combinada con otra transformación (por ejemplo:
scale(2, 1) translate(10px,20px)
) tendremos lo siguiente:
|2 0 0| |1 0 10px| |2 0 20px|
|0 1 0| x |0 1 20px| = |0 1 20px|
|0 0 1| |0 0 1 | |0 0 1 |
Y
Xf = 2*Xi + 20px;
Yf = Yi + 20px;
1 = 1 (to complete the multiplication)