花式图像装饰:单元素魔法

Avatar of Temani Afif
Temani Afif

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

正如标题所说,我们将装饰图像!网上有很多其他文章讨论了这个主题,但我们在这里要讲的内容有点不同,因为它更像是一个挑战。挑战是什么?仅使用 <img> 标签,没有任何其他元素来装饰图像。

没错,没有额外的标记,没有 div,也没有伪元素。就一个标签。

听起来很难吧?但在这篇文章(以及这个小系列中的其他文章)的最后,我会证明 CSS 足够强大,可以让我们在只使用一个元素的限制下,获得出色而惊艳的效果。

花式图像装饰系列

让我们从第一个例子开始

在深入研究代码之前,让我们列出一些无需额外元素或伪元素就可以为 <img> 设置样式的可能性。我们可以使用 borderbox-shadowoutline,当然还有 background。为图像添加背景可能看起来很奇怪,因为我们无法看到它,因为它会在图像后面——但诀窍是使用 padding 和/或 border 在图像周围创建空间,然后在该空间内绘制我们的背景。

我想你已经知道接下来会发生什么了,因为我提到了 background,对吧?没错,渐变!我们即将进行的所有装饰都依赖于大量渐变。如果你已经 关注我 一段时间了,我想这对你来说可能并不意外。😁

让我们回到第一个例子

img {
  --s: 10px; /* control the size */
  padding: var(--s);
  border: calc(2 * var(--s)) solid #0000;
  outline: 1px solid #000;
  outline-offset: calc(-1 * var(--s));
  background: conic-gradient(from 90deg at 1px 1px, #0000 25%, #000 0);
}

我们使用变量 --s 定义 padding 和透明的 border,以在图像周围创建一个等于该变量三倍的空间。

为什么我们使用 paddingborder 而不是其中一个?我们只需要其中一个就可以,但我需要这种组合来使用我的渐变,因为 background-clip 的默认初始值为 border-box,而 background-origin 等于 padding-box

这是一个分步说明,用于理解逻辑

最初,图像上没有任何边框,所以我们的渐变会使用 1px 的厚度创建两个段。 (我在这个特定的演示中使用的是 3px,这样更容易看到。) 我们添加一个有色边框,渐变在填充区域内仍然给我们相同的结果 (由于 background-origin),但它在边框后面重复。如果我们使边框颜色透明,我们可以使用重复,并获得我们想要的框架。

演示中的 outline 具有负偏移。这会在渐变的顶部创建一个正方形。就是这样!我们使用一个渐变和一个 outline 为图像添加了一个不错的装饰。我们本来可以使用更多渐变!但我总是尽量使代码尽可能简单,我发现添加一个 outline 更好。

这里有一个仅使用渐变的解决方案,我仅使用 padding 来定义空间。结果相同,但语法更复杂。

让我们尝试另一个想法

为此,我拿走了之前的示例,删除了 outline,并应用了 clip-path 来裁剪每边的渐变。clip-path 的值有点冗长且令人困惑,但这里有一个插图,可以更好地看到它的要点

Side-by-side comparison of the image with and without using clip-path.

我想你已经了解了主要思想。我们将结合使用背景、轮廓、剪切和一些遮罩来实现不同类型的装饰。我们还将考虑一些很酷的悬停动画作为额外奖励!我们到目前为止所看到的仅仅是对即将出现的内容的简要概述!

仅角框架

这一个使用四个渐变。每个渐变覆盖一个角,悬停时,我们将它们扩展以在图像周围创建完整的框架。让我们剖析其中一个渐变的代码

--b: 5px; /* border thickness */
background: conic-gradient(from 90deg at top var(--b) left var(--b), #0000 90deg, darkblue 0) 0 0;
background-size: 50px 50px; 
background-repeat: no-repeat;

我们将绘制一个大小为 50px 50px 的渐变,并将其放置在左上角 (0 0)。对于渐变的配置,这里有一个分步说明,展示了我是如何达到该结果的。

我们倾向于认为渐变只适用于两种颜色之间的过渡。但实际上,我们可以用它们做更多事情!它们在创建不同形状时特别有用。诀窍是确保我们在颜色之间有硬停止——如上面的例子——而不是平滑过渡

#0000 25%, darkblue 0

这基本上是在说:“用透明颜色填充渐变,直到区域的 25%,然后用 darkblue 填充剩余区域。”

你可能对 0 值感到困惑。这是一个简化语法的技巧。实际上,我们应该使用它来在颜色之间创建一个硬停止

#0000 25%, darkblue 25%

这样更有逻辑!透明颜色在 25% 结束,darkblue 恰好在透明度结束的地方开始,形成了一个硬停止。如果我们用 0 替换第二个,浏览器会为我们完成工作,因此这是一种更有效的做法。

规范 中的某处,它说

如果 颜色停止过渡提示 的位置小于列表中之前任何颜色停止或过渡提示的指定位置,将其位置设置为之前任何颜色停止或过渡提示的最大指定位置。

0 始终小于任何其他值,因此浏览器会始终将其转换为声明中之前出现的最大值。在本例中,该数字为 25%

现在,我们将相同的逻辑应用于所有角,最终得到以下代码

img {
  --b: 5px; /* border thickness */
  --c: #0000 90deg, darkblue 0; /* define the color here */
  padding: 10px;
  background:
    conic-gradient(from 90deg  at top    var(--b) left  var(--b), var(--c)) 0 0,
    conic-gradient(from 180deg at top    var(--b) right var(--b), var(--c)) 100% 0,
    conic-gradient(from 0deg   at bottom var(--b) left  var(--b), var(--c)) 0 100%,
    conic-gradient(from -90deg at bottom var(--b) right var(--b), var(--c)) 100% 100%;
  background-size: 50px 50px; /* adjust border length here */
  background-repeat: no-repeat;
}

我引入了 CSS 变量以避免一些冗余,因为所有渐变都使用相同的颜色配置。

对于悬停效果,我所做的只是增加渐变的大小以创建完整的框架

img:hover {
  background-size: 51% 51%;
}

是的,它是 51% 而不是 50%——这会创建一个小的重叠并避免可能出现的间隙。

让我们尝试使用相同技术的其他想法

这次我们只使用两个渐变,但动画更复杂。首先,我们更新每个渐变的位置,然后增加它们的大小以创建完整的框架。我还引入了更多变量,以便更好地控制颜色、大小、厚度,甚至图像和框架之间的间隙。

img {
  --b: 8px;  /* border thickness*/
  --s: 60px; /* size of the corner*/
  --g: 14px; /* the gap*/
  --c: #EDC951; 

  padding: calc(var(--b) + var(--g));
  background-image:
    conic-gradient(from  90deg at top    var(--b) left  var(--b), #0000 25%, var(--c) 0),
    conic-gradient(from -90deg at bottom var(--b) right var(--b), #0000 25%, var(--c) 0);
  background-position:
    var(--_p, 0%) var(--_p, 0%),
    calc(100% - var(--_p, 0%)) calc(100% - var(--_p, 0%));
  background-size: var(--s) var(--s);
  background-repeat: no-repeat;
  transition: 
    background-position .3s var(--_i,.3s), 
    background-size .3s calc(.3s - var(--_i, .3s));
}
img:hover {
  background-size: calc(100% - var(--g)) calc(100% - var(--g));
  --_p: calc(var(--g) / 2);
  --_i: 0s;
}

为什么 --_i--_p 变量在名称中带下划线?下划线是我用来考虑“内部”变量的命名约定的一部分,这些变量用于优化代码。它们没什么特别的,但我希望在用于控制框架的变量 (如 --b--c 等) 与我用来缩短代码的变量之间有所区别。

代码可能看起来很复杂,不容易理解,但我写了一个 三部分系列,详细介绍了这种技术。我强烈建议您至少阅读第一篇文章,以了解我是如何得到上面代码的。

这里有一个插图,可以更好地理解不同的值

Showing the same image of two classic cars three times to illustrate the CSS variables used in the code.

框架显示

让我们尝试另一种类型的动画,在悬停时显示完整的框架

很酷,对吧?如果你仔细观察,你会注意到线条在鼠标移出时会以相反的方向消失,这使得效果更加花哨!我在 之前的一篇文章 中使用过类似的效果。

但这次,我没有覆盖整个元素,而是只覆盖了一小部分,通过定义一个 height 来获得类似于这样的效果

这是我们框架的顶部边框。我们在图像的每一侧重复相同的过程,我们就有了悬停效果

img {
  --b: 10px; /* the border thickness*/
  --g: 5px; /* the gap on hover */
  --c: #8A9B0F; 

  padding: calc(var(--g) + var(--b));
  --_g: no-repeat linear-gradient(var(--c) 0 0);
  background: 
    var(--_g) var(--_i, 0%) 0,
    var(--_g) 100% var(--_i, 0%),
    var(--_g) calc(100% - var(--_i, 0%)) 100%,
    var(--_g) 0 calc(100% - var(--_i, 0%));
  background-size: var(--_i, 0%) var(--b),var(--b) var(--_i, 0%);
  transition: .4s, background-position 0s;
  cursor: pointer;
}
img:hover {
  --_i: 100%;
}

如你所见,我将相同的渐变应用了四次,每次都有不同的位置,以便一次只覆盖一侧。

再来一个?走着!

这一个看起来有点棘手,而且确实需要一些想象力才能理解两个圆锥渐变是如何实现这种魔法的。这里有一个演示,说明了其中一个渐变

伪元素模拟渐变。它最初不在视野内,悬停时,我们首先更改它的位置以获得框架的顶端。然后我们增加高度以获得右端。渐变形状类似于我们在上一节中使用的形状:两个段覆盖两侧。

但为什么我要将渐变的宽度设为 200%?你可能会认为 100% 就足够了,对吧?

100% 应该足够了,但是如果我保持它的宽度等于 100%,我就无法像我想要的那样移动渐变。这是与 background-position 工作方式相关的另一个小技巧。我在之前的一篇文章中介绍了这一点。我还在 Stack Overflow 上发布了一个答案来解决这个问题。我知道阅读的内容很多,但它真的值得你的时间。

既然我们已经解释了一个渐变的逻辑,第二个渐变就很容易了,因为它做的事情完全一样,只是覆盖了左边缘和下边缘。我们所要做的就是交换几个值,我们就完成了。

img {
  --c: #8A9B0F; /* the border color */
  --b: 10px; /* the border thickness*/
  --g: 5px;  /* the gap */

  padding: calc(var(--g) + var(--b));
  --_g: #0000 25%, var(--c) 0;
  background: 
    conic-gradient(from 180deg at top    var(--b) right var(--b), var(--_g))
     var(--_i, 200%) 0 / 200% var(--_i, var(--b))  no-repeat,
    conic-gradient(            at bottom var(--b) left  var(--b), var(--_g))
     0 var(--_i, 200%) / var(--_i, var(--b)) 200%  no-repeat;
  transition: .3s, background-position .3s .3s;
  cursor: pointer;
}
img:hover {
  --_i: 100%;
  transition: .3s, background-size .3s .3s;
}

如您所见,两个渐变几乎相同。我只是交换了大小和位置的值。

帧旋转

这次我们不会在图像周围绘制框架,而是调整现有框架的外观。

你可能想知道我到底是如何将一条直线变成一条倾斜线的。不,魔法与那不同。这只是我们通过将四个渐变的简单动画组合在一起而得到的错觉。

让我们看看顶部渐变的动画是如何制作的。

我只是更新了重复渐变的位置。还没有什么花哨的!让我们对右侧做同样的事情。

你开始明白诀窍了吗?两个渐变在拐角处相交,创造了直线变为倾斜线的错觉。让我们删除轮廓并隐藏溢出,以便更好地看到它。

现在,我们添加另外两个渐变来覆盖剩余的边缘,我们就完成了。

img {
  --g: 4px; /* the gap */
  --b: 12px; /* border thickness*/
  --c: #669706; /* the color */

  padding: calc(var(--g) + var(--b));
  --_c: #0000 0 25%, var(--c) 0 50%;
  --_g1: repeating-linear-gradient(90deg ,var(--_c)) repeat-x;
  --_g2: repeating-linear-gradient(180deg,var(--_c)) repeat-y;
  background:
    var(--_g1) var(--_p, 25%) 0, 
    var(--_g2) 0 var(--_p, 125%),
    var(--_g1) var(--_p, 125%) 100%, 
    var(--_g2) 100% var(--_p, 25%);
  background-size: 200% var(--b), var(--b) 200%;
  transition: .3s;
}
img:hover {
  --_p: 75%;
}

如果我们使用这段代码并稍作调整,我们可以获得另一个很酷的动画。

你能在这个例子中找到逻辑吗?这是你的作业!代码看起来可能很吓人,但它使用与我们之前看过的例子相同的逻辑。尝试隔离每个渐变,并想象它如何动画。

总结

一篇文章中包含这么多渐变!

当然很多,我早就提醒过你!但是,如果挑战是使用单个元素和伪元素来装饰图像,我们只剩下很少的选择,而渐变是最强大的选择。

如果你对某些解释感到困惑,别担心。我总是推荐我的一些旧文章,在那里我会更详细地介绍我们在本挑战中循环使用的一些概念。

我将以最后一个演示结尾,以让你在该系列的下一篇文章发布之前有所期待。这次,我使用 radial-gradient() 来创建另一个有趣的悬停效果。我会让你分析代码,以了解它是如何工作的。如果你遇到困难,请在评论中提出问题!

花式图像装饰系列