我最近遇到了一个有趣的问题 —— 我需要实现一个具有可变纵横比(由用户决定)的卡片,且纵横比值定义在了 --ratio
这个自定义属性中。具有特定纵横比的卡片是 CSS 中的一个经典问题,也是近年来变得容易解决的问题,尤其是有了 aspect-ratio
之后,但这里棘手的部分是我们需要在每张卡片沿对角线交点处分别添加一个圆锥渐变,如图:
用户设置的纵横比卡片。
这里的挑战是,用 linear-gradient()
对沿着可变纵横比框的对角线做唐突的改变,比较容易,例如使用像“向左上角”这样的方向随纵横比变化,但 conic-gradient()
需要一个角度或一个百分比来表示它绕了一整圈走了多远的渐变并不好构建。
简单的解决方案
CSS 规范现在包括了 三角函数和反三角函数,它可以在这里帮助我们 —— 对角线与垂直线的角度是纵横比 atan(var(--ratio))
的反正切(矩形的左边缘和上边缘与对角线形成直角三角形,其中对角线与垂直线形成的角度的切线是宽度超过高度 —— 正是我们的纵横比)。
对角线与垂直线(边)的夹角。
把它写成代码,我们有:
--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 解决方案怎么办?好吧,这有点麻烦,但我们还是可以做到!
hacky CSS 解决方案
这是我从 SVG 渐变的特殊性中得到的一个想法,老实说,当我第一次看到这个问题 时,我发现它非常令人沮丧.
假设我们有一个从底部到顶部的 50%
的渐变(因为在 CSS 中,这是一个角度为 0°
的渐变)。现在假设我们在 SVG 中有相同的渐变,我们将两个渐变的角度更改为相同的值。
在 CSS 中,这是:
linear-gradient(45deg, var(--stop-list));
在 SVG 中,我们有:
<linearGradient id='g' y1='100%' x2='0%' y2='0%'
gradientTransform='rotate(45 .5 .5)'>
<!-- 渐变停止 -->
</linearGradient>
如下所示,这两个不会给我们相同的结果。虽然 CSS 渐变实际上是在 45°
,旋转了相同 45°
的 SVG 渐变沿着对角线在橙色和红色之间有明显的过渡,即使我们的盒子不是方形的,所以对角线不是在 45°
!
45°
CSS 与 SVG 渐变(例子)。
这是因为我们的 SVG 渐变被绘制在一个 1x1
方形框内,旋转了 45°
,这使得沿着方形对角线从橙色突然变为红色。然后这个正方形被拉伸以适应矩形,这基本上改变了对角线的角度。
基本思路
我们不能在这里使用 SVG,因为它只有线性和径向渐变,而没有圆锥渐变。但是,我们可以将 CSS 圆锥渐变放在一个方形框中,并使用 45°
角使它们沿对角线相交:
aspect-ratio: 1/ 1;
width: 19em;
background:
/* 对角线之下 */
conic-gradient(from 45deg at 0 100%,
#319197, #ff7a18, #af002d 45deg, transparent 0%),
/* 对角线之上 */
conic-gradient(from calc(.5turn + 45deg) at 100% 0,
#ff7a18, #af002d, #319197 45deg);
然后我们可以使用缩放 transform
来拉伸这个方框 —— 诀窍是 3/2
中的 /
在用作 aspect-ratio
值时是一个分隔符,但在 calc()
中被解析为除法:
--ratio: 3/ 2;
transform: scaley(calc(1/(var(--ratio))));
我们可以在下面嵌入的可编辑代码中更改 --ratio
的值来查看,这样,两个圆锥渐变总是沿对角线相交:CodePen
在 Firefox 中启用标志。
这种方法的问题以及如何解决这些问题
不过,缩放实际的 .card
元素很少是一个好主意。对于我的用例,卡片位于网格上并且在它们上设置方向比例会弄乱布局(网格单元仍然是方形的,即使我们已经缩放了其中的 .card
元素)。它们也有被 scaley()
函数奇怪地拉伸的文本内容。
缩放实际卡片的问题(例子)
解决方案是为实际卡片提供所需的 aspect-ratio
,并使用绝对定位的 ::before
放置在文本内容后面(z-index: -1
)以创建我们的 background
。这个伪元素获得它的 .card
父元素的 width
并且最初是正方形的。我们还设置了之前的方向缩放和圆锥梯度。请注意,由于我们绝对定位的 ::before
与它的 .card
父级的上边缘顶部对齐,我们也应该相对于这条边缘缩放它(transform-origin
需要有一个值沿 y 轴为 0
,而 x 轴值无关紧要,可以是任何值)。
body {
--ratio: 3/ 2;
/* 其他装饰用的布局样式 */
}
.card {
position: relative;
aspect-ratio: var(--ratio);
&::before {
position: absolute;
z-index: -1; /* 移到文字下方 */
aspect-ratio: 1/ 1; /* 让卡片成为正方形 */
width: 100%;
/* 让它缩放到它所对其的顶部边缘 */
transform-origin: 0 0;
/* 使用 Transform 给他指定的缩放比 */
transform: scaley(calc(1 / (var(--ratio))));
/* 设置背景 */
background: /* 对角线之下 */
conic-gradient(from 45deg at 0 100%,
#319197, #af002d, #ff7a18 45deg, transparent 0%),
/* 对角线之上 */
conic-gradient(from calc(.5turn + 45deg) at 100% 0,
#ff7a18, #af002d, #319197 45deg);
content: '';
}
}
这要好得多,因为它可以在下面的嵌入中看到,它也是可编辑的,因此我们可以修改 --ratio
并查看当我们更改其值时一切将如何完美地适应。
CodePen 供代码参考与效果预览。
内边距问题
由于我们没有在卡片上设置 padding
,文本可能会一直延伸到边缘,甚至稍微超出边界,因为它有点倾斜。
缺少 padding
会导致问题。
这应该不会太难修复,对吧?我们只是添加了一个 padding
,对吧?好吧,当我们这样做时,我们发现布局失效了!
添加 padding
会破坏布局。(例子)
这是因为我们在 .card
元素上设置的 aspect-ratio
是由 box-sizing
指定的 .card
框的纵横比。由于我们没有明确设置任何 box-sizing
值,它的当前值是默认值也就是 content-box
。在这个框周围添加一个相同值的 padding
会给我们一个不同纵横比的 padding-box
,让它不再与其它的元素的 ::before
重合。
为了更好地理解这一点,假设我们的 aspect-ratio
为 4/1
,content-box
的宽度为 16rem
(256px
)。这意味着 content-box
的高度是这个宽度的四分之一,计算得出结果是 4rem
(64px
)。所以 content-box
是一个 16rem×4rem
(256px×64px
)大的矩形。
现在假设我们沿着每条边添加一个 1rem
(16px
)的 padding
。现在,padding-box
的宽度为 18rem
(288px
,如上面的动画 GIF 所示)—— 计算得出 content-box
的宽度为 16rem
( 256px
) ,再加上左侧的 1rem
(16px
) 和来自 padding
的右侧的 1rem
。类似地,padding-box
的高度为 6rem
(96px
)—— 计算得出 content-box
的高度,即 4rem
(64px
),再加上顶部的 1rem
( 16px
)和底部的 1rem
padding
。
这意味着 padding-box
是一个 18rem×6rem
(288px×96px
)的矩形,并且由于 18 = 3⋅6
,它的纵横比为 3/1
,与我们为 aspect-ratio
属性设置的 4/1
值不一样!同时,::before
伪元素的宽度等于其父元素 padding-box
的宽度(我们计算为 18rem
或 288px
),而且它的纵横比(通过缩放设置的)仍然是 4/1
,所以它的视觉高度计算可得是 4.5rem
(72px
)。这就解释了为什么用这个伪元素创建的 background
—— 垂直缩小到一个 18rem×4.5rem
(288px×72px
)的矩形 —— 现在却比实际的卡片 —— 一个带有 padding
的 18rem×6rem
(288px× 96px
) 矩形 —— 要小。
因此,看起来解决方案非常简单 —— 我们只需要将 box-sizing
设置为 border-box
就可以解决我们的问题,因为这会在这个盒子上应用 aspect-ratio
(与 padding-box
当我们没有 border
时一致)。
果然,这可以解决问题……但仅限于 Firefox!
显示 Chromium(上图)和 Firefox(下图)之间的区别。
文本应该垂直居中对齐,因为我们给了 .card
元素一个网格布局,并在它们上设置了 place-content: center
。然而,这不会发生在 Chromium 浏览器中。当我们删除最后一个声明时,这变得更加明显 —— 不知何故,卡片网格中的单元格也获得了 3/1
的纵横比,并溢出了卡片的 content-box
:
使用 place-content: center
与否来检查卡片的网格。(例子)
幸运的是,这是一个 已知的 Chromium 错误,并且应该可以在未来几个月内得到修复。
与此同时,我们可以做的就是从 .card
元素中移除 box-sizing
、padding
和 place-content
声明,移动子元素(或用 ::after
伪元素 —— 我指的是,如果只需要一行代码,不过我们很懒惰。当然如果我们希望文本保持可以被选择,则实际的子元素应该是更好的主意)并使其成为带有 padding
的 grid
。
.card {
/* 和以前一致,
减去 box-sizing、place-content 和内边距定义
最后两个我们移动的子元素 */
&__content {
place-content: center;
padding: 1em
}
}
CodePen 供代码参考与效果预览。
添加圆角
假设我们也希望我们的卡片有圆角。由于像 ::before
伪元素上我们用 scaley
这样的定向的 transform
创建的 background
也会扭曲圆角,因此实现这一点的最简单方法是设置一个 border- radius
在实际的 .card
元素上,并使用 overflow: hidden
切掉卡片之外的所有内容。
非均匀缩放会扭曲圆角。(例子)
但是,如果在某些时候我们希望我们的 .card
的其他子代在它之外可见,这就会成为问题。所以,我们要做的是直接在创建卡片背景的 ::before
伪元素上设置 border-radius
,并在这个 border-radius
上沿 y 轴反转方向缩放 transform
:
$r: .5rem;
.card {
/* 和之前一致 */
&::before {
border-radius: #{$r}/ calc(#{$r}*var(--ratio));
transform: scaley(calc(1 / (var(--ratio))));
/* 和之前一致 */
}
}
CodePen 供代码参考与效果预览。
最终效果
把上面的代码合并起来,这是一个交互式演示,允许通过拖动滑块来更改纵横比 —— 每次滑块值更改时,--ratio
变量都会更新:CodePen。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!