您现在的位置:首页 >> 前端 >> 内容

在 html 中用加色法混合颜色

时间:2015/5/30 10:13:19 点击:

  核心提示:概要本文通过解决一个假想的问题介绍了 css screen 混合模式,并介绍了如何用 svg 滤镜、canvas 2d、canvas webgl 实现相同的效果。问题下面的图片演示三种颜色光叠加的效果...

概要

本文通过解决一个假想的问题介绍了 css screen 混合模式,并介绍了如何用 svg 滤镜、canvas 2d、canvas webgl 实现相同的效果。

问题

下面的图片演示三种颜色光叠加的效果,请在 html 中实现这种效果。

在 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 用 在 html 中用加色法混合颜色 引用该图片,本文不讨论这种方法。

观察重叠部分发现该部分的颜色不仅受自己的影响、还受它下面背景颜色的影响,重叠部分的颜色是自己的颜色和背景颜色混合的结果。换句话说,一个像素绘制出来的颜色等于像素颜色和背景像素颜色的混合,即

C=B(Cb,Cs),其中,
C 是绘制的颜色
B 是混合函数
Cb 是背景颜色
Cs 是前景颜色,即像素的颜色

这里面 C 的 r、g、b 颜色分量都是 [0, 1] 的小数而不是 [0, 255] 的整数。显然,对同一个像素来说不同的 B 得到不同的 C。红绿蓝分别是 rgb(1, 0, 0)rgb(0, 1, 0)rgb(0, 0, 1)B 如果满足 B(Cb,Cs)=min(Cs+Cb,1) 就能合成白色。

重点:不同的 B 得到不同的 C

不可行的方法

html 中经常用到下面 3 个方法,

css opacity 属性 css rgba()/hsla() 颜色 用 在 html 中用加色法混合颜色 引用带 alpha 通道的图像

它们使用相同的混合函数,叫做 α 混合简单 α 复合

C=B(Cb,Cs)=Cs×αs+Cb×αb×(1−αs)=Cs×αs+Cb×(1−αs)

αs 是前景透明度,αb 是背景透明度,上面的式子计算混合后的 r、g、b 颜色,混合后的透明度 αo 由公式 αo=αs+αb×(1−αs) 给出。很多时候背景不透明,即 αb 是 1,上面把 1 代入了 αb

简单 α 复合 - http://dev.w3.org/fxtf/compositing/#simplealphacompositing
opacity - http://stackoverflow.com/questions/8743482/calculating-opacity-value-mathematically

下面给上面的式子代入几组实际值。设 Cs 是不透明红 rgba(1, 0, 0, 1)Cb 是不透明蓝 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

红蓝得红,混合失败。另外一组,Cs = rgba(1, 0, 0, 0.5)Cb = 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) 的白色,α 需要满足下面的方程组,

{0.5α+0α+0(1−α)=1(1−α)=1={0.5α=11−α=1={α=2α=0

上面的方程组无解,即无论如何设置 α 都无法通过 B 混合 rgb(0.5, 0, 0.5)rgb(0, 1, 0) 得到 rgb(1, 1, 1)

回过头来观察式子 Cs×αs+Cb×(1−αs),可以看出结果介于 CsCb 之间。红绿蓝混合时,白色的红色分量只能通过红色得到,这要求红色的 α 是 1,但 α = 1 造成背景颜色蓝或者绿被忽略,而忽略任何一个分量都无法得到白色。因此这个混合函数不合适。

可行的方法

如果可以自己逐一计算像素的颜色,得出要求的效果自然不在话下。除了自己计算外,如果存在正好能够实现要求效果的固定函数,则调用该函数也可以。

在 html 中处理颜色有 3 种工具,css、svg、canvas。

css

css 有个模块叫复合与混合,这个模块定义了若干固定函数,其中一个叫 screen,它的 B

C=B(Cb,Cs)=1−[(1−Cb)×(1−Cs)]=Cs+Cb−Cs×Cb

css 复合与混合 - http://dev.w3.org/fxtf/compositing/

假设 add 是 min(Cs+Cb,1),screen 虽然不是 add 但是也可以把红绿蓝合成白色,实现要求的效果。至于 add、screen 或其它混合函数哪个能更精确地反映光线的混合,我也搞不清楚。

通过指定 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/xlinkxlink: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 特性指出的操作组合两个输入图像 i1i2。当 operator=arithmetic 时需要另外的 4 个特性 k1k2k3k4,默认值是 0,并按如下方式分别计算结果像素的 3 个通道,我不清楚它如何处理 α 通道,

result=k1×i1×i2+k2×i1+k3×i2+k4?(svg.1)

既然知道 mix-blend-mode: screen 的混合函数 B=Cs+Cb?Cs×Cb,设 Csi1Cbi2,有,

result===k1×i1×i2?1×i1×i2?1×Cs×Cb+++k2×i11×i1Cs+++k3×i21×i2Cb++k40

所以 可以实现效果。
支持 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-modeisolation

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 模式 B=Cs+Cb?Cs×Cb 混合的结果是 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%xy 的值相对于应用滤镜的元素,x=-10 y=10 以应用滤镜的元素为准向左 10 向下 10。xywidthheight 的数值的解释由另外一个特性 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?

接受两个图像 i1i2 逐一扫描 i1i2 的像素,用 svg.1 产生新像素,放置到滤镜区域相应的位置。

对图像 i1i2 应用 screen 混合模式。

in 的默认值是 中上一个 filter primitive 的结果;如果自己是第一个,则默认 SourceGraphicin=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 是背景的一个快照,要使用 BackgroundImageinin2 的参数必须指定容器元素的 enable-background=new 以要求容器存储背景快照供滤镜使用。enable-background 默认值是 accumulate,不存,也无法使用 BackgroundImage。两个不同的单词可能是要强调输入图像和背景图像的这种差异。当然也可能是我想多了,人家就是喜欢出其不意,如之奈何?

混合 3 个形状需要应用两次滤镜,出于演示的目的正好一个用 一个用

创建 3 个分别是红绿蓝的 svg 形状 为了使用 BackgroundImage,把这 3 个形状放到一个组里面,并设置组的 enable-background=new 放置第 1 个形状 把形状 2 放到和形状 1 部分重叠的位置,此时形状 1 可以视为形状 2 的背景,对形状 2 应用滤镜 ,重叠的部分就会经过 svg.1 计算 形状 3 用

svg,仅限 ie 10+

没有指出 xywidthheight 所以它们都取默认值。

只有 ie 10+ 支持上面的代码。ie 此刻又迸射出耀眼的光芒,

别的浏览器玩儿蛋去吧!

这是我心里想象的 ie 工作人员心里的想象,请不要以为他们一定是那样想的。

Appendix A: The deprecated enable-background property
http://dev.w3.org/fxtf/filters/#AccessBackgroundImage

svg 最近的风向是不赞成 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????????=????????a00a10a20a300a01a11a21a310a02a12a22a320a03a13a23a330a04a14a24a341????????×????????RGBA1????????


(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

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

绘制 色方块 全局复合模式 = screen 绘制 绿 色方块 绘制 色方块

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 赋值,它代表刚才那个点的颜色。

gl.drawArrays 依次执行下列步骤,

webgl 一次从数组读取由 gl.vertexAttribPointerstride 参数指出的字节,这些字节视为 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 个字节开始取 sizetype,每个 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.LINESgl.LINE_STRIPgl.LINE_LOOP,直线段。数组中每个顶点代表直线的一个端点或者说顶点,顶点之间的点由 webgl 以线性插值的方式计算出来 gl.TRIANGLESgl.TRIANGLE_STRIPgl.TRIANGLE_FAN,平面三角形。数组中每个顶点代表三角形的一个顶点,顶点之间的点由 webgl 以线性插值的方式计算出来

为了绘制一个方块,调用 gl.drawArrays(gl.TRIANGLE_FAN, first, 4);,意思是从当前绑定的数组的第 first 个顶点开始用连续的 4 个顶点组成 1 个三角扇,这 4 个点的位置是事先规划好的,排列如下

0      3

1      2

webgl.1

4 个点的三角扇包含 2 个三角形,分别是 0 - 1 - 20 - 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.bindBuffergl.ARRAY_BUFFERgl.ELEMENT_ARRAY_BUFFER 分别对应 arrids 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 的索引去获取顶点,依次绘制 arr0 - 3 - 1 - 2

我假设读者通过前面的阅读和练习已经理解了 gl.drawArraysgl.drawElements,下面简要介绍设置颜色。

前面在片段着色器里硬编码了个颜色,不透明黑,要画 3 个颜色的方块就需要 3 个片段着色器。如果能让片段着色器接受一个 javascript 传入的变量,有点像顶点着色器里面的特性 attribute,从 javascript 指定颜色,那就可以只写一个片段着色器。出于两个原因,不使用 attribute

只有顶点着色器可以定义 attribute,片段着色器不可以 方块是单色的,不需要像 attribute 那样每个顶点都传一个值

着色器程序总共可以定义 3 种变量:attributeuniformvarying

这里使用 uniformvarying 用来从顶点着色器往片段着色器传值,当然也可以实现效果。

uniform 的意思是,每次调用 gl.drawArrays 绘制一系列的顶点之前,先设置一个在绘制过程中保持不变的值,绘制这些点的过程中,着色器程序可以读取但不能修改该定值。对比 attributeattributegl.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 e
gl.FUNC_ADD - 默认值 e(x,y)=clamp(x+y)
gl.FUNC_SUBTRACT e(x,y)=clamp(x−y)
gl.FUNC_REVERSE_SUBTRACT e(x,y)=clamp(y−x)


这里的 clamp(x) 把 x 限制为 [0, 1],小于 0 的值变成 0,大于 1 的值变成 1。

fg 分别对应 gl.blendFunc(sfactor, dfactor) 中的 sfactordfactor,均从下列枚举中取值

factor f
gl.ZERO - dfactor 默认值 f(x)=x×0
gl.ONE - sfactor 默认值 f(x)=x×1
gl.SRC_COLOR f(x)=x×Cs
gl.ONE_MINUS_SRC_COLOR f(x)=x×(1−Cs)
gl.DST_COLOR f(x)=x×Cb
gl.ONE_MINUS_DST_COLOR f(x)=x×(1−Cb)
gl.SRC_ALPHA f(x)=x×αs
gl.ONE_MINUS_SRC_ALPHA f(x)=x×(1−αs)
gl.DST_ALPHA f(x)=x×αb
gl.ONE_MINUS_DST_ALPHA f(x)=x×(1−αb)
gl.SRC_ALPHA_SATURATE f(x)=x×min(αs,αb)


所以,当 dfactor = gl.ONEsfactor 取默认值 gl.ONEmode 取默认值 gl.FUNC_ADD 时有

C=e(f(Cs),g(Cb))=clamp(Cs×1+Cb×1)=clamp(Cs+Cb)

CsCb 都是正数时 clamp(Cs+Cb)=min(Cs+Cb,1),就是前面用过的 add 混合模式。相应的 js 代码是

gl.blendFunc(gl.ONE, gl.ONE);
gl.enable(gl.BLEND); // 必须明显的启用混合

而如果让 sfactor = gl.ONE_MINUS_DST_COLORdfactor = gl.ONEmode 取默认值 gl.FUNC_ADD,有

C=e(f(Cs),g(Cb))=clamp(Cs×(1−Cb)+Cb)=clamp(Cs+Cb−Cs×Cb)

CsCb 都是正数时 clamp(Cs+Cb−Cs×Cb)=Cs+Cb−Cs×Cb,就是前面用过的 screen 混合模式。相应的 js 代码是

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 不是颜色分量相加

svg,仅限 ie 10+

svg feBlend

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)

<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 混合

<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>

 


Tags:在H HT TM ML 
作者:网络 来源:see11see 的