当前位置: 首页 > Web前端 > CSS

CSSHoudini实现动态波浪纹

时间:2023-03-30 22:27:43 CSS

作者:黄浩群CSSHoudini被誉为CSS领域最激动人心的创新。CSS本身长期缺乏语法特性,扩展性几乎为零,对新特性的支持效率太低,兼容性差。而Houdini直接将CSSAPI暴露给开发者,将之前完全黑盒化的浏览器解析流程对外开放。开发者可以自定义自己的CSS属性,从而自定义和扩展浏览器的显示行为。背景我们知道,浏览器在渲染页面时,首先会解析页面的HTML和CSS,生成渲染树,然后通过布局和绘制将整个页面内容呈现出来。在Houdini出现之前,这个过程中我们操作的空间非常小,尤其是layout和painting环节,可以说是完全封闭,使得我们很难通过对支持不足的CSS特性进行兼容polyfill和类似的方法。另一方面,语法特性的缺失也极大地限制了CSS的编程灵活性。社区中sass、less、stylus等CSS预处理技术的出现,多半是因为这个原因。他们都希望通过预编译来突破CSS。CSS的局限性让CSS拥有更强大的组织和书写能力。于是慢慢的,我们不再手写CSS,更方便灵活的CSS扩展语言成为了web开发的主角。看到这种情况,CSSHoudini终于坐不住了。什么是CSS胡迪尼?CSSHoudini为浏览器解析过程开放了一系列API。这些API允许开发者干预浏览器CSS引擎的运行,带来更多的CSS解决方案。CSSHoudini目前提供了以下API:CSSPropertiesandValuesAPI允许在CSS中定义和使用变量,是目前支持最多的API。CSS变量以--开头并通过var():div{--font-color:#9e4a9b;调用color:var(--font-color);}另外,CSS变量也可以在其他节点中使用,但是是有作用域限制的,也就是说,自己定义的CSS变量只能自己使用或者自己使用子节点:.container{--font-color:#9e4a9b;}.container.text{color:var(--font-color);}定义和使用CSS变量可以让我们的CSS代码更加简洁明了。例如,我们可以简单地通过改变变量来改变box-shadow的颜色:.text{--box-shadow-color:#3a4ba2;box-shadow:0030pxvar(--box-shadow-color);}.text:hover{--box-shadow-color:#7f2c2b;}绘画API允许开发者编写自己的绘画模块,定义绘画属性例如背景图像。自定义的重点是我们需要描述“如何绘制”的逻辑,所以我们使用registerPaint来描述我们的绘制逻辑:registerPaint('rect',class{paint(ctx,size,properties,args){//@去做}});registerPaint方法注册一个Paint类rect供调用,这个类的核心在于它的paint方法。paint方法用于描述自定义绘制逻辑,它接收四个参数:ctx:一个CanvasContext对象,所以paint中的绘制方法与canvas绘制相同。size:包含节点的大小信息,也是画布可绘制范围(画板)的大小信息。properties:包含节点的CSS属性,需要调用静态方法inputProperties声明注入。args:CSS中调用Paint类时传入的参数,需要调用静态方法inputArguments声明注入。写完Paint类后,我们只需要在CSS中这样调用就可以应用到我们自定义的绘制逻辑中:browsers已经支持了,实现起来也比较简单。后面我们会通过demo进一步演示。LayoutAPI允许开发人员编写自己的LayoutModule并自定义显示等布局属性。同样,“如何布局”的逻辑需要自己写:number,//blockSize:number,//autoBlockSize:number,//childFragments:sequence}}})registerLayout方法用于注册一个Layout类供调用,其layout方法用于描述自定义布局逻辑,最后返回一个对象,包含布局后的位置和大小信息以及子节点的顺序信息,引擎会根据这个对象进行布局渲染。同样,你只需要调用:.wrapper{display:layout('block-like');}所以使用LayoutAPI,你可以完全实现手动兼容flex布局。相比Painting,Layout的写法更复杂,涉及盒模型的深入概念,支持度不高,这里不再赘述。WorkletsregisterPaint、registerLayout等API不是全局存在的,为什么可以直接调用呢?这是因为上面的JS代码并没有直接执行,而是通过Worklets加载执行的。Worklets类似于WebWorkers,是运行在主代码之外的独立工作进程,但比Workers轻量级,最适合CSS渲染任务。与WebWorker一样,Worklets有一个与主进程隔离的全局空间。在这个空间里,没有window对象,但是有registerPaint、registerLayout等全局API。因此,我们需要像这样引入自定义的JS代码:"layoutworklet.js");}基础:三步使用PaintingAPI自定义background-image属性,用于为活动节点绘制矩形背景。背景颜色值由节点--rect-color指定的CSS变量确定。1、编写一个Paint类:新建一个paintworklet.js,使用registerPaint方法注册一个Paint类rect,定义属性的绘制逻辑:registerPaint("rect",class{staticgetinputProperties(){return["--rect-color"];}paint(ctx,geom,properties){constcolor=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中声明它。2、Worklets加载Paint类:在HTML中通过Worklets加载上一步实现的paintworklet.js,并注册Paint类:

3、使用Paint类:在CSS中使用时,只需要调用paint方法即可:.rect{width:100vw;高度:100vh;背景图像:油漆(矩形);--rect-color:rgb(255,64,129);}可见通过CSSHoudini我们可以像操作画布一样灵活的实现我们想要的样式功能。进阶:实现动态波浪根据以上步骤,我们演示如何使用CSSPaintingAPI实现动态波浪效果:
//paintworklet.jsregisterPaint('wave',class{staticgetinputProperties(){return['--animation-tick'];}paint(ctx,geom,properties){lettick=Number(properties.get('--animation-tick'));const{width,height}=geom;constinitY=height*0.4;tick=tick*2;ctx.beginPath();ctx.moveTo(0,initY+Math.sin(tick/20)*10);for(leti=1;i<=width;i++){ctx.lineTo(i,initY+Math.sin((i+tick)/20)*10);}ctx.lineTo(宽度,高度);ctx.lineTo(0,高度);ctx.lineTo(0,initY+Math.sin(tick/20)*10);ctx.closePath();ctx.fillStyle='rgba(255,255,255,0.5)';ctx.填充();}})在paintworklet中,sin函数用于绘制波浪线。由于AnimationWorklets还在实验阶段,空缺的很少,这里我们使用worklet外的requestAnimationFrameAPI来做动画驱动,让波纹动起来,完成后可以看到下面的效果。不过实际上这个效果有点生硬,sin函数太有规律了,现实中的波浪应该是不规则的起伏。这种不规则性主要体现在两个方面:1)波纹的高度(Y)随着位置(X)的变化而变化,不规则变化按照x-y正交性分解图形后,我们想要的不规则性可以认为是固定的片刻。随着x轴变化,波纹高度y不规则变化;2)固定某一点(X固定),纹波高度(Y)随时间不规则变化。动态过程需要考虑时间维度。我们希望的不规则性还需要体现在时间的影响上,比如风吹前后的秒数,同时一个位置的海浪高度一定是不规则变化的。说到不规则性,有些朋友可能会想到使用Math.random方法。但是这里的不规则性不适合用随机数来实现,因为前后两次取的随机数是不连续的,而前后两点的波是连续的。这个不难理解,你见过锯齿状的波浪吗?或者你见过前一瞬间高达10米,下一瞬间跌落到2米的海浪吗?为了实现这种连续的不规则特征,我们弃用了sin函数并引入了一个包simplex-noise。由于影响波高的维度有两个,位置X和时间T,所以这里需要用到noise2D方法,提前在三维空间构造一个连续的不规则曲面://paintworklet.jsimportSimplexNoisefrom'simplex-noise';constsim=newSimplexNoise(()=>1);registerPaint('wave',class{staticgetinputProperties(){return['--animation-tick'];}paint(ctx,geom,properties){consttick=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;constinitY=height*ih;constspeedT=tick*ratio;ctx.beginPath();for(letx=0,speedX=0;x<=width;x++){speedX+=ratio*1;vary=initY+sim.noise2D(speedX,speedT)*amp;ctx[x===0?'moveTo':'lineTo'](x,y);}ctx.lineTo(宽度,高度);ctx.lineTo(0,高度);ctx.lineTo(0,initY+sim.noise2D(0,speedT)*amp);ctx.closePath();ctx.fillStyle=填充颜色;ctx.填充();}})修改peak和offset项等参数,就可以画出不同的波浪图案,效果如下,大功告成!参考文章CSSPaintingAPILevel1CSSLayoutAPILevel1CSSMagicianHoudiniAPI介绍