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 --ratiopropiedad 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 leftque 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 --angleen JavaScript a partir del --ratiovalor.

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 á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 1x1cuadro 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 gradientUnitsatributo de linearGradientsu valor predeterminado de objectBoundingBoxa 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/ 2es un separador cuando se usa como aspect-ratiovalor, 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 --ratioen 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.enabledbandera esté configurada trueen about:config. Y si estás usando Safari… bueno, ¡lo siento!

Problemas con este enfoque y cómo solucionarlos

Sin embargo, escalar el .cardelemento 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 .cardelementos 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-ratioy usar un contenido absolutamente posicionado ::beforedetrás del texto (usando z-index: -1) para crear nuestro background. Este pseudoelemento obtiene la propiedad widthde su .cardpadre 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 ::beforeestá alineada superiormente con el borde superior de su .cardpadre, también debemos escalarlo en relación con este borde ( transform-origindebe tener un valor de 0a 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 --ratioy ver cómo todo se adapta bien a medida que cambias su valor.

Problemas de relleno

Como no hemos configurado un paddingen 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-ratiohemos configurado en nuestros .cardelementos es el del .cardcuadro especificado por box-sizing. Como no hemos establecido explícitamente ningún box-sizingvalor, su valor actual es el predeterminado content-box. Agregar un paddingdel mismo valor alrededor de este cuadro nos da una padding-boxrelación de aspecto diferente que ::beforeya no coincide con la de su pseudoelemento.

Para entender mejor esto, digamos que nuestro aspect-ratioes 4/ 1y el ancho de content-boxes 16rem( 256px). Esto significa que la altura del content-boxes un cuarto de este ancho, lo que se calcula como 4rem( 64px). Entonces content-boxes un rectángulo 16rem×4rem( ).256px×64px

Ahora digamos que agregamos un paddingde 1rem( 16px) a lo largo de cada borde. Por lo tanto, el ancho de padding-boxes 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 1rema la derecha del padding. De manera similar, la altura de padding-boxes 6rem( 96px): se calcula como la altura de content-box, que es 4rem( 64px), más 1rem( 16px) en la parte superior e 1reminferior de padding).

Esto significa que padding-boxes 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×96px18 = 3⋅63/ 14/ 1aspect-ratio::beforepadding-box18rem288px4/ 14.5rem72pxbackground18rem×4.5rem288px×72px18rem×6rem288px×96pxpadding

Entonces, parece que la solución es bastante sencilla: debemos configurarlo box-sizingpara border-boxsolucionar nuestro problema, ya que se aplica aspect-ratioen este cuadro (idéntico a padding-boxcuando 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 .cardelementos un diseño de cuadrícula y place-content: centerlo 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/ 1tambié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-sizingdel 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 .paddingplace-content.card::aftergridpadding

.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 transformcomo la scaleydel ::beforepseudoelemento que crea nuestro backgroundtambién distorsiona el redondeo de las esquinas, resulta que la forma más sencilla de lograrlo es establecer una border-radiusen el elemento real .cardy 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 .cardsea visible fuera de él. Entonces, lo que vamos a hacer es establecer border-radiusdirectamente en el ::beforepseudo que crea la tarjeta backgrounde invertir la escala direccional transforma 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 --ratiovariable se actualiza:

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Subir