本文为稀土技术社区首发签约文章,14 天内制止转载,14 天后未获授权制止转载,侵权必究!

我的新的专栏 — 现代 CSS 与 Web 动画处理方案。

将专注于完结杂乱布局,兼容设备差异,制造酷炫动画,制造杂乱交互,提高可拜访性及构建奇思妙想作用等方面的内容。

在兼顾基础概述的一起,重视对技巧的发掘,结合实际进行运用,欢迎咱们关注。

正文从这儿开端。

在上一篇文章中 — 现代 CSS 之高阶图片渐隐消失术,咱们凭借了 CSS @Property 及 CSS Mask 特点,成功的完结了这样一种图片突变消失的作用:

现代 CSS 高阶技巧,像 Canvas 相同自在绘图构建款式!

CodePen Demo — 根据 @property 和 mask 的文本渐隐消失术

可是,这个作用的缺陷也十分显着,尽管凭借了 SCSS 简化了十分多的代码,可是,假如咱们检查编译后的 CSS 文件,会发现,在运用 SCSS 只有 80 的代码的情况下,编译后的 CSS 文件行数高达 2400+ 行,实在是太夸张了。

现代 CSS 高阶技巧,像 Canvas 相同自在绘图构建款式!

究其原因在于,咱们运用原生的 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 高阶技巧,像 Canvas 相同自在绘图构建款式!

看似有点点杂乱,其实十分好了解。仔细看咱们的 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 办法,完结相似于这样一个布景图画:

现代 CSS 高阶技巧,像 Canvas 相同自在绘图构建款式!

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 变量相关的信息

现代 CSS 高阶技巧,像 Canvas 相同自在绘图构建款式!

  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 能够查到:

现代 CSS 高阶技巧,像 Canvas 相同自在绘图构建款式!

还记得咱们上面传入了 3 个 CSS 变量吗?这儿咱们只需求简略改动上面的 3 个 变量,就能够得到不相同的图形。让咱们试一试:

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

现代 CSS 高阶技巧,像 Canvas 相同自在绘图构建款式!

又或许:

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

现代 CSS 高阶技巧,像 Canvas 相同自在绘图构建款式!

运用 registerPaint 完结自界说 mask

有了上面的衬托,下面咱们开端完结咱们今日的主题,运用 registerPaint 自界说办法复原完结这个作用,简化 CSS 代码量:

现代 CSS 高阶技巧,像 Canvas 相同自在绘图构建款式!

自界说的 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://www.6hu.cc/files/2022/12/1670858168-7f4d7e84e679867.jpg);
}

作用如下:

现代 CSS 高阶技巧,像 Canvas 相同自在绘图构建款式!

当然,咱们的方针是运用 registerPaint 完结自界说 mask,那么需求添加一些 CSS 代码:


div {
    width: 300px;
    height: 300px;
    background: url(https://www.6hu.cc/files/2022/12/1670858168-7f4d7e84e679867.jpg);
    mask: paint(maskSet);
    --size-m: 10;
    --size-n: 10;
}

这儿,咱们 mask: paint(maskSet) 表明运用咱们自界说的 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 的中心在于,透过色彩的透明度来隐藏一个元素的部分或许悉数可见区域。因而,整个图片将变成这样:

现代 CSS 高阶技巧,像 Canvas 相同自在绘图构建款式!

当然,咱们这个自界说 mask 办法也是能够用于 background 的,假如咱们把这个办法作用于 backgorund,你会更好了解一点。

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

实际的图片作用是这样:

现代 CSS 高阶技巧,像 Canvas 相同自在绘图构建款式!

好,回归正题,咱们持续。咱们终究的作用还是要动画作用,Hover 的时候让图片方块化消失,必定还是要和 CSS @property 自界说变量产生关联的,咱们简略改造下代码,加入一个 CSS @property 自界说变量。

@property --transition-time {
  syntax: '<number>';
  inherits: false;
  initial-value: 1;
}
div {
    width: 300px;
    height: 300px;
    background: url(https://www.6hu.cc/files/2022/12/1670858168-7f4d7e84e679867.jpg);
    mask: paint(maskSet);
    --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,凭借这个进程,咱们完结了整个渐隐的动画

看看终究的作用:

现代 CSS 高阶技巧,像 Canvas 相同自在绘图构建款式!

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):

现代 CSS 高阶技巧,像 Canvas 相同自在绘图构建款式!

Chrome 和 Edge 根据 Chromium 内核的浏览器很早就已经支持,而干流浏览器中,Firefox 和 Safari 现在还不支持。

CSS Houdini 尽管强壮,现在看来要想大规模上生产环境,仍需一段时刻的等候。让咱们给时刻一点时刻!

最终

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

假如还有什么疑问或许建议,能够多多交流,原创文章,文笔有限,孤陋寡闻,文中若有不正之处,万望奉告。