
海淘科技與你分享《建站基礎(chǔ)知識(shí):用CSS Houdini畫(huà)一片星空 》
要問(wèn)2018最讓人興奮的CSS技術(shù)是什么,CSS Houdini當(dāng)之無(wú)愧,甚至可以去掉2018這個(gè)限定。其實(shí)這個(gè)技術(shù)在2016年就出來(lái)了,但是在今年3月發(fā)布的Chrome 65才正式支持。
CSS Houdini可以做些什么?谷歌開(kāi)發(fā)者文檔列了幾個(gè)Demo,我們先來(lái)看一下這幾個(gè)Demo。
給textarea加一個(gè)方格背:
使用以下CSS代碼:
textarea { background-image: paint(checkerboard); }
給div添加一個(gè)鉆石形狀背景:
使用以下CSS:
div { --top-width: 80; --top-height: 20; -webkit-mask-image: paint(demo); }
點(diǎn)擊圓圈擴(kuò)散動(dòng)畫(huà):
這3個(gè)例子都是用了Houdini里面的CSS Paint API。
第1個(gè)例子如果使用傳統(tǒng)的CSS屬性,我們最多可能就是使用漸變來(lái)做顏色的變化,但是做不到這種一個(gè)格子一個(gè)格子的顏色變化的,而第2個(gè)例子也是沒(méi)有辦法直接用CSS畫(huà)個(gè)鉆石的形狀。這個(gè)時(shí)候你可能會(huì)想到會(huì)SVG/Canvas的方法,SVG和Canvas的特色是矢量路徑,可以畫(huà)出各種各樣的矢量圖形,而Canvas還能控制任意像素點(diǎn),所以用這兩種方式也是可以畫(huà)出來(lái)的。
但是Canvas和HTML相結(jié)合的時(shí)候就會(huì)顯得有點(diǎn)笨拙,就像第2個(gè)例子畫(huà)一個(gè)鉆石的形狀,用Canvas你需要利用類似于BFC定位的方式,把Cavans調(diào)到合適的定位,還要注意z-index的覆蓋關(guān)系,而使用SVG可能會(huì)更簡(jiǎn)單一點(diǎn),可以設(shè)置background-image為一張鉆石的SVG圖片,但是無(wú)法像Canavas一樣很方便地做一些變量控制,例如隨時(shí)改一下鉆石邊框的顏色粗細(xì)等。
而第1個(gè)例子給textarea加格子背景,只能使用background-image 加SVG的方式,但是你不知道這個(gè)textarea有多大,SVG的格子需要準(zhǔn)備多少個(gè)呢?當(dāng)然你可能會(huì)說(shuō)誰(shuí)會(huì)給textarea加一個(gè)這樣的背景呢。但這只是一個(gè)示例,其它的場(chǎng)景可能也會(huì)遇到類似的問(wèn)題。
第3個(gè)例子點(diǎn)擊圓圈擴(kuò)散動(dòng)畫(huà),這個(gè)也可以在div里面absolute定位一個(gè)canvas元素,但是我們又遇到另外一個(gè)問(wèn)題:無(wú)法很方便復(fù)用,假設(shè)這種圈圈擴(kuò)散效果在其它地方也要用到,那就得在每個(gè)地方都寫(xiě)一個(gè)canvas元素并初始化。
所以傳統(tǒng)的方式存在以下問(wèn)題:
需要調(diào)好和其它html元素的定位和z-index關(guān)系等
編輯框等不能方便地改背景,不能方便地做變量控制
不能方便地進(jìn)行復(fù)用
其實(shí)還有另外一個(gè)更重要的問(wèn)題就是性能問(wèn)題,用Cavans畫(huà)這種效果時(shí)需要自己控制好幀率,一不小心電腦CPU風(fēng)扇可能就要呼嘯起來(lái),特別是不能把握重繪的時(shí)機(jī),如果元素大小沒(méi)有變化是不需要重繪,如果元素被拉大了,那么需要進(jìn)行重繪,或者當(dāng)鼠標(biāo)hover的時(shí)候做動(dòng)畫(huà)才需要重繪。
CSS Houdini在解決這種自定義圖形圖像繪制的問(wèn)題提供了很好的解決方案,可以用Canvas畫(huà)一個(gè)你想要的圖形,然后注冊(cè)到CSS系統(tǒng)里面,就能在CSS屬性里面使用這個(gè)圖形了。以畫(huà)一個(gè)星空為例,一步步說(shuō)明這個(gè)過(guò)程。
畫(huà)一個(gè)黑夜的夜空
CSS Houdini只能工作在localhost域名或者是https的環(huán)境,否則的話相關(guān)API是不可見(jiàn)(undefined)的。如果沒(méi)有https環(huán)境的話,可以裝一個(gè)http-server的npm包,然后在本地啟動(dòng),訪問(wèn)localhost:8080就可以了,新建一個(gè)index.html,寫(xiě)入:
通過(guò)在JS調(diào)用CSS.paintWorklet.addModule注冊(cè)一個(gè)CSS圖形starry-sky,然后在CSS里面就可以使用這個(gè)圖形,寫(xiě)在background-image、border-image或者mask-image等屬性里面。如上面代碼的:
body { background-image: paint(starry-sky); }
注冊(cè)paint worket的時(shí)候需要給它一個(gè)獨(dú)立的JS,作為這個(gè)worklet的工作環(huán)境,這個(gè)環(huán)境里面是沒(méi)有window/document等對(duì)象的和Web Worker一樣。如果你不想寫(xiě)管理太多JS文件,可以借助blob,blob是可以存放任何類型的數(shù)據(jù)的,包括JS文件。
Worklet需要的starry-sky.js的代碼如下所示:
class StarrySky { paint (ctx, paintSize, properties) { // 使用Canvas的API進(jìn)行繪制 ctx.fillRect(0, 0, paintSize.width, paintSize.height); } }
// 注冊(cè)這個(gè)屬性
registerPaint('starry-sky', StarrySky);
寫(xiě)一個(gè)類,實(shí)現(xiàn)paint接口,這個(gè)接口會(huì)傳一個(gè)canvas的context變量、當(dāng)前畫(huà)布的大小即當(dāng)前DOM元素的大小,以及當(dāng)前DOM元素的CSS屬性properties。
在paint函數(shù)里面調(diào)用canvas的繪制函數(shù)fillRect進(jìn)行填充,默認(rèn)填充色為黑色。訪問(wèn)index.html,就會(huì)看到整個(gè)頁(yè)面變成黑色了。我們的Hello World的CSS Houdini Painter就跑起來(lái)了,沒(méi)錯(cuò),就是這么簡(jiǎn)單。
但是有一點(diǎn)需要強(qiáng)調(diào)的是,瀏覽器實(shí)現(xiàn)并不是給那個(gè)DOM元素添加一個(gè)Canvas然后隱藏起來(lái),這個(gè)Paint Worket實(shí)際上是直接影響了當(dāng)前DOM元素重繪過(guò)程,相當(dāng)于我們給它添加了一個(gè)重繪的步驟,下文會(huì)繼續(xù)提及。
如果不想獨(dú)立寫(xiě)一個(gè)JS,用blob可以這樣:
let blobURL = URL.createObjectURL( new Blob([ '(', function(){ class StarrySky { paint (ctx, paintSize, properties) { ctx.fillRect(0, 0, paintSize.width, paintSize.height); } } registerPaint('starry-sky', StarrySky); }.toString(), ')()' ], { type: 'application/javascript' } ) ); CSS.paintWorklet.addModule(blobURL);
畫(huà)星星
Cavans星星效果網(wǎng)上找一個(gè)就好了,例如這個(gè)Codepen,代碼如下:
paint (ctx, paintSize, poperties) { let xMax= paintSize.width; let yMax = paintSize.height; // 黑色夜空 ctx.fillRect(0, 0, xMax, yMax); // 星星的數(shù)量 let hmTimes = xMax + yMax; for (let i = 0; i <= hmTimes; i++) { // 星星的xy坐標(biāo),隨機(jī) let x = Math.floor((Math.random() * xMax) + 1); let y = Math.floor((Math.random() * yMax) + 1); // 星星的大小 let size = Math.floor((Math.random() * 2) + 1); // 星星的亮暗 let opacityOne = Math.floor((Math.random() * 9) + 1); let opacityTwo = Math.floor((Math.random() * 9) + 1); let hue = Math.floor((Math.random() * 360) + 1); ctx.fillStyle = `hsla(${hue}, 30%, 80%, .${opacityOne + opacityTwo})`
效果如下:
為什么它要用fillRect來(lái)畫(huà)星星呢,星星不應(yīng)該是圓的么?因?yàn)槿绻胊rc的話性能會(huì)明顯降低。由于星星比較小,所以使用了這種方式,當(dāng)然改成arc也是可以的,因?yàn)槲覀冎皇钱?huà)一次就好了。
控制星星的密度
現(xiàn)在要做一個(gè)可配參數(shù)控制星星的密度,就好像border-radius可以控制一樣。借助CSS變量,給body添加一個(gè)自定義屬性--star-density:
body { --star-density: 0.8; background-image: paint(starry-sky); }
規(guī)定密度系數(shù)從0到1變化,通過(guò)paint函數(shù)的properties參數(shù)獲取到屬性。但是我們發(fā)現(xiàn)body/html的自定義屬性無(wú)法獲取,可以繼承給body的子元素,但無(wú)法在body上獲取,所以改成畫(huà)在body:before上面:
body:before { content: ""; position: absolute; left: 0; top: 0; width: 100%; height: 100%; --star-density: 0.5; background-image: paint(starry-sky); }
然后給class StarrySky添加一個(gè)靜態(tài)方法:
class StarrySky { static get inputProperties() { return ['--star-density']; } }
告知我們需要獲取哪些CSS屬性,可以是自定義的,也可以是常規(guī)的CSS屬性。然后在paint方法的properties里面就可以拿到屬性值:
class StarrySky { paint (ctx, paintSize, properties) { // 獲取自定義屬性值 let starDensity = +properties.get('--star-density').toString() || 1; // 最大只能為1 starDensity > 1 && (starDensity = 1); // 星星的數(shù)量剩以這個(gè)系數(shù) let hmTimes = Math.round((xMax + yMax) * starDensity); } }
讓星星的數(shù)量剩以傳進(jìn)來(lái)的系數(shù)進(jìn)而達(dá)控制密度的目的。上面設(shè)置星星的數(shù)量為最大值的一半,效果如下:
重繪
當(dāng)拉頁(yè)面的時(shí)候會(huì)發(fā)現(xiàn)所有星星的位置都發(fā)生了變化,這是因?yàn)橛|發(fā)了重繪。
在paint函數(shù)里面添加一個(gè)console.log,拉動(dòng)頁(yè)面的時(shí)候就可以觀察到瀏覽器在不斷地執(zhí)行paint函數(shù)。因?yàn)檫@個(gè)CSS屬性是寫(xiě)在body:befoer上面的,占滿了body,body大小改變就會(huì)觸發(fā)重繪。而如果寫(xiě)在一個(gè)寬度固定的div里面,拉動(dòng)頁(yè)面不會(huì)觸發(fā)重繪,觀察到paint函數(shù)沒(méi)有執(zhí)行。如果改了div或者body的任何一個(gè)CSS屬性也會(huì)觸發(fā)重繪。所以這個(gè)很方便,不需要我們自己去監(jiān)聽(tīng)resize之類的DOM變化。
頁(yè)面拉大時(shí),右邊新拉出來(lái)的空間星星沒(méi)有畫(huà)大,所以本身需要重繪。而重繪給我們?cè)斐傻膯?wèn)題是星星的位置發(fā)生變化,正常情況下應(yīng)該是頁(yè)面拉大拉小,星星的位置應(yīng)該是要不變的。所以需要記錄一下星星的一些相關(guān)信息。
記錄星星的數(shù)據(jù)
可以在SkyStarry這個(gè)類里面添加一個(gè)成員變量stars,保存所有star的信息,包括位置和透明度等,在paint的時(shí)候判斷一下stars的長(zhǎng)度,如果為0則進(jìn)行初始化,否則使用直接上一次初始化過(guò)的星星,這樣就能保證每次重繪都是用的同樣的星星了。但是在實(shí)際的操作過(guò)程中,發(fā)現(xiàn)一個(gè)問(wèn)題,它會(huì)初始化兩次starry-sky.js,在paint的時(shí)候也會(huì)隨機(jī)切換,如下圖所示:
這樣就造成了有兩個(gè)stars的數(shù)據(jù),在重繪過(guò)程中來(lái)回切換。原因可能是因?yàn)镃SS Houdini的本意并不想讓你保存實(shí)例數(shù)據(jù),但是既然它設(shè)計(jì)成一個(gè)類,使用類的實(shí)例數(shù)據(jù)應(yīng)該也是合情合理的。這個(gè)問(wèn)題我想到的一個(gè)解決方法是把random函數(shù)變成可控的,只要隨機(jī)化種子一樣,那么生成的random系列就是一樣的,而這個(gè)隨機(jī)化種子由CSS變量傳進(jìn)來(lái)。所以就不能用Math.random了,自己實(shí)現(xiàn)一個(gè)random,如下代碼所示:
random () { let x = Math.sin(this.seed++) * 10000; return x - Math.floor(x); }
只要初始化seed一樣,那么就會(huì)生成一樣的random系列。seed和星星密度類似,由CSS變量控制:
body:before { --starry-sky-seed: 1; --star-density: 0.5; background-image: paint(starry-sky); }
然后在paint函數(shù)里面通過(guò)properties拿到seed:
paint (ctx, paintSize, properties) { if (!this.stars) { let starOpacity = +properties.get('--star-opacity').toString(); // 得到隨機(jī)化種子,可以不傳,默認(rèn)為0 this.seed = +(properties.get('--starry-sky-seed').toString() || 0); this.addStars(paintSize.width, paintSize.height, starDensity); } }
通過(guò)addStars函數(shù)添加星星,這個(gè)函數(shù)調(diào)用上面自定義的random函數(shù):
random () { let x = Math.sin(this.seed++) * 10000; return x - Math.floor(x); } addStars (xMax, yMax, starDensity = 1) { starDensity > 1 && (starDensity = 1); // 星星的數(shù)量 let hmTimes = Math.round((xMax + yMax) * starDensity); this.stars = new Array(hmTimes); for (let i = 0; i < hmTimes; i++) { this.stars[i] = { x: Math.floor((this.random() * xMax) + 1), y: Math.floor((this.random() * yMax) + 1), size: Math.floor((this.random() * 2) + 1), // 星星的亮暗 opacityOne: Math.floor((this.random() * 9) + 1), opacityTwo: Math.floor((this.random() * 9) + 1), hue: Math.floor((this.random() * 360) + 1) }; } }
這段代碼由Math.random改成this.random保證只要隨機(jī)化種子一樣,生成的所有數(shù)據(jù)也都是一樣的。這樣就能解決上面提到的初始化兩次數(shù)據(jù)的問(wèn)題,因?yàn)榉N子是一樣的,所以兩次的數(shù)據(jù)也是一樣的。
但是這樣有點(diǎn)單調(diào),每次刷新頁(yè)面星星都是固定的,少了點(diǎn)靈氣。可以給這個(gè)隨機(jī)化種子做下優(yōu)化,例如實(shí)現(xiàn)單個(gè)小時(shí)內(nèi)是一樣的,過(guò)了一個(gè)小時(shí)后刷新頁(yè)面就會(huì)變。通過(guò)以下代碼可以實(shí)現(xiàn):
const ONE_HOUR = 36000 * 1000; this.seed = +(properties.get('--starry-sky-seed').toString() || 0) + Date.now() / ONE_HOUR >> 0;
這樣拉動(dòng)頁(yè)面的時(shí)候星星就不會(huì)變了。
但是在從小拉大的時(shí)候,右邊會(huì)沒(méi)有星星:
因?yàn)榈谝淮蔚漠?huà)布沒(méi)那么大,以后又沒(méi)有更新星星的數(shù)據(jù),所以右邊就空了。
增量更新星星數(shù)據(jù)
不能全部更新星星的數(shù)據(jù),不然第4步就白做了。只能把右邊沒(méi)有的給它補(bǔ)上。所以需要記錄一下兩次畫(huà)布的大小,如果第二次的畫(huà)布大了,則增加星星,否則刪掉邊界外的星星。
所以需要有一個(gè)變量記錄上一次畫(huà)布的大小:
class StarrySky { constructor () { // 初始化 this.lastPaintSize = this.paintSize = { width: 0, height: 0 }; this.stars = []; } }
把相關(guān)的操作抽成一個(gè)函數(shù),包括從CSS變量獲取設(shè)置,增量更新星星等,這樣可以讓主邏輯變得清晰一點(diǎn):
paint (ctx, paintSize, properties) { // 更新當(dāng)前paintSize this.paintSize = paintSize; // 獲取CSS變量設(shè)置,把密度、seed等存放到類的實(shí)例數(shù)據(jù) this.updateControl(properties); // 增量更新星星 this.updateStars(); // 黑色夜空 for (let star of this.stars) { // 畫(huà)星星,略 } }
增量更新星星需要做兩個(gè)判斷,一個(gè)為是否需要?jiǎng)h除掉一些星星,另一個(gè)為是否需要添加,根據(jù)畫(huà)布的變化:
updateStars () { // 如果當(dāng)前的畫(huà)布比上一次的要小,則刪掉一些星星 if (this.lastPaintSize.width > this.paintSize.width || this.lastPaintSize.height > this.paintSize.height) { this.removeStars(); } // 如果當(dāng)前畫(huà)布變大了,則增加一些星星 if (this.lastPaintSize.width < this.paintSize.width || this.lastPaintSize.height < this.paintSize.height) { this.addStars(); } this.lastPaintSize = this.paintSize; }
刪除星星removeStar的實(shí)現(xiàn)很簡(jiǎn)單,只要判斷x, y坐標(biāo)是否在當(dāng)前畫(huà)布內(nèi),如果是的話則保留:
removeStars () { let stars = [] for (let star of stars) { if (star.x <= this.paintSize.width && star.y <= this.paintSize.height) { stars.push(star); } } this.stars = stars; }
添加星星的實(shí)現(xiàn)也是類似的道理,判斷x, y坐標(biāo)是否在上一次的畫(huà)布內(nèi),如果是的話則不添加:
addStars () { let xMax = this.paintSize.width, yMax = this.paintSize.height; // 星星的數(shù)量 let hmTimes = Math.round((xMax + yMax) * this.starDensity); for (let i = 0; i < hmTimes; i++) { let x = Math.floor((this.random() * xMax) + 1), y = Math.floor((this.random() * yMax) + 1); // 如果星星落在上一次的畫(huà)布內(nèi),則跳過(guò) if (x < this.lastPaintSize.width && y < this.lastPaintSize.height) { continue; } this.stars.push({ x: x, y: y, size: Math.floor((this.random() * 2) + 1), // 星星的亮暗 }); } }
這樣當(dāng)拖動(dòng)頁(yè)面的時(shí)候就會(huì)觸發(fā)重繪,重繪的時(shí)候就會(huì)調(diào)paint更新星星。
讓星星閃起來(lái)
通過(guò)做星星透明度的動(dòng)畫(huà),可以讓星星閃起來(lái)。如果用Cavans標(biāo)簽,可以借助window.requestAnimationFrame注冊(cè)一個(gè)函數(shù),然后用當(dāng)前時(shí)間減掉開(kāi)始的時(shí)間模以一個(gè)值就得到當(dāng)前的透明度系數(shù)。使用Houdini也可以使用這種方式,區(qū)別是我們可以把動(dòng)態(tài)變化透明度系數(shù)當(dāng)作當(dāng)前元素的CSS變量或者叫自定義屬性,然后用JS動(dòng)態(tài)改變這個(gè)自定義屬性,就能夠觸發(fā)重繪,這個(gè)已在第3點(diǎn)重繪部分提到。
給元素添加一個(gè)--star-opacity的屬性:
body:before { --star-opacity: 1; --star-density: 0.5; --starry-sky-seed: 1; background-image: paint(starry-sky); }
在星星的時(shí)候,每個(gè)星星的透明度再乘以這個(gè)系數(shù):
// 獲取透明度系數(shù) this.starOpacity = +properties.get('--star-opacity').toString(); for (let star of this.stars) { // 每個(gè)星星的透明度都乘以這個(gè)系數(shù) let opacity = +('.' + (star.opacityOne + star.opacityTwo)) * this.starOpacity; ctx.fillStyle = `hsla(${star.hue}, 30%, 80%, ${opacity})`; ctx.fillRect(star.x, star.y, star.size, star.size); }
然后在requestAnimationFrame動(dòng)態(tài)改變這個(gè)CSS屬性:
let start = Date.now(); // before無(wú)法獲取,所以需要改成正常元素 let node = document.querySelector('.starry-sky'); window.requestAnimationFrame(function changeOpacity () { let now = Date.now(); // 每隔一1s,透明度從0.5變到1 node.style.setProperty('--star-opacity', (now - start) % 1000 / 2 + 0.5); window.requestAnimationFrame(changeOpacity); });
這樣就能重新觸發(fā)paint函數(shù)重新渲染了,但是這個(gè)效果其實(shí)是有問(wèn)題的,因?yàn)榈糜幸粋€(gè)alternate輪流交替的效果,即0.5變到1,再?gòu)?變到0.5,而不是每次都是0.5到1。 模擬CSS animation的alternate這個(gè)也好解決,可以規(guī)定奇數(shù)秒就是變大,而偶數(shù)秒就是變小,這個(gè)好實(shí)現(xiàn),略。
但實(shí)際上可以不用這么麻煩,因?yàn)楦淖僀SS屬性直接用animation就可以了,如下代碼所示:
body:before { --star-opacity: 1; --star-density: 0.5; --starry-sky-seed: 1; background-image: paint(starry-sky); animation: shine 1s linear alternate infinite; } @keyframes shine { from { --star-opacity: 1; } to { --star-opacity: 0.6; } }
這樣也能觸發(fā)重繪,但是我們發(fā)現(xiàn)它只有在from和to這兩個(gè)點(diǎn)觸發(fā)了重繪,沒(méi)有中間過(guò)渡的過(guò)程??梢酝茰y(cè)因?yàn)樗J(rèn)為--star-opacity的屬性值不是一個(gè)數(shù)字,而是一個(gè)字符串,所以這兩關(guān)鍵幀就沒(méi)有中間的過(guò)渡效果了。因此我們得告訴它這是一個(gè)整型,不是一個(gè)字符串。類型化CSS對(duì)象模型(Typed CSSOM)提供了這個(gè)API。
類型化CSS對(duì)象模型一個(gè)很大的作用就是把所有的CSS單位都用一個(gè)相應(yīng)的對(duì)象來(lái)表示,提供加減乘除等運(yùn)算,如:
// 10 px let length = CSS.px(10); // 在循環(huán)里面改length的值,不用自己去拼字符串 div.attributeStyleMap.set('width', length.add(CSS.px(1)))
這樣的好處是不用自己去拼字符串,另外還提供了轉(zhuǎn)換,如transform的值轉(zhuǎn)成matrix,度數(shù)轉(zhuǎn)成rad的形式等等。
它還提供了注冊(cè)自定義類型屬性的能力,使用以下API:
CSS.registerProperty({ name: '--star-opacity', // 指明它是一個(gè)數(shù)字類型 syntax: '', inherits: false, initialValue: 1 });
這樣注冊(cè)之后,CSS系統(tǒng)就知道--star-opacity是一個(gè)number類型,在關(guān)鍵幀動(dòng)畫(huà)里面就會(huì)有一個(gè)漸變的過(guò)渡效果。
類型CSS對(duì)象模型在Chrome 66已經(jīng)正式支持,但是registerProperty API仍然沒(méi)有開(kāi)放,需要打開(kāi)chrome://flags,搜索web platform,從disabled改成enabled就可以使用。
這個(gè)給我們提供了做動(dòng)畫(huà)新思路,CSS animation 加 Canvas的模式,CSS animation負(fù)責(zé)改變屬性數(shù)據(jù)并觸發(fā)重繪,而Canvas去獲取動(dòng)態(tài)變化的數(shù)據(jù)更新視圖。所以它是一個(gè)數(shù)據(jù)驅(qū)動(dòng)的動(dòng)畫(huà)模式,這也是當(dāng)前做動(dòng)畫(huà)的一個(gè)流行方式。
在我們這個(gè)例子里面,由于星星數(shù)太多,1s有60幀,每幀都要計(jì)算和繪制1000個(gè)星星,CPU使用率達(dá)到90%多,所以這個(gè)性能有問(wèn)題,如果用Cavans標(biāo)簽可以使用雙緩沖技術(shù),CSS Houdini好像沒(méi)有這個(gè)東西。但是可以換一個(gè)思路,改成做整體的透明度動(dòng)畫(huà),不用每個(gè)星星都算一下。
如下代碼所示:
body { background-color: #000; } body:before { background-image: paint(starry-sky); animation: shine 1s linear alternate infinite; } @keyframes shine { from { opacity: 1; } to { opacity: 0.6; } }
這個(gè)的效果和每個(gè)星星都單獨(dú)算是一樣的,CPU消耗12%左右,這個(gè)應(yīng)該還是可以接受的。
效果如下圖所示:
如果用Canvas標(biāo)簽,可以設(shè)置globalAlpha全局透明度屬性,而使用CSS Houdini我們直接使用opacity就行了。
一個(gè)完整的Demo:CSS Houdini Starry Sky,需要使用Chrome,因?yàn)槟壳爸挥蠧hrome支持。
總的來(lái)說(shuō),CSS Houdini的Paint Worket提供了CSS和Canvas的粘合,讓我們可以用Canvas畫(huà)出想要的CSS效果,并借助CSS自定義屬性進(jìn)行控制,通過(guò)使用JS或者CSS的animation/transition改變自定義屬性的值觸發(fā)重繪,從而產(chǎn)生動(dòng)畫(huà)效果,這也是數(shù)據(jù)驅(qū)動(dòng)的開(kāi)發(fā)思想。并討論了在畫(huà)這個(gè)星空的過(guò)程中遇到的一些問(wèn)題,以及相關(guān)的解決方案。
本文只是介紹了CSS Houdini里面的Paint Worket和Typed CSSOM,它還有另外一個(gè)Layout Worklet,利用它可以自行實(shí)現(xiàn)一個(gè)flex布局或者其它自定義布局,這樣的好處是:一方面當(dāng)有新的布局出現(xiàn)的時(shí)候可以借助這個(gè)API進(jìn)行polyfill就不用擔(dān)心沒(méi)有實(shí)現(xiàn)的瀏覽器不兼容,另一方面可以發(fā)揮想象力實(shí)現(xiàn)自己想要的布局,這樣在布局上可能會(huì)百花齊放了,而不僅僅使用W3C給的那幾種布局。
綜上是上海網(wǎng)站建設(shè)公司——海淘科技與你分享的《建站基礎(chǔ)知識(shí):用CSS Houdini畫(huà)一片星空 》。更多建站基礎(chǔ)知識(shí)供你查閱,可直接點(diǎn)擊:建站基礎(chǔ)知識(shí)。