概要
本文通过解决一个假想的问题介绍了 css screen 混合模式,并介绍了如何用 svg 滤镜、canvas 2d、canvas webgl 实现相同的效果。
问题
下面的图片演示三种颜色光叠加的效果,请在 html 中实现这种效果。
约定
词语 | 指代 |
---|---|
混合 | blend |
加色 | additive color - 名词 |
特性 | attribute,比如 ,说 id 是元素 a 的特性 |
透明度 | α、alpha |
伪输入图像 | pseudo input image |
着色器 | shader |
着色器程序 | shader program |
xml 应用程序 | XML application |
chrome | google chrome 41 |
firefox | firefox developer edition 40 |
ie | internet explorer 11 |
3 个浏览器 | 上面 3 个版本的浏览器 |
opera 已经基于 webkit 了,所以未测试 opera,若在 chrome 中可用那我就认为在 opera 中也可用。
分析
当然可以用 photoshop 制作图片,html 用
引用该图片,本文不讨论这种方法。
观察重叠部分发现该部分的颜色不仅受自己的影响、还受它下面背景颜色的影响,重叠部分的颜色是自己的颜色和背景颜色混合的结果。换句话说,一个像素绘制出来的颜色等于像素颜色和背景像素颜色的混合,即
这里面 rgb(1, 0, 0)
、rgb(0, 1, 0)
、rgb(0, 0, 1)
,
重点:不同的
B 得到不同的C
不可行的方法
html 中经常用到下面 3 个方法,
css opacity 属性 css rgba()/hsla() 颜色 用![在 html 中用加色法混合颜色 在 html 中用加色法混合颜色]()
引用带 alpha 通道的图像
它们使用相同的混合函数,叫做 α 混合或简单 α 复合,
简单 α 复合 - http://dev.w3.org/fxtf/compositing/#simplealphacompositing
opacity - http://stackoverflow.com/questions/8743482/calculating-opacity-value-mathematically
下面给上面的式子代入几组实际值。设 rgba(1, 0, 0, 1)
,rgb(0, 0, 1)
,它俩混合的结果不用计算都知道仍然是不透明红,计算过程如下,
r = 1 x 1 + 0 x (1 - 1) = 1
g = 0 x 1 + 0 x (1 - 1) = 0
b = 0 x 1 + 1 x (1 - 1) = 0
红蓝得红,混合失败。另外一组,rgba(1, 0, 0, 0.5)
,rgb(0, 0, 1)
,有,
r = 1 x 0.5 + 0 x (1 - 0.5) = 0.5
g = 0 x 0.5 + 0 x (1 - 0.5) = 0
b = 0 x 0.5 + 1 x (1 - 0.5) = 0.5
要想让得到的 rgb(0.5, 0, 0.5)
和 rgb(0, 1, 0)
的绿色混合以得到 rgb(1, 1, 1)
的白色,α 需要满足下面的方程组,
上面的方程组无解,即无论如何设置 α 都无法通过 rgb(0.5, 0, 0.5)
和 rgb(0, 1, 0)
得到 rgb(1, 1, 1)
。
回过头来观察式子
可行的方法
如果可以自己逐一计算像素的颜色,得出要求的效果自然不在话下。除了自己计算外,如果存在正好能够实现要求效果的固定函数,则调用该函数也可以。
在 html 中处理颜色有 3 种工具,css、svg、canvas。
css
css 有个模块叫复合与混合,这个模块定义了若干固定函数,其中一个叫 screen,它的
css 复合与混合 - http://dev.w3.org/fxtf/compositing/
假设 add 是
通过指定 html 元素的 css 属性 mix-blend-mode: screen
来让元素和其背后的元素以 screen 方式混合。css 目前没办法逐像素计算目标区域的颜色。
html 中的 svg
本文把 svg 写在 html 内 。svg 是 xml 应用程序,遵循 xml 语法,但是放在 html 中又可以采用部分 html 语法。如果大家按照 xml svg 的知识去看本文的代码可能会有疑问,所以在写 svg 之前先说一下 html 中的 svg。
html 不支持名字空间,忽略
里面由特性定义的名字空间,所以本文的 svg 没有 xmlns=http://www.w3.org/2000/svg
或者 xmlns:xlink=http://www.w3.org/1999/xlink
,xlink:href
在 html 中是个普通的特性名,冒号和名字空间无关 没有歧义时可以省略特性值周围的引号 xml 中没有内容的元素比如
也可以写做
,叫做自闭合;html 不存在自闭合,但内嵌的 svg 元素可以使用自闭合
html 中的 svg 元素可以自闭合 - http://www.w3.org/TR/html-markup/syntax.html#svg-mathml
所有没有内容的 xml 元素都叫 empty 元素,可以自闭合;html 不存在 empty 元素,但是定义了一些 void 元素,void 元素不能有内容,只有开始标记没有结束标记。
所有 void 元素是,area, base, br, col, command, embed, hr, img, input, keygen, link, meta, param, source, track, wbr
http://www.w3.org/TR/html-markup/syntax.html#syntax-elements所有 html 元素开始标记的 > 前面可以写一个 /,和不写 / 一样。
解释为
而不是
,
是 void 元素,所以没问题;解释为
,
不是 void 元素,所以可能会出问题,
red = html, green = xhtml
有些元素可以省略结束标记,但不是 void 元素,比如
- ;有些元素有时候没有内容,但既不是 void 元素也不能省略结束标记,比如
<script src=xxx>
;有些元素可以省略开始标记。
http://www.w3.org/TR/html5/syntax.html#optional-tags浏览器从网站获取的文件 mimetype = text/html 导致调用 html 解析器。
另外,svg 很多要素都没有浏览器支持;当支持的时候,可能各个浏览器有差异。
有了这些知识下面看 svg。
svg
svg 里面的元素也是 dom 元素,也可以应用 css 混合。css 混合在 css 部分讲述 svg 有个规范定义了复合,和 css 混合效果差不多,关键字comp-op
。我不知道有哪个浏览器支持该规范
svg 滤镜
和
svg 滤镜 - http://www.w3.org/tr/svg11/filters.html
svg 复合 - http://www.w3.org/TR/SVGCompositing/
按其 operator
特性指出的操作组合两个输入图像 operator=arithmetic
时需要另外的 4 个特性 k1
、k2
、k3
、k4
,默认值是 0,并按如下方式分别计算结果像素的 3 个通道,我不清楚它如何处理 α 通道,
既然知道 mix-blend-mode: screen
的混合函数
所以
可以实现效果。
支持 screen 混合模式,
,所以应该也能实现效果。
canvas
canvas 分为 2d 和 webgl,它里面的形状都是画上去的,由像素组成,不是 dom 元素,无法应用 css 混合;但是 canvas 2d 有个全局复合操作,和 css 混合是同一个概念在两种不同语言中的实现,支持 css 混合的所有固定函数。当然自己计算像素也行。
全局复合操作 - http://dev.w3.org/fxtf/compositing/#canvascompositingandblending
webgl 没有与 css、canvas 2d 完全相同的混合概念,但也有自己的混合函数,解决本文提出的问题不在话下。webgl 有个特点是无论你干什么都需要写着色器代码、写调用编译着色器的函数的代码、写调用连接着色器的函数的代码。
如何运行示例代码
下面是框架代码,后面给出的示例代码需要放在框架代码的 里
additive color
依次执行下面 3 个步骤,缺一不可,
新建一个空 html 文件 拷贝框架代码,粘贴到空的 html 文件 确保 html 文件编码为 utf8,保存示例 - css mix-blend-mode
ie 不认识 mix-blend-mode
和 isolation
mix-blend-mode - http://dev.w3.org/fxtf/compositing/#mix-blend-mode
isolation - http://dev.w3.org/fxtf/compositing/#isolation
元素的 mix-blend-mode
属性是说,我知道你的颜色,但是在显示的时候不要只显示你的颜色,而是要显示你的颜色和你背景颜色混合后的颜色,至于如何混合,我会通过 mix-blend-mode
属性的值指出。
因此设计一个容器 p position: relative
,里面有红绿蓝三个方块 p position: absolute
,三个方块之间有重叠部分,通过 mix-blend-mode: screen
指出重叠部分颜色的计算方法。
mix-blend-mode: screen 不是颜色分量相加
上面的代码实现了刚才的设计,并且额外设置了容器 p 的一个属性 isolation: isolate
。元素的 isolation: isolate
是说,我的子元素不会和我外面的元素混合。isolation
属性另外一个可能的取值兼默认值是 auto
,没有限制、随便混和。
容器 p 放在 html 的 里,
默认的颜色是不透明白,假设没有通过
isolation: isolate
限定容器元素的子元素不能与容器外的元素混合,红绿蓝 3 个方块 p 就要和白色以 screen 模式混合。红色 rgb(1, 0, 0)
和白色 rgb(1, 1, 1)
以 screen 模式 rgb(1, 1, 1)
白色,
r = 1 + 1 - 1 x 1 = 1
g = 0 + 1 - 0 x 1 = 1
b = 0 + 1 - 0 x 1 = 1
绿蓝方块和白色混合也得到白色,结果就是一片白,不是要求的效果,所以设置容器的 isolation: isolate
。
示例 - svg
和
svg filter primitive 必须包含在
元素内
是 svg filter primitive
所以?
定义一个矩形滤镜区域,默认值是 x=-10%
、y=-10%
、width=120%
、height=120%
,x
和 y
的值相对于应用滤镜的元素,x=-10 y=10
以应用滤镜的元素为准向左 10 向下 10。x
、y
、width
、height
的数值的解释由另外一个特性 filterUnits 决定,
filterUnits
是默认值 objectBoundingBox
x=10
是说 x 是应用滤镜的元素的宽度的 10 倍
x=100%
是说 x 是应用滤镜的元素的宽度的 1 倍
如果 filterUnits=userSpaceOnUse
x=10
是说 x 是 10 个用户单位,用户单位具体是啥要看包含这个元素的 svg 的宽度或高度用的单位,默认 px
x=100%
含义不变
filterUnits - http://www.w3.org/TR/SVG11/filters.html#FilterEffectsRegion
这个连接指向 15.5 Filter effects region,因为指向 filterUnits 的连接打开后其内容只是一个指向 FilterEffectsRegion 的连接
如果 svg 宽度单位是 px,高度单位是 cm,用户单位是啥?
这个我也不清楚。难道 x 对应 svg 的 width,y 对应 svg 的 height?
接受两个图像 i1
和 i2
,
逐一扫描 i1
和 i2
的像素,用
对图像 i1
和 i2
应用 screen 混合模式。
in
的默认值是
中上一个 filter primitive 的结果;如果自己是第一个,则默认 SourceGraphic
。in=SourceGraphic in2=BackgroundImage
分别使用当前图片和当前图片的背景图片。
in - http://www.w3.org/TR/SVG11/filters.html#FilterPrimitiveInAttribute
同样是图片,为啥一个叫 source graphic,一个叫 background image?
http://www.w3.org/TR/SVG11/filters.html#AccessingBackgroundImage
简而言之出于性能考虑伪输入图像
BackgroundImage
是背景的一个快照,要使用BackgroundImage
作in
或in2
的参数必须指定容器元素的enable-background=new
以要求容器存储背景快照供滤镜使用。enable-background
默认值是accumulate
,不存,也无法使用BackgroundImage
。两个不同的单词可能是要强调输入图像和背景图像的这种差异。当然也可能是我想多了,人家就是喜欢出其不意,如之奈何?
混合 3 个形状需要应用两次滤镜,出于演示的目的正好一个用
一个用
。
BackgroundImage
,把这 3 个形状放到一个组里面,并设置组的 enable-background=new
放置第 1 个形状
把形状 2 放到和形状 1 部分重叠的位置,此时形状 1 可以视为形状 2 的背景,对形状 2 应用滤镜
,重叠的部分就会经过
。
svg,仅限 ie 10+
没有指出
的 x
、y
、width
、height
所以它们都取默认值。
只有 ie 10+ 支持上面的代码。ie 此刻又迸射出耀眼的光芒,
别的浏览器玩儿蛋去吧!
这是我心里想象的 ie 工作人员心里的想象,请不要以为他们一定是那样想的。
Appendix A: The deprecated enable-background property
http://dev.w3.org/fxtf/filters/#AccessBackgroundImagesvg 最近的风向是不赞成
enable-background
了,enable-background=new
要换成isolation=isolate
以“兼容 css 复合与混合”。大家留意一下,isolation
是 css 属性,在样式表里面指定;svg 发明了个presentation attribute
,这种特性也可以在样式表中以 css 属性的形式指定以兼容 css,而 svg 这个isolation
不是所谓的presentation attribute
,不能在样式表里指定。这还兼容个毛?svg 就是这样,当你拿它和 html 比的时候,一眼看上去都差不多,似乎能很容易混用,实际上有很多出其不意的不一样,烦得要死。无论如何还是要换一下试试。换成
isolation=isolate
后连 ie 都没法读取背景图片了,前面的 css 部分说过 ie 不认识样式表中的isolation
,现在看来 ie 也不认识isolation
特性,3 个浏览器没有能运行的。http://dev.w3.org/fxtf/filters/ 里面的示例 Example of feComposite 就是从 http://www.w3.org/TR/SVG11/filters.html 拷贝的同名示例,只是把enable-background=new
换成了isolation=isolate
,但是紧跟其后的连接 View this example as SVG 指向的 svg 文件里面用的仍然是enable-background=new
。这么看来 ie 是被坑了,但从大的方面看 svg 本身就很坑,废弃
enable-background
一点都不亏。svg 很好玩,但是能别用 xml 语法吗?
一般只要 chrome 和 firefox 能用,ie 我常常忽略,前面 css 的示例代码就没管 ie。现在这段代码,由于只有 ie 10+ 支持伪输入图像 BackgroundImage
,chrome 和 firefox 上都运行不了,不能说是达到了要求的效果,最好有在 3 个浏览器上都能运行的 svg 例子。所幸对于这个简单的问题,TIMTOWTDI!下面凑一个能在 3 个浏览器上运行的 svg 解法,说凑是因为它没有把 3 个元素两两混合,而是在滤镜里生成了两个方块,和应用滤镜的那个方块元素混合。在滤镜里生成方块指的是把滤镜矩形设置为单一的颜色然后临时保存,这要用到
,
说的是当把图像传递给
时,对图像的每一个像素左乘下面的矩阵以得到新的像素,
a00 a01 a02 a03 a04
a10 a11 a12 a13 a14
a20 a21 a22 a23 a24
a30 a31 a32 a33 a34
0 0 0 0 1 - 最后一行总是它,不写在 values 里
即新像素 (r', g', b', a')
等于
给出的矩阵乘以原像素 (r, g, b, a)
,
把 (r, g, b, a, 1)
变成 (1, 0, 0, a, 1)
,这是红色。因此可以写下面的代码,
对蓝方块应用滤镜
滤镜从蓝方块生成一个红方块和一个绿方块,偏移,混合
上一步的结果和蓝方块混合,蓝方块在滤镜中通过伪输入图像 SourceGraphic
引用
svg feBlend
也可以做红绿蓝 3 张图片,在滤镜里用
引用,代码类似下面,记得先做 3 张 100px * 100px 的红绿蓝图片放到 html 的同一目录。
总结
svg 滤镜的思路就是
和
,前者自己计算像素,后者调用固定函数。由于 chrome 和 firefox 不支持在滤镜中读取背景图像所以给了两段绕弯的代码,第 2 段代码还依赖 3 张图片。
示例 - canvas 2d
方法 1. CanvasRenderingContext2D.prototype.globalCompositeOperation
绘制 红 色方块 全局复合模式 = screen 绘制 绿 色方块 绘制 蓝 色方块globalCompositeOperation 是 HTML Canvas 2D Context 规范定义在接口 CanvasRenderingContext2D 上的一个特性。chrome 里面访问不到 CanvasRenderingContext2D.prototype.globalCompositeOperation,firefox 和 ie 里面存在该 js 属性,定义了 get 和 set 访问函数。
不能直接在代码里使用
CanvasRenderingContext2D.prototype.globalCompositeOperation
,因为这句代码会调用get
函数,而get
需要通过this
访问实际的画布上下文对象。当然正常情况下也不会那么写,正常情况是先获取某个画布的 2d 上下文,然后访问上下文的属性,var t = theCanvas.getContext(2d); console.log(t.globalCompositeOperation); // 默认 source-over
有了上面的
t
,可以写
Object.getOwnPropertyDescriptor(CanvasRenderingContext2D.prototype, globalCompositeOperation).get.call(t);
interface CanvasRenderingContext2D
http://www.w3.org/TR/2dcontext/#canvasrenderingcontext2d
canvas 2d 全局复合
<script> !function () { var canvas = document.querySelector(.s3-1), cc = canvas.getContext(2d); cc.fillStyle = red; cc.fillRect(50, 20, 100, 100); cc.globalCompositeOperation = screen; cc.fillStyle = lime; cc.fillRect(30, 40, 100, 100); cc.fillStyle = blue; cc.fillRect(70, 60, 100, 100); }(); </script>方法 2.
绘制 红 色方块 选定一个部分重叠的方块,使用CanvasRenderingContext2D.prototype.putImageData
CanvasRenderingContext2D.prototype.getImageData
把该部分像素读入内存,对每个像素,和 绿 色应用min(Cs+Cb,1) ,然后写入画布 选定一个部分重叠的方块,读取,每个像素和 蓝 色应用min(Cs+Cb,1) ,写入画布因为红绿蓝两两混合时每个分量必然一个是 0 一个是 1,所以并没有调用 Math.min 而是直接把分量置 1,或者说置 255。
在 canvas 中把颜色分量置 1,未使用 min(x + y, 1)
<script> !function () { var canvas = document.querySelector(.s3-2), cc = canvas.getContext(2d), i, len, idata, arr; cc.fillStyle = red; cc.fillRect(50, 20, 100, 100); for (i = 0, idata = cc.getImageData(30, 40, 100, 100), arr = idata.data, len = arr.length; i < len; i += 4) arr[i + 1] = arr[i + 3] = 255; cc.putImageData(idata, 30, 40); for (i = 0, idata = cc.getImageData(70, 60, 100, 100), arr = idata.data, len = arr.length; i < len; i += 4) arr[i + 2] = arr[i + 3] = 255; cc.putImageData(idata, 70, 60); }(); </script>
示例 - canvas webgl
webgl api - https://msdn.microsoft.com/en-us/library/dn621085(v=vs.85).aspx
webgl methods - https://msdn.microsoft.com/en-us/library/dn302341(v=vs.85).aspx
glBlendFunc + glBlendEquation 效果演示 - http://www.andersriggelsen.dk/glblendfunc.php本文假设你对 webgl 一无所知。
本节的目标是在阅读本节内容之后,对 webgl 一无所知的读者能掌握 webgl 的基本思路、写出基本的 webgl 程序。如果不是这个情况,请跟帖指出,我会修正本节内容直至达到前述目标。
绘制方块
设
var gl = theCanvas.getContext(webgl);
,webgl 通过下面两个函数之一进行绘制,gl.drawArrays(mode, first, count);
gl.drawElements(mode, count, type, offset);
这两个函数差不多,先解释
使用gl.drawArrays
。在调用gl.drawArrays
之前gl
必须满足下列条件,gl.useProgram
绑定了 1 个着色器程序 使用gl.bindBuffer
绑定了 1 个数组,数组里面有内容可用 使用gl.enableVertexAttribArray
启用了至少 1 个在顶点着色器里定义的特性 使用gl.vertexAttribPointer
描述了启用的特性上面 4 个条件就是发起 1 次
gl.drawArrays
调用所需的代码 + 数据,代码用opengl es
着色器语言glsl
写成,数据在javascript
代码里面提供,并调用 webgl 的 javascript api 建立js 数据
到glsl 代码
的联系。1 个 webgl 程序可以有很多着色器程序,每个着色器程序一定有 1 个顶点着色器和 1 个片段着色器。每绘制 1 个点,webgl 都依次调用顶点着色器和片段着色器。顶点着色器的唯一任务是给全局变量
gl_Position
赋值,它代表 1 个点的位置;片段着色器的唯一任务是给全局变量gl_FragColor
赋值,它代表刚才那个点的颜色。webgl 一次从数组读取由
gl.drawArrays
依次执行下列步骤,gl.vertexAttribPointer
的stride
参数指出的字节,这些字节视为 1 个顶点 把这么多字节按gl.vertexAttribPointer
指出的方式拆分后分别赋值给顶点着色器里面用attribute
定义的变量,调用了几次gl.vertexAttribPointer
就要给几个变量赋值 进入顶点着色器的main
函数,main
里面一般会使用刚才赋值过的特性 顶点着色器的main
结束,进入片段着色器的main
函数 片段着色器的main
结束,1 个顶点渲染完毕,从数组读取下一个顶点 重复上述过程,直至处理了由count
参数指出的顶点数webgl 实际上读取的是从 javascript 数组拷贝到显卡上的数组
gl.vertexAttribPointer( index, - 特性在和 gl.ARRAY_BUFFER 绑定的缓冲区中的索引 size, - 1 | 2 | 3 | [4],每个特性有几个分量,比如 vec3 有 3 个分量 type, - gl.BYTE | gl.UNSIGNED_BYTE | gl.SHORT | gl.UNSIGNED_SHORT | [gl.FLOAT] normalized, - true,转化到 [-1.0, 1.0] stride, - [0, 255],默认 0,单位字节,必须是 type 的整数倍 offset - 默认 0,单位字节,必须是 type 的整数倍 )
读作:为了给顶点着色器里面定义的第
index
号特性赋值,从数组中取stride
个字节作为一个顶点,从这个顶点的第offset
个字节开始取size
个type
,每个type
依次对应特性的一个分量。假设在顶点着色器里定义了 2 个特性
attribute vec3 position; attribute vec2 resolution; void main() { gl_Position = ???; }
每次进入顶点着色器的时候都希望这俩变量被赋值,以便在顶点着色器的
main
里面使用它们。在 javascript 里面用一个Float32Array
保存顶点,数组形如[x0, y0, z0, w0, h0, x1, y1, z1, w1, h1, ...]
下面的调用
gl.vertexAttribPointer(idPosition, 3, gl.FLOAT, false, 5 * 4, 0); gl.vertexAttribPointer(idResolution, 2, gl.FLOAT, false, 5 * 4, 3 * 4); // 4 是 Float32Array 数组的元素 Float32 的字节数,对应 gl.FLOAT // 5 是说一个顶点有 5 个 Float32,5 * 4 是这个顶点的字节数 // 第 2 个调用里面的 3 是说 resolution 从每个顶点的第 3 个 Float32 开始 // // idPosition 和 idColor 是 gl.getAttribLocation 返回的一个整数, // 代表顶点着色器里面的特性 position 和 resolution。position 和 resolution // 是在顶点着色器里面定义的变量,不能直接在 javascript 里面用,需要通过 // gl.getAttribLocation 建立一个对应关系 // // 如果给 resolution 的 offset 参数传 0 则 resolution 和 position 重叠, // 这没有问题但是数值可能没有意义
让 webgl 这样取值
| 数组中每个顶点的长度是 5 * 4 = 20(stride)个字节 | | x0, y0, z0, w0, h0, x1, y1, z1, w1, h1, ... | | | | | | | 从数组 arr 的第 3 * 4 = 12(offset)个字节开始取 2(size)个 | | | gl.FLOAT(type)组成一个 vec2(arr[3], arr[4]),把这个 vec2 | | | 赋值给顶点着色器特性 resolution | | | 从数组 arr 的第 0(offset)个字节开始取 3(size)个 gl.FLOAT(type)组成 | 一个 vec3(arr[0], arr[1], arr[2]),把这个 vec3 赋值给顶点着色器特性 position
每个顶点的画布分辨率
resolution
都一样,所以一般不这么传递,放在这里只是为了举例。
gl.drawArrays(mode, first, count);
的mode
参数从 webgl 定义的枚举里面取值,分 3 种类型gl.POINTS
,点。数组中每个顶点代表一个点gl.LINES
、gl.LINE_STRIP
、gl.LINE_LOOP
,直线段。数组中每个顶点代表直线的一个端点或者说顶点,顶点之间的点由 webgl 以线性插值的方式计算出来gl.TRIANGLES
、gl.TRIANGLE_STRIP
、gl.TRIANGLE_FAN
,平面三角形。数组中每个顶点代表三角形的一个顶点,顶点之间的点由 webgl 以线性插值的方式计算出来为了绘制一个方块,调用
gl.drawArrays(gl.TRIANGLE_FAN, first, 4);
,意思是从当前绑定的数组的第first
个顶点开始用连续的 4 个顶点组成 1 个三角扇,这 4 个点的位置是事先规划好的,排列如下0 3 1 2
4 个点的三角扇包含 2 个三角形,分别是
webgl.1 0 - 1 - 2
和0 - 2 - 3
,三角扇绘制的三角形的第 1 个顶点总是first
处的那个顶点 这 2 个三角形共享 1 条边0 - 2
,两条边的方向相反,第 1 个是2 -> 0
,第 2 个是0 -> 2
,说这样的 2 个三角具有相同的朝向。三角扇两个相邻三角形的朝向一定相同 如果改变了顶点的顺序,得到的三角扇可能就不是一个方块有了这些知识下面写一个绘制黑色方块的程序,里面出现了顶点着色器和片段着色器代码,
ie 只支持experimental-webgl
这里面调用的函数glProgram
在正式示例中定义,如果要运行需拷贝glProgram
函数上面是注意事项
<script> !function () { var canvas = document.querySelector(.s4-rect), cc = canvas.getContext(webgl), // 片段着色器源代码 // 片段着色器必须用 precision mediump float 指出 float 的默认精度。 // 不像顶点着色器,片段着色器里面的 float 没有默认精度,不指定 float 默认 // 精度的话编译片段着色器就会失败。这是个比较荒唐的事实 // http://stackoverflow.com/questions/28540290/why-it-is-necessary-to-set-precision-for-the-fragment-shader // // 这个片段着色器代码就一句话,把所有顶点的颜色设置成不透明黑。由于是 4 个 // 顶点组成的方块,方块上除了 4 个顶点之外的点的颜色都由 webgl 通过线性插值 // 得出,不会进入片段着色器的 main,插值的结果还是不透明黑 sfs = precision mediump float; void main() { gl_FragColor = vec4(0, 0, 0, 1); }, // 顶点着色器源代码 // vec2 是两个 float。每处理一个顶点,从数组中取得的两个 float 都会赋值给 // 这个 attribute vec2 posxy,posxy 把数组传进来的内容原封不动地作为 // gl_Position 的 x 和 y // // gl_Position 的 4 个坐标 x, y, z, w 都是 [-1, 1] 的小数 // // 绘制了 4 个顶点,所以顶点着色器的 main 总共进入 4 次。方块上其它点的坐标 // 由线性插值生成 svs = attribute vec2 posxy; void main() { gl_Position = vec4(posxy, 0, 1); }, // glProgram 的定义在下面的示例代码中给出 program = glProgram(cc, sfs, svs), // 顶点数组。一会要通过 gl.bufferData 给顶点数组写入内容 arr = cc.createBuffer(), i; // gl.drawArrays 先决条件:gl.useProgram cc.useProgram(program); // gl.drawArrays 先决条件:gl.bindBuffer cc.bindBuffer(cc.ARRAY_BUFFER, arr); // 通过 gl.bufferData 往 arr 写入内容,作为顶点位置 // 参数里面并没有出现 arr,之所以能写进去是因为前面用 bindBuffer 指出了 arr 是 // 当前的 gl.ARRAY_BUFFER // 最后一个参数是 gl.STATIC_DRAW,不用考虑 // 对照图 webgl.1 cc.bufferData(cc.ARRAY_BUFFER, new Float32Array([ -0.3, +0.3, // 0 - 左上 -0.3, -0.7, // 1 - 左下 +0.7, -0.7, // 2 - 右下 +0.7, +0.3 // 3 - 右上 ]), cc.STATIC_DRAW); // gl.drawArrays 先决条件:gl.enableVertexAttribArray // 用 gl.getAttribLocation(program, posxy) 获取顶点着色器里面定义的特性 // posxy 对应的整数索引,保存这个整数索引供 gl.enableVertexAttribArray 使用 i = cc.getAttribLocation(program, posxy); cc.enableVertexAttribArray(i); // gl.drawArrays 先决条件:gl.vertexAttribPointer // 一次从数组里取出 stride 个字节。如果只调用了一次 // gl.vertexAttribPointer,stride 也可以填 0,会自动计算 stride cc.vertexAttribPointer(i, 2, cc.FLOAT, false, 2 * 4, 0); // gl.drawArrays cc.drawArrays(cc.TRIANGLE_FAN, 0, 4); }(); </script>
上面是
gl.drawArrays
,它从用gl.bindBuffer(gl.ARRAY_BUFFER, arr)
绑定的arr
中依次读取每个顶点。gl.drawElements
需要用gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ids)
额外绑定一个数组作为gl.ARRAY_BUFFER
的索引,这样一来它使用两个数组,arr
保存顶点,ids
保存遍历arr
的顺序。设// vertex 0, vertex 1, vertex 2, vertex 3, ... arr = [ x0, y0, x1, y1, x2, y2, x3, y3, ...] ids = [0, 3, 1, 2]
并且
已经调用了两次gl.bindBuffer
让gl.ARRAY_BUFFER
和gl.ELEMENT_ARRAY_BUFFER
分别对应arr
和ids
gl.vertexAttribPointer
指出每个顶点是 2 个type
索引数组ids
的元素类型是Uint16
或者说gl.UNSIGNED_SHORT
则
gl.drawArrays(gl.TRIANGLE_FAN, 0, 4)
绘制 4 次顶点,依次是0 - 1 - 2 - 3
gl.drawElements(gl.TRIANGLE_FAN, 4, gl.UNSIGNED_SHORT, 0)
绘制 4 次顶点,它依次读取ids
的每个元素,以元素的值作为arr
的索引去获取顶点,依次绘制arr
的0 - 3 - 1 - 2
我假设读者通过前面的阅读和练习已经理解了
gl.drawArrays
和gl.drawElements
,下面简要介绍设置颜色。前面在片段着色器里硬编码了个颜色,不透明黑,要画 3 个颜色的方块就需要 3 个片段着色器。如果能让片段着色器接受一个 javascript 传入的变量,有点像顶点着色器里面的特性
只有顶点着色器可以定义attribute
,从 javascript 指定颜色,那就可以只写一个片段着色器。出于两个原因,不使用attribute
attribute
,片段着色器不可以 方块是单色的,不需要像attribute
那样每个顶点都传一个值着色器程序总共可以定义 3 种变量:
attribute
、uniform
、varying
这里使用
uniform
。varying
用来从顶点着色器往片段着色器传值,当然也可以实现效果。
uniform
的意思是,每次调用gl.drawArrays
绘制一系列的顶点之前,先设置一个在绘制过程中保持不变的值,绘制这些点的过程中,着色器程序可以读取但不能修改该定值。对比attribute
,attribute
对gl.drawArrays
绘制的每 1 个顶点都赋值 1 次;uniform
只在gl.drawArrays
开始前赋值一次。因为
uniform
在 1 次绘制中只赋值 1 次,所以它不从数组里面取值,gl.uniformXxx
用于设置uniform
的值。所以下面的示例中,顶点着色器定义 1 个
attribute
以接受顶点,片段着色器定义 1 个uniform
以接受颜色,调用 3 次gl.drawArrays
以绘制 3 个方块。混合颜色
这里需要把
C=B(Cb,Cs) 换个形式以反映 webgl 的混合方法,换成C=e(f(Cs),g(Cb)) 。看上去更复杂了,但马上就会发现,它很简单。
e 对应gl.blendEquation(mode)
,mode
是 3 个枚举值之一
mode |
|
---|---|
gl.FUNC_ADD - 默认值 |
|
gl.FUNC_SUBTRACT |
|
gl.FUNC_REVERSE_SUBTRACT |
这里的
gl.blendFunc(sfactor, dfactor)
中的 sfactor
和 dfactor
,均从下列枚举中取值
factor |
|
---|---|
gl.ZERO - dfactor 默认值 |
|
gl.ONE - sfactor 默认值 |
|
gl.SRC_COLOR |
|
gl.ONE_MINUS_SRC_COLOR |
|
gl.DST_COLOR |
|
gl.ONE_MINUS_DST_COLOR |
|
gl.SRC_ALPHA |
|
gl.ONE_MINUS_SRC_ALPHA |
|
gl.DST_ALPHA |
|
gl.ONE_MINUS_DST_ALPHA |
|
gl.SRC_ALPHA_SATURATE |
所以,当 dfactor = gl.ONE
、sfactor
取默认值 gl.ONE
、mode
取默认值 gl.FUNC_ADD
时有
当
gl.blendFunc(gl.ONE, gl.ONE);
gl.enable(gl.BLEND); // 必须明显的启用混合
而如果让 sfactor = gl.ONE_MINUS_DST_COLOR
、dfactor = gl.ONE
、mode
取默认值 gl.FUNC_ADD
,有
当
gl.blendFunc(gl.ONE_MINUS_DST_COLOR, gl.ONE);
gl.enable(gl.BLEND); // 必须明显的启用混合
代码
现在的情况是
会用gl.drawArrays(gl.TRIANGLE_FAN, 0, 4);
绘制方块 知道将要用 uniform
给片段着色器传递一个代表颜色的变量 知道如何设置混合
下面看具体的代码
webgl 混合
<script>
!function () {
var canvas = document.querySelector(.s4),
ch = canvas.height, cw = canvas.width,
cc = canvas.getContext(webgl) || canvas.getContext(experimental-webgl),
svs =
attribute vec2 position; +
uniform vec2 resolution; +
void main() { +
vec2 p = position / resolution * 2.0 - 1.0; +
gl_Position = vec4(p * vec2(1, -1), 0, 1); +
},
sfs =
precision mediump float; +
uniform vec3 color; +
void main() { gl_FragColor = vec4(color, 1); },
rects = [
50, 20, 50, 120, 150, 120, 150, 20, // red
30, 40, 30, 140, 130, 140, 130, 40, // lime
70, 60, 70, 160, 170, 160, 170, 60 // blue
],
buffer = cc.createBuffer(),
attrs = { position: 0, },
unifs = { color: 0, resolution: 0 },
program = glProgram(cc, sfs, svs, attrs, unifs);
cc.useProgram(program);
cc.uniform2f(unifs.resolution, cw, ch);
cc.bindBuffer(cc.ARRAY_BUFFER, buffer);
cc.vertexAttribPointer(attrs.position, 2, cc.UNSIGNED_BYTE, false, 0, 0);
cc.bufferData(cc.ARRAY_BUFFER, new Uint8Array(rects), cc.STATIC_DRAW);
//cc.blendFunc(cc.ONE, cc.ONE);
cc.blendFunc(cc.ONE_MINUS_DST_COLOR, cc.ONE);
cc.enable(cc.BLEND);
draw(cc, 0, [1, 0, 0]);
draw(cc, 4, [0, 1, 0]);
draw(cc, 8, [0, 0, 1]);
function draw(gl, offset, color) {
gl.uniform3fv(unifs.color, color);
gl.drawArrays(gl.TRIANGLE_FAN, offset, 4);
}
}();
function glProgram(gl, sfs, svs, attrs, unifs) {
var prop, i,
program = glLink(gl,
glCompile(gl, sfs, gl.FRAGMENT_SHADER),
glCompile(gl, svs, gl.VERTEX_SHADER));
for (prop in attrs) {
i = gl.getAttribLocation(program, prop);
attrs[prop] = i;
gl.enableVertexAttribArray(i);
}
for (prop in unifs) unifs[prop] = gl.getUniformLocation(program, prop);
return program;
function glCompile(gl, source, type) {
var shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
return shader;
}
function glLink(gl, fs, vs) {
var program = gl.createProgram();
gl.attachShader(program, fs);
gl.attachShader(program, vs);
gl.linkProgram(program);
return program;
}
}
</script>
要点
uniform
不像 attribute
那样要调用 gl.enableVertexAttribArray
,使用 gl.getUniformLocation
获取 uniform
在 javascript 中的索引后就能用了 js 中的顶点数组使用了大于 1 的整数坐标,这个整数坐标传入顶点着色器后,顶点着色器要根据当前的画布尺寸换算出相应的小数,然后才给 gl_Position
赋值 代码包含 add 和 screen 混合模式,就一句代码,add 被注释掉了
全部代码
mix-blend-mode: screen 不是颜色分量相加
mix-blend-mode: screen 不是颜色分量相加
svg,仅限 ie 10+
svg,仅限 ie 10+
svg feBlend
svg feBlend
canvas 2d 全局复合
canvas 2d 全局复合
<script>
!function () {
var canvas = document.querySelector(.s3-1),
cc = canvas.getContext(2d);
cc.fillStyle = red;
cc.fillRect(50, 20, 100, 100);
cc.globalCompositeOperation = screen;
cc.fillStyle = lime;
cc.fillRect(30, 40, 100, 100);
cc.fillStyle = blue;
cc.fillRect(70, 60, 100, 100);
}();
</script>
在 canvas 中把颜色分量置 1,未使用 min(x + y, 1)
在 canvas 中把颜色分量置 1,未使用 min(x + y, 1)
<script>
!function () {
var canvas = document.querySelector(.s3-2),
cc = canvas.getContext(2d),
i, len, idata, arr;
cc.fillStyle = red;
cc.fillRect(50, 20, 100, 100);
for (i = 0,
idata = cc.getImageData(30, 40, 100, 100),
arr = idata.data,
len = arr.length; i < len; i += 4)
arr[i + 1] = arr[i + 3] = 255;
cc.putImageData(idata, 30, 40);
for (i = 0,
idata = cc.getImageData(70, 60, 100, 100),
arr = idata.data,
len = arr.length; i < len; i += 4)
arr[i + 2] = arr[i + 3] = 255;
cc.putImageData(idata, 70, 60);
}();
</script>
webgl 混合
webgl 混合
<script>
!function () {
var canvas = document.querySelector(.s4),
ch = canvas.height, cw = canvas.width,
cc = canvas.getContext(webgl) || canvas.getContext(experimental-webgl),
svs =
attribute vec2 position; +
uniform vec2 resolution; +
void main() { +
vec2 p = position / resolution * 2.0 - 1.0; +
gl_Position = vec4(p * vec2(1, -1), 0, 1); +
},
sfs =
precision mediump float; +
uniform vec3 color; +
void main() { gl_FragColor = vec4(color, 1); },
rects = [
50, 20, 50, 120, 150, 120, 150, 20, // red
30, 40, 30, 140, 130, 140, 130, 40, // lime
70, 60, 70, 160, 170, 160, 170, 60 // blue
],
buffer = cc.createBuffer(),
attrs = { position: 0, },
unifs = { color: 0, resolution: 0 },
program = glProgram(cc, sfs, svs, attrs, unifs);
cc.useProgram(program);
cc.uniform2f(unifs.resolution, cw, ch);
cc.bindBuffer(cc.ARRAY_BUFFER, buffer);
cc.vertexAttribPointer(attrs.position, 2, cc.UNSIGNED_BYTE, false, 0, 0);
cc.bufferData(cc.ARRAY_BUFFER, new Uint8Array(rects), cc.STATIC_DRAW);
//cc.blendFunc(cc.ONE, cc.ONE);
cc.blendFunc(cc.ONE_MINUS_DST_COLOR, cc.ONE);
cc.enable(cc.BLEND);
draw(cc, 0, [1, 0, 0]);
draw(cc, 4, [0, 1, 0]);
draw(cc, 8, [0, 0, 1]);
function draw(gl, offset, color) {
gl.uniform3fv(unifs.color, color);
gl.drawArrays(gl.TRIANGLE_FAN, offset, 4);
}
}();
function glProgram(gl, sfs, svs, attrs, unifs) {
var prop, i,
program = glLink(gl,
glCompile(gl, sfs, gl.FRAGMENT_SHADER),
glCompile(gl, svs, gl.VERTEX_SHADER));
for (prop in attrs) {
i = gl.getAttribLocation(program, prop);
attrs[prop] = i;
gl.enableVertexAttribArray(i);
}
for (prop in unifs) unifs[prop] = gl.getUniformLocation(program, prop);
return program;
function glCompile(gl, source, type) {
var shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
return shader;
}
function glLink(gl, fs, vs) {
var program = gl.createProgram();
gl.attachShader(program, fs);
gl.attachShader(program, vs);
gl.linkProgram(program);
return program;
}
}
</script>