CSS 网格和自定义形状,第三部分

Avatar of Temani Afif
Temani Afif on

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

第一部分第二部分 之后,我带着第三篇文章回来了,以探索更多花哨的形状。与之前的文章一样,我们将结合 CSS 网格和裁剪和遮罩来为图片库创建花哨的布局。

CSS 网格和自定义形状系列

我是否应该在阅读之前阅读之前的文章?

这不是强制性的,但强烈建议尽可能多地涵盖技巧。您也可以按任何顺序阅读它们,但按时间顺序阅读是一个好主意,以了解我们是如何到达这里的。

不要再说了,让我们直接跳到我们的第一个例子。

在深入研究 CSS 之前,让我们检查一下标记

<div class="gallery">
  <img src="..." alt="...">
  <img src="..." alt="...">
  <img src="..." alt="...">
  <img src="..." alt="...">
</div>

只有几个 <img> 标签在一个 div 包装器中,对吧?请记住,本系列的主要挑战是尽可能少地使用 HTML。我们在这个系列中看到的例子都使用完全相同的 HTML 标记。没有额外的 div、包装器等等。我们所需要的是包含在包装器元素中的图像。

现在让我们检查一下 CSS

.gallery {
  --g: 6px; /* the gap */

  display: grid;
  width: 450px; /* the size */
  aspect-ratio: 1; /* equal height */
  grid: auto-flow 1fr / repeat(3, 1fr);
  gap: var(--g);
}
.gallery img:nth-child(2) {
  grid-area: 1 / 2 / span 2 / span 2;
}
.gallery img:nth-child(3) {
  grid-area: 2 / 1 / span 2 / span 2;
}

基本上,这是一个具有三个相等列的方形网格。从那里,所有发生的只是第二张和第三张图像被明确地放置在网格上,允许第一张和最后一张图像自动围绕它们布局。

这种自动行为是 CSS 网格的一个强大功能,称为“自动放置”。行数也是如此——没有一个被明确定义。浏览器会“隐式”地根据项目的放置来创建它们。我有一个 非常详细的文章,探讨了这两个概念。

您可能想知道那些 gridgrid-area 属性值是怎么回事。它们看起来很奇怪,很难理解!这是因为我选择了 CSS grid 简写属性,它非常有用,但接受了来自其组成属性的大量值。您可以在 年鉴 中看到它们。

但您真正需要知道的是

grid: auto-flow 1fr / repeat(3, 1fr);

…相当于这个

grid-template-columns: repeat(3, 1fr);
grid-auto-rows: 1fr;
DevTools style rules for the grid property.
您也可以使用您最喜欢的 DevTools 进一步验证。

grid-area 属性也是如此。如果我们打开 DevTools 并检查我们的声明:grid-area: 1/2/span 2/span 2; 你会得到以下结果

grid-area: 1 / 2 / span 2 / span 2;

…这与写出所有这些内容是一样的

grid-row-start: 1; /* 1st row */
grid-column-start: 2; /* 2nd column */
grid-row-end: span 2; /* take 2 rows */
grid-column-end: span 2; /* take 2 columns */

另一个 grid-area 声明也是如此。当我们将所有内容放在一起时,这就是我们得到的结果

The different images labeled by number on the grid.

是的,第二张和第三张图像在中间重叠。这并非错误!我故意将它们彼此叠加,以便我可以应用一个 clip-path 从每个图像中剪切一部分,并获得最终结果

Showing the effect with and without clip-path.

我们如何做到这一点?我们可以使用 CSS clip-path 属性剪切第二张图像(img:nth-child(2))的左下角

clip-path: polygon(0 0, 100% 0, 100% 100%, calc(50% + var(--g) / 4) 100%, 0 calc(50% - var(--g) / 4))

以及第三张图像的右上角

clip-path: polygon(0 0, calc(50% - var(--g) / 4) 0, 100% calc(50% + var(--g) / 4), 100% 100%, 0 100%);

我知道,我知道。那是很多数字等等。我确实有一篇 文章,详细介绍了这种技术。

就是这样,我们有了第一个图像网格!我在 <img> 选择器上添加了一个灰度 过滤器 以获得那个整洁的小悬停效果。

分割图像显示

让我们尝试一些不同的东西。我们可以利用我们学到的关于剪切图像角落的知识,并将其与一个不错的效果结合起来,在悬停时显示完整的图像。

此网格配置比上一个简单得多,因为我们只需要两个重叠的图像

.gallery {
  display: grid;
}
.gallery > img {
  grid-area: 1 / 1;
  width: 350px; /* the size */
  aspect-ratio: 1; /* equal height */
}

两个相同大小的图像相互叠加(感谢 grid-area: 1 / 1)。

悬停效果依赖于 clip-path 的动画。我们将剖析第一张图像的代码以了解其工作原理,然后将相同的内容插入第二张图像中,并更新值。请注意,我们有三种不同的状态

  1. 当没有图像被悬停时,每张图像的一半被显示出来。
  2. 当我们悬停在第一张图像上时,它会更完整地显示出来,但保留了一个小角落裁剪。
  3. 当我们悬停在第二张图像上时,第一张图像只显示了一个小三角形。
Showing the three clipping states of the hover effect.

在每种情况下,我们都具有三角形形状。这意味着我们需要为 clip-path 值提供一个三点多边形。

什么?第二种状态不是三角形,更像是一个切角的正方形。

你说得对,但如果我们仔细观察,可以发现一个“隐藏”的三角形。让我们在图像上添加一个 box-shadow

哈!你注意到它了吗?

Showing the transition between states with the overflow shape revealed to explain how it works.

这是一种什么样的魔法?这是一个鲜为人知的事实,clip-path 接受超出 0%-100% 范围的值,这使我们能够创建“溢出”的形状。(是的,我刚编造了这个词。不客气。)这样,我们只需要处理三个点,而不是从可见部分创建相同形状所需的五个点。优化 CSS 胜利!

这是我们在将多边形值插入 clip-path 属性后得到的代码

.gallery > img:first-child {
  clip-path: polygon(0 0, calc(100% + var(--_p)) 0 , 0 calc(100% + var(--_p)))
}
.gallery > img:last-child {
  clip-path: polygon(100% 100%, 100% calc(0% - var(--_p)), calc(0% - var(--_p)) 100%)
}

请注意 --_p 变量。我使用它来稍微优化代码,因为我们添加了悬停过渡。我们无需更新整个 clip-path,只需更新此变量以获得移动效果。这是一个视频,展示点在每个状态之间应该如何移动

我们可以将一个 transition 添加到 <img> 选择器上,然后更新状态的 --_p 变量以获得最终效果

.gallery {
  --g: 8px; /* the gap */
}
.gallery > img {
  /* etc. */
  --_p: calc(-1 * var(--g));
  transition: .4s .1s;
}
.gallery:hover > img:last-child,
.gallery:hover > img:first-child:hover{
  --_p: calc(50% - var(--g));
}
.gallery:hover > img:first-child,
.gallery:hover > img:first-child:hover + img {
  --_p: calc(-50% - var(--g));
}

如果我们不考虑图像之间的间隙(在代码中定义为 --g),那么 --_p 的三个值分别是 0%50%-50%。每个值定义了我们之前解释过的一种状态。

饼图图像显示

让我们从上一个例子提高难度级别,并尝试使用四张图像而不是两张图像来完成相同的技巧。

很酷,对吧?每张图像都是圆周的四分之一,并且在悬停时,我们有一个动画将图像转换为覆盖剩余图像的完整圆形。效果看起来可能很不可能,因为无法旋转点并将它们转换为填充圆形。实际上,我们根本没有旋转任何点。这是一个幻觉!

对于这个例子,我将只关注 clip-path 动画,因为网格配置与上一个例子相同:四个大小相同的图像相互叠加。

还有一个视频值得无聊而冗长的解释

clip-path 由七个点组成,其中三个点位于固定位置,其他点按照视频中显示的方式移动。效果在运行缓慢时看起来不那么酷,但我们可以看到 clip-path 在形状之间是如何变化的。

如果我们添加 border-radius 并使其更快,效果会更好一点

通过使其像原始示例中那样更快,我们获得了圆周的四分之一转换为完整圆形的完美幻觉。这是我们第一个图像序列中 clip-path 的多边形值

.gallery > img:nth-child(1) {
  clip-path: polygon(50% 50%, calc(50% * var(--_i, 0)) calc(120% * var(--_i, 0)), 0 calc(100% * var(--_i, 0)),0 0, 100% 0, 100% calc(100% * var(--_i, 0)), calc(100% - 50% * var(--_i, 0)) calc(120% * var(--_i, 0)));
}
.gallery > img:hover {
 --_i: 1;
}

和往常一样,我使用一个变量来优化代码。变量将在 01 之间切换以更新多边形。

其他图像也是如此,但具有不同的 clip-path 配置。我知道这些值可能看起来很难理解,但您始终可以使用在线工具(例如 Clippy)来可视化这些值。

图像马赛克

您知道马赛克,对吧?它是一种艺术风格,用较小的单个碎片(如彩色石头)来创建装饰性图案。但它也可以是由其他较小图像组成的合成图像。

而且,您猜对了:我们完全可以在 CSS 中做到这一点!

首先,让我们想象一下,如果没有 clip-path,只有五张重叠的图像,情况会怎么样

我在这个视频中有点作弊,因为我正在检查代码以识别每个图像的区域,但这就是您在脑子里需要做的事情。对于每个图像,尝试完成缺失的部分以查看完整的矩形,并通过此操作,我们可以确定每个图像的位置和大小。

我们需要找到网格需要多少列和多少行

  1. 我们有两个大图像并排放置,每个图像占据网格宽度的一半和整个网格高度。这意味着我们可能需要 **两列**(一个用于两个图像)和 **一行**(用于整个网格高度)。
  2. 我们中间的图像与另外两个图像重叠。这意味着我们实际上需要 **四列** 而不是两列,尽管我们仍然只需要 **一行**。
  3. 最后两个图像分别占据网格的一半,就像前两个图像一样。但它们只有网格高度的一半。我们可以使用我们已经拥有的现有列,但我们需要 **两行** 而不是一行来解释这些图像占网格高度的一半。
这让我们得到了一个整齐的 4×2 网格。

我不想让你认为我的切分方式是唯一的。这仅仅是我理解的方式。我相信有很多其他配置可以实现相同的布局!

让我们利用这些信息定义我们的网格,然后将图像放置在上面。

.gallery {
  display: grid;
  grid: repeat(2, 1fr) / repeat(4, 1fr); 
  aspect-ratio: 2;
}
.gallery img:nth-child(1) {
  grid-area: 1 / 1 / span 2 / span 2;
}
.gallery img:nth-child(2) {
  grid-area: 1 / 2 / span 2 / span 2;
}
.gallery img:nth-child(3) {
  grid-area: span 2 / span 2 / -1 / -1;
}
.gallery img:nth-child(4) {
  grid-area: 2 / 1 / span 1 / span 2;
}
.gallery img:nth-child(5) {
  grid-area: span 1 / span 2 / -1 / -1;
}

我认为你现在已经理解了发生了什么,因为我们已经看到了使用相同方法的一些示例。我们定义一个网格,并使用 `grid-area` 明确地将图像放置在上面,以便图像重叠。

好的,但是这次的 `aspect-ratio` 不同。

确实如此!如果你回到我们之前做出的推理,我们有前两个图像并排放置,它们是正方形,大小相同。这意味着网格的宽度需要等于其高度的两倍。因此,`aspect-ratio: 2`。

现在是 `clip-path` 值了。我们有四个三角形和一个菱形。

Showing the three unique shapes and the clip-path values that create them.
我们只展示了我们创建的三个独特形状,而不是五个总形状。

再说一次,我使用 Clippy 来处理所有这些数学内容。但说实话,我可以手动写出许多简单的形状,因为我多年来一直与 `clip-path` 密切合作,我相信你也可以通过练习做到!

复杂的图像马赛克

让我们提高难度,尝试另一个马赛克,这次对称性更少,形状更复杂。

别担心,你会发现它与我们刚刚制作的马赛克一样!再说一次,让我们想象每张图片都是一个矩形,然后根据我们看到的定义网格。

我们从两个图像开始。

它们都是正方形。第一个图像的大小是第二个图像的一半。第一个图像占据网格宽度的不到一半,而第二个图像占据超过一半,这给了我们 **两列**,它们的大小不同(第一个等于第二个的一半)。第一个图像的高度是它的一半,所以让我们自动假设我们也需要 **两行**。

让我们在布局中添加另一个图像。

这个图像让事情变得更加复杂!我们需要画一些线来确定如何更新网格配置。

我们将从 2×2 网格变为 **四列** 和 **三行**。相当不对称,对吧?在我们尝试弄清楚完整的尺寸之前,让我们看看当我们添加其他图像时,相同的布局是否仍然有效。

看起来我们仍然需要更多的行和列才能让所有东西都到位。根据该图像中的线条,我们将总共有 **五列** 和 **四行**。

即使布局很复杂,逻辑也很简单,对吧?我们逐个添加图像,找到适合所有图像的正确配置。现在我们需要确定每列和每行的尺寸。

如果我们说最小的行/列等于网格的一个分数 ( `1fr` **)**,我们将得到

grid-template-columns: 1fr 1fr 2fr 3fr 5fr;

…用于列,以及

grid-template-rows: 3fr 1fr 2fr 2fr;

…用于行。我们可以再次使用 `grid` 简写属性来合并这些内容。

grid: 3fr 1fr 2fr 2fr / 1fr 1fr 2fr 3fr 5fr;

你知道怎么做!将图像放置在网格上,并对它们应用 `clip-path`。

.gallery img:nth-child(1) {
  grid-area: 1 / 1 /span 2 / span 3;
  clip-path: polygon(0 0, 100% 0, 0 100%);
}
.gallery img:nth-child(2) {
  grid-area: 1/2/span 3/span 3;
  clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%);
}
.gallery img:nth-child(3) {
  grid-area: 1 / span 2 / -1 / -1;
  clip-path: polygon(0 0, 100% 0, 100% 100%);
}
.gallery img:nth-child(4) {
  grid-area: span 3 / 1 / -1 / span 3;
  clip-path: polygon(25% 0, 100% 60%, 50% 100%, 0 100%, 0 20%);
}
.gallery img:nth-child(5) {
  grid-area: span 3/span 3/-1/-1;
  clip-path: polygon(50% 0, 100% 100%, 0 100%);
}

我们可以在这里停止,我们的代码很好,但我们将做更多工作来优化 `clip-path` 值。由于图像之间没有间隙,我们可以利用图像重叠的事实来简化代码。这里有一个视频说明这个想法。

如你所见,中间的图像(带有相机的图像)不需要 `clip-path`,因为其他图像与它重叠,为我们提供了形状,而无需任何额外的工作!请注意,我们可以在左下角的图像上使用相同的溢出三点 `clip-path` 概念,以使代码更简洁。

最后,我们得到一个看起来很复杂的图像网格,只有四个 `clip-path` 声明——它们都是三点多边形!

总结

哇,对吧?我不知道你是什么感受,但我对如今 CSS 的能力永远不会感到厌倦。不久前,所有这些都需要冗长的技巧,并且肯定需要一些 JavaScript。

在本系列中,我们探索了许多不同类型的图像网格,从基本内容到今天制作的复杂马赛克。我们还获得了许多使用 CSS 剪切的实践经验——你肯定能在其他项目中使用!

但在结束之前,我有一些作业留给你……

这里有两个马赛克,我希望你使用我们在这里介绍的方法制作。一个比较“简单”,另一个有点棘手。如果你能在评论区分享你的作品,那就太棒了!我很想知道你的方法与我的方法有何不同!