CSS Houdini 號稱 CSS 領域最使人振奮的革新。CSS 自己長期欠缺語法特性,可拓展性幾乎爲零,而且新特性的支持效率過低,兼容性差。而 Houdini 直接將 CSS 的 API 暴露給開發者,以往徹底黑盒的瀏覽器解析流開始對外開放,開發者能夠自定義屬於本身的 CSS 屬性,從而定製和擴展瀏覽器的展現行爲。javascript
咱們知道,瀏覽器在渲染頁面時,首先會解析頁面的 HTML 和 CSS,生成渲染樹(rendering tree),再經由佈局(layout)和繪製(painting),呈現出整個頁面內容。在 Houdini 出現以前,這個流程上咱們能操做的空間少之甚少,尤爲是 layout 和 painting 環節,能夠說是徹底封閉,使得咱們很難經過 polyfill 等相似的手段爲欠支持的 CSS 特性提供兼容。而另外一方面,語法特性的缺失也極大地限制了 CSS 的編程靈活性,社區中 sass、less、stylus 等 CSS 預處理技術的出現大多都源於這個緣由,它們都但願經過預編譯,突破 CSS 的侷限性,讓 CSS 擁有更強大的組織和編寫能力。因此慢慢地,咱們都再也不手寫 CSS,更方便、更靈活的 CSS 擴展語言成了 web 開發的主角。看到這樣的狀況,CSS Houdini 終於坐不住了。css
CSS Houdini 對外開放了瀏覽器解析流程的一系列 API,這些 API 容許開發者介入瀏覽器的 CSS engine 運做,帶來了更多的 CSS 解決方案。html
CSS Houdini 目前主要提供瞭如下幾個 API:前端
容許在 CSS 中定義變量和使用變量,是目前支持程度最高的一個 API。CSS 變量以 --
開頭,經過 var()
調用:java
div {
--font-color: #9e4a9b;
color: var(--font-color);
}
複製代碼
此外,CSS 變量也能夠在其餘節點中使用,只不過是有做用域限制的,也就是說自身定義的 CSS 變量只能被自身或自身的子節點使用:web
.container {
--font-color: #9e4a9b;
}
.container .text {
color: var(--font-color);
}
複製代碼
定義和使用 CSS 變量可讓咱們的 CSS 代碼變得更加簡潔明瞭,好比咱們能夠單純經過改變變量來改變 box-shadow 的顏色:編程
.text {
--box-shadow-color: #3a4ba2;
box-shadow: 0 0 30px var(--box-shadow-color);
}
.text:hover {
--box-shadow-color: #7f2c2b;
}
複製代碼
容許開發者編寫本身的 Paint Module,自定義諸如 background-image 這類的繪製屬性。自定義的重點在於,"怎麼畫" 的邏輯須要咱們來描述,所以咱們利用 registerPaint 來描述咱們的繪製邏輯:canvas
registerPaint('rect', class {
paint(ctx, size, properties, args) {}
});
複製代碼
registerPaint 方法註冊了一個 Paint 類 rect 以供調用,這個類的核心在於它的 paint 方法。paint 方法用於描述自定義的繪製邏輯,它接收四個參數:api
ctx
:一個 Canvas 的 Context 對象,所以 paint 中的繪製方式跟 canvas 繪製是同樣的。size
:包含節點的尺寸信息,同時也是 canvas 可繪製範圍(畫板)的尺寸信息。properties
:包含節點的 CSS 屬性,須要調用靜態方法 inputProperties
聲明注入。args
: CSS 中調用 Paint 類時傳入的參數,須要調用靜態方法 inputArguments
聲明注入。編寫完 Paint 類以後,咱們在 CSS 中只須要這樣調用,就能應用到咱們自定義的繪製邏輯:瀏覽器
.wrapper {
background-image: paint(rect);
}
複製代碼
Painting API 目前在高版本 Chrome、Opera 瀏覽器已有支持,且實現起來比較簡單,後邊咱們還將經過 demo 進一步演示。
容許開發者編寫本身的 Layout Module,自定義諸如 display 這類的佈局屬性。一樣的,"如何佈局" 的邏輯須要咱們本身編寫:
registerLayout('block-like', class {
layout(children, edges, constraints, properties, breakToken) {
// ...
return {
// inlineSize: number,
// blockSize: number,
// autoBlockSize: number,
// childFragments: sequence<LayoutFragment>
}
}
})
複製代碼
registerLayout 方法用於註冊一個 Layout 類以供調用,它的 layout 方法用於描述自定義的佈局邏輯,最終返回一個包含佈局後的位置尺寸信息和子節點序列信息的對象,引擎將根據這個對象進行佈局渲染。
一樣的,調用時只需:
.wrapper {
display: layout('block-like');
}
複製代碼
所以利用 Layout API,你徹底能夠實現對 flex 佈局的手工兼容。相比 Painting,Layout 的編寫顯得更加複雜,涉及到盒模型的深刻概念,且支持度不高,這裏就不細講了。
registerPaint、registerLayout 這些 API 在全局上並不存在,爲何能夠直接調用呢?這是由於上述的 JS 代碼並非直接執行的,而是經過 Worklets 載入執行的。Worklets 相似於 Web Worker,是一個運行於主代碼以外的獨立工做進程,但比 Worker 更爲輕量,負責 CSS 渲染任務是最合適的了。和 Web Worker 同樣,Worklets 擁有一個隔離於主進程的全局空間,在這個空間裏,沒有 window 對象,卻有 registerPaint、registerLayout 這些全局 API。所以,咱們須要這樣引入自定義 JS 代碼:
if ("paintWorklet" in CSS) {
CSS.paintWorklet.addModule("paintworklet.js");
}
複製代碼
if ("layoutWorklet" in CSS) {
CSS.layoutWorklet.addModule("layoutworklet.js");
}
複製代碼
咱們來自定義 background-image 屬性,它將用於給做用節點繪製一個矩形背景,背景色值由該節點上的一個 CSS 變量 --rect-color
指定。
新建一個 paintworklet.js,利用 registerPaint 方法註冊一個 Paint 類 rect,定義屬性的繪製邏輯:
registerPaint("rect", class {
static get inputProperties() {
return ["--rect-color"];
}
paint(ctx, geom, properties) {
const color = properties.get("--rect-color")[0];
ctx.fillStyle = color;
ctx.fillRect(0, 0, geom.width, geom.height);
}
});
複製代碼
上邊定義了一個名爲 rect 的 Paint 類,當 rect 被使用時,會實例化 rect 並自動觸發 paint 方法執行渲染。paint 方法中,咱們獲取節點 CSS 定義的 --rect-color
變量,並將元素的背景填充爲指定顏色。因爲須要使用屬性 --rect-color
,咱們須要在靜態方法 inputProperties
中聲明。
HTML 中經過 Worklets 載入上一步驟實現的 paintworklet.js 並註冊 Paint 類:
<div class="rect"></div>
<script> if ("paintWorklet" in CSS) { CSS.paintWorklet.addModule("paintworklet.js"); } </script>
複製代碼
CSS 中使用的時候,只須要調用 paint 方法:
.rect {
width: 100vw;
height: 100vh;
background-image: paint(rect);
--rect-color: rgb(255, 64, 129);
}
複製代碼
能夠看得出利用 CSS Houdini,咱們能夠像操做 canvas 同樣靈活自如地實現咱們想要的樣式功能。
根據上述步驟,咱們演示一下如何用 CSS Painting API 實現一個動態波浪的效果:
<!-- index.html -->
<div id="wave"></div>
<style> #wave { width: 20%; height: 70vh; margin: 10vh auto; background-color: #ff3e81; background-image: paint(wave); } </style>
<script> if ("paintWorklet" in CSS) { CSS.paintWorklet.addModule("paintworklet.js"); const wave = document.querySelector("#wave"); let tick = 0; requestAnimationFrame(function raf(now) { tick += 1; wave.style.cssText = `--animation-tick: ${tick};`; requestAnimationFrame(raf); }); } </script>
複製代碼
// paintworklet.js
registerPaint('wave', class {
static get inputProperties() {
return ['--animation-tick'];
}
paint(ctx, geom, properties) {
let tick = Number(properties.get('--animation-tick'));
const {
width,
height
} = geom;
const initY = height * 0.4;
tick = tick * 2;
ctx.beginPath();
ctx.moveTo(0, initY + Math.sin(tick / 20) * 10);
for (let i = 1; i <= width; i++) {
ctx.lineTo(i, initY + Math.sin((i + tick) / 20) * 10);
}
ctx.lineTo(width, height);
ctx.lineTo(0, height);
ctx.lineTo(0, initY + Math.sin(tick / 20) * 10);
ctx.closePath();
ctx.fillStyle = 'rgba(255, 255, 255, 0.5)';
ctx.fill();
}
})
複製代碼
paintworklet 中,利用 sin 函數繪製波浪線,因爲 AnimationWorklets 尚處於實驗階段,開放較少,這裏咱們在 worklet 外部用 requestAnimationFrame API 來作動畫驅動,讓波浪紋動起來。完成後能看到下邊這樣的效果。
然而事實上這個效果略顯僵硬,sin 函數太過於規則了,現實中的波浪應該是不規則波動的,這種不規則主要體如今兩個方面:
1)波紋高度(Y)隨位置(X)變化而不規則變化
把圖按照 x-y 正交分解以後,咱們但願的不規則,能夠認爲是固定某一時刻,隨着 x 軸變化,波紋高度 y 呈現不規則變化;
2)固定某點(X 固定),波紋高度(Y)隨時間推動而不規則變化
動態過程須要考慮時間維度,咱們但願的不規則,還須要體如今時間的影響中,好比風吹過的前一秒和後一秒,同一個位置的波浪高度確定是不規則變化的。
提到不規則,有朋友可能想到了用 Math.random 方法,然而這裏的不規則並不適合用隨機數來實現,由於先後兩次取的隨機數是不連續的,而先後兩個點的波浪是連續的。這個不難理解,你見過長成鋸齒狀的波浪嗎?又或者你見過上一刻 10 米高、下一刻就掉到 2 米的波浪嗎?
爲了實現這種連續不規則的特徵,咱們棄用 sin 函數,引入了一個包 simplex-noise。因爲影響波高的有兩個維度,位置 X 和時間 T,這裏須要用到 noise2D 方法,它提早在一個三維的空間中,構建了一個連續的不規則曲面:
// paintworklet.js
import SimplexNoise from 'simplex-noise';
const sim = new SimplexNoise(() => 1);
registerPaint('wave', class {
static get inputProperties() {
return ['--animation-tick'];
}
paint(ctx, geom, properties) {
const tick = Number(properties.get('--animation-tick'));
this.drawWave(ctx, geom, 'rgba(255, 255, 255, 0.4)', 0.004, tick, 15, 0.4);
this.drawWave(ctx, geom, 'rgba(255, 255, 255, 0.5)', 0.006, tick, 12, 0.4);
}
/** * 繪製波紋 */
drawWave(ctx, geom, fillColor, ratio, tick, amp, ih) {
const {
width,
height
} = geom;
const initY = height * ih;
const speedT = tick * ratio;
ctx.beginPath();
for (let x = 0, speedX = 0; x <= width; x++) {
speedX += ratio * 1;
var y = initY + sim.noise2D(speedX, speedT) * amp;
ctx[x === 0 ? 'moveTo' : 'lineTo'](x, y);
}
ctx.lineTo(width, height);
ctx.lineTo(0, height);
ctx.lineTo(0, initY + sim.noise2D(0, speedT) * amp);
ctx.closePath();
ctx.fillStyle = fillColor;
ctx.fill();
}
})
複製代碼
修改峯值和偏置項等參數,能夠再畫多一個不同的波浪紋,效果以下,完工!
CSS Painting API Level 1
CSS Layout API Level 1
CSS 魔術師 Houdini API 介紹
若是你以爲這篇內容對你有價值,歡迎點贊並關注咱們前端團隊的 官網 和咱們的微信公衆號 WecTeam,每週都有優質文章推送~