Tarjeta de relación de aspecto variable con degradados cónicos que se encuentran a lo largo de la diagonal

Recientemente me encontré con un problema interesante. Tuve que implementar una cuadrícula de tarjetas con una relación de aspecto variable (establecida por el usuario) que se almacenó en una --ratio
propiedad personalizada. Las cajas con una determinada relación de aspecto son un problema clásico en CSS y se ha vuelto más fácil de resolver en los últimos años, especialmente desde que obtuvimos aspect-ratio
, pero la parte complicada aquí fue que cada una de las tarjetas necesitaba tener dos gradientes cónicos en esquinas opuestas que se unieron. la diagonal. Algo como esto:
El desafío aquí es que, si bien es fácil hacer un cambio abrupto en lo linear-gradient()
largo de la diagonal de un cuadro de relación de aspecto variable usando, por ejemplo, una dirección to top left
que cambia con la relación de aspecto, a conic-gradient()
necesita un ángulo o un porcentaje que representa hasta qué punto ha dado la vuelta al círculo completo.
Consulte esta guía para repasar cómo funcionan los gradientes cónicos.
La solución sencilla
La específica ahora incluye funciones trigonométricas y trigonométricas inversas, que podrían ayudarnos aquí: el ángulo de la diagonal con la vertical es el arcotangente de la relación de aspecto atan(var(--ratio))
(los bordes izquierdo y superior del rectángulo y la diagonal forman un triángulo rectángulo donde la tangente del ángulo formado por la diagonal con la vertical es el ancho sobre la altura (precisamente nuestra relación de aspecto).
Poniéndolo en código, tenemos:
--ratio: 3/ 2;aspect-ratio: var(--ratio);--angle: atan(var(--ratio));background: /* below the diagonal */ conic-gradient(from var(--angle) at 0 100%, #319197, #ff7a18, #af002d calc(90deg - var(--angle)), transparent 0%), /* above the diagonal */ conic-gradient(from calc(.5turn + var(--angle)) at 100% 0, #ff7a18, #af002d, #319197 calc(90deg - var(--angle)));
Sin embargo, actualmente ningún navegador implementa funciones trigonométricas y trigonométricas inversas, por lo que la solución simple es solo una solución futura y no funcionaría en ningún lugar hoy.
La solución JavaScript
Por supuesto, podemos calcular el --angle
en JavaScript a partir del --ratio
valor.
let angle = Math.atan(1/ratio.split('/').map(c = +c.trim()).reduce((a, c) = c/a, 1));document.body.style.setProperty('--angle', `${+(180*angle/Math.PI).toFixed(2)}deg`)
Pero ¿qué pasa si usar JavaScript no es suficiente? ¿Qué pasa si realmente necesitamos una solución CSS pura? Bueno, es un poco complicado, ¡pero se puede hacer!
La solución CSS hacky
Esta es una idea que se me ocurrió a partir de una peculiaridad de los gradientes SVG que, sinceramente, me encontré muy frustrante cuando la encontré por primera vez.
Digamos que tenemos un degradado con una transición pronunciada 50%
de abajo hacia arriba, ya que en CSS es un degradado en 0°
ángulo. Ahora decimos que tenemos el mismo gradiente en SVG y cambiamos el ángulo de ambos gradientes al mismo valor.
En CSS, eso es:
linear-gradient(45deg, var(--stop-list));
En SVG tenemos:
linearGradient id='g' y1='100%' x2='0%' y2='0%' gradientTransform='rotate(45 .5 .5)' !-- the gradient stops --/linearGradient
Como se puede ver a continuación, estos dos no nos dan el mismo resultado. Si bien el gradiente de CSS realmente está en 45°
, el gradiente de SVG girado de la misma manera 45°
tiene esa transición brusca entre naranja y rojo a lo largo de la diagonal, aunque nuestro cuadro no es cuadrado, por lo que la diagonal no está en 45°
.
Esto se debe a que nuestro degradado SVG se dibuja dentro de un 1x1
cuadro cuadrado, girado por 45°
, lo que coloca el cambio abrupto de naranja a rojo a lo largo de la diagonal cuadrada. Luego, este cuadrado se estira para ajustarse al rectángulo, lo que básicamente cambia el ángulo diagonal.
Tenga en cuenta que esta distorsión de gradiente SVG ocurre solo si no cambiamos el gradientUnits
atributo de linearGradient
su valor predeterminado de objectBoundingBox
a userSpaceOnUse
.
idea basica
No podemos usar SVG aquí ya que solo tiene gradientes lineales y radiales, pero no cónicos. Sin embargo, podemos poner nuestros gradientes cónicos CSS en un cuadro cuadrado y usar el 45°
ángulo para que se unan a lo largo de la diagonal:
aspect-ratio: 1/ 1;width: 19em;background: /* below the diagonal */ conic-gradient(from 45deg at 0 100%, #319197, #ff7a18, #af002d 45deg, transparent 0%), /* above the diagonal */ conic-gradient(from calc(.5turn + 45deg) at 100% 0, #ff7a18, #af002d, #319197 45deg);
Luego podemos estirar este cuadro cuadrado usando una escala transform
; el truco es que '/' en 3/ 2
es un separador cuando se usa como aspect-ratio
valor, pero se analiza como una división dentro de a calc()
:
--ratio: 3/ 2;transform: scaley(calc(1/(var(--ratio))));
Puedes jugar cambiando el valor de --ratio
en el código editable insertado a continuación para ver que, de esta manera, los dos gradientes cónicos siempre se encuentran a lo largo de la diagonal:
Tenga en cuenta que esta demostración solo funcionará en un navegador que admita aspect-ratio
. Esta propiedad se admite de fábrica en Chrome 88+ (la versión actual es 90), pero Firefox aún necesita que la layout.css.aspect-ratio.enabled
bandera esté configurada true
en about:config. Y si estás usando Safari… bueno, ¡lo siento!
Problemas con este enfoque y cómo solucionarlos
Sin embargo, escalar el .card
elemento real rara vez sería una buena idea. Para mi caso de uso, las tarjetas están en una cuadrícula y establecen una escala direccional en ellas estropea el diseño (las celdas de la cuadrícula siguen siendo cuadradas, a pesar de que hemos escalado los .card
elementos en ellas). También tienen contenido de texto que la scaley()
función amplía de forma extraña.
La solución es darle a las tarjetas reales el aspecto deseado aspect-ratio
y usar un contenido absolutamente posicionado ::before
detrás del texto (usando z-index: -1
) para crear nuestro background
. Este pseudoelemento obtiene la propiedad width
de su .card
padre e inicialmente es cuadrado. También configuramos la escala direccional y los gradientes cónicos de antes. Tenga en cuenta que dado que nuestra posición absoluta ::before
está alineada superiormente con el borde superior de su .card
padre, también debemos escalarlo en relación con este borde ( transform-origin
debe tener un valor de 0
a lo largo del eje y , mientras que el valor del eje x no importa y puede ser cualquier cosa).
body { --ratio: 3/ 2; /* other layout and prettifying styles */}.card { position: relative; aspect-ratio: var(--ratio); ::before { position: absolute; z-index: -1; /* place it behind text content */ aspect-ratio: 1/ 1; /* make card square */ width: 100%; /* make it scale relative to the top edge it's aligned to */ transform-origin: 0 0; /* give it desired aspect ratio with transforms */ transform: scaley(calc(1/(var(--ratio)))); /* set background */ background: /* below the diagonal */ conic-gradient(from 45deg at 0 100%, #319197, #af002d, #ff7a18 45deg, transparent 0%), /* above the diagonal */ conic-gradient(from calc(.5turn + 45deg) at 100% 0, #ff7a18, #af002d, #319197 45deg); content: ''; }}
Tenga en cuenta que hemos pasado de CSS a SCSS en este ejemplo.
Esto es mucho mejor, como se puede ver en el inserto a continuación, que también es editable para que puedas jugar con él --ratio
y ver cómo todo se adapta bien a medida que cambias su valor.
Problemas de relleno
Como no hemos configurado un padding
en la tarjeta, el texto puede llegar hasta el borde e incluso ligeramente fuera de los límites dado que está un poco inclinado.
Eso no debería ser demasiado difícil de solucionar, ¿verdad? Simplemente agregamos un padding
, ¿verdad? Bueno, cuando hacemos eso, descubrimos que el diseño se rompe.
Esto se debe a que lo que aspect-ratio
hemos configurado en nuestros .card
elementos es el del .card
cuadro especificado por box-sizing
. Como no hemos establecido explícitamente ningún box-sizing
valor, su valor actual es el predeterminado content-box
. Agregar un padding
del mismo valor alrededor de este cuadro nos da una padding-box
relación de aspecto diferente que ::before
ya no coincide con la de su pseudoelemento.
Para entender mejor esto, digamos que nuestro aspect-ratio
es 4/ 1
y el ancho de content-box
es 16rem
( 256px
). Esto significa que la altura del content-box
es un cuarto de este ancho, lo que se calcula como 4rem
( 64px
). Entonces content-box
es un rectángulo 16rem×4rem
( ).256px×64px
Ahora digamos que agregamos un padding
de 1rem
( 16px
) a lo largo de cada borde. Por lo tanto, el ancho de padding-box
es 18rem
( 288px
, como se puede ver en el GIF animado de arriba), calculado como el ancho de content-box
, que es 16rem
( 256px
) más 1rem
( 16px
) a la izquierda y 1rem
a la derecha del padding
. De manera similar, la altura de padding-box
es 6rem
( 96px
): se calcula como la altura de content-box
, que es 4rem
( 64px
), más 1rem
( 16px
) en la parte superior e 1rem
inferior de padding
).
Esto significa que padding-box
es un rectángulo 18rem×6rem
( ) y, como , tiene una relación de aspecto que es diferente del valor que hemos establecido para la propiedad. Al mismo tiempo, el pseudoelemento tiene un ancho igual al de su padre (que hemos calculado como o ) y su relación de aspecto (establecida mediante escala) sigue siendo , por lo que su altura visual se calcula como ( ). Esto explica por qué la tarjeta creada con este pseudo (reducido verticalmente a un rectángulo ( )) ahora es más corta que la tarjeta real (un rectángulo ( ) ahora con la extensión .288px×96px
18 = 3⋅6
3/ 1
4/ 1
aspect-ratio
::before
padding-box
18rem
288px
4/ 1
4.5rem
72px
background
18rem×4.5rem
288px×72px
18rem×6rem
288px×96px
padding
Entonces, parece que la solución es bastante sencilla: debemos configurarlo box-sizing
para border-box
solucionar nuestro problema, ya que se aplica aspect-ratio
en este cuadro (idéntico a padding-box
cuando no tenemos un border
).
Efectivamente, esto soluciona las cosas... ¡pero sólo en Firefox!
El texto debe estar alineado verticalmente en el medio, ya que le hemos dado a nuestros .card
elementos un diseño de cuadrícula y place-content: center
lo hemos configurado. Sin embargo, esto no sucede en los navegadores Chromium y se vuelve un poco más obvio por qué cuando eliminamos esta última declaración: de alguna manera, la celda en la cuadrícula de la tarjeta 3/ 1
también obtiene la relación de aspecto y desborda la de la tarjeta content-box
:
Afortunadamente, este es un error conocido de Chromium que probablemente debería solucionarse en los próximos meses.
Mientras tanto, lo que podemos hacer para solucionar esto es eliminar las declaraciones y box-sizing
del elemento , mover el texto en un elemento secundario (o en el pseudo si es solo una frase y somos vagos, aunque sea un elemento real). child es la mejor idea si queremos que el texto siga siendo seleccionable) y convertirlo en a con un .padding
place-content
.card
::after
grid
padding
.card { /* same as before, minus the box-sizing, place-content and padding declarations the last two of which which we move on the child element */ __content { place-content: center; padding: 1em }}
Esquinas redondeadas
Digamos que también queremos que nuestras tarjetas tengan esquinas redondeadas. Dado que una dirección transform
como la scaley
del ::before
pseudoelemento que crea nuestro background
también distorsiona el redondeo de las esquinas, resulta que la forma más sencilla de lograrlo es establecer una border-radius
en el elemento real .card
y cortar todo lo que esté fuera de ese redondeo con overflow: hidden
.
Sin embargo, esto se vuelve problemático si en algún momento queremos que algún otro descendiente nuestro .card
sea visible fuera de él. Entonces, lo que vamos a hacer es establecer border-radius
directamente en el ::before
pseudo que crea la tarjeta background
e invertir la escala direccional transform
a lo largo del eje y en el componente yborder-radius
de esto :
$r: .5rem;.card { /* same as before */ ::before { border-radius: #{$r}/ calc(#{$r}*var(--ratio)); transform: scaley(calc(1/(var(--ratio)))); /* same as before */ }}
Resultado final
Poniéndolo todo junto, aquí hay una demostración interactiva que permite cambiar la relación de aspecto arrastrando un control deslizante: cada vez que cambia el valor del control deslizante, la --ratio
variable se actualiza:
Deja un comentario