本文译者为 360 奇舞团前端开发工程师
原文标题:Shines, Perspective, And Rotations: Fancy CSS 3D Effects For Images
原文链接:www.smashingmagazine.com/2023/07/shi...
原文作者: Temani Afif
闪光、视差和旋转:精美的 CSS 3D 图像效果
摘要:CSS 拥有各种技巧,能够将图像变成简洁的交互式元素。本文收集了一系列精美的 3D 图像效果,展示了这些 CSS 的强大功能。准备好了解它们的工作原理吧,我们将通过使用CSS的功能来为图像添加透视、深度、旋转,甚至光滑的光泽效果,这些效果可以在你的下一个项目中使用。
我们都认为 3D 效果很酷,对吧?我也这么认为,尤其是当它们与微妙的动画结合在一起时。在本文中,我们将探索一些 CSS 技巧来创建令人惊叹的 3D 效果!
"为什么我们还需要一篇关于 CSS 3D 效果的文章......不是已经有很多了吗?"
是的,但这篇文章有点特别,因为我们将使用尽可能少的 HTML。实际上,这是我们用来为图像制作一些非常惊人的 CSS 效果的唯一标记:
ini
<img src="" alt="">
就是这样!我们只需要一个 <img>
标签。其他的一切都将在CSS中完成。
下面是它的工作原理。我们将探索三种不同的效果,它们彼此之间没有联系,但可能会互相借鉴一些。你不需要一口气读完整篇文章。实际上,我建议一次阅读一个部分,花时间理解概念和底层代码的作用,然后再继续查看另一种效果。
目录
-
CSS 3D 闪光
-
CSS 3D 视差
-
CSS 3D 旋转
CSS 3D 闪光
对于第一个效果,我们将在图像上添加一种闪光的动画,并在鼠标悬停时稍微旋转。
codepen链接:codepen.io/t\_afif/pen...
看到了吗?图像在开始时略微倾斜,但在悬停时会自动恢复水平,同时表面反射出光泽。这是一种很好的方式,可以在UI界面中增加一些逼真感,而不会过分夸张。
在这个演示中,我首先在CSS中给图像添加了旋转效果:
css
img { transform: perspective(400px) rotate3d(1, -1, 0, 8deg);}img:hover { transform: perspective(400px) rotate3d(1, -1, 0, -8deg);}
rotate3d
允许我们定义图像旋转的轴线。我不会深入讲解具体的数学细节,但为了获得一个对角轴线,我们将z轴设为0
,并在x轴和y轴上使用1
或-1
。
然后,我们使用perspective
属性为图像添加一点不平衡感。400px
这个值或应用于旋转的值8deg
这两个值没有特定的逻辑,但我发现小角度结合大透视效果会产生不错的结果。你可以随意修改它们,也许你会为你的特定用例找到更好的值。
我们可以简化这个过程!为了避免在:hover
时重复编写代码,我们可以使用CSS变量在悬停时更新旋转角度的符号。这样,我们就不需要重新编写整个声明,只需将8deg
更改为-8deg
即可。
css
img { transform: perspective(400px) rotate3d(1, -1, 0, calc(var(--i, 1) * 8deg));}img:hover { --i: -1;}
请注意我在代码中使用了 calc()
。通过将度数值乘以1(由变量 --i
定义),我们得到默认值8deg
。然后,通过在悬停时将1更改为-1,让 calc()
完成繁重的计算工作。
这很有趣!但当我们开始处理光泽效果时,它变得更加有趣。一个直观的方法是在图像上方放置一个覆盖层来制作光泽效果。但请记住,我们只使用一个单独的 <img>
元素,添加覆盖层需要更多的标记。
你可能会想到使用伪元素。但很遗憾,在这里伪元素不适用于 <img>
标签。
我们要做的是使用CSS遮罩和动画渐变来"模拟"闪光效果。我说"模拟"是因为,实际上,你看到的图像是部分透明的。在悬停时,透明度会更新以创建出闪亮的效果。
我知道这并不容易理解,但如果你考虑到我们的背景是黑色的 ,是的,这就是技巧的一部分!使图像部分透明类似于使图像变暗。当图像被悬停时,我们调整透明度使其变亮。
以下是一个使用opacity
来更好理解我所说的内容的简化示例:
codepen链接:codepen.io/t\_afif/pen...
这就是基本的想法。现在,我们将使用linear-gradient()
遮罩来实现闪光效果。
css
/* *对角渐变,在中心部分不透明,在两侧半透明。*/mask: linear-gradient(135deg, #000c 40%, #000, #000c 60%);
在CSS中进行遮罩处理时,颜色并不重要,因为默认的遮罩模式会自动处理。关键是透明度通道,它决定了透明的程度。在我们的例子中,对角部分是不透明的,而两侧是部分透明的。#000c
相当于rgb(0 0 0 / 80%)
.
codepen链接:codepen.io/t\_afif/pen...
渐变效果非常微妙,因为我们只稍微减少了透明度。这是一件好事,因为我们不希望用户注意到图像默认情况下是部分透明的。
下一步是对渐变效果进行动画处理。我们增加它的尺寸,直到不透明的中心超出视野范围。然后,我们将其从图像的左上角移动到右下角
css
img { mask: linear-gradient(135deg, #000c 40%, #000, #000c 60%) 100% 100%/ /* 初始位置 右下角 */ 240% 240%; /* 宽 高 */}img:hover { mask-position: 0 0; /* 在悬停时移动到左上角 */}
来看一下吧!我们在悬停时有一个漂亮的闪光效果!
codepen链接:codepen.io/t\_afif/pen...
很酷,对吧?现在让我们将上面的闪光效果与3D旋转相结合,以获得完整的效果。
css
img { transform: perspective(400px) rotate3d(1,-1,0,calc(var(--i,1)*8deg)); mask: linear-gradient(135deg,#000c 40%,#000,#000c 60%) 100% 100%/240% 240%; transition: .4s; cursor: pointer;}img:hover { --i: -1; mask-position: 0 0;}
只需一个HTML元素和几行CSS代码,我们就可以实现这个效果。下面是一个图示,用来说明遮罩中使用的不同数值:
*演示 CSS 遮罩如何覆盖图像以及图像如何在悬停时滑动。*
演示 CSS 遮罩如何覆盖图像以及图像如何在悬停时滑动。
绿色框表示了渐变区域,蓝色线条定义了我们使用的颜色位置。初始状态下,渐变框被放置在100% 100%
的位置,悬停时,我们将其滑动到0 0
的位置。滑动效果将沿着图像移动渐变的对角部分(不透明部分),从而创建闪光效果。
这是完整的演示,我甚至为你提供了第二个变体样式,供你详细研究并了解其工作原理。
codepen链接:codepen.io/t\_afif/pen...
CSS 3D 视差
通常,我们认为"视差效果(parallax)"是一种用于在滚动过程中以不同速度改变元素位置的有趣效果。但我们也可以利用它来为图像创建流畅的悬停效果。
codepen链接:codepen.io/t\_afif/pen...
就像我们在上一节制作的闪光效果一样,我们开始时有一个略微倾斜的图像,在悬停时变得平直。但是,与其应用闪光效果不同,我们使用过渡将图像稍微滑动,使其看起来像是焦点随图像一起旋转,增加了立体感。
你可能会认为我们需要叠加两个相同图像的版本才能实现这个效果,但实际上不需要!这个效果只需要一个图像和几行用于"模拟"视差效果的CSS代码。是的,我称这个效果为"模拟"效果,因为它实际上并不是真正的视差实现,而是一种通过组合运动来欺骗大脑的效果!如果你想看到真正的视差效果,这是一个很好的例子。
图像在这里非常重要。为了获得完美的视觉效果,建议选择一个主要元素位于中心,背景是均匀的图像。就这个效果而言,这可能有些局限性,因此它可能并不适用于每个图像。
图像在悬停时旋转并改变视角,就像上一节中的闪光效果一样。然而,这一次,我们沿着 y 轴 ( rotateY()
) 而不是所有三个轴 ( rotate3d()
) 旋转。
css
img { transform: perspective(400px) rotateY(8deg);}img:hover { transform: perspective(400px) rotateY(-8deg);}
我们通过CSS裁剪和平移的结合来完成滑动运动。这是效果中最棘手的部分。以下是一个简化的演示,来说明主要思路:
codepen链接:codepen.io/t\_afif/pen...
我们有一个放置在方框中的图像,方框周围有一个绿色边框表示裁剪区域。裁剪区域是一个正方形,图像在右边稍微超出边界。在悬停时,我们将图像向左滑动(使用 transform: translateX()
),而裁剪区域保持原位。
如果我们隐藏超出裁剪区域的图像部分(使用 overflow: hidden
),并且应用和上一节中相同的旋转,那么我们就得到了我们想要的"模拟"视差效果:
codepen链接:codepen.io/t\_afif/pen...
但是,那个演示中使用了一个额外的 <div>
元素来实现效果。我们的挑战是在没有该元素的情况下完成相同的效果。这就是 clip-path
能够发挥作用的地方:
css
img { --f: .1; /* 视差系数(数值越小越好) */ --_f: calc(100 * var(--f) / (1 + var(--f))); width: 250px; /* image size */ aspect-ratio: calc(1 + var(--f)); object-fit: cover; clip-path: inset(0 var(--_f) 0 0); transition: .5s;}img:hover { clip-path: inset(0 0 0 var(--_f)); transform: translateX(calc(-1 * var(--_f)))}
--f
变量控制着这个效果,它描述了图像应该移动的程度。你会注意到我在使用它来计算一个略大于1的宽高比,以创建一个非方形的图像,然后我们通过裁剪来获取一个方形图像。--_f
定义了我们需要从图像中裁剪的部分,以获得1:1
的方形比例。
展示图像处于悬停状态时,clip-path
值如何变化。
clip-path
定义了裁剪区域,我们希望该区域保持固定。这就是为什么我们在悬停时添加了一个平移效果,将图像向与 clip-path
相反的方向移动。
codepen链接:codepen.io/t\_afif/pen...
我们将旋转效果加入其中,效果很完美:
css
img { --f: .1; /* 视差系数(数值越小越好) */ --r: 10px; /* 半径 */ --_f: calc(100%*var(--f)/(1 + var(--f))); --_a: calc(90deg*var(--f)); width: 250px; /* image size */ aspect-ratio: calc(1 + var(--f)); object-fit: cover; clip-path: inset(0 var(--_f) 0 0 round var(--r)); transform: perspective(400px) translateX(0px) rotateY(var(--_a)); transition: .5s;}img:hover { clip-path: inset(0 0 0 var(--_f) round var(--r)); transform: perspective(400px) translateX(calc(-1*var(--_f))) rotateY(calc(-1*var(--_a)));}
我稍微圆化了裁剪区域的边缘,使效果更加华丽。如果你想知道为什么我没有使用 border-radius
属性,那是因为该属性在裁剪区域上的效果不太好。幸运的是,clip-path
属性接受 round
值来实现类似的圆角效果。
就是这样!我们已经完成了这个图像上的炫酷悬停效果。
codepen链接:codepen.io/t\_afif/pen...
你可以调整视差系数和旋转角度,然后选择最适合你自己工作的图像。
CSS 3D 旋转
对于最后一个演示,我们将为图像添加深度,并将其转化为一个3D盒子。
codepen链接:codepen.io/t\_afif/pen...
对于这个效果,我将跳过旋转部分,因为它与我们刚刚在上一个示例中创建的相同。我们将专注于使用 outline
和 clip-path
属性来实现3D效果。下图说明了它们如何结合在一起形成一个3D盒子。
一个轮廓围绕的图像 (1),然后将轮廓偏移以覆盖整个图像 (2),以便可以将其裁剪成一个盒子的形状(3) 。
这是它的工作原理。首先,我们在图像的顶部和底部添加了一些padding,并应用了一个半透明黑色的outline
。
其次,我们应用了负的 outline-offset
,这样轮廓就会覆盖图像的左侧和右侧,而顶部和底部保持不变:
css
img { --d: 18px; /* 深度 */ padding-block: var(--d); outline: var(--d) solid #0008; outline-offset: calc(-1 * var(--d));}
请注意,我创建了一个变量 --d
,用于控制轮廓的厚度。这是赋予图像深度的关键。
最后一步是添加 clip-path
。我们需要一个具有八个点的多边形来实现。
显示剪切多边形形状的八个点。
红色的点是固定的,绿色的点是我们将通过动画来展现深度的点。我知道这离一个3D盒子还有些距离,但接下来的视觉效果,当我们添加旋转时,会给出更好的说明。
图像初始时的深度朝一个方向(左侧),然后在悬停时旋转以隐藏深度,使其呈现平面外观(中间)。我们还可以改变深度的方向(右侧)。
初始时,图像带有一定的旋转角度和透视效果。右侧的绿色点与红色点对齐。因此,我们隐藏右侧的轮廓,使其仅在左侧可见。这样我们就得到了深度在左侧的3D盒子。
在悬停时,我们将左侧的绿色点移动,并旋转图像。在动画进行到一半时,所有的绿色点与红色点对齐,旋转角度为0deg,隐藏了轮廓,使图像呈现平面外观。
然后,我们继续旋转,右侧的绿色点移动,而左侧的点保持不变。我们得到了相同的3D效果,但深度在右侧。
请先容忍我,接下来的代码块可能一开始看起来非常混乱。这是因为引入了一些新的变量和我们在 clip-path
属性上绘制的八个点的多边形。
css
@property --_l { syntax: "<flength>"; initial-value: 0px; inherits: true;}@property --_r { syntax: "<length>"; initial-value: 0px; inherits: true;}img { --d: 18px; /* depth */ --a: 20deg; /* angle */ --x: 10px; --_d: calc(100% - var(--d)); --_l: 0px; --_r: 0px; clip-path: polygon( /* 左侧的两个绿点 */ var(--_l) calc(var(--_d) - var(--x)), var(--_l) calc(var(--d) + var(--x)), /* 顶部的两个红点 */ var(--d) var(--d),var(--_d) var(--d), /* 右侧的两个绿点 */ calc(var(--_d) + var(--_r)) calc(var(--d) + var(--x)), calc(var(--_d) + var(--_r)) calc(var(--_d) - var(--x)), /* 底部的两个红点 */ var(--_d) var(--_d),var(--d) var(--_d) ); transition: transform .3s, --_r .15s, --_l .15s .15s;}/* 在悬停时更新多边形的点的位置 */img:hover{ --_l: var(--d); --_r: var(--d); --_i: -1; transition-delay: 0s, .15s, 0s;}
我使用了注释来帮助解释代码的作用。请注意,我使用变量 --_l
和 --_r
来定义绿色点的位置。我将这些变量从0动画渐变到深度(--d
)的值。在顶部的 @property
声明中,我们可以指定变量的值类型(<length>
),以便对其进行动画处理。
注意:
目前并非所有浏览器都支持 @property
。因此,我在演示中添加了一个备用方案,使用了稍微不同的动画效果。
在将多边形绘制在 clip-path
属性上之后,代码接下来应用了一个处理旋转的过渡效果(transition
)。完整的旋转持续时间为 0.3 秒
,所以绿色点需要在一半的持续时间(0.15 秒
)内进行过渡。在悬停状态下,多边形左侧的点立即移动(0 秒
),而右侧的点在一半的持续时间后移动(通过一个0.15
秒的延迟实现)。当我们离开悬停状态时,我们使用不同的延迟,因为我们需要右侧的点立即移动(0 秒
),而左侧的点在一半的持续时间后移动。
那个 --x
变量是什么意思?如果你查看我提供的第一张用来说明 clip-path
点的图像,你会注意到绿色点与顶部和底部边缘有轻微的偏移,这对模拟3D效果是合理的。--x
变量控制了偏移量的大小,但是背后的数学计算比较复杂,不容易用CSS来表达。因此,我们根据每种情况手动更新它,直到找到一个合适的值。
这就是我们的最终结果!
codepen链接:codepen.io/t\_afif/pen...
总结
我希望你喜欢这种对 CSS 3D 图像效果的探索,可能在这次探索中受到了一些挑战。我们使用了许多高级的CSS特性,包括蒙版、裁剪、渐变、过渡和计算,为图像创建了一些非常令人惊叹的悬停效果,这些效果通常不会经常见到。
而且我们只需要一行HTML代码就完成了这些效果。没有<div>
元素,没有类名或ID,没有伪元素,只需要一个 <img>
标签就足够了。是的,更多的标记可能会使CSS更简单,但它依赖于一个简单的HTML元素意味着CSS可以更广泛地使用。CSS足够强大,可以在一个元素上完成所有这些效果!