和 Houdini, CSS Paint API 打个招呼吧
瀏覽器發展至今,我很久沒有過這種期待了。
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-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 搞點事情!先來個小前菜:在一個元素上畫一個叉。這個效果的實際應用就是占位符,常見于一些模型設計/線框圖中,表示該占位需要放一張圖片。·
效果如下,代碼在此:
?
繪制代碼會被寫入 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-width 和 border-top-color。
值得注意的是,paint worklet 在處理 border-top-width 時會轉化為以像素為單位的數值。這個處理方式堪稱完美,正是 ctx.lineWidth 所希望的處理方式。什么?怎么知道會轉成像素的?看看 demo 中的第三個占位符,它的 border-top-width 是 1rem,但 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 初探
轉載于:https://www.cnblogs.com/rubyxie/articles/8872094.html
總結
以上是生活随笔為你收集整理的和 Houdini, CSS Paint API 打个招呼吧的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: linux自动删除30天前的日志文件
- 下一篇: 两个栈实现一个队列,两个队列实现一个栈