要问2018最让人兴奋的CSS技术是什么,CSS Houdini当之无愧,甚至可以去掉2018这个限定。其实这个技术在2016年就出来了,但是在今年3月发布的Chrome 65才正式支持。

CSS Houdini可以做些什么?谷歌开发者文档列了几个demo,我们先来看一下这几个demo:

(1)给textarea加一个方格背景(demo)

使用以下CSS代码:

textarea {background-image: paint(checkerboard);
}复制代码

(2)给div添加一个钻石形状背景(demo)

使用以下CSS:

div {--top-width: 80;--top-height: 20;-webkit-mask-image: paint(demo);
}复制代码

(3)点击圆圈扩散动画(demo)

这3个例子都是用了Houdini里面的CSS Paint API。

第1个例子如果使用传统的CSS属性,我们最多可能就是使用渐变来做颜色的变化,但是做不到这种一个格子一个格子的颜色变化的,而第2个例子也是没有办法直接用CSS画个钻石的形状。这个时候你可能会想到会SVG/Canvas的方法,SVG和Canvas的特色是矢量路径,可以画出各种各样的矢量图形,而Canvas还能控制任意像素点,所以用这两种方式也是可以画出来的。

但是Canvas和html相结合的时候就会显得有点笨拙,就像第2个例子画一个钻石的形状,用Canvas你需要利用类似于BFC定位的方式,把Cavans调到合适的定位,还要注意z-index的覆盖关系,而使用SVG可能会更简单一点,可以设置background-image为一张钻石的svg图片,但是无法像Canavas一样很方便地做一些变量控制,例如随时改一下钻石边框的颜色粗细等。

而第1个例子给textarea加格子背景,只能使用background-image + svg的方式,但是你不知道这个textarea有多大,svg的格子需要准备多少个呢?当然你可能会说谁会给textarea加一个这样的背景呢。但这只是一个示例,其它的场景可能也会遇到类似的问题。

第3个例子点击圆圈扩散动画,这个也可以在div里面absolute定位一个canvas元素,但是我们又遇到另外一个问题:无法很方便复用,假设这种圈圈扩散效果在其它地方也要用到,那就得在每个地方都写一个canvas元素并初始化。

所以传统的方式存在以下问题:

(1)需要调好和其它html元素的定位和z-index关系等

(2)编辑框等不能方便地改背景,不能方便地做变量控制

(3)不能方便地进行复用

其实还有另外一个更重要的问题就是性能问题,用Cavans画这种效果时需要自己控制好帧率,一不小心电脑CPU风扇可能就要呼啸起来,特别是不能把握重绘的时机,如果元素大小没有变化是不需要重绘,如果元素被拉大了,那么需要进行重绘,或者当鼠标hover的时候做动画才需要重绘。

CSS Houdini在解决这种自定义图形图像绘制的问题提供了很好的解决方案,可以用Canvas画一个你想要的图形,然后注册到CSS系统里面,就能在CSS属性里面使用这个图形了。以画一个星空为例,一步步说明这个过程。

1. 画一个黑夜的夜空

CSS Houdini只能工作在localhost域名或者是https的环境,否则的话相关API是不可见(undefined)的。如果没有https环境的话,可以装一个http-server的npm包,然后在本地启动,访问localhost:8080就可以了,新建一个index.html,写入:

<!DOCType>
<html>
<head><meta charset="utf-8">
<style>
body {background-image: paint(starry-sky);
}
</style>
</head>
<body>
<script>CSS.paintWorklet.addModule('starry-sky.js');
</script>
</body>
</html>复制代码

通过在JS调用CSS.paintWorklet.addModule注册一个CSS图形starry-sky,然后在CSS里面就可以使用这个图形,写在background-image、border-image或者mask-image等属性里面。如上面代码的:

body {background-image: paint(starry-sky);
}复制代码

注册paint worket的时候需要给它一个独立的js,作为这个worklet的工作环境,这个环境里面是没有window/document等对象的和web worker一样。如果你不想写管理太多js文件,可以借助blob,blob是可以存放任何类型的数据的,包括JS文件。

Worklet需要的starry-sky.js的代码如下所示:

class StarrySky {paint (ctx, paintSize, properties) {// 使用Canvas的API进行绘制ctx.fillRect(0, 0, paintSize.width, paintSize.height);}
}
// 注册这个属性
registerPaint('starry-sky', StarrySky);复制代码

写一个类,实现paint接口,这个接口会传一个canvas的context变量、当前画布的大小即当前dom元素的大小,以及当前dom元素的css属性properties.

在paint函数里面调用canvas的绘制函数fillRect进行填充,默认填充色为黑色。访问index.html,就会看到整个页面变成黑色了。我们的Hello World的CSS Houdini Painter就跑起来了,没错,就是这么简单。

但是有一点需要强调的是,浏览器实现并不是给那个dom元素添加一个Canvas然后隐藏起来,这个Paint Worket实际上是直接影响了当前dom元素重绘过程,相当于我们给它添加了一个重绘的步骤,下文会继续提及。

如果不想独立写一个js,用blob可以这样:

let blobURL = URL.createObjectURL( new Blob([ '(',function(){class StarrySky {paint (ctx, paintSize, properties) {ctx.fillRect(0, 0, paintSize.width, paintSize.height);}}registerPaint('starry-sky', StarrySky);}.toString(),')()' ], { type: 'application/javascript' } )
);CSS.paintWorklet.addModule(blobURL);复制代码

2. 画星星

Cavans星星效果网上找一个就好了,例如这个Codepen,代码如下:

paint (ctx, paintSize, poperties) {let xMax= paintSize.width;let yMax = paintSize.height;// 黑色夜空ctx.fillRect(0, 0, xMax, yMax);// 星星的数量let hmTimes = xMax + yMax;  for (let i = 0; i <= hmTimes; i++) {// 星星的xy坐标,随机let x = Math.floor((Math.random() * xMax) + 1); let y = Math.floor((Math.random() * yMax) + 1); // 星星的大小let size = Math.floor((Math.random() * 2) + 1); // 星星的亮暗let opacityOne = Math.floor((Math.random() * 9) + 1); let opacityTwo = Math.floor((Math.random() * 9) + 1); let hue = Math.floor((Math.random() * 360) + 1); ctx.fillStyle = `hsla(${hue}, 30%, 80%, .${opacityOne + opacityTwo})`; ctx.fillRect(x, y, size, size); } }复制代码

效果如下:

为什么它要用fillRect来画星星呢,星星不应该是圆的么?因为如果用arc的话性能会明显降低。由于星星比较小,所以使用了这种方式,当然改成arc也是可以的,因为我们只是画一次就好了。

3. 控制星星的密度

现在要做一个可配参数控制星星的密度,就好像border-radius可以控制一样。借助CSS变量,给body添加一个自定义属性--star-density:

body {--star-density: 0.8;background-image: paint(starry-sky);
}复制代码

规定密度系数从0到1变化,通过paint函数的propertis参数获取到属性。但是我们发现body/html的自定义属性无法获取,可以继承给body的子元素,但无法在body上获取,所以改成画在body:before上面:

body:before {content: "";position: absolute;left: 0;top: 0;width: 100%;height: 100%;--star-density: 0.5;background-image: paint(starry-sky);
}复制代码

然后给class StarrySky添加一个静态方法:

class StarrySky {static get inputProperties() {return ['--star-density'];}
}复制代码

告知我们需要获取哪些CSS属性,可以是自定义的,也可以是常规的CSS属性。然后在paint方法的properties里面就可以拿到属性值:

class StarrySky {paint (ctx, paintSize, properties) {// 获取自定义属性值let starDensity = +properties.get('--star-density').toString() || 1;// 最大只能为1starDensity > 1 && (starDensity = 1);// 星星的数量剩以这个系数let hmTimes = Math.round((xMax + yMax) * starDensity);}
}复制代码

让星星的数量剩以传进来的系数进而达控制密度的目的。上面设置星星的数量为最大值的一半,效果如下:

3. 重绘

当拉页面的时候会发现所有星星的位置都发生了变化,这是因为触发了重绘。

在paint函数里面添加一个console.log,拉动页面的时候就可以观察到浏览器在不断地执行paint函数。因为这个CSS属性是写在body:befoer上面的,占满了body,body大小改变就会触发重绘。而如果写在一个宽度固定的div里面,拉动页面不会触发重绘,观察到paint函数没有执行。如果改了div或者body的任何一个CSS属性也会触发重绘。所以这个很方便,不需要我们自己去监听resize之类的DOM变化。

页面拉大时,右边新拉出来的空间星星没有画大,所以本身需要重绘。而重绘给我们造成的问题是星星的位置发生变化,正常情况下应该是页面拉大拉小,星星的位置应该是要不变的。所以需要记录一下星星的一些相关信息。

4. 记录星星的数据

可以在SkyStarry这个类里面添加一个成员变量stars,保存所有star的信息,包括位置和透明度等,在paint的时候判断一下stars的长度,如果为0则进行初始化,否则使用直接上一次初始化过的星星,这样就能保证每次重绘都是用的同样的星星了。但是在实际的操作过程中,发现一个问题,它会初始化两次starry-sky.js,在paint的时候也会随机切换,如下图所示:

这样就造成了有两个stars的数据,在重绘过程中来回切换。原因可能是因为CSS Houdini的本意并不想让你保存实例数据,但是既然它设计成一个类,使用类的实例数据应该也是合情合理的。这个问题我想到的一个解决方法是把random函数变成可控的,只要随机化种子一样,那么生成的random系列就是一样的,而这个随机化种子由CSS变量传进来。所以就不能用Math.random了,自己实现一个random,如下代码所示:

    random () {let x = Math.sin(this.seed++) * 10000;return x - Math.floor(x);}复制代码

只要初始化seed一样,那么就会生成一样的random系列。seed和星星密度类似,由CSS变量控制:

body:before {--starry-sky-seed: 1;--star-density: 0.5;background-image: paint(starry-sky);
}复制代码

然后在paint函数里面通过properties拿到seed:

paint (ctx, paintSize, properties) {if (!this.stars) {let starOpacity = +properties.get('--star-opacity').toString();// 得到随机化种子,可以不传,默认为0this.seed = +(properties.get('--starry-sky-seed').toString() || 0);this.addStars(paintSize.width, paintSize.height, starDensity);}
}复制代码

通过addStars函数添加星星,这个函数调用上面自定义的random函数:

random () {let x = Math.sin(this.seed++) * 10000;return x - Math.floor(x);
}addStars (xMax, yMax, starDensity = 1) {starDensity > 1 && (starDensity = 1); // 星星的数量let hmTimes = Math.round((xMax + yMax) * starDensity);  this.stars = new Array(hmTimes);for (let i = 0; i < hmTimes; i++) {this.stars[i] = { x: Math.floor((this.random() * xMax) + 1), y: Math.floor((this.random() * yMax) + 1), size: Math.floor((this.random() * 2) + 1), // 星星的亮暗opacityOne: Math.floor((this.random() * 9) + 1), opacityTwo: Math.floor((this.random() * 9) + 1), hue: Math.floor((this.random() * 360) + 1)};  }
}复制代码

这段代码由Math.random改成this.random保证只要随机化种子一样,生成的所有数据也都是一样的。这样就能解决上面提到的初始化两次数据的问题,因为种子是一样的,所以两次的数据也是一样的。

但是这样有点单调,每次刷新页面星星都是固定的,少了点灵气。可以给这个随机化种子做下优化,例如实现单个小时内是一样的,过了一个小时后刷新页面就会变。通过以下代码可以实现:

const ONE_HOUR = 36000 * 1000;
this.seed = +(properties.get('--starry-sky-seed').toString() || 0)+ Date.now() / ONE_HOUR >> 0;复制代码

这样拉动页面的时候星星就不会变了。

但是在从小拉大的时候,右边会没有星星:

因为第一次的画布没那么大,以后又没有更新星星的数据,所以右边就空了。

5. 增量更新星星数据

不能全部更新星星的数据,不然第4步就白做了。只能把右边没有的给它补上。所以需要记录一下两次画布的大小,如果第二次的画布大了,则增加星星,否则删掉边界外的星星。

所以需要有一个变量记录上一次画布的大小:

class StarrySky {constructor () {// 初始化this.lastPaintSize = this.paintSize = {width: 0,height: 0};this.stars = [];}
}复制代码

把相关的操作抽成一个函数,包括从CSS变量获取设置,增量更新星星等,这样可以让主逻辑变得清晰一点:

paint (ctx, paintSize, properties) {// 更新当前paintSizethis.paintSize = paintSize;// 获取CSS变量设置,把密度、seed等存放到类的实例数据this.updateControl(properties);// 增量更新星星this.updateStars();// 黑色夜空for (let star of this.stars) {// 画星星,略}
}复制代码

增量更新星星需要做两个判断,一个为是否需要删除掉一些星星,另一个为是否需要添加,根据画布的变化:

updateStars () {// 如果当前的画布比上一次的要小,则删掉一些星星if (this.lastPaintSize.width > this.paintSize.width ||this.lastPaintSize.height > this.paintSize.height) {this.removeStars();}   // 如果当前画布变大了,则增加一些星星if (this.lastPaintSize.width < this.paintSize.width ||  this.lastPaintSize.height < this.paintSize.height) {this.addStars();}   this.lastPaintSize = this.paintSize;
}复制代码

删除星星removeStar的实现很简单,只要判断x, y坐标是否在当前画布内,如果是的话则保留:

removeStars () {let stars = []for (let star of stars) {if (star.x <= this.paintSize.width &&  star.y <= this.paintSize.height) {stars.push(star);}   }   this.stars = stars;
}复制代码

添加星星的实现也是类似的道理,判断x, y坐标是否在上一次的画布内,如果是的话则不添加:

addStars () {let xMax = this.paintSize.width,yMax = this.paintSize.height;// 星星的数量let hmTimes = Math.round((xMax + yMax) * this.starDensity); for (let i = 0; i < hmTimes; i++) {let x = Math.floor((this.random() * xMax) + 1), y = Math.floor((this.random() * yMax) + 1); // 如果星星落在上一次的画布内,则跳过if (x < this.lastPaintSize.width && y < this.lastPaintSize.height) {continue;}   this.stars.push({x: x,y: y,size: Math.floor((this.random() * 2) + 1), // 星星的亮暗}); }
}复制代码

这样当拖动页面的时候就会触发重绘,重绘的时候就会调paint更新星星。

6. 让星星闪起来

通过做星星透明度的动画,可以让星星闪起来。如果用Cavans标签,可以借助window.requestAnimationFrame注册一个函数,然后用当前时间减掉开始的时间模以一个值就得到当前的透明度系数。使用Houdini也可以使用这种方式,区别是我们可以把动态变化透明度系数当作当前元素的CSS变量或者叫自定义属性,然后用JS动态改变这个自定义属性,就能够触发重绘,这个已在第3点重绘部分提到。

给元素添加一个--star-opacity的属性:

body:before {--star-opacity: 1;--star-density: 0.5;--starry-sky-seed: 1;background-image: paint(starry-sky);
}复制代码

在星星的时候,每个星星的透明度再乘以这个系数:

// 获取透明度系数
this.starOpacity = +properties.get('--star-opacity').toString();
for (let star of this.stars) {// 每个星星的透明度都乘以这个系数let opacity = +('.' + (star.opacityOne + star.opacityTwo)) * this.starOpacity;ctx.fillStyle = `hsla(${star.hue}, 30%, 80%, ${opacity})`;ctx.fillRect(star.x, star.y, star.size, star.size);
}复制代码

然后在requestAnimationFrame动态改变这个CSS属性:

let start = Date.now();
// before无法获取,所以需要改成正常元素
let node = document.querySelector('.starry-sky');
window.requestAnimationFrame(function changeOpacity () {let now = Date.now();// 每隔一1s,透明度从0.5变到1node.style.setProperty('--star-opacity', (now - start) % 1000 / 2 + 0.5);window.requestAnimationFrame(changeOpacity);
});复制代码

这样就能重新触发paint函数重新渲染了,但是这个效果其实是有问题的,因为得有一个alternate轮流交替的效果,即0.5变到1,再从1变到0.5,而不是每次都是0.5到1. 模拟CSS animation的alternate这个也好解决,可以规定奇数秒就是变大,而偶数秒就是变小,这个好实现,略。

但实际上可以不用这么麻烦,因为改变CSS属性直接用animation就可以了,如下代码所示:

body:before {--star-opacity: 1;--star-density: 0.5;--starry-sky-seed: 1;background-image: paint(starry-sky);animation: shine 1s linear alternate infinite;
}@keyframes shine {from {--star-opacity: 1;}to {--star-opacity: 0.6;}
}复制代码

这样也能触发重绘,但是我们发现它只有在from和to这两个点触发了重绘,没有中间过渡的过程。可以推测因为它认为--star-opacity的属性值不是一个数字,而是一个字符串,所以这两关键帧就没有中间的过渡效果了。因此我们得告诉它这是一个整型,不是一个字符串。类型化CSS对象模型(Typed CSSOM)提供了这个API。

类型化CSS对象模型一个很大的作用就是把所有的CSS单位都用一个相应的对象来表示,提供加减乘除等运算,如:

// 10 px
let length = CSS.px(10);
// 在循环里面改length的值,不用自己去拼字符串
div.attributeStyleMap.set('width', length.add(CSS.px(1)))复制代码

这样的好处是不用自己去拼字符串,另外还提供了转换,如transform的值转成matrix,度数转成rad的形式等等。

它还提供了注册自定义类型属性的能力,使用以下API:

CSS.registerProperty({name: '--star-opacity',// 指明它是一个数字类型syntax: '<number>',inherits: false,initialValue: 1
});复制代码

这样注册之后,CSS系统就知道--star-opacity是一个number类型,在关键帧动画里面就会有一个渐变的过渡效果。

类型CSS对象模型在Chrome 66已经正式支持,但是registerProperty API仍然没有开放,需要打开chrome://flags,搜索web platform,从disabled改成enabled就可以使用。

这个给我们提供了做动画新思路,CSS animation + Canvas的模式,CSS animation负责改变属性数据并触发重绘,而Canvas去获取动态变化的数据更新视图。所以它是一个数据驱动的动画模式,这也是当前做动画的一个流行方式。

在我们这个例子里面,由于星星数太多,1s有60帧,每帧都要计算和绘制1000个星星,CPU使用率达到90%多,所以这个性能有问题,如果用Cavans标签可以使用双缓冲技术,CSS Houdini好像没有这个东西。但是可以换一个思路,改成做整体的透明度动画,不用每个星星都算一下。

如下代码所示:

body {background-color: #000;
}
body:before {background-image: paint(starry-sky);animation: shine 1s linear alternate infinite;
}@keyframes shine {from {opacity: 1;}to {opacity: 0.6;}
}复制代码

这个的效果和每个星星都单独算是一样的,CPU消耗12%左右,这个应该还是可以接受的。

效果如下图所示:

如果用Canvas标签,可以设置globalAlpha全局透明度属性,而使用CSS Houdini我们直接使用opacity就行了。

一个完整的Demo:CSS Houdini Starry Sky,需要使用Chrome,因为目前只有Chrome支持。

总的来说,CSS Houdini的Paint Worket提供了CSS和Canvas的粘合,让我们可以用Canvas画出想要的CSS效果,并借助CSS自定义属性进行控制,通过使用JS或者CSS的animation/transition改变自定义属性的值触发重绘,从而产生动画效果,这也是数据驱动的开发思想。并讨论了在画这个星空的过程中遇到的一些问题,以及相关的解决方案。

本文只是介绍了CSS Houdini里面的Paint Worket和Typed CSSOM,它还有另外一个Layout Worklet,利用它可以自行实现一个flex布局或者其它自定义布局,这样的好处是:一方面当有新的布局出现的时候可以借助这个API进行polyfill就不用担心没有实现的浏览器不兼容,另一方面可以发挥想象力实现自己想要的布局,这样在布局上可能会百花齐放了,而不仅仅使用W3C给的那几种布局。

【再一次强推书】高效前端已上市,京东、亚马逊、淘宝等均有售

【人人网招聘高级前端】

用CSS Houdini画一片星空相关推荐

  1. CSS Houdini

    前言 最近看了几篇文章,是关于 CSS Houdini 的.作为一个前端搬砖的还真不知道这玩意,虽然不知道的东西挺多的,但是这玩意有点高大上啊. Houdini 是一组底层 API,它们公开了 CSS ...

  2. CSS Houdini实现动态波浪纹

    我们知道,浏览器在渲染页面时,首先会解析页面的 HTML 和 CSS,生成渲染树(rendering tree),再经由布局(layout)和绘制(painting),呈现出整个页面内容.在 Houd ...

  3. [css]我要用css画幅画(四)

    接着之前的[css]我要用css画幅画(三), 今天,我画了两朵云,并给小明介绍了个朋友:静静. github:https://github.com/bee0060/Css-Paint , 完整代码在 ...

  4. 原创:纯手工打造CSS像素画--笨笨熊系列图标

    纯手工打造CSS像素画--笨笨熊系列图标 作者:冰极峰 转载请注明出处 在cssplay网站看到有一组CSS像素画,于是也想摩仿一下,于是在网络上找到一组头像图标,看其结构比较简单,就拿它开刀吧!先看 ...

  5. [css] 你了解CSS Houdini吗?说说它的运用场景有哪些?

    [css] 你了解CSS Houdini吗?说说它的运用场景有哪些? Houdini是一组底层API,它们公开了CSS引擎的各个部分,从而使开发人员能够通过加入浏览器渲染引擎的样式和布局过程来扩展CS ...

  6. html5 css 三角形,css怎么画三角形?

    css怎么画三角形?下面本篇文章就来给大家介绍一下使用CSS画三角形的方法.有一定的参考价值,有需要的朋友可以参考一下,希望对大家有所帮助. css怎么画三角形? 三角形实现原理:宽度width为0: ...

  7. houdini_通过卡通了解CSS Houdini的指南

    houdini 易于阅读的指南. (An easy-to-read guide.) Hey there! 嘿! Thanks for joining me on the journey to unde ...

  8. 神级程序员8000行css代码画出一个蒙娜丽莎,堪比达芬奇!

    代码画出的蒙娜丽莎 今天逛CODEPEN找HTML5动画案例的时候,偶尔看到一位神级大师的作品,用纯CSS代码画出一副蒙娜丽莎,虽然分辨率不高,但是仍然让我很是震撼,一看代码,整整8000行,基本一行 ...

  9. 【CSS如何画简单的三角形或者梯形】

    CSS如何画简单的三角形或者梯形 三角形 直角三角形 梯形 直角梯形 三角形 1.设置一个盒子,类名为root.将盒子的width和height设置为0px. 盒子不需要设置宽度和高度,由边框bord ...

  10. 用python画月亮和星空_用canvas画一轮明月,夜空与流星

    今天是中秋节,于是突发奇想,欸不如用canvas来画一画月亮吧. 于是一副用canvas画出的星空就这样诞生了. 在这里我用了ES6语法,星星,月亮和流星都单独写成了一个module. 于是我把js一 ...

最新文章

  1. sqlserver 批量插入数据(此方式同样适用mysql)
  2. 【Karma】多环境自动测试框架 -- 基础教程
  3. 计算机组成 指令扩展,计算机组成原理课程设计--指令扩展设计.doc
  4. 场景/故事/story——寻物者发布消息场景、寻失主发布消息场景、消息展示场景、登录网站场景...
  5. Linq 读取Xml 数据
  6. Python 嵌套列表解析
  7. CNN卷积神经网络:权值更新公式推导
  8. html中css二级联动,html二级联动学习笔记
  9. 爱奇艺PPS如何登陆账号
  10. 程序员求生指南:告别大小周,摆脱监视,直奔年终奖!
  11. android判断某文件下是否you_Android_判断文件是否存在并创建代码
  12. MySQL-快速入门(12)备份、还原
  13. 系统设计2:数据库设计
  14. 手游反抗战兴起,《原神》打响“起义“第一枪
  15. kali虚拟机分辨率设置
  16. 关于项目开发的量化考核。。。
  17. 深度模型框架(持续更新)
  18. 应届生怎么样成为产品经理?
  19. 双下划线一粗一细怎么加_为什么下划线粗细不一样
  20. 古人诚不欺我-南怀瑾大师

热门文章

  1. 我的第一篇博客-缓存显示图片
  2. .net 面试题 (1)
  3. [书目20080225]软件工程与项目管理解析
  4. 【Retinex】【Frankle-McCann Retinex】matlab代码注释
  5. python 合并不同文件夹下名称相同的文件
  6. 理解图像中的低频分量和高频分量
  7. 如何手动合成年度夜间灯光影像
  8. html ui 下拉列表,html - 如何给样式Material-ui选择字段下拉菜单?
  9. exclips为什么j创建局java出错_clips.BuildRule出错
  10. java 对象流 乱码,JAVA 中的 IO 流