不止于 CSS

  • By Coco
  • Last update: Jan 7, 2023
  • Comments: 12

logo

CSS 奇技淫巧,在这里,都有。

本系列文章围绕 CSS/Web动画 展开,谈一些有趣的话题,内容天马行空,想到什么说什么,不仅是为了拓宽解决问题的思路,更涉及一些容易忽视或是十分有趣的 CSS 细节。

所有文章都在 Issues 中,同步更新到我的个人博客,觉得不错的可以点个 star 收藏支持。

iCSS 前端趣闻

所有文章也都会同步更新到我的公众号,想 Get 到最有意思的 CSS 资讯,千万不要错过我的 iCSS 公众号 😄

文章列表

120、Amazing!!CSS 也能实现极光?

119、神奇的滤镜!巧妙实现内凹的平滑圆角

118、利用 clip-path 实现动态区域裁剪

117、使用 CSS 轻松实现一些高频出现的奇形怪状按钮

116、巧用渐变实现高级感拉满的背景光动画

115、巧用滤镜实现高级感拉满的文字快闪切换效果

114、3D 穿梭效果?使用 CSS 轻松搞定

113、仅仅使用 HTML/CSS 实现进度条的 N 种方式

112、CSS 奇技淫巧 | 巧妙实现文字二次加粗再加边框

111、利用 CSS Overview 面板重构优化你的网站

110、小技巧 | 一行代码实现头像与国旗的融合

109、CSS 奇技淫巧 | 妙用 drop-shadow 实现线条光影效果

108、CSS 奇技淫巧 | 妙用混合模式实现文字镂空波浪效果

107、妙用 background 实现花式文字效果

106、实现一个会动的鸿蒙 LOGO

105、巧用模糊实现文字的 3D 效果

104、奇思妙想 CSS 3D 动画 | 仅使用 CSS 能制作出多惊艳的动画?

103、CSS 奇思妙想 | 使用 resize 实现强大的图片拖拽切换预览功能

102、CSS 即将支持嵌套,SASS/LESS 等预处理器已无用武之地?

101、【Web动画】科技感十足的暗黑字符雨动画

100、CSS 世界中的方位与顺序

99、巧妙的实现带圆角的三角形

98、CSS 奇思妙想 | 全兼容的毛玻璃效果

97、试试酷炫的 3D 视角

96、Web 动画原则及技巧浅析

95、CSS ::marker 让文字序号更有意思

94、Single Div 绘图技巧

93、新时代创意布局不完全指南

92、有意思的 ::maker 伪元素

91、使用 CSS prefers-* 规范,提升网站的可访问性与健壮性

90、小技巧!CSS 提取图片主题色功能探索

89、一种巧妙的使用 CSS 制作波浪效果的思路

88、探秘神奇的曲线动画 motion-path

87、新时代布局中一些有意思的特性

86、CSS 还能这样玩?奇思妙想渐变的艺术

85、CSS @property,让不可能变可能

84、CSS 文字装饰 text-decoration & text-emphasis

83、老生常谈之 CSS 实现三角形

82、巧用 SVG 滤镜制作有意思动效

81、有意思!不规则边框的生成方案

80、小技巧!CSS 整块文本溢出省略特性探究

79、奇思妙想 CSS 文字动画

78、巧用 -webkit-box-reflect 倒影实现各类动效

77、使用 mask 实现视频弹幕人物遮罩过滤

76、你可能不知道的 transition 技巧与细节

75、CSS奇思妙想 -- 使用 CSS 创造艺术图案

74、生僻标签 fieldset 与 legend 的妙用

73、CSS 奇思妙想边框动画

72、CSS 技巧一则:动态高度过渡动画

71、如何不使用 overflow: hidden 实现 overflow: hidden

70、水平垂直居中深入挖掘

69、一行 CSS 代码的魅力

68、使用纯 CSS 实现滚动阴影效果

67、探究 position-sticky 失效问题

66、CSS 艺术 -- 使用 background 创造各种美妙的背景

bg9

65、使用 tabindex 配合 focus-within 巧妙实现父选择器

64、CSS 技巧一则 -- 不定宽溢出文本适配滚动

textscroll

63、奇妙的 CSS MASK

62、使用 display: contents 增强页面语义

61、CSS 故障艺术

60、巧妙实现带圆角的渐变边框

59、深入理解 CSS(Cascading Style Sheets)中的层叠(Cascading)

58、巧用 CSS 实现酷炫的充电动画

57、使用 sroll-snap-type 优化滚动

56、在 CSS 中使用三角函数绘制曲线图形及展示动画

55、CSS 阴影动画优化技巧一则

54、Web 字体 font-family 再探秘

53、你所不知道的 CSS 负值技巧与细节

52、A Guide to CSS Rules

51、CSS 属性选择器的深入挖掘

50、探秘 flex 上下文中神奇的自动 margin

49、巧妙使用 CSS 控制动画行进

48、CSS 火焰,不在话下

47、不可思议的纯 CSS 实现鼠标跟随

46、有趣的 box-decoration-break

45、不可思议的纯 CSS 进度条效果

44、探究 CSS 混合模式\滤镜导致 CSS 3D 失效问题

43、你所不知道的 CSS 阴影技巧与细节

42、滚动视差? CSS不在话下

41、神奇的选择器 :focus-within

40、Pure CSS Button Effect

39、妙用 scale 与 transfrom-origin,精准控制动画方向

38、不可思议的纯 CSS 导航栏下划线跟随效果

如何使用纯 CSS 制作下述下划线跟随效果?

underline

37、两行 CSS 代码实现图片任意颜色赋色技术

36、text-fill-colorcolor 的异同

35、你所不知道的 CSS 滤镜技巧与细节

34、你所不知道的 CSS 动画技巧与细节

33、fixed 定位失效 || 不受控制的 position:fixed

32、CSS 新特性contain,控制页面的重绘与重排

31、纯 CSS 实现波浪效果!

CSSWaVe

30、奇妙的 CSS shapes(CSS图形)

29、不可思议的混合模式 background-blend-mode

28、不可思议的混合模式 mix-blend-mode

27、神奇的 conic-gradient 角向渐变

26、奇妙的-webkit-background-clip: text

25、vh、vw、vmin、vmax 知多少

24、纯 CSS 实现瀑布流布局

23、谈谈 CSS 关键字 initial、inherit 和 unset

22、纯 CSS 方式实现 CSS 动画的暂停与播放

21、提高 CSS 动画性能的正确姿势 | 盒子端 CSS 动画性能提升研究

20、巧妙地制作背景色渐变动画!

如何实现下述的背景色渐变动画?

lineargradient

19、深入探讨 CSS 特性检测 @supports 与 Modernizr

18、使用 position:sticky 实现粘性布局

17、再探究字体的渲染规则及 fallback 机制

16、你该知道的字体 font-family

15、reset.css 知多少

14、CSS命名方式是否有必要规范

13、引人瞩目的 CSS 自定义属性(CSS Variable)

12、结构性伪类选择器

11、IFC、BFC、GFC 与 FFC 知多少

10、巧妙的实现 CSS 斜线

使用单个标签,如何实现下图所示的斜线效果:

CSS slash

9、巧妙的多列等高布局

规定下面的布局,实现多列等高布局,要求两列背景色等高。

<div class="container">
    <div class="left">多列等高布局左</div> 
    <div class="right">多列等高布局右</div>
</div>

8、纯CSS的导航栏Tab切换方案

不用 Javascript,使用纯 CSS 方案,实现类似下图的导航栏 Tab 切换:

纯CSS的导航栏切换方案

7、全兼容的最后一条边界线问题

看看下图,常见于一些导航栏中,要求每行中最后一列的右边框消失,如何在所有浏览器中最便捷最优雅的实现?

6、全兼容的多列均匀布局问题

如何实现下列这种多列均匀布局:

image

5、纯 CSS 实现单行居中显示文字,多行居左显示,最多两行超过用省略号结尾

image

4、从倒影说起,谈谈 CSS 继承 inherit

3、层叠顺序(stacking level)与堆栈上下文(stacking context)知多少?

2、类似下面这样的条纹边框,只使用一个标签,可以有多少种实现方式 -- 从条纹边框的实现谈盒子模型:

image

1、下面这个左边竖条图形,只使用一个标签,可以有多少种实现方式:

image


(正在写但未完成的)

100、CSS 在夜间模式/深色模式中的使用与探索

99、如何使用 CSS 让你的浏览器卡死崩溃

101、噪声在CSS中的应用

95、CSS Ellipsis Beginning of String

106、浅谈 HTML 与 CSS 的图片处理(Picture 与 image-set)

107、CSS 中的障眼法

108、CSS 奇思妙想 | 仅使用 CSS 实现斐波那契螺旋线

110、学会内联使用 CSS 变量,减少代码提升效率

112、CSS at-rules(@) 规则扫盲

115、The CSS Paint API

Contact Me

如果有任何问题或者疑问,可以加 QQ 群:418766876 一起讨论,一键加群

也欢迎想讨论各种天马行空的 CSS 相关问题的小伙伴们加入。

qun

Stargazers over time

Stargazers over time

Github

https://github.com/chokcoco/iCSS

Comments(12)

  • 1

    前端优秀实践不完全指南

    本文其实应该叫,Web 用户体验设计提升指南。

    一个 Web 页面,一个 APP,想让别人用的爽,也就是所谓的良好的用户体验,我觉得他可能包括但不限于:

    • 急速的打开速度
    • 眼前一亮的 UI 设计
    • 酷炫的动画效果
    • 丰富的个性化设置
    • 便捷的操作
    • 贴心的细节
    • 关注残障人士,良好的可访问性
    • ...

    所谓的用户体验设计,其实是一个比较虚的概念,是秉承着以用户为中心的思想的一种设计手段,以用户需求为目标而进行的设计。设计过程注重以用户为中心,用户体验的概念从开发的最早期就开始进入整个流程,并贯穿始终。

    良好的用户体验设计,是产品每一个环节共同努力的结果。

    除去一些很难一蹴而就的,本文将就页面展示交互细节可访问性三个方面入手,罗列一些在实际的开发过程中,积攒的一些有益的经验。通过本文,你将能收获到:

    1. 了解到一些小细节是如何影响用户体验的
    2. 了解到如何在尽量小的开发改动下,提升页面的用户体验
    3. 了解到一些优秀的交互设计细节
    4. 了解基本的无障碍功能及页面可访问性的含义
    5. 了解基本的提升页面可访问性的方法

    页面展示

    就整个页面的展示,页面内容的呈现而言,有一些小细节是需要我们注意的。

    整体布局

    先来看看一些布局相关的问题。

    对于大部分 PC 端的项目,我们首先需要考虑的肯定是最外层的一层包裹。假设就是 .g-app-wrapper

    <div class="g-app-wrapper">
        <!-- 内部内容 -->
    </div>
    

    首先,对于 .g-app-wrapper,有几点,是我们在项目开发前必须弄清楚的:

    1. 项目是全屏布局还是定宽布局?
    2. 对于全屏布局,需要适配的最小的宽度是多少?

    对于定宽布局,就比较方便了,假设定宽为 1200px,那么:

    .g-app-wrapper {
        width: 1200px;
        margin: 0 auto;
    }
    

    利用 margin: 0 auto 实现布局的水平居中。在屏幕宽度大于 1200px 时,两侧留白,当然屏幕宽度小于 1200px 时,则出现滚动条,保证内部内容不乱。

    layout1

    对于现代布局,更多的是全屏布局。其实现在也更提倡这种布局,即使用可随用户设备的尺寸和能力而变化的自适应布局。

    通常而言是左右两栏,左侧定宽,右侧自适应剩余宽度,当然,会有一个最小的宽度。那么,它的布局应该是这样:

    <div class="g-app-wrapper">
        <div class="g-sidebar"></div>
        <div class="g-main"></div>
    </div>
    
    .g-app-wrapper {
        display: flex;
        min-width: 1200px;
    }
    .g-sidebar {
        flex-basis: 250px;
        margin-right: 10px;
    }
    .g-main {
        flex-grow: 1;
    }
    

    layout2

    利用了 flex 布局下的 flex-grow: 1,让 .main 进行伸缩,占满剩余空间,利用 min-width 保证了整个容器的最小宽度。

    当然,这是最基本的自适应布局。对于现代布局,我们应该尽可能的考虑更多的场景。做到:

    image

    底部 footer

    下面一种情形也是非常常见的一个情景。

    页面存在一个 footer 页脚部分,如果整个页面的内容高度小于视窗的高度,则 footer 固定在视窗底部,如果整个页面的内容高度大于视窗的高度,则 footer 正常流排布(也就是需要滚动到底部才能看到 footer)。

    看看效果:

    margintopauto

    嗯,这个需求如果能够使用 flex 的话,使用 justify-content: space-between 可以很好的解决,同理使用 margin-top: auto 也非常容易完成:

    <div class="g-container">
        <div class="g-real-box">
            ...
        </div>
        <div class="g-footer"></div>
    </div>
    
    .g-container {
        height: 100vh;
        display: flex;
        flex-direction: column;
    }
    
    .g-footer {
        margin-top: auto;
        flex-shrink: 0;
        height: 30px;
        background: deeppink;
    }
    

    Codepen Demo -- sticky footer by flex margin auto

    当然,实现它的方法有很多,这里仅给出一种推荐的解法。

    处理动态内容 - 文本超长

    对于所有接收后端接口字段的文本展示类的界面。都需要考虑全面(防御性编程:所有的外部数据都是不可信的),正常情况如下,是没有问题的。

    image

    但是我们是否考虑到了文本会超长?超长了会折行还是换行?

    image

    对于单行文本,使用单行省略:

    {
        width: 200px;
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
    }
    

    image

    当然,目前对于多行文本的超长省略,兼容性也已经非常好了:

    {
        width: 200px;
        overflow : hidden;
        text-overflow: ellipsis;
        display: -webkit-box;
        -webkit-line-clamp: 2;
        -webkit-box-orient: vertical;
    }
    

    image

    处理动态内容 - 保护边界

    对于一些动态内容,我们经常使用 min/max-widthmin/max-height 对容器的高宽限度进行合理的控制。

    在使用它们的时候,也有一些细节需要考虑到。

    譬如经常会使用 min-width 控制按钮的最小宽度:

    .btn {
        ...
        min-width: 120px;
    }
    

    image

    当内容比较少的时候是没问题的,但是当内容比较长,就容易出现问题。使用了 min-width 却没考虑到按钮的过长的情况:

    image

    这里就需要配合 padding 一起:

    .btn {
        ...
        min-width: 88px;
        padding: 0 16px
    }
    

    借用Min and Max Width/Height in CSS中一张非常好的图,作为释义:

    min-width-2

    0 内容展示

    这个也是一个常常被忽略的地方。

    页面经常会有列表搜索,列表展示。那么,既然存在有数据的正常情况,当然也会存在搜索不到结果或者列表无内容可展示的情形。

    对于这种情况,一定要注意 0 结果页面的设计,同时也要知道,这也是引导用户的好地方。对于 0 结果页面,分清楚:

    • 数据为空:其中又可能包括了用户无权限、搜索无结果、筛选无结果、页面无数据
    • 异常状态:其中又可能包括了网络异常、服务器异常、加载失败等待

    不同的情况可能对应不同的 0 结果页面,附带不同的操作引导。

    譬如网络异常:

    image

    或者确实是 0 结果:

    image

    关于 0 结果页面设计,可以详细看看这篇文章:如何设计产品的空白页面?

    小小总结一下,上述比较长的篇幅一直都在阐述一个道理,开发时,不能仅仅关注正常现象,要多考虑各种异常情况,思考全面。做好各种可能情况的处理

    图片相关

    图片在我们的业务中应该是非常的常见了。有一些小细节是需要注意的。

    给图片同时设置高宽

    有的时候和产品、设计会商定,只能使用固定尺寸大小的图片,我们的布局可能是这样:

    image

    对应的布局:

    <ul class="g-container">
        <li>
            <img src="http://placehold.it/150x100">
            <p>图片描述</p>
        </li>
    </ul>
    
    ul li img {
        width: 150px;
    }
    

    当然,万一假设后端接口出现一张非正常大小的图片,上述不加保护的布局就会出问题:

    image

    所以对于图片,我们总是建议同时写上高和宽,避免因为图片尺寸错误带来的布局问题:

    ul li img {
        width: 150px;
        height: 100px;
    }
    

    同时,给 <img> 标签同时写上高宽,可以在图片未加载之前提前占住位置,避免图片从未加载状态到渲染完成状态高宽变化引起的重排问题。

    object-fit

    当然,限制高宽也会出现问题,譬如图片被拉伸了,非常的难看:

    image

    这个时候,我们可以借助 object-fit,它能够指定可替换元素的内容(也就是图片)该如何适应它的父容器的高宽。

    ul li img {
        width: 150px;
        height: 100px;
        object-fit: cover;
    }
    

    利用 object-fit: cover,使图片内容在保持其宽高比的同时填充元素的整个内容框。

    image

    object-fit 还有一个配套属性 object-position,它可以控制图片在其内容框中的位置。(类似于 background-position),m默认是 object-position: 50% 50%,如果你不希望图片居中展示,可以使用它去改变图片实际展示的 position 。

    ul li img {
        width: 150px;
        height: 100px;
        object-fit: cover;
        object-position: 50% 100%;
    }
    

    image

    像是这样,object-position: 100% 50% 指明从底部开始展示图片。这里有一个很好的 Demo 可以帮助你理解 object-position

    CodePen Demo -- Object position

    考虑屏幕 dpr -- 响应式图片

    正常情况下,图片的展示应该没有什么问题了。但是对于有图片可展示的情况下,我们还可以做的更好。

    在移动端或者一些高清的 PC 屏幕(苹果的 MAC Book),屏幕的 dpr 可能大于 1。这种时候,我们可能还需要考虑利用多倍图去适配不同 dpr 的屏幕。

    正好,<img> 标签是有提供相应的属性 srcset 让我们进行操作的。

    <img src='[email protected]'
       srcset='[email protected] 1x,
               [email protected] 2x,
               [email protected] 3x' 
    />
    

    当然,这是比较旧的写法,srcset 新增了新的 w 宽度描述符,需要配合 sizes 一起使用,所以更好的写法是:

    <img 
            src = "photo.png" 
            sizes = “(min-width: 600px) 600px, 300px" 
            srcset = “[email protected] 300w,
                           pho[email protected] 600w,
                           [email protected] 1200w,
    >
    

    利用 srcset,我们可以给不同 dpr 的屏幕,提供最适合的图片。

    上述出现了一些概念,dpr,图片的 srcset ,sizes 属性,不太了解的可以移步 前端基础知识概述

    图片丢失

    好了,当图片链接没问题时,已经处理好了。接下来还需要考虑,当图片链接挂了,应该如何处理。

    处理的方式有很多种。最好的处理方式,是我最近在张鑫旭老师的这篇文章中 -- 图片加载失败后CSS样式处理最佳实践 看到的。这里简单讲下:

    1. 利用图片加载失败,触发 <img> 元素的 onerror 事件,给加载失败的 <img> 元素新增一个样式类
    2. 利用新增的样式类,配合 <img> 元素的伪元素,展示默认兜底图的同时,还能一起展示 <img> 元素的 alt 信息
    <img src="test.png" alt="图片描述" onerror="this.classList.add('error');">
    
    img.error {
        position: relative;
        display: inline-block;
    }
    
    img.error::before {
        content: "";
        /** 定位代码 **/
        background: url(error-default.png);
    }
    
    img.error::after {
        content: attr(alt);
        /** 定位代码 **/
    }
    

    我们利用伪元素 before ,加载默认错误兜底图,利用伪元素 after,展示图片的 alt 信息:

    image

    OK,到此,完整的对图片的处理就算完成了,完整的 Demo 你可以戳这里看看:

    CodePen Demo -- 图片处理

    交互设计优化

    接下来一个大环节是关于一些交互的细节。对于交互设计,一些比较通用的准则:

    • Don’t make me think
    • 符合用户的习惯与预期
    • 操作便利
    • 做适当的提醒
    • 不强迫用户

    过渡与动画

    在我们的交互过程中,适当的增加过渡与动画,能够很好的让用户感知到页面的变化

    譬如我们页面上随处可见 loading 效果,其实就是这样一种作用,让用户感知页面正在加载,或者正在处理某些事务。

    滚动优化

    滚动也是操作网页中非常重要的一环。看看有哪些可以优化的点:

    滚动平滑:使用 scroll-behavior: smooth 让滚动丝滑

    使用 scroll-behavior: smooth,可以让滚动框实现平稳的滚动,而不是突兀的跳动。看看效果,假设如下结构:

    <div class="g-container">
      <nav>
        <a href="#1">1</a>
        <a href="#2">2</a>
        <a href="#3">3</a>
      </nav>
      <div class="scrolling-box">
        <section id="1">First section</section>
        <section id="2">Second section</section>
        <section id="3">Third section</section>
      </div>
    </div>
    

    不使用 scroll-behavior: smooth,是突兀的跳动切换:

    scrol

    给可滚动容器添加 scroll-behavior: smooth,实现平滑滚动:

    {
        scroll-behavior: smooth;
    }
    

    scroll2

    使用 scroll-snap-type 优化滚动效果

    sroll-snap-type 可能算得上是新的滚动规范里面最核心的一个属性样式。

    scroll-snap-type:属性定义在滚动容器中的一个临时点(snap point)如何被严格的执行。

    光看定义有点难理解,简单而言,这个属性规定了一个容器是否对内部滚动动作进行捕捉,并且规定了如何去处理滚动结束状态。让滚动操作结束后,元素停止在适合的位置。

    看个简单示例:

    当然,scroll-snap-type 用法非常多,可控制优化的点很多,限于篇幅无法一一展开,具体更详细的用法可以看看我的另外一篇文章 -- 使用 sroll-snap-type 优化滚动

    控制滚动层级,避免页面大量重排

    这个优化可能稍微有一点难理解。需要了解 CSS 渲染优化的相关知识。

    先说结论,控制滚动层级的意思是尽量让需要进行 CSS 动画(可以是元素的动画,也可以是容器的滚动)的元素的 z-index 保持在页面最上方,避免浏览器创建不必要的图形层(GraphicsLayer),能够很好的提升渲染性能

    这一点怎么理解呢,一个元素触发创建一个 Graphics Layer 层的其中一个因素是:

    • 元素有一个 z-index 较低且包含一个复合层的兄弟元素

    根据上述这点,我们对滚动性能进行优化的时候,需要注意两点:

    1. 通过生成独立的 GraphicsLayer,利用 GPU 加速,提升滚动的性能
    2. 如果本身滚动没有性能问题,不需要独立的 GraphicsLayer,也要注意滚动容器的层级,避免因为层级过高而被其他创建了 GraphicsLayer 的元素合并,被动的生成一个 Graphics Layer ,影响页面整体的渲染性能

    如果你对这点还有点懵,可以看看这篇文章 -- 你所不知道的 CSS 动画技巧与细节

    点击交互优化

    在用户点击交互方面,也有一些有意思的小细节。

    优化手势 -- 不同场景应用不同 cursor

    对于不同的内容,最好给与不同的 cursor 样式,CSS 原生提供非常多种常用的手势。

    在不同的场景使用不同的鼠标手势,符合用户的习惯与预期,可以很好的提升用户的交互体验。

    首先对于按钮,就至少会有 3 种不同的 cursor,分别是可点击,不可点击,等待中:

    {
        cursor: pointer;    // 可点击
        cursor: not-allowed;    // 不可点击
        cursor: wait;    // loading
    }
    

    image

    除此之外,还有一些常见的,对于一些可输入的 Input 框,使用 cursor: text,对于提示 Tips 类使用 cursor: help,放大缩小图片 zoom-inzoom-out 等等:

    image

    一些常用的简单列一列:

    • 按钮可点击: cursor: pointer
    • 按钮禁止点击:cursor: not-allowed
    • 等待 Loading 状态:cursor: wait
    • 输入框:cursor: text;
    • 图片查看器可放大可缩小:cursor: zoom-in/ zoom-out
    • 提示:cursor: help;

    当然,实际 cursor 还支持非常多种,可以在 MDN 或者下面这个 CodePen Demo 中查看这里看完整的列表:

    CodePen Demo -- Cursor Demo

    点击区域优化 -- 伪元素扩大点击区域

    按钮是我们网页设计中十分重要的一环,而按钮的设计也与用户体验息息相关。

    考虑这样一个场景,在摇晃的车厢上或者是单手操作着屏幕,有的时候一个按钮,死活也点不到。

    让用户更容易的点击到按钮无疑能很好的增加用户体验及可提升页面的访问性,尤其是在移动端,按钮通常都很小,但是受限于设计稿或者整体 UI 风格,我们不能直接去改变按钮元素的高宽。

    那么这个时候有什么办法在不改变按钮原本大小的情况下去增加他的点击热区呢?

    这里,伪元素也是可以代表其宿主元素来响应的鼠标交互事件的。借助伪元素可以轻松帮我们实现,我们可以这样写:

    .btn::before{
      content:"";
      position:absolute;
      top:-10px;
      right:-10px;
      bottom:-10px;
      left:-10px;
    }
    

    当然,在 PC 端下这样子看起来有点奇怪,但是合理的用在点击区域较小的移动端则能取到十分好的效果,效果如下:

    608782-20160527112625428-906375003

    在按钮的伪元素没有其它用途的时候,这个方法确实是个很好的提升用户体验的点。

    快速选择优化 -- user-select: all

    操作系统或者浏览器通常会提供一些快速选取文本的功能,看看下面的示意图:

    layout3

    快速单击两次,可以选中单个单词,快速单击三次,可以选中一整行内容。但是如果有的时候我们的核心内容,被分隔符分割,或者潜藏在一整行中的一部分,这个时候选取起来就比较麻烦。

    利用 user-select: all,可以将需要一次选中的内容进行包裹,用户只需要点击一次,就可以选中该段信息:

    .g-select-all {
        user-select: all
    }
    

    给需要一次选中的信息,加上这个样式后的效果,这个细节作用在一些需要复制粘贴的场景,非常好用:

    layout4

    CodePen -- user-select: all 示例

    选中样式优化 -- ::selection

    当然,如果你想更进一步,CSS 还有提供一个 ::selection 伪类,可以控制选中的文本的样式(只能控制color, background, text-shadow),进一步加深效果。

    layout5

    CodePen -- user-select: all && ::selection 控制选中样式

    添加禁止选择 -- user-select: none

    有快速选择,也就会有它的对立面 -- 禁止选择。

    对于一些可能频繁操作的按钮,可能出现如下尴尬的场景:

    • 文本按钮的快速点击,触发了浏览器的双击快速选择,导致文本被选中:

    btn-click

    • 翻页按钮的快速点击,触发了浏览器的双击快速选择:

    img-click

    对于这种场景,我们需要把不可被选中元素设置为不可被选中,利用 CSS 可以快速的实现这一点:

    {
        -webkit-user-select: none; /* Safari */
        -ms-user-select: none; /* IE 10 and IE 11 */
        user-select: none; /* Standard syntax */
    }
    

    这样,无论点击的频率多快,都不会出现尴尬的内容选中:

    btn-click-unselect

    跳转优化

    现阶段,单页应用(Single Page Application)的应用非常广泛,Vue 、React 等框架大行其道。但是一些常见的写法,也容易衍生一些小问题。

    譬如,点击按钮、文本进行路由跳转。譬如,经常会出现这种代码:

    <template>
        ...
        <button @click="gotoDetail">
            Detail
        </button>
        ...
    <template>
    ...
    gotoDetail() {
        this.$router.push({
          name: 'xxxxx',
        });
    }
    

    大致逻辑就是给按钮添加一个事件,点击之后,跳转到另外一个路由。当然,本身这个功能是没有任何问题的,但是没有考虑到用户实际使用的场景。

    实际使用的时候,由于是一个页面跳转,很多时候,用户希望能够保留当前页面的内容,同时打开一个新的窗口,这个时候,他会尝试下的鼠标右键,选择在新标签页中打开页面,遗憾的是,上述的写法是不支持鼠标右键打开新页面的。

    原因在于浏览器是通过读取 <a> 标签的 href 属性,来展示类似在新标签页中打开页面这种选项,对于上述的写法,浏览器是无法识别它是一个可以跳转的链接。简单的示意图如下:

    image

    所以,对于所有路由跳转按钮,建议都使用 <a> 标签,并且内置 href 属性,填写跳转的路由地址。实际渲染出来的 DOM 可能是需要类似这样:

    <a href="/xx/detail">Detail</a>
    

    易用性

    易用性也是交互设计中需要考虑的一个非常重要的环节,能做的有非常多。简单的罗列一下:

    • 注意界面元素的一致性,降低用户学习成本
    • 延续用户日常的使用习惯,而不是重新创造
    • 给下拉框增加一些预设值,降低用户填写成本
    • 同类的操作合并在一起,降低用户的认知成本
    • 任何操作之后都要给出反馈,让用户知道操作已经生效

    先探索,后表态

    这一点非常的有意思,什么叫先探索后表态呢?就是我们不要一上来就强迫用户去做一些事情,譬如登录。

    想一想一些常用网站的例子:

    • 类似虎牙、Bilibili 等视频网站,可以先蓝光体验,一定观看时间后才会要求登录
    • 电商网站,只有到付款的时候,才需要登录

    上述易用性先探索,后表态的内容,部分来源于:Learn From What Leading Companies A/B Test,可以好好读一读。

    字体优化

    字体的选择与使用其实是非常有讲究的。

    如果网站没有强制必须使用某些字体。最新的规范建议我们更多的去使用系统默认字体。也就是 CSS Fonts Module Level 4 -- Generic font families 中新增的 font-family: system-ui 关键字。

    font-family: system-ui 能够自动选择本操作系统下的默认系统字体。

    默认使用特定操作系统的系统字体可以提高性能,因为浏览器或者 webview 不必去下载任何字体文件,而是使用已有的字体文件。 font-family: system-ui 字体设置的优势之处在于它与当前操作系统使用的字体相匹配,对于文本内容而言,它可以得到最恰当的展示。

    举两个例子,天猫的字体定义与 Github 的字体定义:

    • 天猫font-family: "PingFang SC",miui,system-ui,-apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,sans-serif;
    • Githubfont-family: -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol;

    简单而言,它们总体遵循了这样一个基本原则:

    1、尽量使用系统默认字体

    使用系统默认字体的主要原因是性能,并且系统字体的优点在于它与当前操作系统使用的相匹配,因此它的文本展示必然也是一个让人舒适展示效果。

    2、兼顾中西,西文在前,中文在后

    中文或者西文(英文)都要考虑到。由于大部分中文字体也是带有英文部分的,但是英文部分又不怎么好看,但是英文字体中大多不包含中文。通常会先进行英文字体的声明,选择最优的英文字体,这样不会影响到中文字体的选择,中文字体声明则紧随其次。

    3、兼顾多操作系统

    选择字体的时候要考虑多操作系统。例如 MAC OS 下的很多中文字体在 Windows 都没有预装,为了保证 MAC 用户的体验,在定义中文字体的时候,先定义 MAC 用户的中文字体,再定义 Windows 用户的中文字体;

    4、兼顾旧操作系统,以字体族系列 serif 和 sans-serif 结尾

    当使用一些非常新的字体时,要考虑向下兼容,兼顾到一些极旧的操作系统,使用字体族系列 serif 和 sans-serif 结尾总归是不错的选择。

    对于上述的一些字体可能会有些懵,譬如 -apple-system, BlinkMacSystemFont,这是因为不同浏览器厂商对规范的实现有所不同,对于字体定义更多的相关细节,可以再看看这篇文章 -- Web 字体 font-family 再探秘

    可访问性(A11Y)

    可访问性,在我们的网站中,属于非常重要的一环,但是大部分前端(其实应该是设计、前端、产品)同学都会忽视它。

    我潜伏在一个叫无障碍设计小组的群里,其中包含了很多无障碍设计师以及患有一定程度视觉、听力、行动障碍的用户,他们在群里经常会表达出一个观点,就是国内的大部分 Web 网站及 APP 基本没有考虑过残障人士的使用(或者可访问性做的非常差),非常的令人揪心。

    尤其在我们一些重交互、重逻辑的网站中,我们需要考虑用户的使用习惯、使用场景,从高可访问性的角度考虑,譬如假设用户没有鼠标,仅仅使用键盘,能否顺畅的使用我们的网站?

    假设用户没有鼠标,这个真不一定是针对残障人士,很多情况下,用户拿鼠标的手可能在干其他事情,比如在吃东西,又或者在 TO B 类的业务,如超市收银、仓库收货,很可能用户拿鼠标的手操作着其他设备(扫码枪)等等。

    本文不会专门阐述无障碍设计的方方面面,只是从一些我觉得前端工程师需要关注的,并且仅需要花费少量代价就能做好的一些无障碍设计细节。记住,无障碍设计对所有人都更友善

    色彩对比度

    颜色,也是我们天天需要打交道的属性。对于大部分视觉正常的用户,可能对页面的颜色敏感度还没那么高。但是对于一小部分色弱、色盲用户,他们对于网站的颜色会更加敏感,不好的设计会给他们访问网站带来极大的不便。

    什么是色彩对比度

    是否曾关心过页面内容的展示,使用的颜色是否恰当?色弱、色盲用户能否正常看清内容?良好的色彩使用,在任何时候都是有益的,而且不仅仅局限于对于色弱、色盲用户。在户外用手机、阳光很强看不清,符合无障碍标准的高清晰度、高对比度文字就更容易阅读。

    这里就有一个概念 -- 颜色对比度,简单地说,描述就是两种颜色在亮度(Brightness)上的差别。运用到我们的页面上,大多数的情况就是背景色(background-color)与内容颜色(color)的对比差异。

    最权威的互联网无障碍规范 —— WCAG AA规范规定,所有重要内容的色彩对比度需要达到 4.5:1 或以上(字号大于18号时达到 3:1 或以上),才算拥有较好的可读性。

    借用一张图 -- 知乎 -- 助你轻松做好无障碍的15个UI设计工具推荐

    很明显,上述最后一个例子,文字已经非常的不清晰了,正常用户都已经很难看得清了。

    检查色彩对比度的工具

    Chrome 浏览器从很早开始,就已经支持检查元素的色彩对比度了。以我当前正在写作的页面为例子,Github Issues 编辑页面的两个按钮:

    image

    审查元素,分别可以看到两个按钮的色彩对比度:

    image

    可以看到,绿底白字按钮的色彩对比度是没有达到标准的,也被用黄色的叹号标识了出来。

    除此之外,在审查元素的 Style 界面的取色器,改变颜色,也能直观的看到当前的色彩对比度:

    image

    焦点响应

    类似百度、谷歌的首页,进入页面后会默认让输入框获得焦点:

    image

    并非所有的有输入框的页面,都需要进入页面后进行聚焦,但是焦点能够让用户非常明确的知道,当前自己在哪,需要做些什么。尤其是对于无法操作鼠标的用户。

    页面上可以聚焦的元素,称为可聚焦元素,获得焦点的元素,则会触发该元素的 focus 事件,对应的,也就会触发该元素的 :focus 伪类。

    浏览器通常会使用元素的 :focus 伪类,给元素添加一层边框,告诉用户,当前的获焦元素在哪里。

    我们可以通过键盘的 Tab 键,进行焦点的切换,而获焦元素则可以通过元素的 :focus 伪类的样式,告诉用户当前焦点位置。

    当然,除了 Tab 键之外,对于一些多输入框、选择框的表单页面,我们也应该想着如何简化用户的操作,譬如用户按回车键时自动前进到下一字段。一般而言,用户必须执行的触按越少,体验越佳。:thumbsup:

    下面的截图,完全由键盘操作完成

    a11y

    通过元素的 :focus 伪类以及键盘 Tab 键切换焦点,用户可以非常顺畅的在脱离鼠标的情况下,对页面的焦点切换及操作。

    然而,在许多 reset.css 中,经常能看到这样一句 CSS 样式代码,为了样式的统一,消除了可聚焦元素的 :focus 伪类:

    :focus {
        outline: 0;
    }
    

    我们给上述操作的代码。也加上这样一句代码,全程再用键盘操作一下

    a11y2

    除了在 input 框有光标提示,当使用 Tab 进行焦点切换到 select 或者到 button 时,由于没有了 :focus 样式,用户将完全懵逼,不知道页面的焦点现在处于何处。

    保证非鼠标用户体验,合理运用 :focus-visible

    当然,造成上述结果很重要的一个原因在于。:focus 伪类不论用户在使用鼠标还是使用键盘,只要元素获焦,就会触发。

    而其本身的默认样式又不太能被产品或者设计接受,导致了很多人会在焦点元素触发 :focus 伪类时,通过改变 border 的颜色或者其他一些方式替代或者直接禁用。而这样做,从可访问性的角度来看,对于非鼠标用户,无疑是灾难性的。

    基于此,在W3 CSS selectors-4 规范 中,新增了一个非常有意思的 :focus-visible 伪类。

    :focus-visible:这个选择器可以有效地根据用户的输入方式(鼠标 vs 键盘)展示不同形式的焦点。

    有了这个伪类,就可以做到,当用户使用鼠标操作可聚焦元素时,不展示 :focus 样式或者让其表现较弱,而当用户使用键盘操作焦点时,利用 :focus-visible,让可获焦元素获得一个较强的表现样式。

    看个简单的 Demo:

    <button>Test 1</button>
    
    button:active {
      background: #eee;
    }
    button:focus {
      outline: 2px solid red;
    }
    

    使用鼠标点击:

    a11y3

    可以看到,使用鼠标点击的时候,触发了元素的 :active 伪类,也触发了 :focus伪类,不太美观。但是如果设置了 outline: none 又会使键盘用户的体验非常糟糕。尝试使用 :focus-visible 伪类改造一下:

    button:active {
      background: #eee;
    }
    button:focus {
      outline: 2px solid red;
    }
    button:focus:not(:focus-visible) {
      outline: none;
    }
    

    看看效果,分别是在鼠标点击 Button 和使用键盘控制焦点点击 Button:

    a11y4

    CodePen Demo -- :focus-visible example

    可以看到,使用鼠标点击,不会触发 :foucs,只有当键盘操作聚焦元素,使用 Tab 切换焦点时,outline: 2px solid red 这段代码才会生效。

    这样,我们就既保证了正常用户的点击体验,也保证了一批无法使用鼠标的用户的焦点管理体验。

    值得注意的是,有同学会疑惑,这里为什么使用了 :not 这么绕的写法而不是直接这样写呢:

    button:focus {
      outline: unset;
    }
    button:focus-visible {
      outline: 2px solid red;
    }
    

    为的是兼容不支持 :focus-visible 的浏览器,当 :focus-visible 不兼容时,还是需要有 :focus 伪类的存在。

    使用 WAI-ARIA 规范增强语义 -- div 等非可获焦元素模拟获焦元素

    还有一个非常需要注意的点。

    现在很多前端同学在前端开发的过程中,喜欢使用非可获焦元素模拟获焦元素,譬如:

    • 使用 div 模拟 button 元素
    • 使用 ul 模拟下拉列表 select 等等

    当下很多组件库都是这样做的,譬如 element-ui 和 ant-design。

    在使用非可获焦元素模拟获焦元素的时候,一定要注意,不仅仅只是外观长得像就完事了,其行为表现也需要符合原本的 buttonselect 等可聚焦元素的性质,能够体现元素的语义,能够被聚焦,能够通过 Tab 切换等等。

    基于大量类似的场景,有了 WAI-ARIA 标准,WAI-ARIA是一个为残疾人士等提供无障碍访问动态、可交互Web内容的技术规范。

    简单来说,它提供了一些属性,增强标签的语义及行为:

    • 可以使用 tabindex 属性控制元素是否可以聚焦,以及它是否/在何处参与顺序键盘导航
    • 可以使用 role 属性,来标识元素的语义及作用,譬如使用 <div id="saveChanges" tabindex="0" role="button">Save</div> 来模拟一个按钮
    • 还有大量的 aria-* 属性,表示元素的属性或状态,帮助我们进一步地识别以及实现元素的语义化,优化无障碍体验

    使用工具查看标签的语义

    我们来看看 Github 页面是如何定义一个按钮的,以 Github Issues 页面的 Edit 按钮为例子:

    image

    这一块,清晰的描述了这个按钮在可访问性相关的一些特性,譬如 Contrast 色彩对比度,按钮的描述,也就是 Name,是给屏幕阅读器看到的,Role 标识是这个元素的属性,它是一个按钮,Keyboard focusable 则表明他能否被键盘的 Tab 按钮给捕获。

    分析使用非可聚焦元素模拟的按钮

    这里,我随便选取了我们业务中一个使用 span 模拟按钮的场景,是一个面包屑导航,点击可进行跳转,发现惨不忍睹:

    image

    HTML 代码:

    <span class="ssc-breadcrumb-item-link"> Inbound </span>
    

    image

    基本上可访问性为 0,作为一个按钮,它不可被聚焦,无法被键盘用户选中,没有具体的语义,色彩对比度太低,可能视障用户无法看清。并且,作为一个能进行页面跳转的按钮,它没有不是 a 标签,没有 href 属性。

    即便对于面包屑导航,我们可以不将它改造成 <a> 标签,也需要做到最基本的一些可访问性改造:

    <span role="button" aria-label="goto inbound page" tabindex="0" class="ssc-breadcrumb-item-link"> Inbound </span>
    

    不要忘了再改一下颜色,达到最低色彩对比度以上,再看看:

    image

    OK,这样,一个最最最基本的,满足最低可访问性需求的按钮算是勉强达标,当然,这个按钮可以再更进一步进行改造,涉及了更深入的可访问性知识,本文不深入展开。

    分析组件库的 A11Y

    最后,在我们比较常用的 Vue - element-ui、React - ant-design 中,我们来看看 ant-design 在提升可访问性相关的一些功能。

    以 Select 选择框组件为例,ant-design 利用了大量的 WAI-ARIA 属性,使得用 div 模拟的下拉框不仅仅在表现上符合一个下拉框,在语义、行为上都符合一个下拉框,简单的一个例子:

    image

    看看使用 div 模拟下拉框的 DOM 部分:

    image

    再看看在交互体验上:

    a11y5

    上述操作全是在键盘下完成,看着平平无奇,实际上组件库在正常响应可获焦元素切换的同时,给用 div 模拟的 select 加了很多键盘事件的响应,可以利用回车,上下键等对可选项进行选择。其实是下了很多功夫。

    对于 A11Y 相关的内容,篇幅及内容非常之多,本文无法一一展开,感兴趣的可以通读下下列文章:

    总结一下

    本文从页面展示交互细节可访问性三个大方面入手,罗列一些在实际的开发过程中,积攒的一些有益的经验。虽然不够全面,不过从一开始也就没想着大而全,主要是一些可能有用但是容易被忽视的点,也算是一个不错的查缺补漏小指南。

    当然,很多都是我个人的观点想法,可能有一些理解存在一些问题,一些概念没有解读到位,也希望大家帮忙指出。

    最后

    本文到此结束,希望对你有帮助 :)

    想 Get 到最有意思的 CSS 资讯,千万不要错过我的公众号 -- iCSS前端趣闻 😄

    更多精彩 CSS 技术文章汇总在我的 Github -- iCSS ,持续更新,欢迎点个 star 订阅收藏。

    如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。

  • 2

    想要请问一下,页面刷新后如何让页面加载完在一起显示呢?我现在用的方法感觉不是很好。

    body {
        display: none;
    }
    

    我现在是设置 body 为none 然后加载完了再 显示出来

    $(function () {
        $("body").show(33)
    })
    

    但有时 页面还是会左右抖动一下,不知道为什么。。

    请问这个有更好的方法吗?谢谢啦

  • 3

    提供个素材,web安全问题

    a链接的安全问题

    阐述问题:当a链接有target="_blank"属性时,必须添加rel="noreferrer noopener",不然新产生的页面可以通过window.opener来获取到父窗口的window对象。

    <a href="www.baidu.com" target="_blank" rel="noreferrer noopener" >
    

    聊聊 rel=noopener 关于a标签target=“_blank"使用rel=noopener

  • 4

    SVG 滤镜从入门到放弃

    image

    想写一篇关于 SVG 滤镜的文章已久,SVG 滤镜的存在,让本来就非常强大的 CSS 如虎添翼。让仅仅使用 CSS/HTML/SVG 创作的效果更上一层楼。题图为袁川老师使用 SVG 滤镜实现的云彩效果 -- CodePen Demo -- Cloud (SVG filter + CSS)

    什么是 SVG 滤镜

    SVG 滤镜与 CSS 滤镜类似,是 SVG 中用于创建复杂效果的一种机制。很多人看到 SVG 滤镜复杂的语法容易心生退意。本文力图使用最简洁明了的方式让大家尽量弄懂 SVG 滤镜的使用方式。

    本文默认读者已经掌握了一定 SVG 的基本概念和用法。

    SVG 滤镜的种类

    SVG 滤镜包括了:

    feBlend
    feColorMatrix
    feComponentTransfer
    feComposite
    feConvolveMatrix
    feDiffuseLighting
    feDisplacementMap
    feFlood
    feGaussianBlur
    feImage
    feMerge
    feMorphology
    feOffset
    feSpecularLighting
    feTile
    feTurbulence
    feDistantLight
    fePointLight
    feSpotLight
    

    看着内容很多,有点类似于 CSS 滤镜中的不同功能:blur()contrast()drop-shadow()

    SVG 滤镜的语法

    我们需要使用 <defs><filter> 标签来定义一个 SVG 滤镜。

    通常所有的 SVG 滤镜元素都需要定义在 <defs> 标记内。

    现在,基本上现代浏览器,即使不使用 <defs> 包裹 <filter>,也能够定义一个 SVG 滤镜。

    这个 <defs> 标记是 definitions 这个单词的缩写,可以包含很多种其它标签,包括各种滤镜。

    其次,使用 <filter> 标记用来定义 SVG 滤镜。 <filter> 标签需要一个 id 属性,它是这个滤镜的标志。SVG 图形使用这个 id 来引用滤镜。

    看一个简单的 DEMO:

    <div class="cssFilter"></div>
    <div class="svgFilter"></div>
    
    <svg>
        <defs>
            <filter id="blur">
                <feGaussianBlur in="SourceGraphic" stdDeviation="5" />
            </filter>
        </defs>
    </svg>
    
    div {
        width: 100px;
        height: 100px;
        background: #000;
    }
    .cssblur {
        filter: blur(5px);
    }
    .svgFilter{
        filter: url(#blur);
    }
    

    这里,我们在 defsfilter 标签内,运用了 SVG 的 feGaussianBlur 滤镜,也就是模糊滤镜, 该滤镜有两个属性 instdDeviation。其中 in="SourceGraphic" 属性指明了模糊效果要应用于整个图片,stdDeviation 属性定义了模糊的程度。最后,在 CSS 中,使用了 filter: url(#blur) 去调用 HTML 中定义的 id 为 blur 的滤镜。

    为了方便理解,也使用 CSS 滤镜 filter: blur(5px) 实现了一个类似的滤镜,方便比较,结果图如下:

    image

    CodePen Demo - SVG 滤镜

    嘿,可以看到,使用 SVG 的模糊滤镜,实现了一个和 CSS 模糊滤镜一样的效果。

    CSS filter 的 url 模式

    上文的例子中使用了 filter: url(#blur) 这种模式引入了一个 SVG 滤镜效果,url 是 CSS 滤镜属性的关键字之一,url 模式是 CSS 滤镜提供的能力之一,允许我们引入特定的 SVG 过滤器,这极大的增强 CSS 中滤镜的能力。

    相当于所有通过 SVG 实现的滤镜效果,都可以快速的通过 CSS 滤镜 URL 模式一键引入。

    多个滤镜搭配工作

    和 CSS 滤镜一样,SVG 滤镜也是支持多个滤镜搭配混合使用的。

    所以我们经常能看到一个 <filter> 标签内有大量的代码。很容易就懵了~

    再来看个简单的例子:

    <div></div>
    
    <svg>
        <defs>
            <!-- Filter declaration -->
            <filter id="MyFilter">
    
                <!-- offsetBlur -->
                <feGaussianBlur in="SourceAlpha" stdDeviation="5" result="blur" />
                <feOffset in="blur" dx="10" dy="10" result="offsetBlur" />
    
                <!-- merge SourceGraphic + offsetBlur -->
                <feMerge>
                    <feMergeNode in="offsetBlur" />
                    <feMergeNode in="SourceGraphic" />
                </feMerge>
            </filter>
        </defs>
    </svg>
    
    div {
        width: 200px;
        height: 200px;
        background: url(xxx);
        filter: url(#MyFilter);
    }
    

    我们先来看看整个滤镜的最终结果,结果长这样:

    image

    CSS 可能一行代码就能实现的事情,SVG 居然用了这么多代码。(当然,这里 CSS 也不好实现,不是简单容器的阴影,而是 PNG 图片图形的轮廓阴影)

    分解步骤

    首先看这一段:

    <!-- offsetBlur -->
    <feGaussianBlur in="SourceAlpha" stdDeviation="5" result="blur" />
    <feOffset in="blur" dx="10" dy="10" result="offsetBlur" />
    

    首先 <feGaussianBlur in="SourceAlpha" stdDeviation="5" result="blur" /> 这一段,我们上面也讲到了,会生成一个模糊效果,这里多了一个新的属性 result='blur',这个就是 SVG 的一个特性,不同滤镜作用的效果可以通过 result 产出一个中间结果(也称为 primitives 图元),其他滤镜可以使用 in 属性导入不同滤镜产出的 result,继续操作。

    紧接着,<feOffset> 滤镜还是很好理解的,使用 in 拿到了上一步的结果 result = 'blur',然后做了一个简单的位移。

    这里就有一个非常重要的知识点:在不同滤镜中利用 resultin 属性,可以实现在前一个基本变换操作上建立另一个操作,比如我们的例子中就是添加模糊后又添加位移效果。

    结合两个滤镜,产生的图形效果,其实是这样的:

    image

    实际效果中还出现了原图,所以这里我们还使用了 <feMerge> 标签,合并了多个效果。也就是上述这段代码:

    <!-- merge SourceGraphic + offsetBlur -->
    <feMerge>
        <feMergeNode in="offsetBlur" />
        <feMergeNode in="SourceGraphic" />
    </feMerge>
    

    feMerge 滤镜允许同时应用滤镜效果而不是按顺序应用滤镜效果。利用 result 存储别的滤镜的输出可以实现这一点,然后在一个 <feMergeNode> 子元素中访问它。

    • <feMergeNode in="offsetBlur" /> 表示了上述两个滤镜的最终输出结果 offsetBlur,也就是阴影的部分
    • <feMergeNode in="SourceGraphic" /> 中的 in="SourceGraphic" 关键词表示图形元素自身将作为 <filter> 原语的原始输入

    整体再遵循后输入的层级越高的原则,最终得到上述结果。示意流程图如下:

    image

    至此,基本就掌握了 SVG 滤镜的工作原理,及多个滤镜如何搭配使用。接下来,只需要搞懂不同的滤镜能产生什么样的效果,有什么不同的属性,就能大致对 SVG 滤镜有个基本的掌握!

    关于 SVG 滤镜还需要知道的

    上面大致过了一下 SVG 滤镜的使用流程,过程中提到了一些属性,可能也漏掉了一些属性的讲解,本章节将补充说明一下。

    滤镜标签通用属性

    有一些属性是每一个滤镜标签都有,都可以进行设置的。

    | 属性 |作用| | -- | -- | | x, y | 提供左上角的坐标来定义在哪里渲染滤镜效果。 (默认值:0) | | width, height| 绘制滤镜容器框的高宽(默认都为 100%) | | result | 用于定义一个滤镜效果的输出名字,以便将其用作另一个滤镜效果的输入(in) | | in | 指定滤镜效果的输入源,可以是某个滤镜导出的 result,也可以是下面 6 个值 |

    in 属性的 6 个取值

    SVG filter 中的 in 属性,指定滤镜效果的输入源,可以是某个滤镜导出的 result,也可以是下面 6 个值:

    | in 取值 | 作用 | | -- | -- | | SourceGraphic | 该关键词表示图形元素自身将作为 <filter> 原语的原始输入 | | SourceAlpha | 该关键词表示图形元素自身将作为 <filter> 原语的原始输入。SourceAlphaSourceGraphic 具有相同的规则除了 SourceAlpha 只使用元素的非透明部分 | | BackgroundImage | | | BackgroundAlpha | | | FillPaint | | | StrokePaint | |

    更多 SVG 滤镜介绍讲解

    上面已经提到了几个滤镜,我们简单回顾下:

    • <feGaussianBlur > - 模糊滤镜
    • <feOffset > - 位移滤镜
    • <feMerge> - 多滤镜叠加滤镜

    接下来再介绍一些比较常见,有意思的 SVG 滤镜。

    feBlend 滤镜

    <feBlend> 为混合模式滤镜,与 CSS 中的混合模式相类似。

    在 CSS 中,我们有混合模式 mix-blend-modebackground-blend-mode 。我有过非常多篇关于 CSS 混合模式相关的一些应用。如果你还不太了解 CSS 中的混合模式,可以先看看这几篇文章:

    SVG 中的混合模式种类比 CSS 中的要少一些,只有 5 个,其作用与 CSS 混合模式完全一致:

    • normal — 正常
    • multiply — 正片叠底
    • screen — 滤色
    • darken — 变暗
    • lighten— 变亮

    简单一个 Demo,我们有两张图,利用不同的混合模式,可以得到不一样的混合结果 :

    <div></div>
    
    <svg>
        <defs>
            <filter id="lighten" x="0" y="0" width="200" height="250">
                <feImage width="200" height="250" xlink:href="image1.jpg" result="img1" />
                <feImage width="200" height="250" xlink:href="image2.jpg" result="img2" />
                <feBlend mode="lighten" in="img1" in2="img2"/>
            </filter>
        </defs>
    </svg>
    
    .container {
        width: 200px;
        height: 250px;
        filter: url(#lighten);
    }
    

    这里还用到了一个 <feImage> 滤镜,它的作用是提供像素数据作为输出,如果外部来源是一个 SVG 图像,这个图像将被栅格化。

    image

    上述运用了 feBlend 滤镜中的 mode="lighten" 后的结果,两个图像叠加作用了 lighten 混合模式:

    image

    看看全部 5 中混合模式的效果:

    image

    CodePen Demo -- SVG Filter feBlend Demo

    feColorMatrix

    <feColorMatrix> 滤镜也是 SVG 滤镜中非常有意思的一个滤镜,顾名思义,它的名字中包含了矩阵这个单词,表示该滤镜基于转换矩阵对颜色进行变换。每一像素的颜色值(一个表示为[红,绿,蓝,透明度] 的矢量) 都经过矩阵乘法 (matrix multiplated) 计算出的新颜色。

    这个滤镜稍微有点复杂,我们一步一步来看。

    <feColorMatrix> 滤镜有 2 个私有属性 typevalues,type 它支持 4 种不同的类型:saturate | hueRotate | luminanceToAlpha | matrix,其中部分与 CSS Filter 中的一些滤镜效果类似。

    | type 类型 | 作用 | values 的取值范围 | | -- | -- | -- | | saturate | 转换图像饱和度 | 0.0 - 1.0 | | hueRotate | 转换图像色相 | 0.0 - 360 | | luminanceToAlpha | 阿尔法通道亮度(不知道如何翻译 :sad) | 只有一个效果,无需改变 values 的值| | matrix| 使用矩阵函数进行色彩变换 | 需要应用一个 4 x 5 的矩阵|

    在这里,我做了一个简单的关于 <feColorMatrix> 前 3 个属性 saturate | hueRotate | luminanceToAlpha 的效果示意 DEMO -- CodePen - feColorMatrix Demo,可以感受下它们的具体的效果:

    1gif

    saturate、hueRotate 滤镜和 CSS 中的 filter 中的 saturate、hue-rotate 的作用是一模一样的。

    feColorMatrix 中的 type=matrix

    feColorMatrix 中的 type=matrix 理解起来要稍微更复杂点,它的 values 需要传入一个 4x5 的矩阵。

    像是这样:

    <filter id="colorMatrix">
      <feColorMatrix type="matrix" values="1 0 0 0 0, 0 1 0 0 0, 0 0 1 0 0, 0 0 0 1 0"/>
    </filter>
    

    要理解如何运用这些填写矩阵,就不得不直面另外一个问题 -- 图像的表示。

    数字图像的本质是一个多维矩阵。在图像显示时,我们把图像的 R 分量放进红色通道里,B 分量放进蓝色通道里,G 分量放进绿色通道里。经过一系列处理,显示在屏幕上的就是我们所看到的彩色图像了。

    而 feColorMatrix 中的 matrix 矩阵,就是用来表示不同通道的值每一个分量的值,最终通过计算得到我们熟知的 rgba() 值。

    计算逻辑为:

    /* R G B A 1 */ 
    1 0 0 0 0 // R = 1*R + 0*G + 0*B + 0*A + 0 
    0 1 0 0 0 // G = 0*R + 1*G + 0*B + 0*A + 0 
    0 0 1 0 0 // B = 0*R + 0*G + 1*B + 0*A + 0 
    0 0 0 1 0 // A = 0*R + 0*G + 0*B + 1*A + 0
    

    中文的文章,对 feColorMatrix 的 matrix 讲解最好的应该就是大漠老师的这篇 -- 详解feColorMatrix,对具体的表示法感兴趣的可以看看。

    仅仅是使用的话,这里还有一个可视化的 DEMO -- CodePen - feColorMatrix Demo,帮助大家理解记忆:

    2


    到目前为止,大部分 SVG 滤镜的展示讲解都是 CSS 现有能力能够实现的,那 SVG 滤镜的独特与魅力到底在哪呢?有什么是 CSS 能力无法做到的么?下面来看看另外几个有意思的 SVG 滤镜。

    feSpecularLighting/feDiffuseLighting 光照滤镜

    feSpecularLighting 与 feDiffuseLighting 都意为光照滤镜,使用它们可以照亮一个源图形,不同的是,feSpecularLighting 为镜面照明,而 feDiffuseLighting 为散射光照明。

    • feDiffuseLighting:来自外部光源,适合模拟太阳光或者灯光照明
    • feSpecularLighting:指定从反射面反射的二次光

    简单看其中一个 Demo,代码看着有点多,但是一步一步也很好理解:

    <div></div>
    <div class="svg-filter"></div>
    <svg>
        <defs>
            <filter id="filter">
                <!--Lighting effect-->
                <feSpecularLighting in="SourceGraphic" specularExponent="20" specularConstant="0.75" result="spec">
                  <fePointLight x="0" y="0" z="200" />
                </feSpecularLighting>
                <!--Composition of inputs-->
                <feComposite in="SourceGraphic" in2="spec" operator="arithmetic" k1="0" k2="1" k3="1" k4="0" />
            </filter>
        </defs>
    </svg>
    
    div {
        background: url(avator.png);
    }
    .svg-filter {
        filter: url(#filter);
    }
    

    左边是原图,右边是应用了光照滤镜之后的效果。

    image

    CodePen - feSpotLight SVG Light Source

    feMorphology 滤镜

    feMorphology 为形态滤镜,它的输入源通常是图形的 alpha 通道,用来它的两个操作可以使源图形腐蚀(变薄)或扩张(加粗)。

    使用属性 operator 确定是要腐蚀效果还是扩张效果。使用属性 radius 表示效果的程度,可以理解为笔触的大小。

    • operator:erode 腐蚀模式,dilate 为扩张模式,默认为 erode
    • radius:笔触的大小,接受一个数字,表示该模式下的效果程度,默认为 0

    我们将这个滤镜简单的应用到文字上看看效果:

    <div class="g-text">
        <p>Normal Text</p>
        <p class="dilate">Normal Text</p>
        <p class="erode">Normal Text</p>
    </div>
    
    <svg width="0" height="0">
        <filter id="dilate">
            <feMorphology in="SourceAlpha" result="DILATED" operator="dilate" radius="3"></feMorphology>
        </filter>
        <filter id="erode">
            <feMorphology in="SourceAlpha" result="ERODE" operator="erode" radius="1"></feMorphology>
        </filter>
    </svg>
    
    p {
        font-size: 64px;
    }
    .dilate {
        filter: url(#dilate);
    }
    .erode {
        filter: url(#erode);
    }
    

    效果如下:最左边的是正常文字,中间的是扩张的模式,右边的是腐蚀模式,看看效果,非常好理解:

    image

    当然,我们还可以将其运用在图片之上,这时,并非是简单的让图像的笔触变粗或者变细,

    • 对于 erode 模式,会将图片的每一个像素向更暗更透明的方向变化,
    • dilate 模式,则是将每个向像素周围附近更亮更不透明的方向变化

    简单看个示例动画 DEMO,我们有两张图,分别作用 operator="erode"operator="dilate",并且动态的去改变它们的 radius,其中一个的代码示意如下:

    <svg width="450" height="300" viewBox="0 0 450 300">
        <filter id="morphology">
            <feMorphology operator="erode" radius="0">
                <animate attributeName="radius" from="0" to="5" dur="5s" repeatCount="indefinite" />
            </feMorphology>
        </filter>
    
        <image xlink:href="image.jpg" width="90%" height="90%" x="10" y="10" filter="url(#morphology)"></image>
    </svg>
    

    3

    上图左边是扩张模式,右边是腐蚀模式:

    CodePen Demo -- SVG feMorphology Animation

    feTurbulence 滤镜

    turbulence 意为湍流,不稳定气流,而 SVG <feTurbulence> 滤镜能够实现半透明的烟熏或波状图像。 通常用于实现一些特殊的纹理。滤镜利用 Perlin 噪声函数创建了一个图像。噪声在模拟云雾效果时非常有用,能产生非常复杂的质感,利用它可以实现了人造纹理比如说云纹、大理石纹的合成。

    有了 feTurbulence,我们可以自使用 SVG 创建纹理图形作为置换图,而不需要借助外部图形的纹理效果,即可创建复杂的图形效果。

    这个滤镜,我个人认为是 SVG 滤镜中最有意思的一个,因为它允许我们自己去创造出一些纹理,并且叠加在其他效果之上,生成出非常有意思的动效。

    feTurbulence 有三个属性是我们特别需要注意的:typebaseFrequencynumOctaves

    • type:实现的滤镜的类型,可选fractalNoise 分形噪声,或者是 turbulence 湍流噪声。
      • fractalNoise:分形噪声更加的平滑,它产生的噪声质感更接近云雾
      • turbulence:湍流噪声
    • baseFrequency: 表示噪声函数的基本频率的参数,频率越小,产生的图形越大,频率越大,产生的噪声越复杂其图形也越小越精细,通常的取值范围在 0.02 ~ 0.2
    • numOctaves:表示噪声函数的精细度,数值越高,产生的噪声更详细。 默认值为1

    这里有一个非常好的网站,用于示意 feTurbulence 所产生的两种噪声的效果:http://apike.ca/ - feTurbulence

    两种噪声的代码基本一致,只是 type 类型不同:

    <filter id="fractal" >
      <feTurbulence id="fe-turb-fractal" type="fractalNoise" baseFrequency="0.00025" numOctaves="1"/>
    </filter>
    <filter id="turbu">
      <feTurbulence id="fe-turb-turbulence" type="fractalNoise" baseFrequency="0.00025" numOctaves="1"/>
    </filter>
    

    我们通过改变 baseFrequencynumOctaves 参数看看实际产生的两种噪声的效果:

    同时,baseFrequency 允许我们传入两个值,我们可以只改变某一方向上的频率,具体的你可以戳这个 Demo 看看:CodePen -- feTurbulence baseFrequency & numOctaves

    单单一个 <feTurbulence> 滤镜其实是比较难搞懂这滤镜想干什么的,需要将这个滤镜作为纹理或者输入,和其他滤镜一起搭配使用,实现一些效果,下面我们来看看:

    使用 feTurbulence 滤镜实现文字流动的效果

    首先,尝试将 feTurbulence 所产生的纹理和文字相结合。

    简单的代码如下:

    <div>Coco</div>
    <div class="turbulence">Coco</div>
    
    <svg>
        <filter id="fractal" filterUnits="objectBoundingBox" x="0%" y="0%" width="100%" height="100%">
            <feTurbulence id="turbulence" type="fractalNoise" baseFrequency="0.03" numOctaves="1" />
            <feDisplacementMap in="SourceGraphic" scale="50"></feDisplacementMap>
        </filter>
    </svg>
    
    .turbulence {
        filter: url(#fractal);
    }
    

    左边是正常的效果,后边是应用了 <feTurbulence> 的效果,你可以试着点进 Demo,更改 baseFrequencynumOctaves 参数的大小,可以看到不同的效果:

    image

    CodePen Demo -- feTurbulence text demo

    feDisplacementMap 映射置换滤镜

    上面的 Demo 还用到了 feDisplacementMap 滤镜,也需要简单的讲解下。

    feDisplacementMap 为映射置换滤镜,想要用好这个滤镜不太容易,需要掌握非常多的关于 PhotoShop 纹理创建或者是图形色彩相关的知识。该滤镜用来自图像中从 in2 的输入值到空间的像素值置换图像从 in 输入值到空间的像素值。

    说人话就是 feDisplacementMap 实际上是用于改变元素和图形的像素位置的。该滤镜通过遍历原图形的所有像素点,使用 feDisplacementMap 重新映射到一个新的位置,形成一个新的图形。

    在上述的 feTurbulence 滤镜与文字的结合使用中,我们通过 feTurbulence 噪声得到了噪声图形,然后通过 feDisplacementMap 滤镜根据 feTurbulence 所产生的噪声图形进行形变,扭曲,液化,得到最终的效果。

    MDN 上有这个滤镜转化的一个公式(感兴趣的可以研究下,我啃不动了):

    P'(x,y) ← P( x + scale * (XC(x,y) - 0.5), y + scale * (YC(x,y) - 0.5))
    

    使用 feTurbulence 滤镜实现褶皱纸张的纹理

    好,我们继续 feTurbulence ,使用这个滤镜,我们可以生成各种不同的纹理,我们可以尝试使用 feTurbulence 滤镜搭配光照滤镜实现褶皱的纸张纹理效果,代码也非常少:

    <div></div>
    <svg>
        <filter id='roughpaper'>
            <feTurbulence type="fractalNoise" baseFrequency='0.04' result='noise' numOctaves="5" />
    
            <feDiffuseLighting in='noise' lighting-color='#fff' surfaceScale='2'>
                <feDistantLight azimuth='45' elevation='60' />
            </feDiffuseLighting>
        </filter>
    </svg>
    
    div {
        width: 650px;
        height: 500px;
        filter: url(#roughpaper);
    }
    

    效果如下:

    image

    CodePen Demo -- Rough Paper Texture with SVG Filters

    你可以在 Sara Soueidan 的一次关于 SVG Filter 的分享上,找到制作它的教程:Youtube -- SVG Filters Crash Course

    使用 feTurbulence 滤镜实现云彩效果

    最后,我们回到题图上的云彩效果,使用 feTurbulence 滤镜,我们可以非常逼真的使用 SVG 模拟出真实的云彩效果。

    首先,通过随机生成的多重 box-shadow,实现这一一个图形:

    <div></div>
    
    div {
        width: 1px;
        height: 1px;
        box-shadow: rgb(240 255 243) 80vw 11vh 34vmin 16vmin, rgb(17 203 215) 33vw 71vh 23vmin 1vmin, rgb(250 70 89) 4vw 85vh 21vmin 9vmin, rgb(198 241 231) 8vw 4vh 22vmin 12vmin, rgb(198 241 231) 89vw 11vh 31vmin 19vmin, rgb(240 255 243) 5vw 22vh 38vmin 19vmin, rgb(250 70 89) 97vw 35vh 33vmin 16vmin, rgb(250 70 89) 51vw 8vh 35vmin 14vmin, rgb(17 203 215) 75vw 57vh 40vmin 4vmin, rgb(250 70 89) 28vw 18vh 31vmin 11vmin, rgb(250 70 89) 8vw 89vh 31vmin 2vmin, rgb(17 203 215) 13vw 8vh 26vmin 19vmin, rgb(240 255 243) 98vw 12vh 35vmin 5vmin, rgb(17 203 215) 35vw 29vh 27vmin 18vmin, rgb(17 203 215) 67vw 58vh 22vmin 15vmin, rgb(198 241 231) 67vw 24vh 25vmin 7vmin, rgb(17 203 215) 76vw 52vh 22vmin 7vmin, rgb(250 70 89) 46vw 86vh 26vmin 20vmin, rgb(240 255 243) 50vw 20vh 25vmin 1vmin, rgb(250 70 89) 74vw 14vh 25vmin 16vmin, rgb(240 255 243) 31vw 100vh 29vmin 20vmin
    }
    

    这个工作,你可以交给 SASS、LESS 或者 JavaScript 这些能够有循环函数能力的语言去生成,它的效果大概是这样:

    image

    紧接着,通过 feTurbulence 产生分形噪声图形,使用 feDisplacementMap 进行映射置换,最后给图形叠加上这个滤镜效果。

    <svg width="0">
      <filter id="filter">
        <feTurbulence type="fractalNoise" baseFrequency=".01" numOctaves="10" />
        <feDisplacementMap in="SourceGraphic" scale="240" />
      </filter>
    </svg>
    
    div {
        filter: url(#filter);
    }
    

    即可得到这样的云彩效果:

    image

    完整的代码,你可以戳这里到袁川老师的 CodePen 观看:Cloud (SVG filter + CSS)

    总结一下

    关于 SVG 滤镜入门的第一篇总算差不多了,本文简单的介绍了一下 SVG 滤镜的使用方式以及一些常见的 SVG 滤镜并给出了最简单的一些使用效果,希望大家看完能对 SVG 滤镜有一个简单的认识。

    本文罗列的滤镜效果更多的是单个效果或者很少几个组合在一起的效果,实际的使用或者应用到应用场景下其实会是更多滤镜的的组合产生出的一个效果。

    后面的文章将会更加细致的去探讨分析多个 SVG 滤镜组合效果,探讨更复杂的排列组合。

    文章的题目叫SVG 滤镜从入门到放弃因为 SVG 滤镜学起来确实太繁琐太累了,它不像 CSS 滤镜或者混合模式那么容易上手那么简单。当然也由于 SVG 滤镜的功能非常强大,定制化能力强以及它已经存在了非常之久有关。SVG 滤镜的兼容性也很好,它们其实是早于 CSS3 一些特殊效果之前就已经存在的。

    CSS 其实一直在向 SVG 的一些特殊能力靠拢,用更简单的语法让人更易上手,不过 SVG 滤镜还是有其独特的魅力所在。后续将会有更多关于 SVG 滤镜的文章。也希望读到这里的同学不要放弃!

    最后

    好了,本文到此结束,一个简单的 CSS 小技巧,希望对你有帮助 :)

    更多精彩 CSS 技术文章汇总在我的 Github -- iCSS ,持续更新,欢迎点个 star 订阅收藏。

    如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。

  • 5

    浅谈 CSS Cascading 在 CSS @layer 后的变化

    本文未完成


    CSS Cascade 规范

    在 CSS @layer 之前,我们简单看一张图:

    上图表面的是在没有 CSS @layer 之前,CSS 样式申明的优先级排序,根据 CSS Cascading 4(Current Work) 标准,定义的当前规范下申明的层叠顺序优先级如下(越往下的优先级越高,下面的规则按升序排列):

    • Normal user agent declarations
    • Normal user declarations
    • Normal author declarations
    • Animation declarations
    • Important author declarations
    • Important user declarations
    • Important user agent declarations
    • Transition declarations

    按照上述算法,可以得到一个样式优先级的排序,大概是这样(越往下的优先级越高,下面的规则按升序排列):

    1. User Agent - 用户代理普通样式
    2. User - 用户设置的普通样式
    3. Author - 页面作者普通样式
    4. Animations - 动画样式
    5. ❗️Author - 页面作者 !important 样式
    6. ❗️User - 用户设置的 !important 样式
    7. ❗️User Agent - 用户代理的 !important 样式
    8. Transitions - 过渡样式

    简单解释一下用户代理样式:浏览器会有一个基本的样式表来给任何网页设置默认样式。这些样式统称用户代理样式 页面作者样式:网页的作者可以定义文档的样式,这是最常见的样式表。大多数情况下此类型样式表会定义多个,它们构成网站的视觉和体验,即页面主题,可以理解为页面作者样式 用户样式:读者,作为浏览器的用户,可以使用自定义样式表定制使用体验,自定义用户偏好,可以理解为用户样式

    关于 CSS Cascading,也就是层叠规范,你可以看看我的这篇文章加深理解 -- 深入理解 CSS(Cascading Style Sheets)中的层叠(Cascading)

    而当有了 CSS @layer 之后,这个层叠优先级顺序有了更新,具体优先级如下:

    整体会变更为复杂一些,但是总体还是遵循了 !important 样式高于非 !important 样式。

  • 6

    图片和demo不符

    "嗯,什么意思呢,你可以戳进这个 demo 看看,正常情况下的背景图填充如下" 感觉这个demo里面的和后面加菲猫配图讲解的不太相符 代码中background-clip 默认都是border-box 在border之下 但是加菲猫图片说背景图是padding左上角到border 右下角 还是我的理解有问题?

  • 7

    dashed-border box

    For using outline to realize dashed-border, I think this is more make sense.

    .style_outline {
        &::before{
          content:"";
          position:absolute;
          top:0;
          left:0;
          bottom:0;
          right:0;
          outline:20px solid #ffc107;
          background:#fff;
          z-index:-1;
    }
    
  • 8

    现代 CSS 高阶技巧,不规则边框解决方案

    本文是 CSS Houdini 之 CSS Painting API 系列第四篇。

    在上三篇中,我们详细介绍了 CSS Painting API 是如何一步一步,实现自定义图案甚至实现动画效果的!

    在这一篇中,我们将继续探索,尝试使用 CSS Painting API,去实现过往 CSS 中非常难以实现的一个点,那就是如何绘制不规则图形的边框。

    CSS Painting API

    再简单快速的过一下,什么是 CSS Painting API。

    CSS Painting API 是 CSS Houdini 的一部分。而 Houdini 是一组底层 API,它们公开了 CSS 引擎的各个部分,从而使开发人员能够通过加入浏览器渲染引擎的样式和布局过程来扩展 CSS。Houdini 是一组 API,它们使开发人员可以直接访问 CSS 对象模型 (CSSOM),使开发人员可以编写浏览器可以解析为 CSS 的代码,从而创建新的 CSS 功能,而无需等待它们在浏览器中本地实现。

    CSS Paint API 目前的版本是 CSS Painting API Level 1。它也被称为 CSS Custom Paint 或者 Houdini's Paint Worklet。

    我们可以把它理解为 JS In CSS,利用 JavaScript Canvas 画布的强大能力,实现过往 CSS 无法实现的功能。

    过往 CSS 实现不规则图形的边框方式

    CSS 实现不规则图形的边框,一直是 CSS 的一个难点之一。在过往,虽然我们有很多方式利用 Hack 出不规则图形的边框,我在之前的多篇文章中有反复提及过:

    我们来看看这样一个图形:

    利用 CSS 实现这样一个图形是相对简单的,可以利用 mask 或者 background 中的渐变实现,像是这样:

    <div class="arrow-button"></div>
    
    .arrow-button {
        position: relative;
        width: 180px;
        height: 64px;
        background: #f49714;
    
        &::after {
            content: "";
            position: absolute;
            width: 32px;
            height: 64px;
            top: 0;
            right: -32px;
            background: 
                linear-gradient(-45deg, transparent 0, transparent 22px, #f49714 22px, #f49714 100%),
                linear-gradient(-135deg, transparent 0, transparent 22px, #f49714 22px, #f49714 100%);
            background-size: 32px 32px;
            background-repeat: no-repeat;
            background-position: 0 bottom, 0 top;
        }
    }
    

    但是,如果,要实现这个图形,但是只有一层边框,利用 CSS 就不那么好实现了,像是这样:

    image

    在过往,有两种相对还不错的方式,去实现这样一个不规则图形的边框:

    1. 借助 filter,利用多重 drop-shadow()
    2. 借助 SVG 滤镜实现

    我们快速回顾一下这两个方法。

    借助 filter,利用多重 drop-shadow() 实现不规则边框

    还是上面的图形,我们利用多重 drop-shadow(),可以大致的得到它的边框效果。代码如下:

    div {
        position: relative;
        width: 180px;
        height: 64px;
        background: #fff;
    
        &::after {
            content: "";
            position: absolute;
            width: 32px;
            height: 64px;
            top: 0;
            right: -32px;
            background: 
                linear-gradient(-45deg, transparent 0, transparent 22px, #fff 22px, #fff 100%),
                linear-gradient(-135deg, transparent 0, transparent 22px, #fff 22px, #fff 100%);
            background-size: 32px 32px;
            background-repeat: no-repeat;
            background-position: 0 bottom, 0 top;
        }
    }
    div {
        filter: 
            drop-shadow(0px 0px .5px #000)
            drop-shadow(0px 0px .5px #000)
            drop-shadow(0px 0px .5px #000);
    }
    

    可以看到,这里我们通过叠加 3 层 drop-shadow(),来实现不规则图形的边框,虽然 drop-shadow() 是用于生成阴影的,但是多层值很小的阴影叠加下,竟然有了类似于边框的效果:

    image

    借助 SVG 滤镜实现实现不规则边框

    另外一种方式,需要掌握比较深的 SVG 滤镜知识。通过实现一种特殊的 SVG 滤镜,再通过 CSS 的 filter 引入,实现不规则边框。

    看看代码:

    <div></div>
    
    <svg width="0" height="0">
        <filter id="outline">
            <feMorphology in="SourceAlpha" result="DILATED" operator="dilate" radius="1"></feMorphology>
            <feMerge>
                <feMergeNode in="DILATED" />
                <feMergeNode in="SourceGraphic" />
            </feMerge>
        </filter>
    </svg>
    
    div {
        position: relative;
        width: 180px;
        height: 64px;
        background: #fff;
    
        &::after {
            content: "";
            position: absolute;
            width: 32px;
            height: 64px;
            top: 0;
            right: -32px;
            background: 
                linear-gradient(-45deg, transparent 0, transparent 22px, #fff 22px, #fff 100%),
                linear-gradient(-135deg, transparent 0, transparent 22px, #fff 22px, #fff 100%);
            background-size: 32px 32px;
            background-repeat: no-repeat;
            background-position: 0 bottom, 0 top;
        }
    }
    div {
        filter: url(#outline);
    }
    

    简单浅析一下这段 SVG 滤镜代码:

    1. <feMorphology in="SourceAlpha" result="DILATED" operator="dilate" radius="1"></feMorphology> 将原图的不透明部分作为输入,采用了 dilate 扩张模式且程度为 radius="1",生成了一个比原图大 1px 的黑色图块
    2. 使用 feMerge 将黑色图块和原图叠加在一起
    3. 可以通过控制滤镜中的 radius="1" 来控制边框的大小

    这样,也可以实现不规则图形的边框效果:

    image

    CodePen Demo -- 3 ways to achieve unregular border

    利用 CSS Painting API 实现不规则边框

    那么,到了今天,利用 CSS Painting API ,我们有了一种更为直接的方式,更好的解决这个问题。

    还是上面的图形,我们利用 clip-path 来实现一下。

    <div></div>
    
    div {
        position: relative;
        width: 200px;
        height: 64px;
        background: #f49714;
        clip-path: polygon(85% 0%, 100% 50%, 85% 100%, 0% 100%, 0% 0%;); 
    }
    

    我们可以得到这样一个图形:

    image

    当然,本文的主角是 CSS Painting API,既然我们有 clip-path 的参数,其实完全也可以利用 CSS Painting API 的 borderDraw 来绘制这个图形。

    我们尝试一下,改造我们的代码:

    <div></div>
    <script>
    if (CSS.paintWorklet) {              
       CSS.paintWorklet.addModule('/CSSHoudini.js');
    }
    </script>
    
    div {
        position: relative;
        width: 200px;
        height: 64px;
        background: paint(borderDraw);
        --clipPath: 85% 0%, 100% 50%, 85% 100%, 0% 100%, 0% 0%;); 
    }
    

    这里,我们将原本的 clip-path 的具体路径参数,定义为了一个 CSS 变量 --clipPath,传入我们要实现的 borderDraw 方法中。整个图形效果,就是要利用 background: paint(borderDraw) 绘制出来。

    接下来,看看,我们需要实现 borderDraw。核心的点在于,我们通过拿到 --clipPath 参数,解析它,然后通过循环函数利用画布把这个图形绘制出来。

    // CSSHoudini.js 文件
    registerPaint(
        "borderDraw",
        class {
            static get inputProperties() {
                return ["--clipPath"];
            }
    
            paint(ctx, size, properties) {
                const { width, height } = size;
                const clipPath = properties.get("--clipPath");
                const paths = clipPath.toString().split(",");
                const parseClipPath = function (obj) {
                    const x = obj[0];
                    const y = obj[1];
                    let fx = 0,
                        fy = 0;
                    if (x.indexOf("%") > -1) {
                        fx = (parseFloat(x) / 100) * width;
                    } else if (x.indexOf("px") > -1) {
                        fx = parseFloat(x);
                    }
                    if (y.indexOf("%") > -1) {
                        fy = (parseFloat(y) / 100) * height;
                    } else if (y.indexOf("px") > -1) {
                        fy = parseFloat(y);
                    }
                    return [fx, fy];
                };
    
                var p = parseClipPath(paths[0].trim().split(" "));
                ctx.beginPath();
                ctx.moveTo(p[0], p[1]);
                for (var i = 1; i < paths.length; i++) {
                    p = parseClipPath(paths[i].trim().split(" "));
                    ctx.lineTo(p[0], p[1]);
                }
                ctx.closePath();            
                ctx.fill();
            }
        }
    );
    

    简单解释一下上述的代码,注意其中最难理解的 parseClipPath() 方法的解释。

    1. 首先我们,通过 properties.get("--clipPath"),我们能够拿到传入的 --clipPath 参数
    2. 通过 spilt() 方法,将 --clipPath 分成一段段,也就是我们的图形实际的绘制步骤
    3. 这里有一点非常重要,也就是 parseClipPath() 方法,由于我们的 -clipPath 的每一段可能是 100% 50% 这样的构造,但是实际在绘图的过程中,我们需要的实际坐标的绝对值,譬如在一个 100 x 100 的画布上,我们需要将 50% 50% 的百分比坐标,转化为实际的 50 50 这样的绝对值
    4. 在理解了 parseClipPath() 后,剩下的就都非常好理解了,我们通过 ctx.beginPath()ctx.movectx.lineTo 以及 ctx.closePath() 将整个 --clipPath 的图形绘制出来
    5. 最后,利用 ctx.fill() 给图形上色

    这样,我们就得到了这样一个图形:

    image

    都拿到了完整的图形了,那么我们只给这个图形绘制边框,不上色,不就得到了它的边框效果了吗?

    简单改造一些 JavaScript 代码的最后部分:

    // CSSHoudini.js 文件
    registerPaint(
        "borderDraw",
        class {
            static get inputProperties() {
                return ["--clipPath"];
            }
            paint(ctx, size, properties) {
                // ...
                ctx.closePath();            
                // ctx.fill();
                ctx.lineWidth = 1;
                ctx.strokeStyle = "#000";
                ctx.stroke();
            }
        }
    );
    

    这样,我们就得到了图形的边框效果:

    image

    仅仅利用 background 绘制的缺陷

    但是,仅仅利用 [bacg](background: paint(borderDraw)) 来绘制边框效果,会有一些问题。

    上述的图形,我们仅仅赋予了 1px 的边框,如果我们把边框改成 5px 呢?看看会发生什么?

    // CSSHoudini.js 文件
    registerPaint(
        "borderDraw",
        class {
            static get inputProperties() {
                return ["--clipPath"];
            }
            paint(ctx, size, properties) {
                // ...
                ctx.lineWidth = 5;
                ctx.strokeStyle = "#000";
                ctx.stroke();
            }
        }
    );
    

    此时,整个图形会变成:

    image

    可以看到,没有展示完整的 5px 的边框,这是由于整个画布只有元素的高宽大小,而上述的代码中,元素的边框有一部分绘制到了画布之外,因此,整个图形并非我们期待的效果。

    因此,我们需要换一种思路解决这个问题,继续改造一下我们的代码,仅仅需要改造 CSS 代码即可:

    div {
        position: relative;
        width: 200px;
        height: 64px;
        margin: auto;
        clip-path: polygon(var(--clipPath)); 
        --clipPath: 85% 0%, 100% 50%, 85% 100%, 0% 100%, 0% 0%;
        
        &::before {
          content:"";
          position:absolute;
          inset: 0;
          mask: paint(borderDraw);
          background: #000;
        }
    }
    

    这里,我们的元素本身,还是利用了 clip-path: polygon(var(--clipPath)) 剪切了自身,同时,我们借助了一个伪元素,利用这个伪元素去实现具体的边框效果。

    这里其实用了一种内外切割的思想,去实现的边框效果:

    1. 利用父元素的 clip-path: polygon(var(--clipPath)) 剪切掉外围的图形
    2. 利用给伪元素的 mask 作用实际的 paint(borderDraw) 方法,把图形的内部镂空,只保留边框部分

    还是设置 ctx.lineWidth = 5,再看看效果:

    image

    看上去不错,但是实际上,虽然设置了 5px 的边框宽度,但是实际上,上图的边框宽度只有 2.5px 的,这是由于另外一点一半边框实际上被切割掉了。

    因此,我们如果需要实现 5px 的效果,实际上需要 ctx.lineWidth =10

    当然,我们可以通过一个 CSS 变量来控制边框的大小:

    div {
        position: relative;
        width: 200px;
        height: 64px;
        margin: auto;
        clip-path: polygon(var(--clipPath)); 
        --clipPath: 85% 0%, 100% 50%, 85% 100%, 0% 100%, 0% 0%;
        --borderWidth: 5;
        
        &::before {
          content:"";
          position:absolute;
          inset: 0;
          mask: paint(borderDraw);
          background: #000;
        }
    }
    

    在实际的 borderDraw 函数中,我们将传入的 --borderWidth 参数,乘以 2 使用就好:

    
    registerPaint(
        "borderDraw",
        class {
            static get inputProperties() {
                return ["--clipPath", "--borderWidth"];
            }
            paint(ctx, size, properties) {
                const borderWidth = properties.get("--borderWidth");
                // ...
                ctx.lineWidth = borderWidth * 2;
                ctx.strokeStyle = "#000";
                ctx.stroke();
            }
        }
    );
    

    这样,我们每次都能得到我们想要的边框长度:

    image

    CodePen Demo -- CSS Hudini & Unregular Custom Border

    到这里,整个实现就完成了,整个过程其实有多处非常关键的点,会有一点点难以理解,具体可能需要自己实际调试一遍找到实现的原理。

    具体应用

    在掌握了上述的方法后,我们就可以利用这个方式,实现各类不规则图形的边框效果,我们只需要传入对于的 clip-path 参数以及我们想要的边框长度即可。

    好,这样,我们就能实现各类不同的不规则图形的边框效果了。

    像是这样:

    div {
        position: relative;
        width: 200px;
        height: 200px;
        clip-path: polygon(var(--clipPath)); 
        --clipPath: 0% 15%, 15% 15%, 15% 0%, 85% 0%, 85% 15%, 100% 15%, 100% 85%, 85% 85%, 85% 100%, 15% 100%, 15% 85%, 0% 85%;
        --borderWidrh: 1;
        --color: #000;
        
        &::before {
          content:"";
          position:absolute;
          inset: 0;
          mask: paint(borderDraw);
          background: var(--color);
        }
    }
    
    div:nth-child(2) {
        --clipPath: 50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%;
        --borderWidrh: 2;
        --color: #ffcc00;
    }
    div:nth-child(3) {
        --clipPath: 90% 58%90% 58%, 69% 51%, 69% 51%, 50% 21%, 50% 21%, 39% 39%, 39% 39%, 15% 26%, 15% 26%, 15% 55%, 15% 55%, 31% 87%, 31% 87%, 14% 84%, 14% 84%, 44% 96%, 44% 96%, 59% 96%, 59% 96%, 75% 90%, 75% 90%, 71% 83%, 71% 83%, 69% 73%, 69% 73%, 88% 73%, 88% 73%, 89% 87%, 89% 87%, 94% 73%, 94% 73%;
        --borderWidrh: 1;
        --color: deeppink;
    }
    div:nth-child(4) {
        --clipPath: 0% 0%, 100% 0%, 100% 75%, 75% 75%, 75% 100%, 50% 75%, 0% 75%;
        --borderWidrh: 1;
        --color: yellowgreen;
    }
    div:nth-child(5) {
        --clipPath: 20% 0%, 0% 20%, 30% 50%, 0% 80%, 20% 100%, 50% 70%, 80% 100%, 100% 80%, 70% 50%, 100% 20%, 80% 0%, 50% 30%;
        --borderWidrh: 3;
        --color: #c7b311;
    }
    

    得到不同图形的边框效果:

    image

    CodePen Demo -- CSS Hudini & Unregular Custom Border

    又或者是基于它们,去实现各类按钮效果,这种效果在以往使用 CSS 是非常非常难实现的:

    image

    它们的核心原理都是一样的,甚至加上 Hover 效果,也是非常的轻松:

    完整的代码,你可以戳这里:CodePen Demo -- https://codepen.io/Chokcoco/pen/ExRLqdO

    至此,我们再一次利用 CSS Painting API 实现了我们过往 CSS 完全无法实现的效果。这个也就是 CSS Houdini 的魅力,是 JS In CSS 的魅力。

    兼容性?

    好吧,其实上一篇文章也谈到了兼容问题,因为可能有很多看到本篇文章并没有去翻看前两篇文章的同学。那么,CSS Painting API 的兼容性到底如何呢?

    CanIUse - registerPaint 数据如下(截止至 2022-11-23):

    image

    Chrome 和 Edge 基于 Chromium 内核的浏览器很早就已经支持,而主流浏览器中,Firefox 和 Safari 目前还不支持。

    CSS Houdini 虽然强大,目前看来要想大规模上生产环境,仍需一段时间的等待。让我们给时间一点时间!

    最后

    好了,本文到此结束,希望本文对你有所帮助 :)

    想 Get 到最有意思的 CSS 资讯,千万不要错过我的公众号 -- iCSS前端趣闻 😄

    更多精彩 CSS 技术文章汇总在我的 Github -- iCSS ,持续更新,欢迎点个 star 订阅收藏。

    如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。

  • 9

    现代 CSS 高阶技巧,完美的波浪进度条效果!

    本文是 CSS Houdini 之 CSS Painting API 系列第三篇。

    在上两篇中,我们详细介绍了 CSS Painting API 是如何一步一步,实现自定义图案甚至实现动画效果的!

    在这一篇中,我们将继续探索,尝试使用 CSS Painting API,去实现一些过往纯 CSS 无法实现的效果。

    CSS Painting API

    再简单快速的过一下,什么是 CSS Painting API。

    CSS Painting API 是 CSS Houdini 的一部分。而 Houdini 是一组底层 API,它们公开了 CSS 引擎的各个部分,从而使开发人员能够通过加入浏览器渲染引擎的样式和布局过程来扩展 CSS。Houdini 是一组 API,它们使开发人员可以直接访问 CSS 对象模型 (CSSOM),使开发人员可以编写浏览器可以解析为 CSS 的代码,从而创建新的 CSS 功能,而无需等待它们在浏览器中本地实现。

    CSS Paint API 目前的版本是 CSS Painting API Level 1。它也被称为 CSS Custom Paint 或者 Houdini's Paint Worklet。

    我们可以把它理解为 JS In CSS,利用 JavaScript Canvas 画布的强大能力,实现过往 CSS 无法实现的功能。

    利用 CSS Painting API 实现波浪效果

    CSS 实现波浪效果,一直是 CSS 的一个难点之一。在过往,虽然我们有很多方式利用 Hack 出一些波浪效果,我在之前的多篇文章中有反复提及过:

    是的,大部分时候,我们都是利用一些奇技淫巧实现波浪效果,像是这样:

    如今,有了 CSS Painting API,我们已经可以绘制真实的波浪效果了。看看代码:

    <div></div>
    
    <script>
    if (CSS.paintWorklet) {              
        CSS.paintWorklet.addModule('/CSSHoudini.js');
    }
    </script>
    
    div {
        position: relative;
        width: 300px;
        height: 300px;
        background: paint(waveDraw);
        border-radius: 50%;
        border: 2px solid rgba(255, 0, 0, 0.5);
    }
    

    我们定义了一个 waveDraw 方法,接下来,就通过利用 registerPaint 来实现这个方法即可。

    // 文件名为 CSSHoudini.js
    registerPaint(
        "waveDraw",
        class {
            static get inputProperties() {
                return [];
            }
            paint(ctx, size, properties) {
                const { width, height } = size;
                const initY = height * 0.5;
                ctx.beginPath();
                for (let i = 0; i <= width; i++) {
                    ctx.lineTo(i, initY + Math.sin((i) / 20) * 10);
                }
                ctx.lineTo(width, height);
                ctx.lineTo(0, height);
                ctx.lineTo(0, initY);
                ctx.closePath();
    
                ctx.fillStyle = 'rgba(255, 0, 0, 0.9)';
                ctx.fill();
            }
        }
    );
    

    这样,我们就得到了这样一个波浪效果:

    image

    上面的代码其实很好理解,简单解释一下,我们核心就是利用路径绘制,基于 Math.sin() 三角函数,绘制了一段 sin(x) 三角函数的图形。

    1. 整个图形从 ctx.beginPath() 开始,第一个点是 ctx.lineTo(0, initY + Math.sin((i) / 20) * 10),不过 Math.sin(0) = 0,所以等于 ctx.lineTo(0, initY)
    2. initY 在这的作用是控制从什么高度开始绘制波浪图形,我们这里的取值是 initY = height * 0.5,也就是定义成了图形的中间位置
    3. 利用 for (let i = 0; i <= width; i++) 循环,配合 ctx.lineTo(i, initY + Math.sin((i) / 20) * 10),也就是在每一个 x 轴上的点,都绘制一个点
    4. 随后三个在循环体外的 ctx.lineTo 的作用是让整个图形形成一个闭环
    5. 最后 ctx.closePath() 完成整个路径,ctx.fill() 进行上色

    如果不 ctx.fill() 上色,利用 ctx.stroke() 绘制边框,也是可以的,其实我们得到是这样一个图形:

    image

    上图是同时去掉了 CSS 代码里面的 border-radius: 50%,方便大家理解。

    当然,上面的图形,有个很大的问题,没法动起来,所以,我们需要借助一个 CSS @Property 自定义变量,让它拥有一些动画效果。

    我们需要改造一下代码,首先,添加一个 CSS @Property 自定义变量:

    @property --animation-tick {
      syntax: '<number>';
      inherits: false;
      initial-value: 1000;
    }
    div {
      // ... 代码与上述保持一致
      animation: move 20s infinite linear;
      --animation-tick: 1000;
    }
    @keyframes move {
        100% {
            --animation-tick: 0;
        }
    }
    

    我们添加了一个 --animation-tick 变量,并且利用 CSS 动画,让它从 1000 减至 0。

    下一步,利用这个不断在变化的 CSS 自定义变量,我们在 waveDraw 方法中,把它利用上:

    // 文件名为 CSSHoudini.js
    registerPaint(
        "waveDraw",
        class {
            static get inputProperties() {
                return ["--animation-tick"];
            }
            paint(ctx, size, properties) {
                let tick = Number(properties.get("--animation-tick"));
                const { width, height } = size;
                const initY = height * 0.5;
                ctx.beginPath();
                for (let i = 0; i <= width; i++) {
                    ctx.lineTo(i, initY + Math.sin((i + tick) / 20) * 10);
                }
                ctx.lineTo(width, height);
                ctx.lineTo(0, height);
                ctx.lineTo(0, initY);
                ctx.closePath();
    
                ctx.fillStyle = 'rgba(255, 0, 0, 0.9)';
                ctx.fill();
            }
        }
    );
    

    仔细看,和上述的代码变化不大,核心在于,利用三角函数绘制图形的时候,我们把这个变量加入进去。

    从原来的 ctx.lineTo(i, initY + Math.sin((i) / 20) * 10),变成了 ctx.lineTo(i, initY + Math.sin((i + tick) / 20) * 10)

    这样,在这个不断变化的变量的作用下,我们的波浪图形就能运动起来了:

    CodePen Demo -- CSS Houdini Wave

    虽然能动了,但是总是感觉还少了些什么。如果我们把这个波浪效果应用与进度条之类的效果上,我们可以需要可以快速定义波浪的振幅、每个波峰之间的间距、效果的颜色、百分比等等。

    因此,我们需要再通过一个 CSS 变量,让它成为一个实际可用的封装良好的波浪进度条。我们再简单改造一下:

    @property --animation-tick {
      syntax: '<number>';
      inherits: false;
      initial-value: 1000;
    }
    @property --height {
      syntax: '<number>';
      inherits: false;
      initial-value: .7;
    }
    div {
        position: relative;
        width: 300px;
        height: 300px;
        background: paint(waveDraw);
        animation: move 20s infinite linear;
        border-radius: 50%;
        border: 2px solid var(--color1);
        --amplitude: 15;
        --gap: 28;
        --animation-tick: 700;
        --height: 0.7;
        --color1: rgba(255, 0, 0, 0.5);
        --color2: rgba(255, 0, 0, 0.4);
        --color3: rgba(255, 0, 0, 0.3);
        
        transition: --height 8s;
    }
    

    可以看到,我们定义了非常多个 CSS 变量,每次,它们都是有意义的:

    • --animation-tick 表示波浪运动的速率
    • --amplitude 波浪的振幅
    • --gap 波峰间距
    • --initHeight 初始高度
    • --color1--color2--color3 我们会叠加 3 层波浪效果,显得更真实一点,这里 3 个颜色表示 3 层波浪的颜色

    定义好这些 CSS 变量后,我们就可以把它们运用在实际的waveDraw 方法中。看看代码:

    registerPaint(
        "waveDraw",
        class {
            static get inputProperties() {
                return [
                    "--animation-tick", 
                    "--height", 
                    "--gap",
                    "--amplitude",
                    "--color1",
                    "--color2",
                    "--color3"
                ];
            }
            
            paint(ctx, size, properties) {
                let tick = Number(properties.get("--animation-tick"));
                let initHeight = Number(properties.get("--height"));
                let gap = Number(properties.get("--gap"));
                let amplitude = Number(properties.get("--amplitude"));
                let color1 = properties.get("--color1");
                let color2 = properties.get("--color2");
                let color3 = properties.get("--color3");
                
                this.drawWave(ctx, size, tick, amplitude, gap, initHeight, color1);
                this.drawWave(ctx, size, tick * 1.21, amplitude / 0.82, gap + 2, initHeight + 0.02, color2);
                this.drawWave(ctx, size, tick * 0.79, amplitude / 1.19, gap - 2, initHeight - 0.02, color3);
            }
            
            /**
             * ctx
             * size
             * tick 速率
             * amplitude 振幅
             * gap 波峰间距
             * initHeight 初始高度
             * color 颜色
             */
            drawWave(ctx, size, tick, amplitude, gap, initHeight, color) {
                const { width, height } = size;
                const initY = height * initHeight;
                tick = tick * 2;
                
                ctx.beginPath();
                for (let i = 0; i <= width; i++) {
                    ctx.lineTo(i, initY + Math.sin((i + tick) / gap) * amplitude);
                }
                ctx.lineTo(width, height);
                ctx.lineTo(0, height);
                ctx.lineTo(0, initY);
                ctx.closePath();
                ctx.fillStyle = color;
                ctx.fill();
            }
        }
    );
    

    可以看到,我们在 paint() 方法中,调用了 this.drawWave()。每次调用 this.drawWave() 都会生成一个波浪图形,通过 3 层的叠加效果,生成 3 层波浪。并且,把我们在 CSS 中定义的变量全部的应用了起来,分别控制波浪效果的不同参数。

    这样,我们就得到了这样一个波浪效果:

    通过控制 CSS 中的 --height 变量,还可以实现高度的变化,从而完成真实的百分比,实现一种进度条效果。

    div:hover {
        --height: 0;
    }
    

    效果如下:

    很好,非常不错的效果。有了上述一些 CSS 自定义变量的帮助,我们就可以通过封装好的 waveDraw 方法,实现不同颜色,不同大小,不同速率的波浪进度条效果了。

    我们只需要简单的改变一下传入的 CSS 变量参数即可:

    <div></div>
    <div></div>
    <div></div>
    
    
    div {
        position: relative;
        width: 300px;
        height: 300px;
        background: paint(waveDraw);
        animation: move 20s infinite linear;
        border-radius: 50%;
        border: 2px solid var(--color1);
        --amplitude: 15;
        --gap: 28;
        --animation-tick: 700;
        --height: 0.7;
        --color1: rgba(255, 0, 0, 0.5);
        --color2: rgba(255, 0, 0, 0.4);
        --color3: rgba(255, 0, 0, 0.3);
        
        transition: --height 8s;
    }
    div:nth-child(2) {
        --amplitude: 6;
        --gap: 25;
        --animation-tick: 300;
        --height: 0.5;
        --color1: rgba(28, 90, 199, 0.5);
        --color2: rgba(28, 90, 199, 0.4);
        --color3: rgba(28, 90, 199, 0.3);
    }
    div:nth-child(3) {
        --amplitude: 3;
        --gap: 30;
        --animation-tick: 1200;
        --height: 0.3;
        --color1: rgba(178, 120, 33, 0.5);
        --color2: rgba(178, 120, 33, 0.4);
        --color3: rgba(178, 120, 33, 0.3);
    }
    

    看看效果如何:

    CodePen Demo -- CSS Hudini Custom Wave Effects !

    这样,借助 CSS Painting API,我们完美的实现了波浪图形,并且借助它,实现了波浪进度条效果。通过传入不同的 CSS 变量,我们有了快速批量生成不同效果的能力。弥补了过往 CSS 在波浪效果上的缺陷!

    当然,就基于上述的代码,还是有一些可以优化的空间的:

    1. 在上述的 CSS 代码中,可以看到,我们是传入了 3 个关于颜色的 CSS 变量,--color1--color2--color3,正常而言,这里传入 1 个颜色即可,通过转换成 HSL 颜色表示法,替换 L 色值,得到近似的另外两个色值即可。当然,这样做的话会增添非常多的 JavaScript 代码,所以,本文为了方便大家理解,偷懒直接传入了 3 个 CSS 颜色变量值;
    2. 整个波浪效果单轮的动画持续时间我设置为了 20s,但是在本文中,没有去适配动画的手尾衔接,也就是可能会出现每 20s,波浪效果有一个明显的跳动的感觉。解决这个问题,有两个思路
      • 通过精确的计算,让动画的最后一帧和动画的第一帧衔接上
      • --animation-tick 的值设置的非常的大,然后把相应的单轮动画时间设置的非常长,这样,基本也感受不到动画的跳帧
    3. 第三个问题可能就在于兼容性

    兼容性?

    好吧,其实上一篇文章也谈到了兼容问题,因为可能有很多看到本篇文章并没有去翻看前两篇文章的同学。那么,CSS Painting API 的兼容性到底如何呢?

    CanIUse - registerPaint 数据如下(截止至 2022-11-23):

    image

    Chrome 和 Edge 基于 Chromium 内核的浏览器很早就已经支持,而主流浏览器中,Firefox 和 Safari 目前还不支持。

    CSS Houdini 虽然强大,目前看来要想大规模上生产环境,仍需一段时间的等待。让我们给时间一点时间!

    最后

    好了,本文到此结束,希望本文对你有所帮助 :)

    想 Get 到最有意思的 CSS 资讯,千万不要错过我的公众号 -- iCSS前端趣闻 😄

    更多精彩 CSS 技术文章汇总在我的 Github -- iCSS ,持续更新,欢迎点个 star 订阅收藏。

    如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。

  • 10

    哇哦,巧用视觉障眼法,还原 3D 文字特效

    最近群里有这样一个有意思的问题,大家在讨论,使用 CSS 3D 能否实现如下所示的效果:

    这里的核心难点在于,如何利用 CSS 实现一个立体的数字?CSS 能做到吗?

    不是特别好实现,但是,如果仅仅只是在一定角度内,利用视觉障眼法,我们还是可以比较完美的还原上述效果的。

    利用距离、角度及光影构建不一样的 3D 效果

    这是一种很有意思的技巧,在 奇思妙想 CSS 3D 动画 | 仅使用 CSS 能制作出多惊艳的动画? 我们曾经介绍过,当然,制作的过程需要比较多的调试。

    合理的利用距离、角度及光影构建出不一样的 3D 效果。看看下面这个例子,只是简单是设置了三层字符,让它们在 Z 轴上相距一定的距离。

    简单的伪代码如下:

    <div>
      <span class='C'>C</span>
      <span class='S'>S</span>
      <span class='S'>S</span>
      <span></span>
      <span class='3'>3</span>
      <span class='D'>D</span>
    </div>
    
    $bright : #AFA695;
    $gold : #867862;
    $dark : #746853;
    $duration : 10s;
    div {
    	perspective: 2000px;
    	transform-style: preserve-3d;
    	animation: fade $duration infinite;
    }
    span {
    	transform-style: preserve-3d;
    	transform: rotateY(25deg);
    	animation: rotate $duration infinite ease-in;
    	
    	&:after, &:before {
    		content: attr(class);
    		color: $gold;
    		z-index: -1;
    		animation: shadow $duration infinite;
    	}
    	&:after{
    		transform: translateZ(-16px);
    	}
    	&:before {
    		transform: translateZ(-8px);
    	}
    }
    @keyframes fade {
    	// 透明度变化
    }
    @keyframes rotate {
    	// 字体旋转
    }
    @keyframes shadow {
           // 字体颜色变化
    }
    

    简单捋一下,上述代码的核心就是:

    1. 父元素、子元素设置 transform-style: preserve-3d
    2. span 元素的两个伪元素复制两个相同的字,利用 translateZ() 让它们在 Z 轴间隔一定距离
    3. 添加简单的旋转、透明度、字体颜色变化

    可以得到这样一种类似电影开片的标题 3D 动画,其实只有 3 层元素,但是由于角度恰当,视觉上的衔接比较完美,看上去就非常的 3D。

    为什么上面说需要合理的利用距离、角度及光影呢?

    还是同一个动画效果,如果动画的初始旋转角度设置的稍微大一点,整个效果就会穿帮:

    可以看到,在前几帧,能看出来简单的分层结构。又或者,简单调整一下 perspective,设置父容器的 perspective2000px 改为 500px,穿帮效果更为明显:

    也就是说,在恰当的距离,合适的角度,我们仅仅通过很少的元素,就能在视觉上形成比较不错的 3D 效果。

    上述的完整代码,你可以猛击这里:CSS 灵感 -- 3D 文字出场动画

    基于,这个技巧,我们简单的改造一下,我们首先替换一下文字效果:

    <div>
      <span class='2'>2</span>
      <span class='0'>0</span>
      <span class='2'>2</span>
      <span class='3'>3</span>
    </div>
    

    这样,我们就可以得到这样一种效果:

    Wow,有点那个意思了。接下来,我们需要换上喜庆的文字效果。首先,随便在网上找一找烟花 Gif 图,也许是这样:

    我们通过 background-clip: text,给 3 层文字都加上类似这个效果,核心伪代码如下:

    span {
    	position: relative;
    	transform-style: preserve-3d;
    	color: transparent;
    	background: url(xxx.gif);
    	background-clip: text;
    	
    	&:after, &:before {
    		position: absolute;
    		content: attr(class);
    		color: transparent;
    		background: url(xxx.gif);
    	        background-clip: text;
    	}
    	
    	&:before {
    		transform: translateZ(-12px);
    	}
    	&:after {
    		transform: translateZ(-6px);
    	}
    }
    

    这样,我们就得到了带有烟花效果的文字,以及,一点 3D 效果:

    还剩下最后一步,倒影效果怎么制作呢?

    方法有很多种,比较便捷的是使用 webkit-box-reflect 元素。只需要一行代码即可:

    div {
        //...
        -webkit-box-reflect: below -6vw linear-gradient(transparent 20%, rgba(255,255,255, .6));
    }
    

    当然,如果对两个伪元素生成的字形成的 3D 文字视觉上的厚度不满意,也可以同步去调整两个伪元素的 transform: translateZ(-6px) 的值,经过一番调整后,我们就简单的复刻了如题图所示的 2023 3D 文字动画效果:

    完整的代码,你可以戳这里:CodePen Demo -- CSS 3D 2023

    最后

    好了,本文到此结束,非常有意思的一个小技巧,希望本文对你有所帮助 :)

    想 Get 到最有意思的 CSS 资讯,千万不要错过我的公众号 -- iCSS前端趣闻 😄

    更多精彩 CSS 技术文章汇总在我的 Github -- iCSS ,持续更新,欢迎点个 star 订阅收藏。

    如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。

  • 11

    开局一张图,构建神奇的 CSS 效果

    假设,我们有这样一张 Gif 图:

    利用 CSS,我们尝试来搞一些事情。

    图片的 Glitch Art 风

    在这篇文章中 --CSS 故障艺术,我们介绍了利用混合模式制作一种晕眩感觉的视觉效果。有点类似于抖音的 LOGO。

    像是这样:

    假设,我们有这样一张图:

    image

    只需要一个标签即可

    <div class="mix"></div>
    

    给两张同样的图片,叠加上 青色#0ff 和 红色#f00,并且错开一定的距离,两张图都要加上 background-blend-mode: lighten,其中一张再加上 mix-blend-mode: darken

    .mix {
        width: 400px;
        height: 400px;
        background: url($img), #0ff;
        background-blend-mode: lighten;
    
      &::after {
        content: '';
        position: absolute;
        margin-left: 10px;
        width: 400px;
        height: 400px;
        background: url($img), #f00;
        background-blend-mode: lighten;
        mix-blend-mode: darken;
      }
    }
    

    得到如下效果:

    简单解释下:

    1. 因为图片本身不是红色和青色的,所以需要通过 background-image 叠加上这两种颜色,并通过 background-blend-mode: lighten 让其表现出来

    2. 为了保持中间叠加部分的原色,需要再叠加一个 mix-blend-mode: darken 反向处理一下。(不理解的同学可以打开调试,手动关掉几个混合模式,自己感受感受即可)

    完整的 DEMO:

    图片的类抖音 LOGO Glitch 效果

    当然,这里使用 Gif 图也是完全可以的,我们替换下我们的 Gif 图,看看会得到什么样的一种效果:

    有点意思,完整的代码你可以戳这里:iKUN - 使用background-blend-mode | mix-blend-mode 实现类抖音LOGO晕眩效果

    多图融合

    混合模式当然不止是这样。

    我们再来实现一个有趣的效果。

    首先,找一张地球图,可能像是这样(是不是有点眼熟):

    把我们的人物放上去,得到这样一种效果:

    神奇的事情在于,如果,我们给叠加在上面的动图,添加一个混合模式,会发生什么呢?尝试一下:

    通过混合模式 mix-blend-mode: multiply,巧妙的消除了大部分非人物的背景,再通过 filter: contrast(3) 加深这个效果,彻底去掉动图背景,融入了我们的地球背景中。

    这样,我们巧妙的将两张图,融合成了一张图。

    当然,多调试调试,还能有不一样的效果,这里我实现了两种不一样的效果,完整的代码如下:

    <div></div>
    <div class="white"></div>
    
    div {
        position: relative;
        margin: auto;
        width: 400px;
        height: 500px;
        flex-shrink: 0;
        background: url(earth.jpg);
        background-size: cover;
        background-position: 0 -150px;
        background-repeat: no-repeat;
        
        &::before {
            content: "";
            position: absolute;
            top: 240px;
            left: 160px;
            width: 70px;
            height: 90px;
            background: var(cxk.gif);
            background-size: cover;
            background-position: -30px 0;
            mix-blend-mode: multiply;
            filter: contrast(3);
        }
    }
    
    
    .white {
        &::before {
            mix-blend-mode: color-dodge;
            filter: invert(1) contrast(3);
        }
    }
    
    .black {
        &::before {
            background: var(--bgUrl), #000;
            background-size: cover;
            background-position: -70px 0;
            mix-blend-mode: multiply;
            filter: contrast(3);
        }
    }
    

    这样,我们就得到了两种不一样的效果:

    完整的 Demo,你可以戳这里:CodePen Demo -- CSS iKUN Animation

    干掉背景

    上面的效果不错,但是,还远远不够。

    有的时候,我们只想更突出主题,不想过多的看到背景元素。

    怎么办呢?

    这里,我介绍两种还不错的小技巧,当然,这个技巧对图片本身可能会有一点点要求。

    第一个技巧,是我在这篇文章中,曾经介绍过的一个技巧 -- 巧用 background-clip 实现超强的文字动效

    这里的核心在于,借助 background-clip: text 能够只在文字部分展示图片内容的特性,结合滤镜和混合模式的处理,实现一种文字动图效果。达到有效的去除一些背景的干扰。

    我们一起来看看。

    还是这张 Gif 图:

    我们首先通过滤镜 filter: grayscale(1),将他从彩色的,处理成黑白灰的:

    p {
        background: url(xxx);
        filter: grayscale(1);
    }
    

    处理后的图片,大概会是这样:

    image

    基于一张黑白底色的图片,我们再运用 background-clip: text,再通过混合模式 mix-blend-mode: hard-light,并且,很重要的一点,我们把这个效果放在黑色的背景之上:

    body {
      background: #000;
    }
    p {
      color: transparent;
      background: url(xxx) center/cover;
      background-clip: text;
      filter: grayscale(1);
      mix-blend-mode: hard-light;
    }
    

    将会得到这样一种神奇的效果,通过混合模式的叠加处理,文字的亮部将会保留,而暗部则会与黑色背景融合:

    image

    当然,我们更希望的是,人的部分展示保留,而 Gif 图片中的背景部分被隐藏,就完美了!

    这里,我们继续优化下代码,我们希望能把被 grayscale() 处理过的原图的明暗部分置换,刚好,在 filter 中,存在一个 invert() 函数,能够反转输入图像的色值。

    因此,在 grayscale() 之后,再配合一次 invert(1) 函数:

    body {
      background: #000;
    }
    p {
      color: transparent;
      background: url(xxx) center/cover;
      background-clip: text;
      filter: grayscale(1) invert(1);
      mix-blend-mode: hard-light;
    }
    

    OK,至此,我们利用纯 CSS 实现了这样一种 unbelievable 的文字效果:

    gb11

    合理添加混合模式 mix-blend-mode,能够更好的去除背景的干扰,实际使用的时候根据不同图片的颜色需要进行一定的调试。

    CodePen Demo - iKUN Animation

    另一种干掉背景的方式

    那是不是只有上述的方式可以干掉图片的背景,保留主体人物部分呢?

    当然不止,还有其他方式。下面,我们不借助 background-clip: text,通过另外一种借助混合模式和滤镜的方式去掉背景干扰。

    我们借助 Demo 1 的例子继续,就是如下这个效果:

    在这个例子的基础上,我们直接加上 filter: grayscale(1) invert(1)mix-blend-mode: hard-light,像是这样:

    .mix {
        background: url($img), #0ff;
        background-blend-mode: lighten;
        filter: grayscale(1) invert(1);
        mix-blend-mode: hard-light;
    
      &::after {
        content: '';
        position: absolute;
        margin-left: 10px;
        background: url($img), #f00;
        background-blend-mode: lighten;
        mix-blend-mode: darken;
      }
    }
    

    看看效果:

    Wow,怎么做到的呢?我们来调试一些,你就能更好的 Get 到其中的奥妙:

    gb44

    这里,核心发挥作用的还是 filter: grayscale(1) invert(1),而 mix-blend-mode: hard-light 的意义是让一些不那么明显的背景直接比较被干掉。

    完整的代码,你可以戳:CodePen Demo -- iKUN Animation

    再动起来

    有点意思,但还不够。我们再回到 background-clip: text 的效果中。

    背景图在动,我们能不能让文字也动起来呢?这样,整个动画就处于一种 Gif 在图,我们的内容也在动的双重动效之下。

    尝试一下,这里为了尝试更多的效果,我借助了 CSS-doodle 这个库,这里我们核心要做的事情是:

    1. 借助 background-clip: text 只展示文字部分的背景图的特性,首先设置多个重叠在一起的 DIV
    2. 每个 DIV 都借助文章上面介绍的技巧,设置背景图,利用 filter: grayscale(1) invert(1),只展示人的部分
    3. 给每个 DIV 添加文本内容,添加 background-clip: text
    4. 随机给文本设置初始高度定位
    5. 通过动画让文本动起来,并且设置不同的 animation-delay

    上面其实只是最核心的一些流程介绍,可以结合代码一起看看,完整的 CSS-doodle 代码如下:

    <css-doodle grid="10x10">
        :doodle {
            @size: 70vmin 70vmin;
        }
        :container {
            position: relative;
            filter: grayscale(1) invert(1);
        }
        position: absolute;
        inset: 0;
        
        :after {
            position: absolute;
            content:"---------";
            inset: 0;
            font-size: 24px;
            line-height: 0;
            color: transparent;
            background: url(xxx)
            center/cover;
            background-clip: text;
            padding-top: @r(69vmin);
            animation: move .5s @r(-0.99s) infinite linear;
        }
        
        @keyframes move {
            0% {
                padding-left: 0
            }
            100% {
                padding-left: 70vmin;
            }
        }
    </css-doodle>
    
    html,
    body {
        position: relative;
        margin: 0;
        width: 100%;
        height: 100%;
        display: flex;
        align-items: center;
        justify-content: center;
        overflow: hidden;
        background-color: #000;
        cursor: pointer;
    }
    

    这样,我们就得到了一种图在动,内容也在动的效果:

    当然,这个效果可能会有一点绕!实际上你可以想象一下,把图片固定,通过 background-clip: text 透出图片内容,同时,让文本内容动起来,就是如此。如果去掉 background-clip: text 看看下图,可能你会更好理解一点:

    gb9999

    当然,实际上如果去掉 background-clip: text 并不会如上图所示,因为这里每一层会使用一张背景图,background-clip 无法引用于它的子元素,只能应用于本身,所以这个动画也有一个缺陷,如果图层数量太多,效果会比较卡顿。

    CodePen Demo -- CSS Doodle - iKUN Animation

    修改每个 DIV 的文本内容,得到的效果也不相同,像是把内容替换成 .。.,可以得到这样的效果:

    CodePen Demo -- CSS Doodle - iKUN Animation

    3D 视角

    OK,最后我们再来尝试下 3D 视角。

    使用 CSS,我们可以非常轻松的实现 3D 多面体,像是这样:

    如果我们把每边的图片,替换成上述的效果,再把我们的视角放置于中间,会发生什么呢?看看,八面体的图片墙:

    再尝试把视角,放进 3D 照片墙的中间:

    Wow,是不是挺有意思的,完整的 Demo,你可以戳这里:iKUN Animation

    不断改变 perspective,还可以得到不一样的观感体验,感兴趣的,可以自己调试调试。

    总结

    总结一下,本文通过一张 Gif 图,介绍了一些利用 CSS 来实现的有趣例子。

    当然,CSS 的强大远不止这样,本文仅仅是挖掘了一个方向,从将人物凸显的方向,列出了一些我认为比较有意思的动效。

    核心用到了混合模式、滤镜、background-clip、CSS-Doodle 以及配合了一些动画,这些日常中大家可能用的不太多的属性,如果你对这些属性还不是特别了解,希望进阶一下,不妨再看看我的这些文章:

    最后

    好了,本文到此结束,希望本文对你有所帮助 :)

    想 Get 到最有意思的 CSS 资讯,千万不要错过我的公众号 -- iCSS前端趣闻 😄

    更多精彩 CSS 技术文章汇总在我的 Github -- iCSS ,持续更新,欢迎点个 star 订阅收藏。

    如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。

  • 12

    现代 CSS 高阶技巧,像 Canvas 一样自由绘图构建样式!

    在上一篇文章中 -- 现代 CSS 之高阶图片渐隐消失术,我们借助了 CSS @Property 及 CSS Mask 属性,成功的实现了这样一种图片渐变消失的效果:

    CodePen Demo -- 基于 @property 和 mask 的文本渐隐消失术

    但是,这个效果的缺陷也非常明显,虽然借助了 SCSS 简化了非常多的代码,但是,如果我们查看编译后的 CSS 文件,会发现,在利用 SCSS 只有 80 的代码的情况下,编译后的 CSS 文件行数高达 2400+ 行,实在是太夸张了。

    究其原因在于,我们利用原生的 CSS 去控制 400 个小块的过渡动画,控制了 400 个 CSS 变量!代码量因而变得如此之大。

    CSS Houdini 之 CSS Paint API

    那么,如何有效的降低代码量呢?

    又或者说,在今天,是否 CSS 还存在着更进一步的功能,能够实现更为强大的效果?

    没错,是可以的,这也就引出了今天的主角,CSS Houdini 之 CSS Paint API

    首先,什么是 CSS Houdini?

    Houdini 是一组底层 API,它们公开了 CSS 引擎的各个部分,从而使开发人员能够通过加入浏览器渲染引擎的样式和布局过程来扩展 CSS。Houdini 是一组 API,它们使开发人员可以直接访问 CSS 对象模型 (CSSOM),使开发人员可以编写浏览器可以解析为 CSS 的代码,从而创建新的 CSS 功能,而无需等待它们在浏览器中本地实现。

    而 CSS Paint API 则是 W3C 规范中之一,目前的版本是 CSS Painting API Level 1。它也被称为 CSS Custom Paint 或者 Houdini's Paint Worklet。

    简单来说人话,CSS Paint API 的优缺点都很明显。

    CSS Paint API 的优点

    1. 实现更为强大的 CSS 功能,甚至是很多 CSS 原本不支持的功能
    2. 将这些自定义的功能,很好的封装起来,当初一个属性快速复用

    当然,优点看着很美好,缺点也很明显,CSS Paint API 的缺点

    1. 需要写一定量的 JavaScript 代码,有一定的上手成本
    2. 现阶段兼容的问题

    小试牛刀 registerPaint

    CSS Houdini 的一个特性就是 Worklet (en-US)。在它的帮助下,你可以通过引入一行 JavaScript 代码来引入配置化的组件,从而创建模块式的 CSS。不依赖任何前置处理器、后置处理器或者 JavaScript 框架。

    废话不多说,我们直接来看一个最简单的例子。

    <div style="--color: red"></div>
    <div style="--color: blue"></div>
    <div style="--color: yellow"></div>
    
    <script>
    if (CSS.paintWorklet) {              
        CSS.paintWorklet.addModule('/CSSHoudini.js');
    }
    </script>
    
    div {
        margin: auto;
        width: 100px;
        height: 100px;
        background: paint(drawBg);
    }
    
    // 这个文件的名字为 CSSHoudini.js
    // 对应上面 HTML 代码中的 CSS.paintWorklet.addModule('/CSSHoudini.js')
    registerPaint('drawBg', class {
      
       static get inputProperties() {return ['--color']}
       
       paint(ctx, size, properties) {
           const c = properties.get('--color');
          
           ctx.fillStyle = c;
           ctx.fillRect(0, 0, size.width, size.height);
       }
    });
    

    先看看最终的结果:

    看似有点点复杂,其实非常好理解。仔细看我们的 CSS 代码,在 background 赋值的过程中,没有直接写具体颜色,而是借助了一个自定义了 CSS Houdini 函数,实现了一个名为 drawBg 的方法。从而实现的给 Div 上色。

    registerPaint 是以 worker 的形式工作,具体有几个步骤:

    1. 建立一个 CSSHoudini.js,比如我们想用 CSS Painting API,先在这个 JS 文件中注册这个模块 registerPaint('drawBg', class),这个 class 是一个类,下面会具体讲到
    2. 我们需要在 HTML 中引入 CSS.paintWorklet.addModule('CSSHoudini.js'),当然 CSSHoudini.js 只是一个名字,没有特定的要求,叫什么都可以,
    3. 这样,我们就成功注册了一个名为 drawBg 的自定义 Houdini 方法,现在,可以用它来扩展 CSS 的功能
    4. 在 CSS 中使用,就像代码中示意的那样 background: paint(drawBg)
    5. 接下来,就是具体的 registerPaint 实现的 drawBg 的内部的代码

    上面的步骤搞明白后,核心的逻辑,都在我们自定义的 drawBg 这个方法后面定义的 class 里面。CSS Painting API 非常类似于 Canvas,这里面的核心逻辑就是:

    1. 可以通过 static get inputProperties() {} 拿到各种从 CSS 传递进来的 CSS 变量
    2. 通过一套类似 Canvas 的 API 完成整个图片的绘制工作

    而我们上面 DEMO 做的事情也是如此,获取到 CSS 传递进来的 CSS 变量的值。然后,通过 ctx.fillStylectx.fillRect 完成整个背景色的绘制。

    使用 registerPaint 实现自定义背景图案

    OK,了解了上面最简单的 DEMO 之后,接下来我们尝试稍微进阶一点点。利用 registerPaint 实现一个 circleBgSet 的自定义 CSS 方法,实现类似于这样一个背景图案:

    CodePen Demo -- CSS Hudini Example - Background Circle

    首先,我们还是要在 HTML 中,利用 CSS.paintWorklet.addModule('') 注册引入我们的 JavaScript 文件。

    <div style=""></div>
    
    <script>
    if (CSS.paintWorklet) {              
         CSS.paintWorklet.addModule('/CSSHoudini.js'');
    }
    </script>
    

    其次,在 CSS 中,我们只需要在调用 background 属性的时候,传入我们即将要实现的方法:

    div {
        width: 100vw;
        height: 1000vh;
        background: paint(circleBgSet);
        --gap: 3;
        --color: #f1521f;
        --size: 64;
    }
    

    可以看到,核心在于 background: paint(circleBgSet),我们将绘制背景的工作,交给接下来我们要实现的 circleBgSet 方法。同时,我们定义了 3 个 CSS 变量,它们的作用分别是:

    1. --gap:表示圆点背景的间隙
    2. -color:表示圆点的颜色
    3. --size:表示圆点的最大尺寸

    好了,接下来,只需要在 JavaScript 文件中,利用 CSS Painting API 实现 circleBgSet 方法即可。

    来看看完整的 JavaScript 代码:

    // 这个文件的名字为 CSSHoudini.js
    registerPaint(
        "circleBgSet",
        class {
            static get inputProperties() {
                return [
                    "--gap", 
                    "--color",
                    "--size"
                ];
            }
    
            paint(ctx, size, properties) {
                const gap = properties.get("--gap");
                const color = properties.get("--color");
                const eachSize = properties.get("--size");
                const halfSize = eachSize / 2;
                
                const n = size.width / eachSize;
                const m = size.height / eachSize;
                
                ctx.fillStyle = color;
               
                for (var i = 0; i < n + 1; i++) {
                    for (var j = 0; j < m + 1; j++) {
                        
                        let x = i * eachSize + ( j % 2 === 0 ? halfSize : 0);
                        let y = j * eachSize / gap;
                        let radius = i * 0.85;
                        
                        ctx.beginPath();
                        ctx.arc(x, y, radius, 0, 2 * Math.PI);
                        ctx.fill();
                    }
                }
            }
        }
    );
    

    代码其实也不多,并且核心的代码非常好理解。这里,我们再简单的解释下:

    1. static get inputProperties() {},我们在 CSS 代码中定义了一些 CSS 变量,而需要取到这些变量的话,需要利用到这个方法。它使我们能够访问所有 CSS 自定义属性和它们设置的值。

    2. paint(ctx, size, properties) {} 核心绘画的方法,其中 ctx 类似于 Canvas 2D 画布的 ctx 上下文对象,size 表示 PaintSize 对象,可以拿到对于元素的高宽值,而 properties 则是表示 StylePropertyMapReadOnly 对象,可以拿到 CSS 变量相关的信息

    1. 最终,仔细看看我们的 paint() 方法,核心做的就是拿到 CSS 变量后,基于双重循环,把我们要的图案绘制在画布上。这里核心就是调用了下述 4 个方法,对 Canvas 了解的同学不难发现,这里的 API 和 Canvas 是一模一样的。
      • ctx.fillStyle = color
      • ctx.beginPath()
      • ctx.arc(x, y, radius, 0, 2 * Math.PI)
      • ctx.fill()

    这里,其实 CSS Houdini 的画布 API 是 Canvas API 的是一样的,具体存在这样一些映射,我们在官方规范 CSS Painting API Level 1 - The 2D rendering context 可以查到:

    还记得我们上面传入了 3 个 CSS 变量吗?这里我们只需要简单改变上面的 3 个 变量,就可以得到不一样的图形。让我们试一试:

    div {
        width: 100vw;
        height: 1000vh;
        background: paint(circleBgSet);
        // --gap: 3;
        // --color: #f1521f;
        // --size: 64;
        --gap: 6;
        --color: #ffcc00;
        --size: 75;
    }
    

    又或者:

    div {
        width: 100vw;
        height: 1000vh;
        background: paint(circleBgSet);
        // --gap: 3;
        // --color: #f1521f;
        // --size: 64;
        --gap: 4;
        --color: #0bff00;
        --size: 50;
    }
    

    利用 registerPaint 实现自定义 mask

    有了上面的铺垫,下面我们开始实现我们今天的主题,利用 registerPaint 自定义方法还原实现这个效果,简化 CSS 代码量:

    自定义的 paint 方法,不但可以用于 background,你想得到的地方,其实都可以。

    能力越大,责任越大!在 Houdini 的帮助下你能够在 CSS 中实现你自己的布局、栅格、或者区域特性,但是这么做并不是最佳实践。CSS 工作组已经做了许多努力来确保 CSS 中的每一项特性都能正常运行,覆盖各种边界情况,同时考虑到了安全、隐私,以及可用性方面的表现。如果你要深入使用 Houdini,确保你也把以上这些事项考虑在内!并且先从小处开始,再把你的自定义 Houdini 推向一个富有雄心的项目。

    因此,这里,我们利用 CSS Houdini 的 registerPaint 实现自定义的 mask 属性绘制。

    首先,还是一样,HTML 中需要引入一下定义了 registerPaint 方法的 JavaScript 文件:

    <div></div>
    
    <script>
    if (CSS.paintWorklet) {              
        CSS.paintWorklet.addModule('/CSSHoudini.js');
    }
    </script>
    

    首先,我们会实现一张简单的图片:

    
    div {
        width: 300px;
        height: 300px;
        background: url(https://tvax4.sinaimg.cn/large/6f8a2832gy1g8npte0txnj21jk13a4qr.jpg);
    }
    

    效果如下:

    当然,我们的目标是利用 registerPaint 实现自定义 mask,那么需要添加一些 CSS 代码:

    
    div {
        width: 300px;
        height: 300px;
        background: url(https://tvax4.sinaimg.cn/large/6f8a2832gy1g8npte0txnj21jk13a4qr.jpg);
        mask: paint(maskSet);
        --size-m: 10;
        --size-n: 10;
    }
    

    这里,我们 mask: paint(fragmentation) 表示使用我们自定义的 maskSet 方法,并且,我们引入了两个 CSS 自定义变量 --size-m--size-n,表示我们即将要用 mask 属性分隔图片的行列数。

    接下来,就是具体实现新的自定义 mask 方法。当然,这里我们只是重新实现一个 mask,而 mask 属性本身的特性,透明的地方背后的内容将会透明这个特性是不会改变的。

    JavaScript 代码:

    // 这个文件的名字为 CSSHoudini.js
    registerPaint(
        "maskSet",
        class {
            static get inputProperties() {
                return ["--size-n", "--size-m"];
            }
    
            paint(ctx, size, properties) {
                const n = properties.get("--size-n");
                const m = properties.get("--size-m");
                const width = size.width / n;
                const height = size.height / m;
    
                for (var i = 0; i < n; i++) {
                    for (var j = 0; j < m; j++) {
                        ctx.fillStyle = "rgba(0,0,0," + Math.random() + ")";
                        ctx.fillRect(i * width, j * height, width, height);
                    }
                }
            }
        }
    );
    

    这一段代码非常好理解,我们做的事情就是拿到两个 CSS 自定义变量 --size-n--size-m 后,通过一个双循环,依次绘制正方形填满整个 DIV 区域,每个小正方形的颜色为带随机透明度的黑色。

    记住,mask 的核心在于,透过颜色的透明度来隐藏一个元素的部分或者全部可见区域。因此,整个图片将变成这样:

    当然,我们这个自定义 mask 方法也是可以用于 background 的,如果我们把这个方法作用于 backgorund,你会更好理解一点。

    div {
        width: 300px;
        height: 300px;
        background: paint(maskSet);
        // mask: paint(maskSet);
        --size-m: 10;
        --size-n: 10;
    }
    

    实际的图片效果是这样:

    好,回归正题,我们继续。我们最终的效果还是要动画效果,Hover 的时候让图片方块化消失,肯定还是要和 CSS @property 自定义变量发生关联的,我们简单改造下代码,加入一个 CSS @property 自定义变量。

    @property --transition-time {
      syntax: '<number>';
      inherits: false;
      initial-value: 1;
    }
    
    div {
        width: 300px;
        height: 300px;
        background: url(https://tvax4.sinaimg.cn/large/6f8a2832gy1g8npte0txnj21jk13a4qr.jpg);
        mask: paint(fragmentation);
        --size-m: 10;
        --size-n: 10;
        --transition-time: 1;
        transition: --transition-time 1s linear;
    }
    
    div:hover {
      --transition-time: 0;
    }
    

    这里,我们引入了 --transition-time 这个变量。接下来,让他在 maskSet 函数中,发挥作用:

    registerPaint(
        "maskSet",
        class {
            static get inputProperties() {
                return ["--size-n", "--size-m", "--transition-time"];
            }
    
            paint(ctx, size, properties) {
                const n = properties.get("--size-n");
                const m = properties.get("--size-m");
                const t = properties.get("--transition-time");
                const width = size.width / n;
                const height = size.height / m;
    
                for (var i = 0; i < n; i++) {
                    for (var j = 0; j < m; j++) {
                        ctx.fillStyle = "rgba(0,0,0," + (t * (Math.random() + 1)) + ")";
                        ctx.fillRect(i * width, j * height, width, height);
                    }
                }
            }
        }
    );
    

    这里,与上面唯一的变化在于这一行代码:ctx.fillStyle = "rgba(0,0,0," + (t * (Math.random() + 1)) + ")"

    对于每一个小格子的 mask,我们让他的颜色值的透明度设置为 (t * (Math.random() + 1))

    1. 其中 t 就是 --transition-time 这个变量,记住,在 hover 的过程中,它的值会逐渐从 1 衰减至 0
    2. (Math.random() + 1) 表示先生成一个 0 ~ 1 的随机数,再让这个随机数加 1,加 1 的目的是让整个值必然大于 1,处于 1 ~ 2 的范围
    3. 由于一开始 --transition-time 的值一开始是 1,所以乘以 (Math.random() + 1) 的值也必然大于 1,而最终在过渡过程中 --transition-time 会逐渐变为 0, 整个表达式的值也最终会归于 0
    4. 由于上述 (3)的值控制的是每一个 mask 小格子的透明度,也就是说每个格子的透明度都会从一个介于 1 ~ 2 的值逐渐变成 0,借助这个过程,我们完成了整个渐隐的动画

    看看最终的效果:

    CodePen Demo -- CSS Hudini Example

    是的,细心的同学肯定会发现,文章一开头给的 DEMO 是切分了 400 份 mask 的,而我们上面实现的效果,只用了 100 个 mask。

    这个非常好解决,我们不是传入了 --size-n--size-m 两个变量么?只需要修改这两个值,就可以实现任意格子的 Hover 渐隐效果啦。还是上面的代码,简单修改 CSS 变量的值:

    div:nth-child(1) {
        --size-m: 4;
        --size-n: 4; 
    }
    div:nth-child(2) {
        --size-m: 6;
        --size-n: 6; 
    }
    div:nth-child(3) {
        --size-m: 10;
        --size-n: 10; 
    }
    div:nth-child(4) {
        --size-m: 15;
        --size-n: 15; 
    }
    

    结果如下:

    CodePen Demo -- CSS Hudini Example

    到这里,还有一个小问题,可以看到,在消失的过程中,整个效果非常的闪烁!每个格子其实闪烁了很多次。

    这是由于在过渡的过程中,ctx.fillStyle = "rgba(0,0,0," + (t * (Math.random() + 1)) + ")" 内的 Math.random() 每一帧都会重新被调用并且生成全新的随机值,因此整个动画过程一直在疯狂闪烁。

    如何解决这个问题?在这篇文章中,我找到了一种利用伪随机,生成稳定随机函数的方法:Exploring the CSS Paint API: Image Fragmentation Effect

    啥意思呢?就是我们希望每次生成的随机数都是都是一致的。其 JavaScript 代码如下:

    const mask = 0xffffffff;
    const seed = 30; /* update this to change the generated sequence */
    let m_w  = (123456789 + seed) & mask;
    let m_z  = (987654321 - seed) & mask;
    
    let random =  function() {
      m_z = (36969 * (m_z & 65535) + (m_z >>> 16)) & mask;
      m_w = (18000 * (m_w & 65535) + (m_w >>> 16)) & mask;
      var result = ((m_z << 16) + (m_w & 65535)) >>> 0;
      result /= 4294967296;
      return result;
    }
    

    我们利用上述实现的随机函数 random() 替换掉我们代码原本的 Math.random(),并且,mask 小格子的 ctx.fillStyle 函数,也稍加变化,避免每一个 mask 矩形小格子的渐隐淡出效果同时发生。

    修改后的完整 JavaScript 代码如下:

    registerPaint(
        "maskSet",
        class {
            static get inputProperties() {
                return ["--size-n", "--size-m", "--transition-time"];
            }
    
            paint(ctx, size, properties) {
                const n = properties.get("--size-n");
                const m = properties.get("--size-m");
                const t = properties.get("--transition-time");
                const width = size.width / n;
                const height = size.height / m;
                const l = 10;
    
                const mask = 0xffffffff;
                const seed = 100; /* update this to change the generated sequence */
                let m_w = (123456789 + seed) & mask;
                let m_z = (987654321 - seed) & mask;
    
                let random = function () {
                    m_z = (36969 * (m_z & 65535) + (m_z >>> 16)) & mask;
                    m_w = (18000 * (m_w & 65535) + (m_w >>> 16)) & mask;
                    var result = ((m_z << 16) + (m_w & 65535)) >>> 0;
                    result /= 4294967296;
                    return result;
                };
    
                for (var i = 0; i < n; i++) {
                    for (var j = 0; j < m; j++) {
                        ctx.fillStyle = 'rgba(0,0,0,'+((random()*(l-1) + 1) - (1-t)*l)+')';
                        ctx.fillRect(i * width, j * height, width, height);
                    }
                }
            }
        }
    );
    

    还是上述的 DEMO,让我们再来看看效果,分别设置了不同数量的 mask 渐隐消失:

    CodePen Demo -- CSS Hudini Example & Custom Random

    Wow!修正过后的效果不再闪烁,并且消失动画也并非同时进行。在 Exploring the CSS Paint API: Image Fragmentation Effect 这篇文章中,还介绍了一些其他利用 registerPaint 实现的有趣的 mask 渐隐效果,感兴趣可以深入再看看。

    这样,我们就将原本 2400 行的 CSS 代码,通过 CSS Painting API 的 registerPaint,压缩到了 50 行以内的 JavaScript 代码。

    当然,CSS Houdini 的本事远不止于此,本文一直在围绕 background 描绘相关的内容进行阐述(mask 的语法也是背景 background 的一种)。在后续的文章我将继续介绍在其他属性上的应用。

    兼容性?

    那么,CSS Painting API 的兼容性到底如何呢?

    CanIUse - registerPaint 数据如下(截止至 2022-11-23):

    image

    Chrome 和 Edge 基于 Chromium 内核的浏览器很早就已经支持,而主流浏览器中,Firefox 和 Safari 目前还不支持。

    CSS Houdini 虽然强大,目前看来要想大规模上生产环境,仍需一段时间的等待。让我们给时间一点时间!

    最后

    好了,本文到此结束,希望本文对你有所帮助 :)

    想 Get 到最有意思的 CSS 资讯,千万不要错过我的公众号 -- iCSS前端趣闻 😄

    更多精彩 CSS 技术文章汇总在我的 Github -- iCSS ,持续更新,欢迎点个 star 订阅收藏。

    如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。