CSS 幻术 | 有关光影效果的黑魔法

点击上方 前端瓶子君,关注公众号

回复算法,加入前端编程面试算法每日一题群

来源:仿生狮子

https://juejin.cn/post/6965488051695353886

前言

总听人说光和影是孪生兄弟,有光就有影。其实不然,如果没有光线强弱的对比,也就不会有阴影的存在。我们总是依靠物体表面的反光来感知世界,假设所有物体都能完全吸收光线,那世界将变得黑漆漆一片。

所谓只有理解光,才能驾驭阴影。好的设计师往往都是用光高手,能通过复杂的光影向读者传达出物体的质感、空间感以及层次感texture。他们画出来的设计稿都是漂漂亮亮的,这可苦了广大前端同胞!在浏览器中,我们只能用寥寥几个 CSS 属性,束手束脚地同时还想方设法地还原设计稿。毕竟,相比我们用的 CSS 手枪,设计师们用的 AE、C4D 看起来就像大炮一样!

常见光影效果

好在我们可以暂且假以性能为由,继续正大光明地怎么简单怎么来(汗)。就像一提到光影效果,大家第一反应肯定是操起 box-shadow、text-shaodw、drop-shadow 三件套直接开画。就大部分场景来说,这些属性还挺好用的,可用它实现多种效果,比如单侧投影、空心投影和投影动画。若涉及到彩色阴影、长投影或是倒影,就需要结合其它 CSS 属性打辅助了。

彩色投影,可以让伪元素继承父元素的背景,再加模糊滤镜即可。这个思路也可以用来制作毛玻璃效果frosted-glass。

彩色投影
.avator {
  position: relative;
  background: 'xxx';
}
.avator::after {
  content: "";
  position: absolute;
  top: 10%;
  width: 100%;
  height: 100%;
  /* 伪元素继承父元素背景 */
  background: inherit;
  /* 再加一些稀奇古怪的滤镜,调一调参数 */
  filter: blur(10px) brightness(80%) opacity(.8);
  z-index: -1;
}
复制代码

制作倒影可以使用 -webkit-box-reflect 属性box-reflect。兼容性还不错,除了火狐和 IE,其余浏览器都能用。另一种方法则是用伪元素将父元素复制一份,再 transform 倒转一下位置。

https://codepen.io/TheDutchCoder/pen/IKqpA

阴影的另一面是高光。画高光的思路可以直接套画阴影的思路,只不过需要将投影的颜色改为半透明白色。

https://codepen.io/Lionad/pen/rNWMVGb

另一种方法是用背景渐变或伪元素模拟高光。若再配合 CSS 动画,可以轻松实现扫光等效果。

扫光动画
body:before {
  content: "";
  position: absolute;
  top: 0;
  width: 200vw;
  height: 35px;
  background-color: rgba(255, 255, 255, 0.4);
  transform: rotate(45deg);
  animation: scan-light 2s ease-in infinite;
}
@keyframes scan-light {
  from {
    right: -100vw;
  }
  to {
    right: 40vw;
  }
}
复制代码

进阶光影效果

纹理

以上提到的几种绘制光影的方法,主要用来传达物体的形状及位置。比方说,倒影和渐变高光可以用来传达物体的质感,展示物体光滑到足以发生镜面反射的表面。当然,这是理想情况。现实中的物体很少有平滑的表面,就算肉眼可见的光滑表面,微观上而言也是坑坑洼洼不堪入目。

物体的微观表面

一旦我们开始使用 CSS 去模拟高级光影,首先碰到的难题就是如何处理磨砂表面。以下介绍一种简单实现磨砂表面的思路,在寻常场景用用还是阔以的。

物体表面是什么样,我们就给它贴什么样的图片,这种方法叫做材质贴图。我们用一个简单的材质贴图为例,先用 PS 弄一张纯色的背景,然后分别随机填充一些稍微亮一点高光和稍微暗一点的像素点linegradient-material,就能获得类似下图结果。

材质图片

我们再把这张材质平铺为文档背景,就可以得到类似磨砂金属般的表面纹理(由于图片压缩的原因,效果可能不好,可直接前往Codepen查看细节)。

https://codepen.io/Lionad/pen/mdWWxdg

不同的高光和阴影细节会给人不同的感受,比方说这里有一张雪花电视效果图,其高光像素和阴影像素的对比度要比磨砂金属表面的大得多。

https://codepen.io/joeyhoer/pen/CojIk

使用图片的不便之处在于没有办法边调整细节边预览,并且 PS 可能超出前端的技术栈范围了。好在我们还有 SVG 这个神器why-svg。以下是一套“标准的”材质生成代码,可以用来生成非常多种类的材质。

<svg width="0" height="0">
  <filter id="surface">
    <feTurbulence type="fractalNoise" baseFrequency='0.03 0.06' numOctaves="30" />
    <feDiffuseLighting lighting-color='#ffe8d5' surfaceScale='2'>
      <feDistantLight elevation='10' />
    </feDiffuseLighting>
  </filter>
</svg>
<style>
body {
  margin: 0;
  width: 100vw;
  height: 100vh;
  overflow: hidden;
  filter: url(#surface);
}
</style>
复制代码

其中,feDiffuseLighting 是一种噪音滤镜,可以创建出随机的材质图片。feDiffuseLighting 是光源滤镜。feDistantLight 指示用平行光作为光源。

光源?弄个材质还要这么复杂嘛?

先别急,光源其实也就几种:点状光、平行光、聚光。可以简单理解成电灯泡、太阳以及戏剧灯。由于我们还将在表面材质的话题中停留一会儿,暂且只需要用到平行光。

https://developer.mozilla.org/zh-CN/docs/Web/SVG/Element/feDiffuseLighting

大致了解了上面那段 SVG 是什么意思,我们就开始愉快地调参数啦。

先试试降低灯光到表面的距离(减小 elevation)以增加高光面和阴影面的对比度。获得了以下看起来像是某种土壤的纹理。

土壤纹理

接下来拉高灯光,调整光照颜色(lighting-color),再把纹理弄粗糙一些(减小 baseFrequency),获得了类似大理石的纹理(也许有点像白色的牛皮纸)。

大理石纹理

增加 baseFrequency、调整表面基准高度 surfaceScale 获得平滑纹理,再调整灯光高度降低高光和阴影的对比度,得到了白石灰墙壁纹理。

白石灰墙壁
<svg width="0" height="0">
  <filter id="surface">
    <feTurbulence type="fractalNoise" baseFrequency='.95' numOctaves="80" result='noise' />
    <feDiffuseLighting in='noise' lighting-color='#fff' surfaceScale='1.4' result="grind">
      <feDistantLight azimuth='500' elevation='50' />
    </feDiffuseLighting>
    <feGaussianBlur in="grind" stdDeviation=".6"/>
  </filter>
</svg>
复制代码

各位应该发现了白石灰墙壁的代码相比“标准模板”增加了一个高斯模糊滤镜(feGaussianBlur)。原则上滤镜无限叠加,可以做出非常多好玩的效果。比方说,有大佬用 SVG 画云朵。对,你没听错。以下是用 SVG 画的云朵纹理cloud。

云朵材质

菲涅尔效应

说完了粗糙的表面怎么画,我们再看看画光滑表面又有哪些原则。说起水和金属这种有相对光滑的表面的材质,不得不提到菲涅尔效应。

一句话介绍菲涅尔效应:如果你站在湖边低头看脚下的水,你会发现水是透明的,反射不是特别强烈,能看到水底;如果你看远处的湖面,你会看见山和天空的倒影。

菲涅尔效应

每种材质都有各自的菲涅尔值,这是根据其折射率决定的,表明了会有多少光线被物体吸收,又有多少光线从物体表面反弹fresnel。以下用 chaosgroup.com 的案例做说明。

图中有一个粗糙的球体,右侧图片中的球体边缘反射了天光而显得边缘发光,左侧图片中球体边缘则没有此现象。球体表面粗糙,菲涅尔效应会变得非常微弱,所以左图是正确结果;如果球体类似金属或水面,表面光滑,那么正确结果应当类似右图。

了解菲涅尔效应之后,我们就可以根据经验凭空画一些高菲涅尔效应的物体。下图是 Oscar Salazar 用 CSS 画的水滴。他用 box-shadow 给水滴的下缘增加了大量的透明白色阴影来模拟菲涅尔效应。

水滴效果

如果对比了现实中的水滴,你会发现 Oscar Salazar 画的并不“真”。但是因为水滴的放大作用,再加上光影效果,十分抓人眼球,给人“神似”的感觉,所以并不会觉得画得有问题。

现实中的水滴

下图是 Envato Tuts+ 画的毛玻璃效果。左边是原版,右边是增加了菲涅尔效应后的修改版本。修改后的版本看起来像是边缘平滑的玻璃版,而不是塑料板儿一块draw-not-sure。

场景实战

限于技术,还有很多种类的光影效果文中没有提到,以后有机会的话出个续集吧(咕咕咕预定中)。这里我们用一个 CSS 绘制的书籍封面效果作为结尾,也顺便串联一下上文提到的技术。素材只给两张书籍封面图片,点此下载第一张,点此下第二张refferer,目标是实现以下效果compressed。

《乞力马扎罗山的雪》

《八百万种死法》

搭出框架

首先,观察图像,背景是一张纸,上面有一本书;光源在右上角,大概是平行光,光源高度离纸面不远。我们先用 HTML 搭出框架。

<div class="display-container">
  <!-- 纸背景材质层 -->
  <div class="paper" />
  <!-- 书的封面 -->
  <div class="book">
    <!-- 封面的纸的材质层 -->
    <div class="paper" />
    <!-- 用一张图片自动撑开封面高度 -->
    <img class="corner" src="xxx" />
  </div>
</div>
复制代码

处理纹理

然后我们用 SVG 调出类似纸面的纹理效果,打光,然后设为背景。

纸纹理
纸纹理打光后
<svg width="0" height="0">
  <filter id="surface">
    <feTurbulence type="fractalNoise" baseFrequency='.95 .95' numOctaves="80" result='noise' />
    <feDiffuseLighting in='noise' lighting-color='#004F85' surfaceScale='.8' result="grind">
      <feDistantLight azimuth='500' elevation='50' />
    </feDiffuseLighting>
    <feGaussianBlur in="grind" stdDeviation=".5"/>
  </filter>
</svg>
<div class="paper"></div>
<style>
  body {
    width: 100vw;
    height: 100vh;
    overflow: hidden;
  }
  .paper {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
  }
  .paper::before {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    filter: url(#surface);
  }
  .paper::after {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: radial-gradient(ellipse at 100% 0%, rgba(255,255,255,0.25), rgba(255,255,255,0.18) 50%, rgba(255,255,255,0.15) 70%, rgba(0,0,0,.1));
  }
</style>
复制代码

封面细节

紧接着开始绘制书籍封面,谨记有三个部分要处理:材质、高光和阴影。处理完之后结果如下。

为添加图片的封面

有一些小细节要注意。

  • 封面折痕的处理

  • 模仿闭塞阴影

  • 防止边缘过于锐利

先来说说折痕的处理。如果你手头有一本实体书,那再好不过了fold-mark。试着用闪光灯打光,看看折痕上的光影,应该能发现折痕不过就是一道亮面和一道暗面的组合。我们可以用渐变来模拟这条折痕。

封面折痕
.book-cover .book::after {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  z-index: 2;
  background-repeat: no-repeat;
  background-image:
    /* 1. 这条渐变是比较明显的那道折痕 */
    linear-gradient(to right, rgba(0,0,0,0.1) 0.3%, rgba(255,255,255,0.09) 1.1%, transparent 1.3%),
    /* 2. 这条一像素的渐变是封面最左侧的折痕(没有暗面) */
    linear-gradient(to right, rgba(0,0,0,0.2) 0, rgba(255,255,255,0.08) 0%, transparent 0.5%);
  background-size: 50% 100%, 50% 100%;
  background-position: 0% top, 9% top;
}
复制代码

然后再说说闭塞阴影。闭塞阴影的概念非常简单,指两个物体靠得比较近,遮住了光线;靠得越近的地方,阴影就越黑occlusion-shadow。对应到 CSS,drop-shadow 产生的阴影很“实”,但不能叠加;box-shadow 产生的阴影可调节阴影范围,但会向四周扩散。我们可以通过叠加 drop-shadow 和 box-shadow 的方式来模拟出更真实的阴影效果。

.book-cover .book {
  position: relative;
  /* 由于阴影对视觉中心有影响,所以把书整体向右上方挪一些 */
  margin-top: -1vh;
  margin-right: -1vh;
  width: 32%;
  max-width: 600px;
  font-size: 0;
  box-shadow: 
    -55px 40px 30px 0 rgb(0 0 0 / 10%), 
    -27px 25px 35px -5px rgb(0 0 0 / 20%),
    -10px 10px 15px 5px rgb(0 0 0 / 10%), 
    -12px 12px 10px 0 rgb(0 0 0 / 20%),
    -7px 7px 8px 0 rgb(0 0 0 / 10%),
    -5px 5px 5px 0 rgb(0 0 0 / 20%),
    -2px 2px 3px 0 rgb(0 0 0 / 30%);
  filter: drop-shadow(-20px 20px 15px rgba(0, 0, 0, .65));
}
复制代码

实现效果如下图。位置一指闭塞阴影,离书越近则阴影越浓重;位置二是 box-shadow 向外扩散的效果,和光源位置相背,违反了人的认知经验,需要避免。

阴影效果

再是关于如何防止边缘过于锐化。~~还未处理前,书籍的边缘类似以下这张图。~~由于图片被压缩,什么细节都看不出来了,这里直接介绍一下防止边缘锐化的方案吧。把两张图片叠一起,下面那张图片模糊一像素,上面那张图片 border-radius 设置 2 像素,搞定。

封面边缘

成品展示

最后,把所有代码整合到一起,调调参数,改改细节,就完成啦。嘿嘿,再放一张效果图。可以到 Codepen 查看最终效果。

《罗生门》

啥,你想做一本能翻页的书?

那得去康康 turn.js 的实现turnjs。效果如下图,其中也有用到菲涅尔效应哦。

turn.js

阅读更多

希望本文能对你有所帮助,我是仿生狮子,各位下期见~

想看看这篇文章是如何被创造的?你能从我的博客项目中找到答案;欢迎 Star & Follow;也请大家多来我的线上博客逛逛,排版超 Nice 哦~


  1. 人话就是:物体的材质、形状以及位置。↩

  2. How to create a frosted glass effect using CSS?

  3. 《-webkit-box-reflect 属性简介及元素镜像倒影实现》↩

  4. 其实用渐变+随机的方法也能生成类似磨砂金属的材质贴图,这样就可以不需要引入外部图片了,但过于麻烦,不好调参,还不如 base64 来得方便。↩

  5. 请原谅我,我总是潜意识里把 SVG 当作 CSS 的超集,只因为可以用它来画画而且在画画方面可以吊打 CSS。↩

  6. 云朵效果优化(大佬优化大佬)↩

  7. 《理解光泽度的菲涅尔效应》↩

  8. “看起来像”和“现实”并不等同。就像许多画家为了营造气氛或达到想要的效果,会打破光影的物理限制。这是经验性的总结,当然,也见仁见智了。↩

  9. 由于 OSS 设置了防盗链,在第三方网页应该打不开这个链接。不过我开放了空 refferer 的访问,可以使用浏览器直接打开图片再保存。↩

  10. 由于图片压缩,演示的效果会打折。各位可以实操以体验完整效果及完整乐趣。↩

  11. 别说你的书还是新的。↩

  12. 对应游戏画面设置中的“环境光遮蔽”可能好理解一些。↩

  13. CSS 也能实现翻页效果,只不过效果没那么好,见:The Mad Magazine Fold-In Effect in CSS

最后

欢迎关注【前端瓶子君】✿✿ヽ(°▽°)ノ✿

回复「算法」,加入前端编程源码算法群,每日一道面试题(工作日),第二天瓶子君都会很认真的解答哟!

回复「交流」,吹吹水、聊聊技术、吐吐槽!

回复「阅读」,每日刷刷高质量好文!

如果这篇文章对你有帮助,「在看」是最大的支持

 》》面试官也在看的算法资料《《

“在看和转发”就是最大的支持

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: CSS的counter计数器是一种非常有用的技巧,用于在网页中实现自动计数和编号的功能。通过使用计数器,我们可以方便地对网页元素进行编号,比如列表、章节标题等。 首先,我们需要定义一个计数器。可以使用counter-reset属性来定义计数器并初始化它的值。例如,如果我们想要创建一个从1开始的计数器,可以这样写: ``` body { counter-reset: counterName 1; } ``` 这里的counterName是我们给计数器起的名字,可以自定义。1表示计数器的初始值。 接下来,我们可以在需要计数的元素中使用counter-increment属性来递增计数器的值。例如,如果我们想要在每个列表项前显示它的编号,可以这样写: ``` ul li:before { counter-increment: counterName; content: counter(counterName) ". "; } ``` 这里的counter-increment用于递增计数器的值,content用于显示计数器的值。我们使用了counter()函数来获取计数器的值,并在后面加上了一些文字(比如点号和空格)来实现编号的显示效果。 我们还可以根据需要在不同的元素中使用不同的计数器。只需要给不同的计数器起不同的名字,并在对应的元素中使用相应的计数器名字即可。 总的来说,CSS的counter计数器是一种非常灵活和强大的技巧,可以用于各种需要进行自动计数和编号的场景。通过定义计数器、递增计数器和使用counter()函数来获取计数器的值,我们可以轻松地实现网页元素的编号效果。 ### 回答2: 计数器(counter)是CSS中的一个功能强大的特性,可以用于在HTML文档中创建计数器,然后在样式规则中使用这些计数器来生成序号或标记。以下是使用计数器(counter)的一些技巧: 1. 创建一个计数器: 使用 `counter-reset` 属性可以创建一个计数器,并可以为其设定一个初始值。例如,可以创建一个以1为初始值的计数器: ```css .container { counter-reset: my-counter 1; } ``` 2. 更新计数器的值: 使用 `counter-increment` 属性可以更新计数器的值。可以在选择器中的任何位置使用这个属性。例如,每当 `li` 元素出现时,可以将计数器的值增加1: ```css li { counter-increment: my-counter; } ``` 3. 在内容中引用计数器的值: 使用 `content` 属性可以在样式规则中引用计数器的值,并将其插入到生成的内容中。可以使用计数器的名称作为 `content` 的值。例如,将计数器的值作为新的内容插入到列表项前面: ```css li::before { content: counter(my-counter) ". "; } ``` 4. 在不同的元素中使用多个计数器: 可以在同一个文档中使用多个不同的计数器,并为它们设定不同的初始值。这样可以为不同的元素生成不同的序号或标记。例如,可以为不同的标题元素创建不同的计数器: ```css h1 { counter-reset: h1-counter 1; } h2 { counter-reset: h2-counter 1; } ``` 5. 控制计数器的显示方式: 使用 `counter()` 函数可以对计数器的显示方式进行自定义。可以指定计数器的名称,以及任何显示格式。例如,可以将计数器的值格式化为罗马数字: ```css .container::after { content: counter(my-counter, upper-roman); } ``` 总结而言,计数器(counter)是CSS中一项非常实用的黑魔法技巧。通过创建、更新和引用计数器的值,可以在样式规则中生成序号或标记,并且可以通过自定义显示格式来控制计数器的外观。 ### 回答3: CSS中的计数器(counter)是一种非常强大的工具,可以用来计数、标记和显示元素的编号或序号。它可以通过一些技巧来实现各种有趣的效果。 首先,使用counter-reset属性来定义计数器并将其重置为指定的起始值。例如,可以使用"counter-reset: section 0;"来将名为"section"的计数器重置为0。 然后,可以通过counter-increment属性来递增计数器的值。例如,使用"counter-increment: section;"来递增名为"section"的计数器的值。 接下来,在需要显示计数器的地方,可以使用content属性来显示计数器的值。例如,使用"content: counter(section);"来在伪元素的内容中显示名为"section"的计数器的值。 可以进一步利用计数器,实现复杂的效果。例如,可以使用:before伪元素和content属性来在每个元素前面显示计数器的值,从而实现自动标号的效果。例如,使用"content: counter(section) '. ';"来在每个元素前面显示名为"section"的计数器的值,并跟随一个点号。 此外,还可以使用counter()函数获取和修改计数器的当前值。例如,可以使用"counter(section)"来获取名为"section"的计数器的当前值,并将其用作其他属性的值。 总的来说,计数器是CSS中非常强大且灵活的工具,可以用来实现各种复杂的效果。熟练掌握计数器的使用技巧,可以让我们的CSS代码更加精细和有趣。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值