探索 CSS 的本質

前言

CSS,全稱爲Cascading Style Sheets,用於定製文檔樣式;CSS使得網頁的呈現更加豐富,這也是初學前端的人最感到新奇的地方;css

可是隨着對CSS的深刻使用纔會發現:那些被咱們津津樂道的「xx屬性的奇淫技巧」這類東西都只不過是浮在CSS最表層的現象而已;這種對於CSS的使用方式在我看來無異於「盲人摸象」,即只能經過觀察表面現象來總結使用方法,而不是從本質出發尋找解決方案,所以就可能會陷入永遠只能藉助表面現象來解決問題的困境。前端

關於CSS的定位

初步瞭解過瀏覽器關於網頁渲染的機制和原理後,內心有個疑問變得更加突出了,那就是——「CSS在網頁渲染中的定位是什麼」;我結合了webGL這種單純的圖形渲染API中的圖形渲染流程和CSS代碼在瀏覽器中解析後在網頁渲染中的做用,得出了一個本身的結論:git

CSS是一個用於結構化描述渲染信息的輔助性DSL。github

得出這麼一個結論主要是基於如下理由:web

  • 結構化描述CSS代碼能夠解析成CSSOM,而後依附於DOM上,這個得益於CSS語法自己就是鍵值對式的對象描述這一特性;
  • 輔助性:單獨的CSS代碼並不能繪製出任何有效的圖形,它必須結合HTML解析獲得的佈局、位置等結點信息才能進行繪製;即CSS代碼在渲染過程當中並不充當着骨架的做用,更多地是基於骨架賦予更多樣的繪製;
  • DSL:這個就無需多說了,CSS語言自己就不是一門通用性編程語言,它僅僅是針對網頁文檔的渲染而已,其做用範圍與GLSL/HLSL這類着色器編程語言相比簡直就是專注得不能再專注了。

CSS與繪製

既然CSS只是輔助渲染的,那麼CSS所攜帶的樣式信息又是如何轉化成底層的繪製語句呢?編程

這裏就須要涉及到更詳細的瀏覽器渲染管線流程了,由於光從上圖1這種大概的pipeline,咱們根本沒法理解CSS這種字符串信息是如何在瀏覽器內部進行解析而後轉換成具體的底層繪製命令的。不過好在Google內部發表了一個極爲詳細的演講稿來闡述網頁中的像素是在經歷了一個怎樣的pipeline以後才顯示的:Life of a Pixel(這篇演講稿固然是極力推薦閱讀的);多虧了這個演講稿,我終於不用去從Chromium項目源碼中去一點點查找CSS渲染的蛛絲馬跡了……canvas

上圖是我根據上述演講PPT和本身的理解所總結的一個渲染pipeline,看似很完整,可是實際上並非瀏覽器全部的渲染pipeline,這僅僅是一個初步的流程,後續的優化渲染流程還沒涉及;因爲後面的優化渲染pipeline和更新渲染pipeline比較複雜,之後再單獨研究,這裏總結的流程權當是一個簡單的全量渲染pipeline瀏覽器

模擬CSS渲染

若是真要模擬上述流程圖中全部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信息就是經過內置的屬性來一一對應,是最原始的方式;

關於Skia

上面這個模擬思路其實很簡單,因此我也很想去驗證這種思路跟具體的Chrome/Chromium底層繪製有啥不一樣(單純的感興趣);因爲Chromium內部幾乎全部的圖形繪製都交給了Skia圖形庫,因此只能去Skia源碼去查找蛛絲馬跡了;

不過看了一圈Skia項目源碼以後,我發現Skia項目實在是過高度抽象了,層層嵌套,從着色器代碼裏面壓根找不出蛛絲馬跡,由於着色器代碼裏面的信息看起來都是很抽象/通用的數據,沒法直接聯繫到圖元繪製;看來須要較長的時間才能找出我想要的答案,雖然有點遺憾,可是也從Skia自己發現了一些不錯的地方:

  • Skia內部設計了一個着色器語言,名爲SkSL (Skia Shading Language)SkSL實際上就是基於某一固定版本的GLSL語法進行設計的,其做用應該是抹去不一樣GPU驅動API着色器語法的差別,以便對於不一樣的GPU驅動能夠進一步輸出爲目標着色器語言,所以SkSL能夠看作是着色器預編譯語言2

  • SkiaAPI風格也頗有意思,與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中有三大基類:SkCanvasSkBitmapSkPaint3

    • SkCanvas:管理繪製相關的API;
    • SkBitmap:管理bit數據;
    • SkPaint:管理圖元繪製風格相關的狀態;

相關文檔

相關文章
相關標籤/搜索