和 Houdini, CSS Paint API 打個招呼吧

原文鏈接:SAY HELLO TO HOUDINI AND THE CSS PAINT API

作者:Will Boyd

瀏覽器發展至今,我很久沒有過這種期待了。

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 落地實施了這個計劃。撰寫本文時,各個瀏覽器廠商的實現程度:

這個表格信息量有些大,容我細細解釋。

Houdini 就好比是一張拼圖,它是一系列 API 的統稱。開發者可以通過 Layout API 控制元素的布局;通過 Parser API 控制 CSS 表達式處理參數的邏輯…不過看得出來,Houdini 項目之路漫漫。

好消息是,其中一個 API 已經可以用起來了:Paint API。通過 Paint API 開發者可以畫出圖像,然後把這些圖像運用到合適的 CSS 屬性上,比如 bakcground-imagelist-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 搞點事情!先來個小前菜:在一個元素上畫一個叉。這個效果的實際應用就是佔位符,常見於一些模型設計/線框圖中,表示該佔位需要放一張圖片。·

效果如下,代碼在此:

繪製代碼會被寫入 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 鏈接):

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-widthborder-top-color

值得注意的是,paint worklet 在處理 border-top-width 時會轉化為以像素為單位的數值。這個處理方式堪稱完美,正是 ctx.lineWidth 所希望的處理方式。什麼?怎麼知道會轉成像素的?看看 demo 中的第三個佔位符,它的 border-top-width1rem,但 paint worklet 接收以後就變成了 16px

帶鋸齒的邊界

讓我們把目光投向新的舞台 — 用 paint worklet 畫一個帶鋸齒的邊界,代碼在此:

接下來,讓我們詳細看看具體實現:

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 代碼修改不大,具體效果如下:

輸入參數

可以通過輸入參數 (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 了。通過以上所學知識,我們能做出下面這漂亮的褪色圓點圖案:

為了控制這些漸變點,第一步就是先註冊幾個自定義屬性:

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 動畫中的 keyframes(transition 也可以)改變 --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; }}

最終效果如下,完整代碼在此:

看到 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 的作品。

相關推薦

  • 對 Houdini 的介紹:Houdini:CSS 領域最令人振奮的革新
  • 業界分享:@趙錦江(勾三股四) 老師在第四屆 CSS Conf 的分享 CSS Houdini 初探

推薦閱讀:

CSS新浪潮——Styled Components
手機html網頁和電腦上的html網頁在製作上有什麼區別?
Stylus - 讓 CSS 自由到「木有下限」
:before偽元素模擬list-style-image時, 怎樣模擬list的inside效果?
某廠面試題:為何這樣處理移動端適配不行?

TAG:CSS | 前端開發 | 新技術 |