和 Houdini, CSS Paint API 打个招呼吧

简介: Hodini 的出现将赋予开发者前所未有的控制页面视觉表现的能力。这个项目的第一步是实现 CSS Paint API。本篇将解释为什么 Houdini 的到来让人如此兴奋,以及向读者展示如何开始使用 Paint API。

原文链接:SAY HELLO TO HOUDINI AND THE CSS PAINT API
作者:Will Boyd
相关阅读:Houdini:CSS 领域最令人振奋的革新

浏览器发展至今,已经很久没有感受过这种期待了。

Hodini 的出现将赋予开发者前所未有的控制页面视觉表现的能力。这个项目的第一步是实现 CSS Paint API。本篇将解释为什么 Houdini 的到来让人如此兴奋,以及向读者展示如何开始使用 Paint API。

老生常谈的问题

相信每次要使用 CSS 新特性时,你都会看到下面这句话:

Wooo,这个效果太酷了!我想等到(大概两年后吧)大部分浏览器都支持的时候就用上。

但我们并不想等那么久,那干脆用 CSS polyfills 好了。但在一些边界情况下 polyfills 也无能为力。更何况它还可能带来性能问题。在大部分情况下原生浏览器的实现都优于 polyfills。

如果对此你还有疑问,可以看看这篇说的 CSS polyfill 的坏处

新的希望

看到这里,是不是有些失望了?别灰心,很快你不用等浏览器厂商,可以直接自己实现一个新忒性。这就是 Houdini 要做的事,它来自可拓展的 Web Manifesto,允许开发者直接操作浏览器的 CSS 引擎,开发者拥有极大的权限,甚至能干预浏览器原生的渲染流程。

这些自定义的 CSS 属性可以在 worklet 中定义,worklet 也用 JavaScript 编写,只是浏览器执行它们的方式和我们认知里不同,稍后会详聊这部分。成功使用之后, worklet 将在访问者的浏览器内植入了新特性,用户就能看到新特性下的视觉效果了。

这就表示,开发者不用再等待浏览器厂商了,只要支持了 Houdini 就能用上新特性。甚至是浏览器压根不打算实现的,开发者也能自力更生传达完美的效果给用户。

浏览器支持

好消息是 Apple、Google、微软、Mozilla、Opera 都是 Houdini 项目的推动者。不过到目前为止只有 Google Chrome 落地实施了这个计划。撰写本文时,各个浏览器厂商的实现程度:

01.005eb0aacbaf.png

这个表格信息量有些大,容我细细解释。

Houdini 就好比是一张拼图,它是一系列 API 的统称。开发者可以通过 Layout API 控制元素的布局;通过 Parser API 控制 CSS 表达式处理参数的逻辑…不过看得出来,Houdini 项目之路漫漫。

好消息是,其中一个 API 已经可以用起来了:Paint API。通过 Paint API 开发者可以画出图像,然后把这些图像运用到合适的 CSS 属性上,比如 bakcground-image 和 list-style-image

暂时你还只能在 Chrome 上做试验。Chrome 65+ 已默认开启该接口,65 以下的 Chrome 需要通过访问 chrome://flags 开启 Experimental Web Platform features

可以通过以下任意一种方式确认 Chrome 是否支持该 API:

if ('paintWorklet' in CSS) {
    // 逻辑写这里
}
@supports (background: paint(id)) {
    /* 样式在此 */
}

也可以通过这个 Codepen demo 确认,如果访问链接看到的是两个绿色打钩,就说明浏览器已经准备好了!

技术性提示

Paint API 必须要在支持 https 服务器上或者本地 localhost 上才能使用。所以如果你是在本地开发,可以用 http-server 在本地快速搭建一个服务器。

要记得禁用浏览器缓存,让最新的 worklets 立马生效。

目前暂时无法在 worklets 中打断点或者插入 debugger ,不过 console.log() 还是可以用的。

简单的 Paint Worklet

让我们用 Paint API 搞点事情!先来个小前菜:在一个元素上画一个叉。这个效果的实际应用就是占位符,常见于一些模型设计/线框图中,表示该占位需要放一张图片。·

效果如下,代码在此

02.633f9c6d1bb4.jpg

绘制代码会被写入 paint worklet 中,它的作用域和功能都有限。Paint Worklet 无法操作 DOM 和全局方法(比如 setInterval)。这样的特性保证了 worklet 的高效和可多线程化(目前还不支持,但这点是众望所归)。

class PlaceholderBoxPainter {
    paint(ctx, size) {
        ctx.lineWidth = 2;
        ctx.strokeStyle = '#666';

        // 从左上角到右下角的一条线
        ctx.beginPath();
        ctx.moveTo(0, 0);
        ctx.lineTo(size.width, size.height);
        ctx.stroke();

        // 从右上角到左下角的一条线
        ctx.beginPath();
        ctx.moveTo(size.width, 0);
        ctx.lineTo(0, size.height);
        ctx.stroke();
    }
}

registerPaint('placeholder-box', PlaceholderBoxPainter);

当重绘元素被触发时,paint() 方法就会被调用。它接收两个传入参数,第一个是将被绘制的 ctx对象,和 CanvasRenderingContext2D 对象差不多,不过多了些限制(比如无法绘制文字)。size决定了绘制元素的宽和高。

接下来,浏览器页面将接收这个 paint worklet,给页面加一个 <div class="placeholder"> 标签。

<script>
    CSS.paintWorklet.addModule('worklet.js');
</script>

<div class="placeholder"></div>

最后,将 worklet 和 <div> 通过 css 关联起来:

.placeholder {
    background-image: paint(placeholder-box);

    /* 其他样式... */
}

嗯,就是这样。

恭喜!看来你已经知道怎么用 Paint API 了!

Input Property 的使用

现在我们写的叉中,线的粗细程度和颜色都是硬编码的,如果想要改成对齐容器边框的粗细和颜色要怎么写呢?

我们可以通过 input property(输入属性)实现,这一特性由 Typed Object Model (也可以称之为 Typed OM)提供。Typed OM 同属于 Houdini,但和 Paint API 不同的是,需要手动开启 chrome://flags 中的 Experimental Web Platform features

可以通过下面的代码确认是否成功启用该特性:

if ('CSSUnitValue' in window) {
    // 样式在此
}

启用之后,就可以修改原来的 paint worklet 让它可以接收 input property 了:

class PlaceholderBoxPropsPainter {
    static get inputProperties() {
        return ['border-top-width', 'border-top-color'];
    }

    paint(ctx, size, props) {
        // 默认值
        ctx.lineWidth = 2;
        ctx.strokeStyle = '#666';

        // 设置线的宽度为(如果存在的)顶边宽度
        let borderTopWidthProp = props.get('border-top-width');
        if (borderTopWidthProp) {
            ctx.lineWidth = borderTopWidthProp.value;
        }

        // 设置线的样式为(如果存在的)定边样式
        let borderTopColorProp = props.get('border-top-color');
        if (borderTopColorProp) {
            ctx.strokeStyle = borderTopColorProp.toString();
        }

        // 上面 demo 中的代码从这里开始...
    }
}

registerPaint('placeholder-box-props', PlaceholderBoxPropsPainter);

通过添加 inputProperties,paint worklet 就知道要去哪里找 CSS 属性。paint() 函数也能够接收第三个传入参数 props,通过它获取到 CSS 属性值。现在,我们的占位符看着自然多了(codepen 链接):

03.713b193468b4.png

用 border 也可以,不过要记得这个属性其实是简写,背后其实有12个属性:

.shorthand {
    border: 1px solid blue;
}

.expanded {
    border-top-width: 1px;
    border-right-width: 1px;
    border-bottom-width: 1px;
    border-left-width: 1px;
    border-top-style: solid;
    border-right-style: solid;
    border-bottom-style: solid;
    border-left-style: solid;
    border-top-color: blue;
    border-right-color: blue;
    border-bottom-color: blue;
    border-left-color: blue;
}

paint worklet 需要指明具体属性,到目前为止的例子里,我们用到的属性是 border-top-width 和 border-top-color

值得注意的是,paint worklet 在处理 border-top-width 时会转化为以像素为单位的数值。这个处理方式堪称完美,正是 ctx.lineWidth 所希望的处理方式。什么?怎么知道会转成像素的?看看 demo 中的第三个占位符,它的 border-top-width 是 1rem,但 paint worklet 接收以后就变成了 16px

带锯齿的边界

让我们把目光投向新的舞台 — 用 paint worklet 画一个带锯齿的边界,代码在此

04.fc7958e7c9d4.png

接下来,让我们详细看看具体实现:

class JaggedEdgePainter {
    static get inputProperties() {
        return ['--tooth-width', '--tooth-height'];
    }

    paint(ctx, size, props) {
        let toothWidth = props.get('--tooth-width').value;
        let toothHeight = props.get('--tooth-height').value;

        // 为确保「牙齿」排列集中,需要进行一系列计算
        let spaceBeforeCenterTooth = (size.width - toothWidth) / 2;
        let teethBeforeCenterTooth = Math.ceil(spaceBeforeCenterTooth / toothWidth);
        let totalTeeth = teethBeforeCenterTooth * 2 + 1;
        let startX = spaceBeforeCenterTooth - teethBeforeCenterTooth * toothWidth;

        // 从左开始画
        ctx.beginPath();
        ctx.moveTo(startX, toothHeight);

        // 给所有「牙齿」画上锯齿
        for (let i = 0; i < totalTeeth; i++) {
            let x = startX + toothWidth * i;
            ctx.lineTo(x + toothWidth / 2, 0);
            ctx.lineTo(x + toothWidth, toothHeight);
        }

        // 闭合「牙齿」的曲线,并填色
        ctx.lineTo(size.width, size.height);
        ctx.lineTo(0, size.height);
        ctx.closePath();
        ctx.fill();
    }
}

registerPaint('jagged-edge', JaggedEdgePainter);

这里我们又用上了 inputProperties,需要控制每个「牙齿」的宽度和高度。还用到了自定义属性(也被称为CSS 变量--tooth-width 和 --tooth-height。这确实比占用现有的 CSS 属性要好,但想在 paint worklet 中使用自定义属性还要多走一步。

你看,浏览器能够识别它已知的 CSS 属性值和对应的变量值,知道某一个属性需要「长度」作为它的属性值(比如上面的 border-top-width)。但自定义属性是开发者控制的,会有各种各样的属性值,浏览器不知道哪个属性该对应什么样的值才合法。所以要用自定义属性就多了一步,需要告知浏览器识别属性值。

Properties and Values API 做的就是这件事情。这个 API 也是 Houdini 的一部分,同样需要手动开启(译者:方法同上,不再赘述)。

可以通过 JS 确认是否成功开启:

if ('registerProperty' in CSS) {
    // 这里写代码
}

确认开启后,在 paint worklet 外面加上下面这一段:

CSS.registerProperty({
    name: '--tooth-width',
    syntax: '<length>',
    initialValue: '40px'
});
CSS.registerProperty({
    name: '--tooth-height',
    syntax: '<length>',
    initialValue: '20px'
});

在 --tooth-width 和 --tooth-height 上填长度相关的值后,浏览器就知道在 paint worklet 中使用这两个属性时,需要把对应值转成像素。甚至可以用 calc() !如果不小心写成非长度值,则会传入 initialValue 不至于报错。

.jagged {
    background: paint(jagged-edge);
    /* 其他样式... */
}

.slot:nth-child(1) .jagged {
    --tooth-width: 50px;
    --tooth-height: 25px;
}

.slot:nth-child(2) .jagged {
    --tooth-width: 2rem;
    --tooth-height: 3rem;
}

.slot:nth-child(3) .jagged {
    --tooth-width: calc(33vw - 31px);
    --tooth-height: 2em;
}

并不是只允许使用 <length> 类型,更多可选类型请参考这里

比如我们也能定义 --tooth-color 自定义属性,并规定属性值是 <color>。不过在实现锯齿边距上,我还有个更好的方案:在 paint worklet 中用 -webkit-mask-image 。这个方案不用修改锯齿背景色就能实现各种各样背景的锯齿了:

.jagged {
    --tooth-width: 80px;
    --tooth-height: 30px;
    -webkit-mask-image: paint(jagged-edge);

    /* 其他样式... */
}

.slot:nth-child(1) .jagged {
    background-image: linear-gradient(to right, #22c1c3, #fdbb2d);
}

.slot:nth-child(2) .jagged {
    /* 图源来自游戏 Iconoclasts http://www.playiconoclasts.com/ */
    background-image: url('iconoclasts.png');
    background-size: cover;
    background-position: 50% 0;
}

paint worklet 代码修改不大,具体效果如下:

05.9ba309ff7814.png

输入参数

可以通过输入参数 (input arguments) 向 paint worklet 中传参,从 CSS 中传入参数:

.solid {
    background-image: paint(solid-color, #c0eb75);

    /* 其他的样式... */
}

paint worklet 中定义了 inputArguments 需要传入什么样的参数。paint() 函数可以通过第四个传入参数获取到所有 inputArguments,第四个参数是名为 args 的数组:

class SolidColorPainter {
    static get inputArguments() {
        return ['<color>'];
    }

    paint(ctx, size, props, args) {
        ctx.fillStyle = args[0].toString();
        ctx.fillRect(0, 0, size.width, size.height);
    }
}

registerPaint('solid-color', SolidColorPainter);

说实话,我并非这种写法的拥趸。而且我认为相比之下,自定义属性更灵活,还可以通过变量名得到自文档化的 CSS。

动画革命

最后一个 demo 了。通过以上所学知识,我们能做出下面这漂亮的褪色圆点图案

06.e607e085b15d.png

为了控制这些渐变点,第一步就是先注册几个自定义属性:

CSS.registerProperty({
    name: '--dot-spacing',
    syntax: '<length>',
    initialValue: '20px'
});
CSS.registerProperty({
    name: '--dot-fade-offset',
    syntax: '<percentage>',
    initialValue: '0%'
});
CSS.registerProperty({
    name: '--dot-color',
    syntax: '<color>',
    initialValue: '#fff'
});

注册之后 paint worklet 就能使用这些变量啦,接下来就是进行一系列计算,画出想要的褪色效果:

class PolkaDotFadePainter {
    static get inputProperties() {
        return ['--dot-spacing', '--dot-fade-offset', '--dot-color'];
    }

    paint(ctx, size, props) {
        let spacing = props.get('--dot-spacing').value;
        let fadeOffset = props.get('--dot-fade-offset').value;
        let color = props.get('--dot-color').toString();

        ctx.fillStyle = color;
        for (let y = 0; y < size.height + spacing; y += spacing) {
            for (let x = 0; x < size.width + spacing; x += spacing * 2) {
                // 通过变换 x 在每一行中创建交错的点
                let staggerX = x + ((y / spacing) % 2 === 1 ? spacing : 0);

                // 通过 fade offset和每个点的横坐标,计算出该点的半径
                let fadeRelativeX = staggerX - size.width * fadeOffset / 100;
                let radius = spacing * Math.max(Math.min(1 - fadeRelativeX / size.width, 1), 0);

                // 画出目标点
                ctx.beginPath();
                ctx.arc(staggerX, y, radius, 0, 2 * Math.PI);
                ctx.fill();
            }
        }
    }
}

registerPaint('polka-dot-fade', PolkaDotFadePainter);

最后,还要在 CSS 中用上这个 paint worklet 才能看到效果:

.polka-dot {
    --dot-spacing: 20px;
    --dot-fade-offset: 0%;
    --dot-color: #40e0d0;
    background: paint(polka-dot-fade);

    /* 其他样式... */
}

现在,故事的转折点来了!动画效果可以通过改变自定义属性的方式实现。当属性值发生变化时,paint worklet 会被调用,然后浏览器重绘元素,最终实现动画效果。

那么来试试通过 CSS 动画中的 keyframestransition 也可以)改变 --dot-fade-offset 和 --dot-color

.polka-dot {
    --dot-spacing: 20px;
    --dot-fade-offset: 0%;
    --dot-color: #fc466b;
    background: paint(polka-dot-fade);

    /* 其他样式... */
}

.polka-dot:hover, .polka-dot:focus {
    animation: pulse 2s ease-out 6 alternate;

    /* 其他样式... */
}

@keyframes pulse {
    from {
        --dot-fade-offset: 0%;
        --dot-color: #fc466b;
    }
    to {
        --dot-fade-offset: 100%;
        --dot-color: #3f5efb;
    }
}

最终效果如下,完整代码在此

banner.54e9e80d1008.gif

看到 houdini 的潜力了吧!是不是酷毙了,paint worlets + 自定义属性的组合将会给动画带来革命!

优点和缺点

让我们再回顾一下 Houdini 的优点(着重回顾本篇大量用到的 CSS Paint API):

  • 不受限制,开发者能创造各种各样的视觉效果。
  • 不需要新增 DOM 节点。
  • 在浏览器渲染管道中执行,效率高。
  • 比起 polyfill,更加性能友好,也更健壮。
  • 这是浏览器原生支持的接口,开发者能有不用 hack 的选择了。
  • 用于实现视觉效果的 CSS 常常被诟病不像一门编程语言,几乎无法表达完整的逻辑。那现在可以用 paint worklet 编写视觉效果上的逻辑了。
  • 动画革命。
  • 快浏览器厂商一步实现特性,而且这些特性能实实在在地展现在用户的设备上。
  • 五大浏览器厂商都表示支持 Houdini。

当然了,缺点也不能避而不谈:

  • Houdini 的实现之路漫漫。
  • 虽然它可以缓解兼容问题,但首先,浏览器们得先兼容 Houdini…
  • 浏览器加载 paint worklet 并执行它需要时间,这是异步的,可能导致样式上的闪动。
  • 开发者工具尚不支持 paint worklet 的断点调试(也不支持 debugger),不过 console.log()还能用。

结论

Houdini 将会改变我们现在编写 CSS 的方式。虽然可能它将历时不短,但从目前可用的部分(比如,Paint API)来看,潜力惊人。所以,请继续关注 Houdini 啊~

本文中用到的 demo 都在 Github 上了。更多效果请移步 @iamvdo 的作品

原文发布时间为:2018年03月07日
本文作者: Will Boyd
本文来源:前端外刊 如需转载请联系原作者
相关文章
|
4月前
|
前端开发 JavaScript API
Vue 3 新特性:在 Composition API 中使用 CSS Modules
Vue 3 新特性:在 Composition API 中使用 CSS Modules
|
数据采集 前端开发 JavaScript
HTML + CSS + JS 利用邮编查询 API 实现邮编查询工具
邮政编码是地址信息的重要组成部分,可以帮助快递公司、物流公司等对地址进行快速、准确的识别和派送。因此,邮编查询工具应用在许多业务场景中都有广泛的应用,例如:电商平台、物流公司、金融机构等。通过使用邮编查询 API,我们可以快速实现一个邮编查询工具应用,方便用户查询地址对应的邮政编码,提高业务流程的效率。
443 0
|
存储 JavaScript 前端开发
使用 HTML、CSS、JS 和 API 制作一个很棒的天气 Web 应用程序
使用 HTML、CSS、JS 和 API 制作一个很棒的天气 Web 应用程序
140 0
|
人工智能 JavaScript 前端开发
原生JS + HTML + CSS 实现快递物流信息 API 的数据链式展示
全国快递物流查询 API 是一种提供实时、准确、可靠的快递物流信息查询服务的接口。它基于现有的物流信息系统,通过API接口的方式,向用户提供快递物流信息的查询、跟踪、统计等功能。
367 0
原生JS + HTML + CSS 实现快递物流信息 API 的数据链式展示
|
前端开发 JavaScript API
CSS 动画与 Web 动画 API
JavaScript 中有一个用于动画的原生 API,称为 Web Animations API
171 0
CSS 动画与 Web 动画 API
|
前端开发 API UED
W3C发布CSS ANIMATION WORKLET API的草案
W3C发布CSS ANIMATION WORKLET API的草案
227 0
W3C发布CSS ANIMATION WORKLET API的草案
|
Web App开发 前端开发 JavaScript
|
前端开发 API 定位技术
百度地图api改变覆盖物背景实例及css颜色值简介
                               在此鸣谢buptwusuopu的技术支持                                      在调用百度地图api的时候,为了改变覆盖物的颜色,如图中椭圆型的填充色。可以到百度api的库中查找方法http://developer.baidu.com/map/reference/index.php?tit
1493 0
|
16天前
|
JSON API 数据格式
淘宝 / 天猫官方商品 / 订单订单 API 接口丨商品上传接口对接步骤
要对接淘宝/天猫官方商品或订单API,需先注册淘宝开放平台账号,创建应用获取App Key和App Secret。之后,详细阅读API文档,了解接口功能及权限要求,编写认证、构建请求、发送请求和处理响应的代码。最后,在沙箱环境中测试与调试,确保API调用的正确性和稳定性。
|
28天前
|
供应链 数据挖掘 API
电商API接口介绍——sku接口概述
商品SKU(Stock Keeping Unit)接口是电商API接口中的一种,专门用于获取商品的SKU信息。SKU是库存量单位,用于区分同一商品的不同规格、颜色、尺寸等属性。通过商品SKU接口,开发者可以获取商品的SKU列表、SKU属性、库存数量等详细信息。