CSS Houdini实现动态波浪纹

CSS Houdini 号称 CSS 领域最使人振奋的革新。CSS 自己长期欠缺语法特性,可拓展性几乎为零,而且新特性的支持效率过低,兼容性差。而 Houdini 直接将 CSS 的 API 暴露给开发者,以往彻底黑盒的浏览器解析流开始对外开放,开发者能够自定义属于本身的 CSS 属性,从而定制和扩展浏览器的展现行为。javascript

背景

咱们知道,浏览器在渲染页面时,首先会解析页面的 HTML 和 CSS,生成渲染树(rendering tree),再经由布局(layout)和绘制(painting),呈现出整个页面内容。在 Houdini 出现以前,这个流程上咱们能操做的空间少之甚少,尤为是 layout 和 painting 环节,能够说是彻底封闭,使得咱们很难经过 polyfill 等相似的手段为欠支持的 CSS 特性提供兼容。而另外一方面,语法特性的缺失也极大地限制了 CSS 的编程灵活性,社区中 sass、less、stylus 等 CSS 预处理技术的出现大多都源于这个缘由,它们都但愿经过预编译,突破 CSS 的局限性,让 CSS 拥有更强大的组织和编写能力。因此慢慢地,咱们都再也不手写 CSS,更方便、更灵活的 CSS 扩展语言成了 web 开发的主角。看到这样的状况,CSS Houdini 终于坐不住了。css

什么是 CSS Houdini?

CSS Houdini 对外开放了浏览器解析流程的一系列 API,这些 API 容许开发者介入浏览器的 CSS engine 运做,带来了更多的 CSS 解决方案。html

CSS Houdini 目前主要提供了如下几个 API:前端

CSS Properties and Values API

容许在 CSS 中定义变量和使用变量,是目前支持程度最高的一个 API。CSS 变量以 -- 开头,经过 var() 调用:java

div {
  --font-color: #9e4a9b;
  color: var(--font-color);
}
复制代码

此外,CSS 变量也能够在其余节点中使用,只不过是有做用域限制的,也就是说自身定义的 CSS 变量只能被自身或自身的子节点使用:web

.container {
  --font-color: #9e4a9b;
}
.container .text {
  color: var(--font-color);
}
复制代码

定义和使用 CSS 变量可让咱们的 CSS 代码变得更加简洁明了,好比咱们能够单纯经过改变变量来改变 box-shadow 的颜色:编程

.text {
 --box-shadow-color: #3a4ba2;
 box-shadow: 0 0 30px var(--box-shadow-color);
}
.text:hover {
 --box-shadow-color: #7f2c2b;
}
复制代码

Painting API

容许开发者编写本身的 Paint Module,自定义诸如 background-image 这类的绘制属性。自定义的重点在于,"怎么画" 的逻辑须要咱们来描述,所以咱们利用 registerPaint 来描述咱们的绘制逻辑:canvas

registerPaint('rect', class {
  paint(ctx, size, properties, args) {}
});
复制代码

registerPaint 方法注册了一个 Paint 类 rect 以供调用,这个类的核心在于它的 paint 方法。paint 方法用于描述自定义的绘制逻辑,它接收四个参数:api

  • ctx:一个 Canvas 的 Context 对象,所以 paint 中的绘制方式跟 canvas 绘制是同样的。
  • size:包含节点的尺寸信息,同时也是 canvas 可绘制范围(画板)的尺寸信息。
  • properties:包含节点的 CSS 属性,须要调用静态方法 inputProperties 声明注入。
  • args: CSS 中调用 Paint 类时传入的参数,须要调用静态方法 inputArguments 声明注入。

编写完 Paint 类以后,咱们在 CSS 中只须要这样调用,就能应用到咱们自定义的绘制逻辑:浏览器

.wrapper {
  background-image: paint(rect);
}
复制代码

Painting API 目前在高版本 Chrome、Opera 浏览器已有支持,且实现起来比较简单,后边咱们还将经过 demo 进一步演示。

Layout API

容许开发者编写本身的 Layout Module,自定义诸如 display 这类的布局属性。一样的,"如何布局" 的逻辑须要咱们本身编写:

registerLayout('block-like', class {
  layout(children, edges, constraints, properties, breakToken) {
    // ...
    return {
      // inlineSize: number,
      // blockSize: number,
      // autoBlockSize: number,
      // childFragments: sequence<LayoutFragment>
    }
  }
})
复制代码

registerLayout 方法用于注册一个 Layout 类以供调用,它的 layout 方法用于描述自定义的布局逻辑,最终返回一个包含布局后的位置尺寸信息和子节点序列信息的对象,引擎将根据这个对象进行布局渲染。

一样的,调用时只需:

.wrapper {
  display: layout('block-like');
}
复制代码

所以利用 Layout API,你彻底能够实现对 flex 布局的手工兼容。相比 Painting,Layout 的编写显得更加复杂,涉及到盒模型的深刻概念,且支持度不高,这里就不细讲了。

Worklets

registerPaint、registerLayout 这些 API 在全局上并不存在,为何能够直接调用呢?这是由于上述的 JS 代码并非直接执行的,而是经过 Worklets 载入执行的。Worklets 相似于 Web Worker,是一个运行于主代码以外的独立工做进程,但比 Worker 更为轻量,负责 CSS 渲染任务是最合适的了。和 Web Worker 同样,Worklets 拥有一个隔离于主进程的全局空间,在这个空间里,没有 window 对象,却有 registerPaint、registerLayout 这些全局 API。所以,咱们须要这样引入自定义 JS 代码:

if ("paintWorklet" in CSS) {
  CSS.paintWorklet.addModule("paintworklet.js");
}
复制代码
if ("layoutWorklet" in CSS) {
  CSS.layoutWorklet.addModule("layoutworklet.js");
}
复制代码

基础:三步用上 Painting API

咱们来自定义 background-image 属性,它将用于给做用节点绘制一个矩形背景,背景色值由该节点上的一个 CSS 变量 --rect-color 指定。

一、编写一个 Paint 类

新建一个 paintworklet.js,利用 registerPaint 方法注册一个 Paint 类 rect,定义属性的绘制逻辑:

registerPaint("rect", class {
  static get inputProperties() {
    return ["--rect-color"];
  }
  paint(ctx, geom, properties) {
    const color = properties.get("--rect-color")[0];
    ctx.fillStyle = color;
    ctx.fillRect(0, 0, geom.width, geom.height);
  }
});
复制代码

上边定义了一个名为 rect 的 Paint 类,当 rect 被使用时,会实例化 rect 并自动触发 paint 方法执行渲染。paint 方法中,咱们获取节点 CSS 定义的 --rect-color 变量,并将元素的背景填充为指定颜色。因为须要使用属性 --rect-color,咱们须要在静态方法 inputProperties 中声明。

二、Worklets 加载 Paint 类

HTML 中经过 Worklets 载入上一步骤实现的 paintworklet.js 并注册 Paint 类:

<div class="rect"></div>
<script> if ("paintWorklet" in CSS) { CSS.paintWorklet.addModule("paintworklet.js"); } </script>
复制代码

三、使用 Paint 类

CSS 中使用的时候,只须要调用 paint 方法:

.rect {
  width: 100vw;
  height: 100vh;
  background-image: paint(rect);
  --rect-color: rgb(255, 64, 129);
}
复制代码

能够看得出利用 CSS Houdini,咱们能够像操做 canvas 同样灵活自如地实现咱们想要的样式功能。

进阶:实现动态波纹

根据上述步骤,咱们演示一下如何用 CSS Painting API 实现一个动态波浪的效果:

<!-- index.html -->
<div id="wave"></div>

<style> #wave { width: 20%; height: 70vh; margin: 10vh auto; background-color: #ff3e81; background-image: paint(wave); } </style>

<script> if ("paintWorklet" in CSS) { CSS.paintWorklet.addModule("paintworklet.js"); const wave = document.querySelector("#wave"); let tick = 0; requestAnimationFrame(function raf(now) { tick += 1; wave.style.cssText = `--animation-tick: ${tick};`; requestAnimationFrame(raf); }); } </script>
复制代码
// paintworklet.js
registerPaint('wave', class {
  static get inputProperties() {
    return ['--animation-tick'];
  }
  paint(ctx, geom, properties) {
    let tick = Number(properties.get('--animation-tick'));
    const {
      width,
      height
    } = geom;
    const initY = height * 0.4;
    tick = tick * 2;

    ctx.beginPath();
    ctx.moveTo(0, initY + Math.sin(tick / 20) * 10);
    for (let i = 1; 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 + Math.sin(tick / 20) * 10);
    ctx.closePath();

    ctx.fillStyle = 'rgba(255, 255, 255, 0.5)';
    ctx.fill();
  }
})
复制代码

paintworklet 中,利用 sin 函数绘制波浪线,因为 AnimationWorklets 尚处于实验阶段,开放较少,这里咱们在 worklet 外部用 requestAnimationFrame API 来作动画驱动,让波浪纹动起来。完成后能看到下边这样的效果。

然而事实上这个效果略显僵硬,sin 函数太过于规则了,现实中的波浪应该是不规则波动的,这种不规则主要体如今两个方面:

1)波纹高度(Y)随位置(X)变化而不规则变化

把图按照 x-y 正交分解以后,咱们但愿的不规则,能够认为是固定某一时刻,随着 x 轴变化,波纹高度 y 呈现不规则变化;

2)固定某点(X 固定),波纹高度(Y)随时间推动而不规则变化

动态过程须要考虑时间维度,咱们但愿的不规则,还须要体如今时间的影响中,好比风吹过的前一秒和后一秒,同一个位置的波浪高度确定是不规则变化的。

提到不规则,有朋友可能想到了用 Math.random 方法,然而这里的不规则并不适合用随机数来实现,由于先后两次取的随机数是不连续的,而先后两个点的波浪是连续的。这个不难理解,你见过长成锯齿状的波浪吗?又或者你见过上一刻 10 米高、下一刻就掉到 2 米的波浪吗?

为了实现这种连续不规则的特征,咱们弃用 sin 函数,引入了一个包 simplex-noise。因为影响波高的有两个维度,位置 X 和时间 T,这里须要用到 noise2D 方法,它提早在一个三维的空间中,构建了一个连续的不规则曲面:

// paintworklet.js
import SimplexNoise from 'simplex-noise';
const sim = new SimplexNoise(() => 1);

registerPaint('wave', class {
  static get inputProperties() {
    return ['--animation-tick'];
  }

  paint(ctx, geom, properties) {
    const tick = Number(properties.get('--animation-tick'));

    this.drawWave(ctx, geom, 'rgba(255, 255, 255, 0.4)', 0.004, tick, 15, 0.4);
    this.drawWave(ctx, geom, 'rgba(255, 255, 255, 0.5)', 0.006, tick, 12, 0.4);
  }
  
  /** * 绘制波纹 */
  drawWave(ctx, geom, fillColor, ratio, tick, amp, ih) {
    const {
      width,
      height
    } = geom;
    const initY = height * ih;
    const speedT = tick * ratio;

    ctx.beginPath();
    for (let x = 0, speedX = 0; x <= width; x++) {
      speedX += ratio * 1;
      var y = initY + sim.noise2D(speedX, speedT) * amp;
      ctx[x === 0 ? 'moveTo' : 'lineTo'](x, y);
    }
    ctx.lineTo(width, height);
    ctx.lineTo(0, height);
    ctx.lineTo(0, initY + sim.noise2D(0, speedT) * amp);
    ctx.closePath();

    ctx.fillStyle = fillColor;
    ctx.fill();
  }
})
复制代码

修改峰值和偏置项等参数,能够再画多一个不同的波浪纹,效果以下,完工!

参考文章

CSS Painting API Level 1
CSS Layout API Level 1
CSS 魔術師 Houdini API 介紹


若是你以为这篇内容对你有价值,欢迎点赞并关注咱们前端团队的 官网 和咱们的微信公众号 WecTeam,每周都有优质文章推送~

相关文章
相关标签/搜索