具有锥形渐变且沿对角线交汇的可变纵横比卡片

Avatar of Ana Tudor
Ana Tudor

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 200 美元的免费积分!

我最近遇到了一个有趣的问题。我需要实现一个卡片网格,这些卡片具有可变(用户设置)的纵横比,该纵横比存储在自定义属性 --ratio 中。具有特定纵横比的盒子是 CSS 中的一个经典问题,近年来解决起来变得更容易,尤其是在我们获得 aspect-ratio 之后,但这里棘手的部分是每个卡片都需要在相对的角上有两个锥形渐变,并在对角线上交汇。如下所示

A 3 by 3 grid of square cards with color backgrounds made from conic gradients. The gradients appear like stripes that extend from opposite corners of the card. Each card reads Hello Gorgeous in a fancy script font.
用户设置纵横比的卡片。

这里的挑战在于,虽然使用例如 to top left 这样的方向(随纵横比变化)沿可变纵横比盒子的对角线进行突然更改很容易,但 conic-gradient() 需要一个角度或一个百分比来表示它围绕完整圆圈旋转了多远。

查看 本指南 以复习锥形渐变的工作原理。

简单的解决方案

规范现在包含了 三角函数和反三角函数,这可能对我们有所帮助——对角线与垂直线的夹角是纵横比 atan(var(--ratio)) 的反正切(矩形的左边缘、上边缘和对角线形成一个直角三角形,其中对角线与垂直线形成的角的正切是宽除以高——正好是我们的纵横比)。

Illustration. Shows a rectangle of width w and height h with a diagonal drawn from the bottom left corner to the top right one. This diagonal is the hypotenuse of the right triangle whose catheti are the left and top edges of the rectangle. In this right triangle, the tangent of the angle between the hypotenuse (diagonal of original rectangle) with the vertical (left edge of original rectangle) is the top edge (w) over the left edge (h).
对角线与垂直边缘的夹角。

将其转换为代码,我们有

--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)));

但是,目前没有浏览器实现三角函数和反三角函数,因此简单的解决方案只是一个未来的解决方案,而不是一个在今天实际可行的解决方案。

JavaScript 解决方案

当然,我们可以在 JavaScript 中从 --ratio 值计算 --angle

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`)

但是如果使用 JavaScript 不行呢?如果我们真的需要一个纯 CSS 解决方案呢?好吧,这有点笨拙,但可以做到!

笨拙的 CSS 解决方案

这个想法来源于 SVG 渐变的一个特性,老实说,当我 第一次遇到 时,我发现它非常令人沮丧。

假设我们有一个渐变,它在 50% 处有一个急剧的过渡,从下到上,因为在 CSS 中,这是一个 角的渐变。现在假设我们在 SVG 中有相同的渐变,并且我们将两个渐变的角度更改为相同的值。

在 CSS 中,它是

linear-gradient(45deg, var(--stop-list));

在 SVG 中,我们有

<linearGradient id='g' y1='100%' x2='0%' y2='0%' 
                gradientTransform='rotate(45 .5 .5)'>
  <!-- the gradient stops -->
</linearGradient>

如下所示,这两个结果并不相同。虽然 CSS 渐变确实在 45° 处,但旋转了相同 45° 的 SVG 渐变在对角线上具有橙色和红色之间的急剧过渡,即使我们的盒子不是正方形,因此对角线也不在 45° 处!

Screenshot. Shows a rectangle with a CSS gradient at 45° (left) vs. a rectangle with a bottom to top SVG gradient rotated by 45° (right). This angle is adjustable via the slider at the bottom. The CSS gradient is really at 45°, but the line of the SVG gradient is perpendicular onto the rectangle's diagonal.
45° CSS 与 SVG 渐变(演示)。

这是因为我们的 SVG 渐变绘制在一个 1x1 的正方形盒子中,旋转了 45°,这使得橙色到红色的突然变化沿着正方形的对角线。然后将此正方形拉伸以适应矩形,这基本上改变了对角线角度。

请注意,只有在我们不将 linearGradientgradientUnits 属性从其默认值 objectBoundingBox 更改为 userSpaceOnUse 时,才会发生这种 SVG 渐变失真。

基本思路

我们不能在这里使用 SVG,因为它只有线性渐变和径向渐变,没有锥形渐变。但是,我们可以将 CSS 锥形渐变放在一个正方形盒子中,并使用 45° 角使它们沿对角线交汇

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);

然后我们可以使用缩放 transform 来拉伸这个正方形盒子——诀窍在于,当用作 aspect-ratio 值时,“/”是分隔符,但在 calc() 中会被解析为除法。

--ratio: 3/ 2;
transform: scaley(calc(1/(var(--ratio))));

您可以尝试在下面的可编辑代码嵌入中更改 --ratio 的值,以查看这样,两个锥形渐变始终沿对角线交汇。

请注意,此演示仅在支持 aspect-ratio 的浏览器中有效。此属性在 Chrome 88+(当前版本为 90)中开箱即用,但 Firefox 仍然需要将 about:config 中的 layout.css.aspect-ratio.enabled 标志设置为 true。如果您使用的是 Safari……好吧,抱歉!

Screenshot showing how to enable the Firefox flag. Go to about:config (type that in the address bar - you may be asked if you're sure you want to mess with that stuff before you're allowed to enter). Use the search bar to look for 'aspect' - this should be enough to bring up the flag. Set its value to true.
在 Firefox 中启用标志。

此方法的问题以及如何解决

但是,缩放实际的 .card 元素很少是一个好主意。在我的用例中,卡片位于网格上,在它们上面设置方向性缩放会弄乱布局(即使我们在其中缩放了 .card 元素,网格单元格仍然是正方形)。它们还具有文本内容,这些内容会被 scaley() 函数奇怪地拉伸。

Screenshot. Shows how the card elements are scaled down vertically, yet the grid cells they're occupying have remained square, just like the cards before the directional scaling.
缩放实际卡片的问题(演示

解决方案是为实际的卡片提供所需的 aspect-ratio,并使用绝对定位的 ::before(使用 z-index: -1)放置在文本内容后面以创建我们的 background。此伪元素获取其 .card 父元素的 width,并且最初是正方形。我们还在其上设置了之前提到的方向性缩放和锥形渐变。请注意,由于我们的绝对定位的 ::before 与其 .card 父元素的上边缘对齐,因此我们也应该相对于此边缘对其进行缩放(transform-origin 沿 y 轴需要值为 0,而 x 轴值无关紧要,可以是任何值)。

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: '';
  }
}

请注意,在这个例子中,我们已经从 CSS 切换到了 SCSS。

这要好得多,正如下面的嵌入所示,它也是可编辑的,因此您可以使用 --ratio 并查看在您更改其值时所有内容如何很好地适应。

填充问题

由于我们没有在卡片上设置 padding,因此文本可能会一直延伸到边缘,甚至由于略微倾斜而超出边界。

Screenshot. Shows a case where the text goes all the way to the edge of the card and even goes out a tiny little bit creating an ugly result.
缺少 padding 导致问题。

这应该不难解决,对吧?我们只需要添加 padding,对吧?好吧,当我们这样做时,我们发现布局坏了!

Animated gif. Shows the dev tools grid overlay t highlight that, while the background (created with the scaled pseudo) still has the desired aspect ratio, the grid cell and the actual card in it are taller.
添加 padding 会破坏布局。(演示

这是因为我们设置在 .card 元素上的 aspect-ratio 是由 box-sizing 指定的 .card 盒子的纵横比。由于我们没有显式设置任何 box-sizing 值,因此其当前值是默认值 content-box。在该盒子周围添加相同值的 padding 会得到一个具有不同纵横比的 padding-box,它不再与 ::before 伪元素的纵横比一致。

为了更好地理解这一点,假设我们的 aspect-ratio4/ 1,并且 content-box 的宽度是 16rem256px)。这意味着 content-box 的高度是此宽度的四分之一,计算结果为 4rem64px)。因此,content-box 是一个 16rem×4rem256px×64px)的矩形。

现在假设我们在每个边缘添加 1rem16px)的 padding。因此,padding-box 的宽度为 18rem288px,如上动画 GIF 中所示)——计算为 content-box 的宽度,即 16rem256px)加上来自 padding 的左侧 1rem16px)和右侧 1rem。类似地,padding-box 的高度为 6rem96px)——计算为 content-box 的高度,即 4rem64px),加上来自 padding 的顶部 1rem16px)和底部 1rem)。

这意味着padding-box是一个18rem×6rem288px×96px)的矩形,并且由于18 = 3⋅6,它的纵横比为3/ 1,这与我们为aspect-ratio属性设置的4/ 1值不同!同时,::before伪元素的宽度等于其父元素的padding-box宽度(我们计算为18rem288px),并且它的纵横比(通过缩放设置)仍然是4/ 1,因此它的视觉高度计算为4.5rem72px)。这就解释了为什么使用此伪元素创建的background——垂直缩放到18rem×4.5rem288px×72px)的矩形——现在比实际卡片——现在带有padding18rem×6rem288px×96px)矩形——更短。

因此,解决方案看起来非常简单——我们需要将box-sizing设置为border-box来解决我们的问题,因为这将aspect-ratio应用于此盒子(当我们没有border时,与padding-box相同)。

当然,这修复了问题……但仅在 Firefox 中!

Screenshot collage. Shows how the text is not middle aligned in Chromium browsers (top), while Firefox (bottom) gets this right.
显示 Chromium(顶部)和 Firefox(底部)之间的差异。

文本应垂直居中对齐,因为我们已为.card元素提供了网格布局并在其上设置了place-content: center。但是,这在 Chromium 浏览器中不会发生,当我们删除最后一个声明时,它会变得更加明显——不知何故,卡片网格中的单元格也获得了3/ 1的纵横比,并溢出了卡片的content-box

Animated gif. For some reason, the grid cell inside the card gets the set aspect ratio and overflows the card's content-box.
检查卡片的网格,带有和不带place-content: centerDEMO)。

幸运的是,这是一个已知的 Chromium 漏洞可能会在未来几个月内得到修复

在此期间,我们可以做的是从.card元素中删除box-sizingpaddingplace-content声明,将文本移动到子元素中(或者如果它只是一行并且我们很懒,则移动到::after伪元素中,尽管如果我们希望文本保持可选择,则实际的子元素是更好的选择),并将其设置为带paddinggrid

.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
  }
}

圆角

假设我们也希望我们的卡片具有圆角。由于像在创建我们background::before伪元素上的scaley这样的方向transform也会扭曲角圆角,因此结果是实现此目的的最简单方法是在实际的.card元素上设置border-radius,并使用overflow: hidden剪切圆角之外的所有内容。

Screenshot. Shows an element that's not scaled at all on the left. This has a perfectly circular border-radius. In the right, there's a non-uniform scaled element - its border-radius is not perfectly circular anymore, but instead distorted by the scaling.
非均匀缩放会扭曲角圆角。(DEMO

但是,如果在某些时候我们希望.card的其他一些后代在它外部可见,这就会变得有问题。因此,我们要做的是直接在创建卡片background::before伪元素上设置border-radius,并在该border-radiusy分量上沿y轴反转方向缩放transform

$r: .5rem;

.card {
  /* same as before */
  
  &::before {
    border-radius: #{$r}/ calc(#{$r}*var(--ratio));
    transform: scaley(calc(1/(var(--ratio))));
    /* same as before */
  }
}

最终结果

将所有内容整合在一起,这是一个交互式演示,允许通过拖动滑块更改纵横比——每次滑块值更改时,都会更新--ratio变量