CSS
,全稱爲Cascading Style Sheets
,用於定製文檔樣式;CSS
使得網頁的呈現更加豐富,這也是初學前端的人最感到新奇的地方;css
可是隨着對CSS
的深刻使用纔會發現:那些被咱們津津樂道的「xx
屬性的奇淫技巧」這類東西都只不過是浮在CSS
最表層的現象而已;這種對於CSS
的使用方式在我看來無異於「盲人摸象」,即只能經過觀察表面現象來總結使用方法,而不是從本質出發尋找解決方案,所以就可能會陷入永遠只能藉助表面現象來解決問題的困境。前端
在初步瞭解過瀏覽器關於網頁渲染的機制和原理後,內心有個疑問變得更加突出了,那就是——「CSS
在網頁渲染中的定位是什麼」;我結合了webGL
這種單純的圖形渲染API
中的圖形渲染流程和CSS
代碼在瀏覽器中解析後在網頁渲染中的做用,得出了一個本身的結論:git
CSS是一個用於結構化描述渲染信息的輔助性DSL。github
得出這麼一個結論主要是基於如下理由:web
CSS
代碼能夠解析成CSSOM
,而後依附於DOM
上,這個得益於CSS
語法自己就是鍵值對式的對象描述這一特性;CSS
代碼並不能繪製出任何有效的圖形,它必須結合HTML
解析獲得的佈局、位置等結點信息才能進行繪製;即CSS
代碼在渲染過程當中並不充當着骨架的做用,更多地是基於骨架賦予更多樣的繪製;CSS
語言自己就不是一門通用性編程語言,它僅僅是針對網頁文檔的渲染而已,其做用範圍與GLSL/HLSL
這類着色器編程語言相比簡直就是專注得不能再專注了。既然CSS
只是輔助渲染的,那麼CSS
所攜帶的樣式信息又是如何轉化成底層的繪製語句呢?編程
這裏就須要涉及到更詳細的瀏覽器渲染管線流程了,由於光從上圖1這種大概的pipeline
,咱們根本沒法理解CSS
這種字符串信息是如何在瀏覽器內部進行解析而後轉換成具體的底層繪製命令的。不過好在Google
內部發表了一個極爲詳細的演講稿來闡述網頁中的像素是在經歷了一個怎樣的pipeline
以後才顯示的:Life of a Pixel(這篇演講稿固然是極力推薦閱讀的);多虧了這個演講稿,我終於不用去從Chromium
項目源碼中去一點點查找CSS
渲染的蛛絲馬跡了……canvas
上圖是我根據上述演講PPT
和本身的理解所總結的一個渲染pipeline
,看似很完整,可是實際上並非瀏覽器全部的渲染pipeline
,這僅僅是一個初步的流程,後續的優化渲染流程還沒涉及;因爲後面的優化渲染pipeline
和更新渲染pipeline
比較複雜,之後再單獨研究,這裏總結的流程權當是一個簡單的全量渲染pipeline
;瀏覽器
若是真要模擬上述流程圖中全部pipeline
,即HTMl + CSS → 像素
,那真是一個巨大的工程,實在是有心無力;我最感興趣的部分其實是光柵化的部分,即Paint Operator → 像素
,所以基於webGL
對這個光柵化作了一個最簡化模型:markdown
能夠看到這個渲染pipeline
是十足的簡單,IMGUI
+ 全量式繪製;由於我只想驗證所謂的Paint Operator
攜帶的繪製信息究竟是如何傳入到真正的底層——即着色器內的,我想知道着色器內部是如何消化和理解Paint Operator
的;因此上面這個模型只是我我的根據Life of a Pixel
一文所想到的一種底層交互模型;dom
import { PaintOperator } from '@/types/css-gl'
import { vec2, vec4 } from 'gl-matrix'
(() => {
const { CSSGL } = WebGLEngine // WebGLEngine是本身手寫的一個webGL渲染庫
const ops: PaintOperator[] = new Array(50).fill(0).map((val, idx) => {
const randomPos: vec2 = [
Math.random() * window.innerWidth,
Math.random() * window.innerHeight
]
const randomSize = vec2.create()
const randomBg: vec4 = [
Math.random(),
Math.random(),
Math.random(),
1.0
]
vec2.random(randomSize, 200)
return {
id: idx,
shape: {
pos: randomPos,
size: randomSize
},
flags: {
background: randomBg
}
}
}) // 隨機構建50個PaintOperator對象
const gl = new CSSGL('test', ops) // 解析PaintOperator數據
gl.paint() // 執行繪製
})()
複製代碼
解析代碼就是上面這些,因爲底層代碼作了抽象,因此大部分代碼都是生成PaintOperator
對象的;能夠看下預設的着色器代碼:
precision highp float; // 高精度
uniform vec2 u_Screen; // 屏幕尺寸
attribute vec2 a_Pos; // 頂點座標
vec2 widthRange = vec2(0.0, u_Screen.x);
vec2 heightRange = vec2(0.0, u_Screen.y);
vec2 outputRange = vec2(-1.0, 1.0); // NDC座標範圍爲[-1, 1]
// 將一個值從原來的範圍等比映射到另外一個範圍
float rangeMap (float source, vec2 sourceRange, vec2 targetRange) {
float bais = source / (sourceRange.y - sourceRange.x); // 在範圍長度中的佔比
float target = bais * (targetRange.y - targetRange.x) + targetRange.x;
return target;
}
void main() {
gl_Position = vec4(
rangeMap(a_Pos.x, widthRange, outputRange),
rangeMap(a_Pos.y, heightRange, outputRange) * -1.0, // 轉換成NDC時y軸須要翻轉
1.0,
1.0
);
}
複製代碼
precision highp float; // 高精度
uniform vec2 u_Screen; // 屏幕尺寸
uniform vec4 u_Background; // 背景色
void main() {
gl_FragColor = u_Background;
}
複製代碼
因爲模型自己很簡單,所以對應的着色器也很簡單;經過着色器代碼不難看出,我所理解的底層着色器接收Paint Operator
信息就是經過內置的屬性來一一對應,是最原始的方式;
上面這個模擬思路其實很簡單,因此我也很想去驗證這種思路跟具體的Chrome/Chromium
底層繪製有啥不一樣(單純的感興趣);因爲Chromium
內部幾乎全部的圖形繪製都交給了Skia
圖形庫,因此只能去Skia
源碼去查找蛛絲馬跡了;
不過看了一圈Skia項目源碼以後,我發現Skia
項目實在是過高度抽象了,層層嵌套,從着色器代碼裏面壓根找不出蛛絲馬跡,由於着色器代碼裏面的信息看起來都是很抽象/通用的數據,沒法直接聯繫到圖元繪製;看來須要較長的時間才能找出我想要的答案,雖然有點遺憾,可是也從Skia
自己發現了一些不錯的地方:
Skia
內部設計了一個着色器語言,名爲SkSL (Skia Shading Language)
;SkSL
實際上就是基於某一固定版本的GLSL
語法進行設計的,其做用應該是抹去不一樣GPU
驅動API
着色器語法的差別,以便對於不一樣的GPU
驅動能夠進一步輸出爲目標着色器語言,所以SkSL
能夠看作是着色器預編譯語言2;
Skia
的API
風格也頗有意思,與Canvas API
很類似,看一下官網的Demo
就知道了:
void draw(SkCanvas* canvas) {
canvas->drawColor(SK_ColorWHITE);
SkPaint paint;
paint.setStyle(SkPaint::kFill_Style);
paint.setAntiAlias(true);
paint.setStrokeWidth(4);
paint.setColor(0xff4285F4);
SkRect rect = SkRect::MakeXYWH(10, 10, 100, 160);
canvas->drawRect(rect, paint);
SkRRect oval;
oval.setOval(rect);
oval.offset(40, 80);
paint.setColor(0xffDB4437);
canvas->drawRRect(oval, paint);
paint.setColor(0xff0F9D58);
canvas->drawCircle(180, 50, 25, paint);
rect.offset(80, 50);
paint.setColor(0xffF4B400);
paint.setStyle(SkPaint::kStroke_Style);
canvas->drawRoundRect(rect, 10, 10, paint);
}
複製代碼
熟悉的命令式以及圖元繪製命名;
Skia
中有三大基類:SkCanvas
、SkBitmap
和SkPaint
3;
SkCanvas
:管理繪製相關的API;SkBitmap
:管理bit數據;SkPaint
:管理圖元繪製風格相關的狀態;Blink
渲染引擎的超強概述