jyzwf / blog

在Issues里记录技术得点滴
17 stars 3 forks source link

学习CSS Houdini #65

Open jyzwf opened 5 years ago

jyzwf commented 5 years ago

关于Houdini的印象还是在去年,然后也只是听过有这么个东西,但也只仅仅停留在听过。但为何今天会重新留意这个东西?因为闲来无聊,就去逛了某大佬的博客,逛着逛着,突然看到他写的一篇关于水波纹,重点不在于此,在于blog的最后来了句,用Houdini来实现,动画会更加容易。于是就打算看看这个东西。 关于水波纹的实现,到codeopen上一搜一大堆,直接来一个链接吧,Ripple Button。如果单用css来实现,可以考虑使用伪类加径向渐变,最后加点transition来实现,如果加上js来获取点击时的位置,就能做到更加好看的效果,这个不是文本的重点:

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <meta http-equiv="X-UA-Compatible" content="ie=edge" />
        <title>水波纹</title>
        <style>
            button {
                display: inline-block;
                text-align: center;
                white-space: nowrap;
                cursor: pointer;
                border: none;
                padding: 8px 18px;
                margin: 10px 1px;
                font-size: 14px;
                font-weight: 500;
                text-transform: uppercase;
                background: transparent;
                background-color: #00bcd4;
                color: rgba(0, 0, 0, 0.87);
            }
            .ripple {
                position: relative;
                overflow: hidden;
            }

            .ripple:after {
                content: '';
                display: block;
                position: absolute;
                width: 100%;
                height: 100%;
                top: 0;
                left: 0;
                pointer-events: none;
                background-image: radial-gradient(
                    circle,
                    #666 10%,
                    transparent 10.01%
                );
                background-repeat: no-repeat;
                background-position: 50%;
                transform: scale(10, 10);
                opacity: 0;
                transition: transform 0.3s, opacity 0.5s;
            }

            .ripple:active:after {
                transform: scale(0, 0);
                opacity: 0.3;
                transition: 0s;
            }
        </style>
    </head>
    <body>
        <button class="button ripple">按钮</button>
    </body>
</html>

Houdini简介

总的来说Houdini就是开发者可以调用CSS的API,来扩展CSS,并且允许开发者参与到浏览器渲染引擎的样式和布局流程中。它提供了诸如painting APILayout APIparsing APITyped OM等API。接下来我们就来试试其中的paint api(由于有些浏览器暂不支持),主要参考官方提供的demo。

准备

Paint API 必须要在支持 https 服务器上或者本地 localhost 上才能使用。所以如果你是在本地开发,可以用 http-server 在本地快速搭建一个服务器。 要记得禁用浏览器缓存,让最新的 worklets 立马生效。 目前暂时无法在 worklets 中打断点或者插入 debugger ,不过 console.log() 还是可以用的。 这里我尝试了使用parcel来本地开发,但必须将worklet放置在dist目录下才能生效: image image

试试painting

同样我们实现其上面水波纹效果

<!DOCTYPE html>
<html lang="en">
    <head>
        <title>Document</title>
        <style>
            #ripple {
                width: 300px;
                height: 300px;
                border-radius: 150px;
                font-size: 5em;
                line-height: 300px;
                background-color: rgb(255, 64, 129);
                border: 0;
                box-shadow: 0 1px 1.5px 0 rgba(0, 0, 0, 0.12),
                    0 1px 1px 0 rgba(0, 0, 0, 0.24);
                color: white;

                --ripple-x: 0;
                --ripple-y: 0;  // 使用css 变量,在后面的worklet中可以被获取到
                --ripple-color: rgba(255, 255, 255, 0.54);
                --animation-tick: 0;
            }

            #ripple:focus {
                outline: none;
            }
            #ripple.animating {
                background-image: paint(ripple);   // 这里就是painting真正使用的地方,其中 ripple 是在worklet中定义的属性
            }
        </style>
    </head>
    <body>
        <div id="ripple">Houdini</div>

        <script src="./index.js"></script>
    </body>
</html>

下面是相关的动画实现,其中借用了 requestAnimationFrame

if (location.protocol === 'http:' && location.hostname !== 'localhost')
    location.protocol = 'https:';
if ('paintWorklet' in CSS) {
    CSS.paintWorklet.addModule('./app.js');
} else {
    document.body.innerHTML =
        'You need support for <a href="https://drafts.css-houdini.org/css-paint-api/">CSS Paint API</a> to view this demo :(';
}

const button = document.querySelector('#ripple');
let start = performance.now();
let x, y;
document.querySelector('#ripple').addEventListener('click', evt => {
    button.classList.add('animating');
    [x, y] = [evt.clientX, evt.clientY];
    start = performance.now();
    requestAnimationFrame(function raf(now) {
        console.log(start, now);
        const count = Math.floor(now - start);
        // 这里借用了css 变量,然后就可以在worklet中动态获取值
        button.style.cssText = `--ripple-x: ${x}; --ripple-y: ${y}; --animation-tick: ${count};`;
        if (count > 1000) {
            button.classList.remove('animating');
            button.style.cssText = `--animation-tick: 0`;
            return;
        }
        requestAnimationFrame(raf);
    });
});

重点来了,我们定义的paint api放在哪里呢?它不能直接像js一样被嵌入调用,而是需要使用worklet来帮助我们,像上面看的一样CSS.paintWorklet.addModule('./app.js');,在paintWorklet中添加一个我们自定义的脚本模块。worklet独立于主线程之外,不直接与DOM互动,特点为轻量且生命周期较短。 worklet生命周期image

  1. Worklet生命周期从渲染引擎开始
  2. 渲染引擎启动js 主线程
  3. 然后将启动多个worklet进程,用于"存放"后续被加载进来的worklet。这些进程独立于主线程,这样就不会阻塞主线程
  4. 主线程开始加载js脚本
  5. js脚本调用worklet.addModule来异步加载worklet
  6. 一旦上述worklet加载完成,worklet就会被加载到两个或多个可用的worklet进程中
  7. 一旦在某处调用了在worklet中被注册的模块,渲染引擎就会执行该worklet中的处理函数。这个调用过程可以并发调用多个worklet实例

下面是我们注册的 ripple

registerPaint(
    'ripple',
    class {
        static get inputProperties() {
            return [
                '--ripple-color',
                '--animation-tick',
                'background-color',
                '--ripple-x',
                '--ripple-y',
            ];
        }
        paint(ctx, geom, properties) {
            console.log(ctx, geom, properties);

            const bgColor = properties.get('background-color').toString();
            const rippleColor = properties.get('--ripple-color').toString();
            const x = parseFloat(properties.get('--ripple-x').toString());
            const y = parseFloat(properties.get('--ripple-y').toString());
            let tick = parseFloat(
                properties.get('--animation-tick').toString()
            );

            if (tick < 0) tick = 0;
            if (tick > 1000) tick = 1000;

            ctx.fillStyle = bgColor;
            ctx.fillRect(0, 0, geom.width, geom.height);

            ctx.fillStyle = rippleColor;
            ctx.globalAlpha = 1 - tick / 1000;

            ctx.arc(x, y, (geom.width * tick) / 1000, 0, 2 * Math.PI);

            ctx.fill();
        }
    }
);

这里调用了registerPaint来注册我们需要的ripple,该函数由一个namepaint类构成,其中inputProperties 静态方法用来获取该元素上的一些css属性值,并且传入到paint方法中作为第三个参数。paint方法就是我们实际"绘画"的地方,它提供了三个参数:一个canvas的context变量、当前画布的大小即当前dom元素的大小,以及当前dom元素的css属性properties(在inputProperties返回的)。其中canvas的context变量没有实现canvas中例如对img的处理、drawFocusIfNeeded/scrollPathIntoView、fillText/strokeText、CanvasTextAlign/CanvasTextBaseline等的实现。总的来说就是利用canvas来进行绘图,这里就有想象力了。

layout api与paint的api编写方式基本一样,不过调用的是registerLayout,以及类里面的调用方法也有所不同,但浏览器的支持性不是那么好,试了一个官方提供的demo都没生效,-_-,还有 animation-worklet,直接告诉我不支持,用其polyfill也还是有bughoudini还是有一段路要走啊......

关于各个浏览器对Houdini的推进程度,看你点击如下链接查看:Is Houdini ready yet‽

至于其他的几个api以及houdini的现状,有兴趣的可以从下面的参考资料中了解:

参考资料:

Houdini:CSS 领域最令人振奋的革新 和 Houdini, CSS Paint API 打个招呼吧 Houdini Samples:houdini的demo CSS-TAG Houdini Editor Drafts:houdini编辑草案 Houdini工作组规范