和Houdini, CSS Paint API 打個招呼吧

blank

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

blank

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

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'inCSS){// 逻辑写这里
}

@supports(background:paint(id)){/* 样式在此 */}

也可以通過這個Codepen demo確認,如果訪問鏈接看到的是兩個綠色打鉤,就說明瀏覽器已經準備好了!

技術性提示

Paint API必須要在支持https服務器上或者本地localhost上才能使用。所以如果你是在本地開發,可以用http-server在本地快速搭建一個服務器。

要記得禁用瀏覽器緩存,讓最新的worklets 立馬生效。

目前暫時無法在worklets中打斷點或者插入debugger ,不過console.log()還是可以用的。

簡單的Paint Worklet

讓我們用Paint API 搞點事情!先來個小前菜:在一個元素上畫一個叉。這個效果的實際應用就是佔位符,常見於一些模型設計/線框圖中,表示該佔位需要放一張圖片。 ·

效果如下,代碼在此

blank

繪製代碼會被寫入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><divclass="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'inwindow){// 样式在此
}

啟用之後,就可以修改原來的paint worklet 讓它可以接收input property 了:

classPlaceholderBoxPropsPainter{staticgetinputProperties(){return['border-top-width','border-top-color'];}paint(ctx,size,props){//默認值ctx.lineWidth=2;ctx.strokeStyle='#666';//設置線的寬度為(如果存在的)頂邊寬度letborderTopWidthProp=props.get('border-top-width');if(borderTopWidthProp){ctx.lineWidth=borderTopWidthProp.value;}//設置線的樣式為(如果存在的)定邊樣式letborderTopColorProp=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鏈接):

blank

border也可以,不過要記得這個屬性其實是簡寫,背後其實有12個屬性:

.shorthand{border:1pxsolidblue;}.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畫一個帶鋸齒的邊界, 代碼在此

blank

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

classJaggedEdgePainter{staticgetinputProperties(){return['--tooth-width','--tooth-height'];}paint(ctx,size,props){lettoothWidth=props.get('--tooth-width').value;lettoothHeight=props.get('--tooth-height').value;//為確保「牙齒」排列集中,需要進行一系列計算letspaceBeforeCenterTooth=(size.width-toothWidth)/2;letteethBeforeCenterTooth=Math.ceil(spaceBeforeCenterTooth/toothWidth);lettotalTeeth=teethBeforeCenterTooth*2+1;letstartX=spaceBeforeCenterTooth-teethBeforeCenterTooth*toothWidth;//從左開始畫ctx.beginPath();ctx.moveTo(startX,toothHeight);//給所有「牙齒」畫上鋸齒for(leti=0;i<totalTeeth;i++){letx=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'inCSS){// 这里写代码
}

確認開啟後,在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(toright,#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代碼修改不大,具體效果如下:

blank

輸入參數

可以通過輸入參數(input arguments)向paint worklet中傳參,從CSS中傳入參數:

.solid{background-image:paint(solid-color,#c0eb75);/* 其他的样式... */}

paint worklet中定義了inputArguments需要傳入什麼樣的參數。 paint()函數可以通過第四個傳入參數獲取到所有inputArguments ,第四個參數是名為args的數組:

classSolidColorPainter{staticgetinputArguments(){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 了。通過以上所學知識,我們能做出下面這漂亮的褪色圓點圖案

blank

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

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 就能使用這些變量啦,接下來就是進行一系列計算,畫出想要的褪色效果:

classPolkaDotFadePainter{staticgetinputProperties(){return['--dot-spacing','--dot-fade-offset','--dot-color'];}paint(ctx,size,props){letspacing=props.get('--dot-spacing').value;letfadeOffset=props.get('--dot-fade-offset').value;letcolor=props.get('--dot-color').toString();ctx.fillStyle=color;for(lety=0;y<size.height+spacing;y+=spacing){for(letx=0;x<size.width+spacing;x+=spacing*2){//通過變換x在每一行中創建交錯的點letstaggerX=x+((y/spacing)%2===1?spacing:0);//通過fade offset和每個點的橫坐標,計算出該點的半徑letfadeRelativeX=staggerX-size.width*fadeOffset/100;letradius=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:pulse2sease-out6alternate;/*其他樣式... */}@keyframespulse{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作品

相關推薦

What do you think?

Written by marketer

blank

《高效前端:Web高效編程與優化實踐》送書活動!

blank

Redux-Saga 漫談