作者: zyl910

一、缘由

Html5画布(Canvas)的上下文(Context2D)提供globalCompositeOperation属性,可用于控制图形的绘制时的合成模式。
查了一下文档,发现多达共有26种合成模式。且文字介绍很简略,部分模式看不太懂。
于是我编写了一个功能丰富的演示页面,能够随时调整globalCompositeOperation等绘制参数,且有详细信息文本框能用于分析像素合成的计算算法的等。使用该页面,能够很好的学习这26种合成模式。
本文重点介绍演示页面的功能,及开发过程中的问题处理。下一篇文章将介绍合成模式的计算算法。

二、合成说明与功能设计

2.1 MDN文档说明

下图是MDN的globalCompositeOperation属性的说明文档的截图。可见,对于每一种合成模式,只是用一段话做一下简介而已。
[Html5] 用于分析26种画布合成模式(globalCompositeOperation)的演示页面

文档上共列出了26种合成模式:

source-over
source-in
source-out
source-atop
destination-over
destination-in
destination-out
destination-atop
lighter
copy
xor
multiply
screen
overlay
darken
lighten
color-dodge
color-burn
hard-light
soft-light
difference
exclusion
hue
saturation
color
luminosity

还好每一种模式都配了一张图片范例,让人稍微有一点头绪。
每一种合成模式的附图,由3张子图片所组成,分别是“existing content”(现有内容)、“new content”(新内容)、“[name]”(合成模式的名称,如“source-over”)。即将“子图1(existing content)”的上面绘制“子图2(new content)”时,该合成模式的处理结果是“子图3([name])”。
每个子图的左上角区域,还有一个小范例,演示了 蓝红方块的合成结果。注意是在蓝色方块(子图1:existing content)的上面绘制红色方块(子图2:new content)的,且红色方块向左上角偏移了一点点,这样便于观察非重叠时的合成情况。

该文档的后半部分,提供了一段简单的JavaScript范例代码,演示了xor合成模式。摘录:

const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
ctx.globalCompositeOperation = 'xor';
ctx.fillStyle = 'blue';
ctx.fillRect(10, 10, 100, 100);
ctx.fillStyle = 'red';
ctx.fillRect(50, 50, 100, 100);

这段范例代码简洁明了,演示了globalCompositeOperation的用法。但有一个小问题——它绘制的蓝红方块的位置关系为“蓝色在红色的左上侧”,于是先前附图里“红色在蓝色的左上侧”不同,不利于对比分析之前的说明。
于是我感觉有可开发一个新的演示页面,使红蓝方块的位置关系与文档一致,并提供合成模式的下拉框,便于随时切换合成模式,观察合成结果。

2.2 统一术语

MDN文档为了使文章更好懂,尽量减少专业术语,便采用了 “existing content”、“new content”这样浅显的名词。
但这也带来一些麻烦,因为该领域的专业资料是用专业术语的,用专业术语才能使逻辑上更清晰。例如 source-over、destination-over 等合成模式的名称。

常用术语是这3个——

使用 Source、Destination等术语,最开始可能会感到比较抽象。但熟悉后,会觉得这些概念很简洁、实用。
在D(Destination)的基础上,绘制S(Source)图像,合成模式的运算用“⊙”运算符代替,合成结果为O(Output)。那么合成处理可以用以下数学式子来简洁的表示:

O = D ⊙ S

在很多时候,输出结果Output与目标Destination(现有内容,existing content)是同一个对象。例如Html5的Canva绘图,目标(Destination)是现有的上下文(Context2D),输出结果(Output)也是该上下文,只是状态不同(合成后的结果)。
故O与D其实是等价的,只是人们为了突出表达状态不同,才用到了O。所以很多时候用“D'”来代替“O”,式子为:

D' = D ⊙ S

为了进一步简写数学式子,可以将运算符(⊙)与等号(=)写在一起,即:

D ⊙= S

这类似编程语言的“复合赋值运算符”——将目标变量D与源值S进行运算,运算结果保存在目标变量D里。

2.3 演示页面功能设计

首先,能显示跟MDN文档一样的红蓝方块,便于对照文档。
提供合成模式(globalCompositeOperation)的下拉框,便于随时切换合成模式,观察合成结果。
红蓝方块能自定义颜色值。即提供文本框能随时修改 Source、Destination 的颜色值。
红蓝方块支持渐变绘制。即“to”复选框右侧的文本框能输入第2颜色进行渐变。勾选“to”复选框时启用渐变,未勾选时不渐变。
红蓝方块支持调整透明度。即“Alpha”复选框右侧的文本框能输入alpha值(值域为 0~1)。
为了解决源渐变绘制时Alpha不同问题,提供“SourceUseImage”复选框。当它复选时,会先在一个临时图片里绘制好Source,再通过globalCompositeOperation进行绘制。默认复选。
这些复选框及文本框能自动生效。具体来说,当焦点离开文本框时,会自动点击“Refresh”按钮,使配置生效。
显示点击信息的坐标及颜色。首先,会在“Current: (0, 0). Destination sample, Source sample.”这一栏显示这些信息,如“Current”是当前点击位置的颜色,其后的括号是点击坐标,“Destination sample”是对应目标像素的颜色,“Source sample”是对应源像素的颜色。
能显示点击像素的详细信息,并尝试给出该合成模式的详细计算过程。见“Current”栏下侧文本框。
除了像MDN文档那样显示 红框在蓝框左上的合成结果(compositeOffset)外,还展示同一位置的合成结果(composite),并显示了合成前的 destination、source 图。若勾选“SourceUseImage”,还会显示半透明处理前的source 图。
在页面背后放上一份颜色名称的表格,便于复制颜色名或rgb值,粘贴到自定义颜色值文本框进行测试。

下面就是演示页面的截图。
[Html5] 用于分析26种画布合成模式(globalCompositeOperation)的演示页面

2.3.1 详细信息文本框

详细信息文本框的内容范例:

Current    : RGBA(0.357, 0.106, 0.427, 0.875), Byte(91, 27, 109, 223), #5b1b6ddf, hsl(287, 0.603, 0.267). Pos(132, 94)
Destination: RGBA(0.000, 0.255, 1.000, 0.753), Byte(0, 65, 255, 192), #0041ffc0, hsl(225, 1.000, 0.500). Pos(132, 394)
Source     : RGBA(0.624, 0.000, 0.000, 0.502), Byte(159, 0, 0, 128), #9f000080, hsl(0, 1.000, 0.312). Pos(469, 431)
compositeMode:	source-over
	Fa = 1; Fb = 1 - As;	Co = As * Cs + Ab * Cb * (1 - As);	Ao = As + Ab * (1 - As)
Ro = As * Rs + Ab * Rb * (1 - As) = 0.502 * 0.624 + 0.753 * 0.000 * (1 - 0.502) = 0.313
Go = As * Gs + Ab * Gb * (1 - As) = 0.502 * 0.000 + 0.753 * 0.255 * (1 - 0.502) = 0.096
Bo = As * Bs + Ab * Bb * (1 - As) = 0.502 * 0.000 + 0.753 * 1.000 * (1 - 0.502) = 0.375
Ao = As + Ab * (1 - As) = 0.502 + 0.753 * (1 - 0.502) = 0.877
Premultiplie:RGBA(0.314, 0.094, 0.376, 0.878), Byte(80, 24, 96, 224), #501860e0, hsl(287, 0.600, 0.235)
Output     : RGBA(0.357, 0.110, 0.427, 0.878), Byte(91, 28, 109, 224), #5b1c6de0, hsl(287, 0.591, 0.269)

说明——

注——

  1. Html5的画布,总是使用直通Alpha(Straight Alpha)模式。因运算公式的中间结果是预乘Alpha(Premultiplie Alpha)的,故最终输出时,需做“预乘Alpha 转 直通Alpha”的转换。
  2. Output计算结果,理应与Current相同的,而有时会发现字节值会有 12的误差。这是因为Chrome在运算过程中可能用到了低精度整数运算等速度优化手段,而本页面严格按照公式,且使用高精度的浮点运算。对于有256种值的8位色彩通道来说,有字节值12的误差,其实只是 2/256=1/128≈0.781% 的误差,人眼看不出差别,故这些速度优化处理很常见。
  3. 只是对常用混合模式,编写了了公式与计算过程。有些混合模式尚没有编写内容。

三、问题处理经验

在演示页面的开发过程中,遇到了一些事先没想到的问题。现在分享一下处理经验。

3.1 源渐变绘制时Alpha不同问题

勾选Source的Alpha复选框,并设为“0.5”,若未启用渐变(Source的“to”复选框 未勾选时),可观察到此时绘制的Source区域是正常的,各像素的Alpha为0.5左右。
若启用启用渐变(Source的“to”复选框 被勾选时),可观察到Source区域的Alpha不太对劲,各像素的Alpha为0.75左右。

为了解决源渐变绘制时Alpha不同问题,提供“SourceUseImage”复选框。当它复选时,会先在一个临时图片里绘制好Source,再通过globalCompositeOperation进行绘制。默认复选。
“SourceUseImage”勾选时,可观察到Source区域的Alpha仍是0.5左右,与配置的值相符。

3.2 Image.onload事件是异步触发的

因“SourceUseImage”复选框,故需要先在另一块区域绘制源图,且需要将它转为Image对象,这样能便于使用 drawImage 进行绘图。
使用Image时要注意,它的加载处理是异步的。
若在设置了Image.src后立即进行绘图,会发现大多数时候是空的。
为了解决这一问题,应处理onload事件,该事件触发时才表示已加载完毕,可进行后续的绘图等操作。

代码摘录:

// sourceUseImage
let sourceImage = null;
if (sourceUseImage) {
	try{
		//let canvasTemp = document.createElement("canvas");
		let canvasTemp = document.getElementById('canvasTemp');
		canvasTemp.style = "display:block";
		canvasTemp.width = blockWidth;
		canvasTemp.height = blockHeight;
		//canvas.getContext("2d").drawImage(image, 0, 0);
		let ctxTemp = canvasTemp.getContext("2d");
		ctxTemp.save();
		ctxTemp.clearRect(0, 0, blockWidth, blockHeight);
		//ctxTemp.globalAlpha = alphaS;
		drawRectS(ctxTemp, 0, 0, blockWidth, blockHeight, sColor0, sColor1);
		ctxTemp.restore();
		// to image.
		sourceImage = new Image();
		sourceImage.onload = function() {
			doRefresh_draw(sourceImage);
		}
		sourceImage.src = ctxTemp.canvas.toDataURL("image/png");
	} catch(ex) {
		sourceImage = null;
		console.log("Make sourceImage fail! ", ex);
	}
}
//console.log("sourceUseImage: ", sourceUseImage, "sourceImage: ", sourceImage);
if (null!=sourceImage) return;
let canvasTemp = document.getElementById('canvasTemp');
canvasTemp.style = "display:none";
doRefresh_draw(sourceImage);

3.3 部分合成模式会将区域外的颜色均清除为透明的

使用 source-out、destination-out 等合成模式时,不仅影响了Sourcet覆盖的区域,且会将区域外的颜色均清除为透明的。
若画布里只需绘制Sourcet,这种情况还可接收。但若是画布里还有其他内容,这种情况会将区域外的其他内容均清理,变为透明。
例如本演示页面上会绘制 compositeOffset、composite、destination、source 这四类图形。因composite是最后进行合成绘制的,当选择使用 source-out、destination-out 等合成模式时,会将compositeOffset、destination、source 的内容均清除,仅保留composite的。

为了解决这一问题,需要在合成绘制前,设置好剪裁区域。
对于Html5画布来说,剪裁功能是这样使用的:先调用beginPath方法开启路径,随后进行rect等操作添加路径形状,最后调用clip将路径转为剪裁区域。
另外为了在处理后恢复为未剪裁的最初环境,可利用Html5画布的 save/restore 机制。save方法用于在处理前保存环境,restore方法用于在处理后回复环境。

代码摘录:

// Top Left: compositeOffset
//ctx.globalCompositeOperation = "source-over";
ctx.save();
ctx.globalAlpha = alphaD;
drawRectD(ctx, blockLeft, blockTop, blockWidth, blockHeight, dColor0, dColor1);
if (useClip) {
	ctx.beginPath();
	ctx.rect(0, 0, blockWidth*2, blockHeight*2);
	ctx.clip();
}
ctx.globalCompositeOperation = compositeModeLast;
ctx.globalAlpha = alphaS;
if (null==sourceImage) {
	drawRectS(ctx, blockLeft-blockOffsetX, blockTop-blockOffsetY, blockWidth, blockHeight, sColor0, sColor1);
} else {
	ctx.drawImage(sourceImage, blockLeft-blockOffsetX, blockTop-blockOffsetY);
}
ctx.restore();

四、小结

源码地址:
https://github.com/zyl910/zhtml5info/blob/master/src/canvas/CanvasComposite.htm

参考文献

发表回复