前言CSS,全称CascadingStyleSheets,用于自定义文档样式;CSS使网页的表现形式更加丰富,这对初学者来说是最新鲜的;但随着对CSS的深入使用,我们才发现:那些我们喜闻乐见的“xx属性奇葩”,不过是漂浮在CSS顶层的现象;这种CSS的使用方式无异于“盲人摸象”,即只能通过观察表面现象来总结使用方法,而不能从本质上寻找解决方案,这样你可能会陷入总是只靠表面现象解决问题的困境。关于CSS的定位在初步了解了浏览器对网页渲染的机制和原理之后,心中的一个疑问更加突出了,那就是——“CSS在网页渲染中的定位是什么”;在分析了一个简单的图形渲染API中的图形渲染过程,以及CSS代码在浏览器中解析后在网页渲染中的作用,得出了自己的结论:CSS是一种辅助DSL,用于结构化描述渲染信息。这样的结论主要基于以下几个原因:结构描述:CSS代码可以解析成CSSOM,然后附加到DOM,得益于CSS语法本身就是键的对象描述——值对这个特性;accessibility:单靠CSS代码无法绘制任何有效的图形,必须结合HTML解析得到的布局、位置等节点信息进行绘制;即渲染过程中的CSS代码不作为骨架,更多的是基于骨架提供更多样的绘制;DSL:这个不用多说,CSS语言本身并不是一种通用的编程语言,它只是为了网页文档的渲染,其作用范围比着色器编程语言具体得多,比如GLSL/HLSL。CSS与绘图既然CSS只是辅助渲染,那么CSS携带的样式信息是如何转化为底层的绘图语句的呢?这里需要涉及到更详细的浏览器渲染管线过程,因为仅仅从上面图1的通用管线来看,我们根本无法理解CSS的字符串信息在浏览器内部是如何被解析然后解析的转化为具体的底层绘图命令。幸运的是,谷歌发表了一篇极其详细的演讲,解释了网页中的像素是通过什么样的管道显示出来的:LifeofaPixel(这篇演讲当然是强烈推荐读书);多亏了这次演讲,我终于不用再从Chromium项目的源码中寻找CSS渲染的蛛丝马迹了……上图是根据上述演讲PPT和自己的理解整理而成。一个渲染管线,看似很完整,但实际上并不是浏览器的所有渲染管线。这只是一个初步的过程,还没有涉及到后续优化的渲染过程;由于优化后的渲染管线和更新后的渲染管线比较复杂,后面会单独研究。这里总结的过程应该是一个简单的全渲染管线;模拟CSS渲染,如果真要模拟上面流程图中的所有流水线,即HTMl+CSS→Pixel,那真是工程浩大,实在是力不从心;我最感兴趣的部分其实是光栅化部分,也就是PaintOperator→Pixel,所以我做了一个基于webGL的这个光栅化的简化模型:是的看到这个渲染管线很简单,IMGUI+全尺寸绘图;因为我只是想验证一下所谓的PaintOperator携带的绘图信息是如何传递到真正的底层——也就是shader内部的,我想知道关于着色器内部的PaintOperator是如何消化理解的;所以上面的模型只是我个人根据LifeofaPixel一文想到的底层交互模型;模拟代码import{PaintOperator}from'@/types/css-gl'import{vec2,vec4}from'gl-matrix'(()=>{const{CSSGL}=WebGLEngine//WebGLEngine是一个手写的webGL渲染库constops:PaintOperator[]=newArray(50).fill(0).map((val,idx)=>{constrandomPos:vec2=[Math.random()*window.innerWidth,Math.random()*window.innerHeight]constrandomSize=vec2.create()constrandomBg: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对象constgl=newCSSGL('test',ops)//解析PaintOperator数据gl.paint()//执行绘制})()解析代码就是上面的,因为底层代码是抽象的,所以大部分代码都是生成PaintOperator对象;你可以看到预设着色器代码:precisionhighpfloat;//高精度uniformvec2u_Screen;//屏幕尺寸属性vec2a_Pos;//顶点坐标vec2widthRange=vec2(0.0,u_Screen.x);vec2heightRange=vec2(0.0,u_Screen.y);vec2outputRange=vec2(-1.0,1.0);//NDC坐标范围是[-1,1]//将一个值从原始范围映射到另一个范围floatrangeMap(floatsource,vec2sourceRange,vec2targetRange){floatbais=source/(sourceRange.y-sourceRange.x);//在范围长度中的比例floattarget=bais*(targetRange.y-targetRange.x)+targetRange.x;returntarget;}voidmain(){gl_Position=vec4(rangeMap(a_Pos.x,widthRange,outputRange),rangeMap(a_Pos.y,heightRange,outputRange)*-1.0,//转换为NDC时,y轴需要被翻转1.0,1.0);}precisionhighpfloat;//高精度uniformvec2u_Screen;//屏幕尺寸统一vec4u_Background;//背景色voidmain(){gl_FragColor=u_Background;}由于模型本身很简单,对应的shader也很简单;通过shader代码不难看出,我理解的底层shader通过内置属性接收PaintOperator信息,一一对应,这是最原始的方式;上面Skia的模拟思路其实很简单,所以我也想验证一下这个思路和具体的Chrome/Chromium底层绘制有什么区别(纯兴趣);因为Chromium里面几乎所有的图形绘制都交给了Skia图形库,所以只能去Skia源码中寻找线索;但是翻看了一下Skia项目源码,发现Skia项目抽象度太高,层层嵌套,从shader代码中完全找不到任何蛛丝马迹,因为shader代码中的信息似乎是非常抽象/通用的数据,不能与原始绘图直接相关;似乎要花很长时间才能找到我想要的答案,虽然有点遗憾,但我也从Skia本身发现了一些好的地方:Skia内部设计了一种名为SkSL(SkiaShadingLanguage)的着色器语言;SkSL其实是基于固定版本的GLSL语法设计的,它的作用应该是抹掉不同GPU驱动的APIshader语法差异,让不同的GPU驱动可以进一步输出为目标shader语言,所以SkSL可以看作shader预编译语言2;Skia的API风格也很有意思,跟Canvas的API很像,直接看官网的demo:voiddraw(SkCanvas*canvas){画布->drawColor(SK_ColorWHITE);SkPaint油漆;paint.setStyle(SkPaint::kFill_Style);paint.setAntiAlias(真);paint.setStrokeWidth(4);paint.setColor(0xff4285F4);SkRectrect=SkRect::MakeXYWH(10,10,100,160);canvas->drawRect(rect,paint);SkRRect椭圆形;椭圆.setOval(矩形);椭圆形偏移量(40、80);paint.setColor(0xffDB4437);canvas->drawRRect(椭圆形,油漆);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和SkPaint3;SkCanvas:管理绘图相关API;SkBitmap:管理位数据;SkPaint:管理图元绘制风格相关的状态;相关文档HowBlinkworks-GoogleDocuments:关于Blink渲染引擎的超级概述WebIDL介绍|Mio的博客https://www.chromium。org/blink再见,CSS着色器(CSS自定义过滤器)|花婚达の室屋:无意中发现了一个废弃的规范W3CCSS规范主页https://www.chromium.org/deve...:Chromium项目SkiaVulkanPerformance-KnowShaders-LearnOpenGL-CNhttps://docs.google.com/docum...?https://github.com/google/ski...?https://www.chromium.org/deve...?