草庐IT

[Html5] 用于分析26种画布合成模式(globalCompositeOperation)的演示页面

zyl910 2023-03-28 原文

作者: zyl910

一、缘由

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

二、合成说明与功能设计

2.1 MDN文档说明

下图是MDN的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(源):待绘制的内容。一般缩写为“S”。即MDN文档里的 子图1“existing content”(现有内容)。
  • Destination(目标):绘制的目标。一般缩写为“D”。即MDN文档里的 子图2“new content”(新内容)。
  • Output(输出结果):合成后的结果。一般缩写为“O”。即MDN文档里的 子图3“[name]”(合成模式的名称,如“source-over”)。

使用 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值,粘贴到自定义颜色值文本框进行测试。

下面就是演示页面的截图。

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)

说明——

  • Current:当前点击位置的颜色信息及坐标信息。格式为“RGBA、Byte、#rrggbbaa、hsl、Pos”,即分别为“归一化的RGBA值、各分量的字节值、十六进制表示的颜色值、hsl格式的颜色值、点击坐标”。
  • Destination:当前点击位置对应目标像素的颜色信息及坐标信息。格式为“RGBA、Byte、#rrggbbaa、hsl、Pos”,即分别为“归一化的RGBA值、各分量的字节值、十六进制表示的颜色值、hsl格式的颜色值、点击坐标”。后面公式里用小写的“b”或“d”来表示目标像素。
  • Source:当前点击位置对应源像素的颜色信息及坐标信息。格式为“RGBA、Byte、#rrggbbaa、hsl、Pos”,即分别为“归一化的RGBA值、各分量的字节值、十六进制表示的颜色值、hsl格式的颜色值、点击坐标”。后面公式里用小写的“s”来表示源像素。
  • compositeMode:合成模式。
  • 公式。为了表示在引用公式,且为了便于与其他内容隔开,公式的的前面加了多个空格。
  • Ro、Go、Bo、Ao:分别显示 R、G、B、A 通道的计算过程。
  • Premultiplie:显示原始计算结果,它是 预乘Alpha(Premultiplie Alpha)模式的颜色值。格式为“RGBA、Byte、#rrggbbaa、hsl”,即分别为“归一化的RGBA值、各分量的字节值、十六进制表示的颜色值、hsl格式的颜色值”。
  • Output:显示计算结果,它是 直通Alpha(Straight Alpha)模式的颜色值。格式为“RGBA、Byte、#rrggbbaa、hsl”,即分别为“归一化的RGBA值、各分量的字节值、十六进制表示的颜色值、hsl格式的颜色值”。

注——

  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

参考文献

有关[Html5] 用于分析26种画布合成模式(globalCompositeOperation)的演示页面的更多相关文章

  1. ruby-on-rails - Rails - 子类化模型的设计模式是什么? - 2

    我有一个模型:classItem项目有一个属性“商店”基于存储的值,我希望Item对象对特定方法具有不同的行为。Rails中是否有针对此的通用设计模式?如果方法中没有大的if-else语句,这是如何干净利落地完成的? 最佳答案 通常通过Single-TableInheritance. 关于ruby-on-rails-Rails-子类化模型的设计模式是什么?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.co

  2. ruby - 使用 ruby​​ 将 HTML 转换为纯文本并维护结构/格式 - 2

    我想将html转换为纯文本。不过,我不想只删除标签,我想智能地保留尽可能多的格式。为插入换行符标签,检测段落并格式化它们等。输入非常简单,通常是格式良好的html(不是整个文档,只是一堆内容,通常没有anchor或图像)。我可以将几个正则表达式放在一起,让我达到80%,但我认为可能有一些现有的解决方案更智能。 最佳答案 首先,不要尝试为此使用正则表达式。很有可能你会想出一个脆弱/脆弱的解决方案,它会随着HTML的变化而崩溃,或者很难管理和维护。您可以使用Nokogiri快速解析HTML并提取文本:require'nokogiri'h

  3. ruby-on-rails - Rails 常用字符串(用于通知和错误信息等) - 2

    大约一年前,我决定确保每个包含非唯一文本的Flash通知都将从模块中的方法中获取文本。我这样做的最初原因是为了避免一遍又一遍地输入相同的字符串。如果我想更改措辞,我可以在一个地方轻松完成,而且一遍又一遍地重复同一件事而出现拼写错误的可能性也会降低。我最终得到的是这样的:moduleMessagesdefformat_error_messages(errors)errors.map{|attribute,message|"Error:#{attribute.to_s.titleize}#{message}."}enddeferror_message_could_not_find(obje

  4. ruby - 解析 RDFa、微数据等的最佳方式是什么,使用统一的模式/词汇(例如 schema.org)存储和显示信息 - 2

    我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i

  5. ruby - 如何在续集中重新加载表模式? - 2

    鉴于我有以下迁移:Sequel.migrationdoupdoalter_table:usersdoadd_column:is_admin,:default=>falseend#SequelrunsaDESCRIBEtablestatement,whenthemodelisloaded.#Atthispoint,itdoesnotknowthatusershaveais_adminflag.#Soitfails.@user=User.find(:email=>"admin@fancy-startup.example")@user.is_admin=true@user.save!ende

  6. ruby-on-rails - Rails HTML 请求渲染 JSON - 2

    在我的Controller中,我通过以下方式在我的index方法中支持HTML和JSON:respond_todo|format|format.htmlformat.json{renderjson:@user}end在浏览器中拉起它时,它会自然地以HTML呈现。但是,当我对/user资源进行内容类型为application/json的curl调用时(因为它是索引方法),我仍然将HTML作为响应。如何获取JSON作为响应?我还需要说明什么? 最佳答案 您应该将.json附加到请求的url,提供的格式在routes.rb的路径中定义。这

  7. ruby-on-rails - 使用 Sublime Text 3 突出显示 HTML 背景语法中的 ERB? - 2

    所以我在关注Railscast,我注意到在html.erb文件中,ruby代码有一个微弱的背景高亮效果,以区别于其他代码HTML文档。我知道Ryan使用TextMate。我正在使用SublimeText3。我怎样才能达到同样的效果?谢谢! 最佳答案 为SublimeText安装ERB包。假设您安装了SublimeText包管理器*,只需点击cmd+shift+P即可获得命令菜单,然后键入installpackage并选择PackageControl:InstallPackage获取包管理器菜单。在该菜单中,键入ERB并在看到包时选择

  8. Ruby Sinatra 配置用于生产和开发 - 2

    我已经在Sinatra上创建了应用程序,它代表了一个简单的API。我想在生产和开发上进行部署。我想在部署时选择,是开发还是生产,一些方法的逻辑应该改变,这取决于部署类型。是否有任何想法,如何完成以及解决此问题的一些示例。例子:我有代码get'/api/test'doreturn"Itisdev"end但是在部署到生产环境之后我想在运行/api/test之后看到ItisPROD如何实现? 最佳答案 根据SinatraDocumentation:EnvironmentscanbesetthroughtheRACK_ENVenvironm

  9. ruby-on-rails - Ruby url 到 html 链接转换 - 2

    我正在使用Rails构建一个简单的聊天应用程序。当用户输入url时,我希望将其输出为html链接(即“url”)。我想知道在Ruby中是否有任何库或众所周知的方法可以做到这一点。如果没有,我有一些不错的正则表达式示例代码可以使用... 最佳答案 查看auto_linkRails提供的辅助方法。这会将所有URL和电子邮件地址变成可点击的链接(htmlanchor标记)。这是文档中的代码示例。auto_link("Gotohttp://www.rubyonrails.organdsayhellotodavid@loudthinking.

  10. ruby - 是否有用于序列化和反序列化各种格式的对象层次结构的模式? - 2

    给定一个复杂的对象层次结构,幸运的是它不包含循环引用,我如何实现支持各种格式的序列化?我不是来讨论实际实现的。相反,我正在寻找可能会派上用场的设计模式提示。更准确地说:我正在使用Ruby,我想解析XML和JSON数据以构建复杂的对象层次结构。此外,应该可以将该层次结构序列化为JSON、XML和可能的HTML。我可以为此使用Builder模式吗?在任何提到的情况下,我都有某种结构化数据-无论是在内存中还是文本中-我想用它来构建其他东西。我认为将序列化逻辑与实际业务逻辑分开会很好,这样我以后就可以轻松支持多种XML格式。 最佳答案 我最

随机推荐