在 html 中用加色法混合顏色


概要

本文通過解決一個假想的問題介紹了 css screen 混合模式,並介紹了如何用 svg 濾鏡、canvas 2d、canvas webgl 實現相同的效果。

問題

下面的圖片演示三種顏色光疊加的效果,請在 html 中實現這種效果。

三個不同顏色的光斑,重疊的部分符合加色法

約定

詞語 指代
混合 blend
加色 additive color - 名詞
特性 attribute,比如 <a id=1>,說 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 用 <img> 引用該圖片,本文不討論這種方法。

觀察重疊部分發現該部分的顏色不僅受自己的影響、還受它下面背景顏色的影響,重疊部分的顏色是自己的顏色和背景顏色混合的結果。換句話說,一個像素繪制出來的顏色等於像素顏色和背景像素顏色的混合,即

\(C = B(C_b, C_s)\),其中,
\(C\) 是繪制的顏色
\(B\) 是混合函數
\(C_b\) 是背景顏色
\(C_s\) 是前景顏色,即像素的顏色

這里面 \(C\) 的 r、g、b 顏色分量都是 [0, 1] 的小數而不是 [0, 255] 的整數。顯然,對同一個像素來說不同的 \(B\) 得到不同的 \(C\)。紅綠藍分別是 rgb(1, 0, 0)rgb(0, 1, 0)rgb(0, 0, 1)\(B\) 如果滿足 \(B(C_b, C_s) = min(C_s + C_b, 1)\) 就能合成白色。

不可行的方法

html 中經常用到下面 3 個方法,

  • css opacity 屬性
  • css rgba()/hsla() 顏色
  • <img> 引用帶 alpha 通道的圖像

它們使用相同的混合函數,叫做 α 混合簡單 α 復合

\(C = B(C_b, C_s) = C_s \times α_s + C_b \times α_b \times (1 - α_s) = C_s \times α_s + C_b \times (1 - α_s)\)

\(α_s\) 是前景透明度,\(α_b\) 是背景透明度,上面的式子計算混合后的 r、g、b 顏色,混合后的透明度 \(α_o\) 由公式 \(α_o = α_s + α_b \times (1 - α_s)\) 給出。很多時候背景不透明,即 \(α_b\) 是 1,上面把 1 代入了 \(α_b\)

簡單 α 復合 - http://dev.w3.org/fxtf/compositing/#simplealphacompositing
opacity - http://stackoverflow.com/questions/8743482/calculating-opacity-value-mathematically

下面給上面的式子代入幾組實際值。設 \(C_s\) 是不透明紅 rgba(1, 0, 0, 1)\(C_b\) 是不透明藍 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

紅藍得紅,混合失敗。另外一組,\(C_s\) = rgba(1, 0, 0, 0.5)\(C_b\) = 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(1 -α) = 1, 0α + (1 - α) = 1\)
\(0.5α = 1, 1 - α = 1\)
\(α = 2, α = 0\)

上面的方程組無解,即無論如何設置 α 都無法通過 \(B\) 混合 rgb(0.5, 0, 0.5)rgb(0, 1, 0) 得到 rgb(1, 1, 1)

回過頭來觀察式子 \(C_s \times α_s + C_b \times (1 - α_s)\),可以看出結果介於 \(C_s\)\(C_b\) 之間。紅綠藍混合時,白色的紅色分量只能通過紅色得到,這要求紅色的 α 是 1,但 α = 1 造成背景顏色藍或者綠被忽略,而忽略任何一個分量都無法得到白色。因此這個混合函數不合適。

可行的方法

如果可以自己逐一計算像素的顏色,得出要求的效果自然不在話下。除了自己計算外,如果存在正好能夠實現要求效果的固定函數,則調用該函數也可以。

在 html 中處理顏色有 3 種工具,css、svg、canvas。

css

css 有個模塊叫復合與混合,這個模塊定義了若干固定函數,其中一個叫 screen,它的 \(B\)

\(C = B(C_b, C_s) = 1 - [(1 - C_b) \times (1 - C_s)] = C_s + C_b - C_s \times C_b\)

css 復合與混合 - http://dev.w3.org/fxtf/compositing/

假設 add 是 \(min(C_s + C_b, 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> 里面由特性定義的名字空間,所以本文的 svg 沒有 xmlns="http://www.w3.org/2000/svg" 或者 xmlns:xlink="http://www.w3.org/1999/xlink"xlink:href 在 html 中是個普通的特性名,冒號和名字空間無關
  • 沒有歧義時可以省略特性值周圍的引號
  • xml 中沒有內容的元素比如 <circle cx=1 cy=1 r=1></circle> 也可以寫做 <circle cx=1 cy=1 r=1 />,叫做自閉合;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 元素開始標記的 > 前面可以寫一個 /,和不寫 / 一樣。<br/> 解釋為 <br> 而不是 <br></br><br> 是 void 元素,所以沒問題;<div/> 解釋為 <div><div> 不是 void 元素,所以可能會出問題,

<span style="color: green;"><span style="color: red;"/>
    red = html, green = xhtml
</span>

有些元素可以省略結束標記,但不是 void 元素,比如 <li>;有些元素有時候沒有內容,但既不是 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 濾鏡 <feComposite><feBlend>

svg 濾鏡 - http://www.w3.org/tr/svg11/filters.html
svg 復合 - http://www.w3.org/TR/SVGCompositing/

<feComposite> 按其 operator 特性指出的操作組合兩個輸入圖像 \(i_1\)\(i_2\)。當 operator=arithmetic 時需要另外的 4 個特性 k1k2k3k4,默認值是 0,並按如下方式分別計算結果像素的 3 個通道,我不清楚它如何處理 α 通道,

\(result = k_1 \times i_1 \times i_2 + k_2 \times i_1 + k_3 \times i_2 + k_4 \cdots (svg.1)\)

既然知道 mix-blend-mode: screen 的混合函數 \(B = C_s + C_b - C_s \times C_b\),設 \(C_s\)\(i_1\)\(C_b\)\(i_2\),有,

\(result = k_1 \times i_1 \times i_2 + k_2 \times i_1 + k_3 \times i_2 + k_4 = -1 \times i_1 \times i_2 + 1 \times i_1 + 1 \times i_2 + 0 = -1 \times C_s \times C_b + C_s + C_b\)

所以 <feComposite operator=arithmetic k1=-1 k2=1 k3=1> 可以實現效果。
<feBlend> 支持 screen 混合模式,<feBlend mode=screen>,所以應該也能實現效果。

canvas

canvas 分為 2d 和 webgl,它里面的形狀都是畫上去的,由像素組成,不是 dom 元素,無法應用 css 混合;但是 canvas 2d 有個全局復合操作,和 css 混合是同一個概念在兩種不同語言中的實現,支持 css 混合的所有固定函數。當然自己計算像素也行。

全局復合操作 - http://dev.w3.org/fxtf/compositing/#canvascompositingandblending

webgl 沒有與 css、canvas 2d 完全相同的混合概念,但也有自己的混合函數,解決本文提出的問題不在話下。webgl 有個特點是無論你干什么都需要寫着色器代碼、寫調用編譯着色器的函數的代碼、寫調用連接着色器的函數的代碼。

如何運行示例代碼

下面是框架代碼,后面給出的示例代碼需要放在框架代碼的 <body>

<!doctype html>
<html>
<head>
    <meta charset=utf-8>
    <style>
        .sample { display: inline-block; vertical-align: top; width: 200px; }
    </style>
    <title>additive color</title>
</head>
<body>

</body>
</html>

依次執行下面 3 個步驟,缺一不可,

  1. 新建一個空 html 文件
  2. 拷貝框架代碼,粘貼到空的 html 文件
  3. 確保 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 屬性的值指出。

因此設計一個容器 div position: relative,里面有紅綠藍三個方塊 div position: absolute,三個方塊之間有重疊部分,通過 mix-blend-mode: screen 指出重疊部分顏色的計算方法。

<div class=sample>
    <style>
        .s1 { height: 180px; isolation: isolate; position: relative; }
        .s1 > div { height: 100px; mix-blend-mode: screen; position: absolute; width: 100px; }
        .s1 > div:nth-of-type(1) { background-color: red;  left: 50px; top: 20px; }
        .s1 > div:nth-of-type(2) { background-color: lime; left: 30px; top: 40px; }
        .s1 > div:nth-of-type(3) { background-color: blue; left: 70px; top: 60px; }
    </style>
    <div class=s1><div></div><div></div><div></div></div>
    <h4>mix-blend-mode: screen 不是顏色分量相加</h4>
</div>

上面的代碼實現了剛才的設計,並且額外設置了容器 div 的一個屬性 isolation: isolate。元素的 isolation: isolate 是說,我的子元素不會和我外面的元素混合。isolation 屬性另外一個可能的取值兼默認值是 auto,沒有限制、隨便混和。

容器 div 放在 html 的 <body> 里,<body> 默認的顏色是不透明白,假設沒有通過 isolation: isolate 限定容器元素的子元素不能與容器外的元素混合,紅綠藍 3 個方塊 div 就要和白色以 screen 模式混合。紅色 rgb(1, 0, 0) 和白色 rgb(1, 1, 1) 以 screen 模式 \(B = C_s + C_b - C_s \times C_b\) 混合的結果是 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 <feComposite> 和 <feBlend>

  • svg filter primitive 必須包含在 <filter> 元素內
  • <feComposite>svg filter primitive
  • 所以?

<filter x=a y=b width=c height=d> 定義一個矩形濾鏡區域,默認值是 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?

<feComposite in=i1 in2=i2 operator=arithmetic k1=a k2=b k3=c k4=d> 接受兩個圖像 i1i2<feComposite> 逐一掃描 i1i2 的像素,用 \(svg.1\) 產生新像素,放置到濾鏡區域相應的位置。

<feBlend in=i1 in2=i2 mode=screen> 對圖像 i1i2 應用 screen 混合模式。

in 的默認值是 <filter> 中上一個 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 個形狀需要應用兩次濾鏡,出於演示的目的正好一個用 <feComposite> 一個用 <feBlend>

  • 創建 3 個分別是紅綠藍的 svg 形狀
  • 為了使用 BackgroundImage,把這 3 個形狀放到一個組里面,並設置組的 enable-background=new
  • 放置第 1 個形狀
  • 把形狀 2 放到和形狀 1 部分重疊的位置,此時形狀 1 可以視為形狀 2 的背景,對形狀 2 應用濾鏡 <feComposite in2=BackgroundImage>,重疊的部分就會經過 \(svg.1\) 計算
  • 形狀 3 用 <feBlend in2=BackgroundImage>
<div class=sample>
    <svg height=180 width=200>
        <filter id=s2-composite x=0 y=0 width=1 height=1>
            <feComposite in2=BackgroundImage operator=arithmetic k1=-1 k2=1 k3=1></feComposite>
        </filter>
        <filter id=s2-blend>
            <feBlend in2=BackgroundImage mode=screen></feBlend>
        </filter>
        <g enable-background=new>
            <rect width=100 height=100 x=50 y=20 fill=red></rect>
            <rect width=100 height=100 x=30 y=40 fill=lime filter=url(#s2-composite)></rect>
            <rect width=100 height=100 x=70 y=60 fill=blue filter=url(#s2-blend)></rect>
        </g>
    </svg>
    <h4>svg,僅限 ie 10+</h4>
</div>

沒有指出 <filter id=s2-blend>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 個元素兩兩混合,而是在濾鏡里生成了兩個方塊,和應用濾鏡的那個方塊元素混合。在濾鏡里生成方塊指的是把濾鏡矩形設置為單一的顏色然后臨時保存,這要用到 <feColorMatrix>

<feColorMatrix type=matrix values="
    a00 a01 a02 a03 a04
    a10 a11 a12 a13 a14
    a20 a21 a22 a23 a24
    a30 a31 a32 a33 a34" />

說的是當把圖像傳遞給 <feColorMatrix> 時,對圖像的每一個像素左乘下面的矩陣以得到新的像素,

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') 等於 <feColorMatrix> 給出的矩陣乘以原像素 (r, g, b, a)

R'       a00 a01 a02 a03 a04       R
G'       a10 a11 a12 a13 a14       G
B'   =   a20 a21 a22 a23 a24   x   B
A'       a30 a31 a32 a33 a34       A
1         0   0   0   0   1        1
<feColorMatrix type=matrix values="
    0 0 0 0 1
    0 0 0 0 0
    0 0 0 0 0
    0 0 0 1 0" />

(r, g, b, a, 1) 變成 (1, 0, 0, a, 1),這是紅色。因此可以寫下面的代碼,

  • 定義一個藍方塊 <rect fill=blue>
  • 對藍方塊應用濾鏡
  • 濾鏡從藍方塊生成一個紅方塊和一個綠方塊,偏移,混合
  • 上一步的結果和藍方塊混合,藍方塊在濾鏡中通過偽輸入圖像 SourceGraphic 引用
<div class=sample>
    <svg height=180 width=200>
        <filter id=s2-2 x=-1 y=-1 width=2 height=2>
            <feOffset dx=-20 dy=-40></feOffset>
            <feColorMatrix result=red type=matrix values="
                           0 0 0 0 1
                           0 0 0 0 0
                           0 0 0 0 0
                           0 0 0 1 0"></feColorMatrix>
            <feOffset in=SourceGraphic dx=-40 dy=-20></feOffset>
            <feColorMatrix type=matrix values="
                           0 0 0 0 0
                           0 0 0 0 1
                           0 0 0 0 0
                           0 0 0 1 0"></feColorMatrix>
            <feBlend mode=screen in2=red></feBlend>
            <feBlend mode=screen in2=SourceGraphic></feBlend>
        </filter>
        <rect x=70 y=60 width=100 height=100 fill=blue filter=url(#s2-2)></rect>
    </svg>
    <h4>svg feBlend</h4>
</div>

也可以做紅綠藍 3 張圖片,在濾鏡里用 <feImage> 引用,代碼類似下面,記得先做 3 張 100px * 100px 的紅綠藍圖片放到 html 的同一目錄。

<svg width=200 height=180>
    <filter id=additive>
        <feImage x=50 y=20 width=100 height=100 result=layer1 xlink:href=05-red.png />
        <feImage x=30 y=40 width=100 height=100 result=layer2 xlink:href=05-lime.png />
        <feImage x=70 y=60 width=100 height=100 result=layer3 xlink:href=05-blue.png />
        <feBlend in=layer1 in2=layer2 mode=screen result=step-1 />
        <feBlend in=step-1 in2=layer3 mode=screen />
    </filter>
    <rect width=200 height=180 filter=url(#additive) />
</svg>

總結

svg 濾鏡的思路就是 <feComposite><feBlend>,前者自己計算像素,后者調用固定函數。由於 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
  • 繪制 色方塊
  • 繪制 色方塊
<div class=sample>
    <canvas class=s3-1 height=180 width=200></canvas>
    <h4>canvas 2d 全局復合</h4>
    <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>
</div>

方法 2. CanvasRenderingContext2D.prototype.putImageData

  • 繪制 色方塊
  • 選定一個部分重疊的方塊,使用 CanvasRenderingContext2D.prototype.getImageData 把該部分像素讀入內存,對每個像素,和 色應用 \(min(C_s + C_b, 1)\),然后寫入畫布
  • 選定一個部分重疊的方塊,讀取,每個像素和 色應用 \(min(C_s + C_b, 1)\),寫入畫布

因為紅綠藍兩兩混合時每個分量必然一個是 0 一個是 1,所以並沒有調用 Math.min 而是直接把分量置 1,或者說置 255。

<div class=sample>
    <canvas class=s3-2 height=180 width=200></canvas>
    <h4>在 canvas 中把顏色分量置 1,未使用 min(x + y, 1)</h4>
    <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>
</div>

示例 - 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 依次執行下列步驟,

  1. webgl 一次從數組讀取由 gl.vertexAttribPointerstride 參數指出的字節,這些字節視為 1 個頂點
  2. 把這么多字節按 gl.vertexAttribPointer 指出的方式拆分后分別賦值給頂點着色器里面用 attribute 定義的變量,調用了幾次 gl.vertexAttribPointer 就要給幾個變量賦值
  3. 進入頂點着色器的 main 函數,main 里面一般會使用剛才賦值過的特性
  4. 頂點着色器的 main 結束,進入片段着色器的 main 函數
  5. 片段着色器的 main 結束,1 個頂點渲染完畢,從數組讀取下一個頂點
  6. 重復上述過程,直至處理了由 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 個三角具有相同的朝向。三角扇兩個相鄰三角形的朝向一定相同
  • 如果改變了頂點的順序,得到的三角扇可能就不是一個方塊

有了這些知識下面寫一個繪制黑色方塊的程序,里面出現了頂點着色器和片段着色器代碼,

  1. ie 只支持 experimental-webgl
  2. 這里面調用的函數 glProgram 在正式示例中定義,如果要運行需拷貝 glProgram 函數

上面是注意事項

<canvas class=s4-rect width=200 height=180></canvas>
<script>
    !function () {
        var canvas = document.querySelector(".s4-rect"),
            cc = canvas.getContext("webgl"),

            // 片段着色器源代碼
            // 片段着色器必須用 precision mediump float 指出 float 的默認精度。
            // 不像頂點着色器,片段着色器里面的 float 沒有默認精度,不指定 float 默認
            // 精度的話編譯片段着色器就會失敗。這是個比較荒唐的事實*
            //
            // 這個片段着色器代碼就一句話,把所有頂點的顏色設置成不透明黑。由於是 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>

* http://stackoverflow.com/questions/28540290/why-it-is-necessary-to-set-precision-for-the-fragment-shader

上面是 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(C_b, C_s)\) 換個形式以反映 webgl 的混合方法,換成 \(C = e(f(C_s), g(C_b))\)。看上去更復雜了,但馬上就會發現,它很簡單。

\(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。

\(f\)\(g\) 分別對應 gl.blendFunc(sfactor, dfactor) 中的 sfactordfactor,均從下列枚舉中取值

factor \(f\)
gl.ZERO - dfactor 默認值 \(f(x) = x \times 0\)
gl.ONE - sfactor 默認值 \(f(x) = x \times 1\)
gl.SRC_COLOR \(f(x) = x \times C_s\)
gl.ONE_MINUS_SRC_COLOR \(f(x) = x \times (1 - C_s)\)
gl.DST_COLOR \(f(x) = x \times C_b\)
gl.ONE_MINUS_DST_COLOR \(f(x) = x \times (1 - C_b)\)
gl.SRC_ALPHA \(f(x) = x \times α_s\)
gl.ONE_MINUS_SRC_ALPHA \(f(x) = x \times (1 - α_s)\)
gl.DST_ALPHA \(f(x) = x \times α_b\)
gl.ONE_MINUS_DST_ALPHA \(f(x) = x \times (1 - α_b)\)
gl.SRC_ALPHA_SATURATE \(f(x) = x \times min(α_s, α_b)\)

所以,當 dfactor = gl.ONEsfactor 取默認值 gl.ONEmode 取默認值 gl.FUNC_ADD 時有

\(C = e(f(C_s), g(C_b)) = clamp(C_s \times 1 + C_b \times 1) = clamp(C_s + C_b)\)

\(C_s\)\(C_b\) 都是正數時 \(clamp(C_s + C_b) = min(C_s + C_b, 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(C_s), g(C_b)) = clamp(C_s \times (1 - C_b) + C_b) = clamp(C_s + C_b - C_s \times C_b)\)

\(C_s\)\(C_b\) 都是正數時 \(clamp(C_s + C_b - C_s \times C_b) = C_s + C_b - C_s \times C_b\),就是前面用過的 screen 混合模式。相應的 js 代碼是

gl.blendFunc(gl.ONE_MINUS_DST_COLOR, gl.ONE);
gl.enable(gl.BLEND); // 必須明顯的啟用混合

代碼

現在的情況是

  • 會用 gl.drawArrays(gl.TRIANGLE_FAN, 0, 4); 繪制方塊
  • 知道將要用 uniform 給片段着色器傳遞一個代表顏色的變量
  • 知道如何設置混合

下面看具體的代碼

<div class=sample>
    <canvas class=s4 height=180 width=200></canvas>
    <h4>webgl 混合</h4>
    <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>
</div>

要點

  • uniform 不像 attribute 那樣要調用 gl.enableVertexAttribArray,使用 gl.getUniformLocation 獲取 uniform 在 javascript 中的索引后就能用了
  • js 中的頂點數組使用了大於 1 的整數坐標,這個整數坐標傳入頂點着色器后,頂點着色器要根據當前的畫布尺寸換算出相應的小數,然后才給 gl_Position 賦值
  • 代碼包含 add 和 screen 混合模式,就一句代碼,add 被注釋掉了

全部代碼

<!doctype html>
<html>
<head>
    <meta charset=utf-8>
    <style>
        .sample { display: inline-block; vertical-align: top; width: 200px; }
    </style>
    <title>additive color</title>
</head>
<body>
    <div class=sample>
        <style>
            .s1 { height: 180px; isolation: isolate; position: relative; }
            .s1 > div { height: 100px; mix-blend-mode: screen; position: absolute; width: 100px; }
            .s1 > div:nth-of-type(1) { background-color: red; left: 50px; top: 20px; }
            .s1 > div:nth-of-type(2) { background-color: lime; left: 30px; top: 40px; }
            .s1 > div:nth-of-type(3) { background-color: blue; left: 70px; top: 60px; }
        </style>
        <div class=s1><div></div><div></div><div></div></div>
        <h4>mix-blend-mode: screen 不是顏色分量相加</h4>
    </div>

    <div class=sample>
        <svg height=180 width=200>
            <filter id=s2-composite x=0 y=0 width=1 height=1>
                <feComposite in2=BackgroundImage operator=arithmetic k1=-1 k2=1 k3=1></feComposite>
            </filter>
            <filter id=s2-blend>
                <feBlend in2=BackgroundImage mode=screen></feBlend>
            </filter>
            <g enable-background=new>
                <rect width=100 height=100 x=50 y=20 fill=red></rect>
                <rect width=100 height=100 x=30 y=40 fill=lime filter=url(#s2-composite)></rect>
                <rect width=100 height=100 x=70 y=60 fill=blue filter=url(#s2-blend)></rect>
            </g>
        </svg>
        <h4>svg,僅限 ie 10+</h4>
    </div>

    <div class=sample>
        <svg height=180 width=200>
            <filter id=s2-2 x=-1 y=-1 width=2 height=2>
                <feOffset dx=-20 dy=-40></feOffset>
                <feColorMatrix result=red type=matrix values="
                               0 0 0 0 1
                               0 0 0 0 0
                               0 0 0 0 0
                               0 0 0 1 0"></feColorMatrix>
                <feOffset in=SourceGraphic dx=-40 dy=-20></feOffset>
                <feColorMatrix type=matrix values="
                               0 0 0 0 0
                               0 0 0 0 1
                               0 0 0 0 0
                               0 0 0 1 0"></feColorMatrix>
                <feBlend mode=screen in2=red></feBlend>
                <feBlend mode=screen in2=SourceGraphic></feBlend>
            </filter>
            <rect x=70 y=60 width=100 height=100 fill=blue filter=url(#s2-2)></rect>
        </svg>
        <h4>svg feBlend</h4>
    </div>

    <div class=sample>
        <canvas class=s3-1 height=180 width=200></canvas>
        <h4>canvas 2d 全局復合</h4>
        <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>
    </div>

    <div class=sample>
        <canvas class=s3-2 height=180 width=200></canvas>
        <h4>在 canvas 中把顏色分量置 1,未使用 min(x + y, 1)</h4>
        <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>
    </div>

    <div class=sample>
        <canvas class=s4 height=180 width=200></canvas>
        <h4>webgl 混合</h4>
        <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>
    </div>
</body>
</html>

參考

復合與混合級別 1
http://www.w3.org/TR/compositing/
http://dev.w3.org/fxtf/compositing/
http://dev.w3.org/fxtf/compositing-1/

svg 1.1 濾鏡
http://www.w3.org/TR/SVG11/filters.html
http://www.w3.org/TR/SVG/filters.html

濾鏡效果模塊級別 1
http://www.w3.org/TR/filter-effects-1/
http://dev.w3.org/fxtf/filters/

svg 復合
http://www.w3.org/TR/SVGCompositing/
http://dev.w3.org/SVG/modules/compositing/master/SVGCompositingPrimer.html

混合模式
http://en.wikipedia.org/wiki/Blend_modes


我在 csdn 用 markdown 編輯器發表了同名文章,發表后發現編輯時預覽的格式和發出去后的格式不太一樣,每次修改還都要審核很久,所以就放到 cnblogs 看了看。markdown 編輯器方面 cnblogs 比 csdn 好的地方在

  • 就是完全不做改動,發布出來也更好看
  • 代碼塊更好看
  • 上傳圖片很容易
  • 可以自己定義 css

也有缺點

  • 對 $$ 表示的多行數學公式(displayed formulas)的支持不如 csdn
  • 不支持有些 markdown 語法。不知道這些 csdn 支持但 cnblogs 不支持的是不是屬於擴展 markdown 語法

不一樣的地方

  • 列表 ul 內嵌列表時前面要空 4 個格,csdn 是 2 個


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM