玩转 Skia 的颜色
颜色是一个渲染引擎最基本的概念之一,Skia 为开发者处理颜色提供了一系列类型和工具。
Skia 项目中常用的颜色类型和工具,比如:如何使用 Skia 源码内置的预定义颜色、如何通过不同的方式设置颜色,如何混合两个颜色,如何格式化颜色等内容
预定义颜色
// 透明色
SkColor SK_ColorTRANSPARENT
// 黑色
SkColor SK_ColorBLACK
// 深灰 dark gray(DKGRAY).
SkColor SK_ColorDKGRAY
// 灰色
SkColor SK_ColorGRAY
// 浅灰 ligth gray(LTGRAY)
SkColor SK_ColorLTGRAY
// 白色
SkColor SK_ColorWHITE
// 红色
SkColor SK_ColorRED
// 绿色
SkColor SK_ColorGREEN
// 蓝色
SkColor SK_ColorBLUE
// 黄色
SkColor SK_ColorYELLOW
// 青色
SkColor SK_ColorCYAN
// 洋红
SkColor SK_ColorMAGENTA
注意,这里定义的颜色并不都与 HTML、CSS 或 SVG 里定义的颜色相同,有些颜色是有区别得,比如 Skia 定义的灰色与 CSS 定义的灰色并不是同一个颜色值(略有差异,但差异不大)。
根据颜色名称获取颜色
根据颜色的名称获取具体的颜色值,如下代码所示:
// #include "include/utils/SkParse.h"
SkColor color;
SkParse::FindColor("gold", &color);
SkPaint paint;
paint.setColor(color);
SkParse
类型在 include/utils/SkParse.h
头文件中定义。
这个类型的静态方法:FindColor
用于根据颜色名称获取颜色值。
使用十六进制数字设置颜色
SkColor 是一个无符号 32 位整型值
,所以下面这种做法也是一种常见的颜色定义做法:
SkColor color = 0xFF123456;
SkPaint paint;
paint.setColor(0xFF123456);
上述代码中颜色值从左到右,0x
代表这是一个十六进制数值,
FF
是颜色的透明度分量,FF
代表不透明,00
代表透明。
123456
代表一个颜色值,这里使用的颜色值与 CSS/SVG 中使用的颜色值是一样的。
使用颜色分量设置颜色
除了以这种方式定义颜色之外,还可以用以下方式定义一个颜色,如下代码所示:
paint.setColor(SkColorSetARGB(255,11,22,33));
SkColorSetARGB
方法的第一个参数是颜色的透明度
分量,第二个参数是颜色的红色
分量,第三个参数是颜色的绿色
分量,第四各参数为颜色的蓝色
分量,四个参数的值均为 0 到 255 之间的值。
颜色计算
你可能觉得既然 SkColor 是 uint32_t 类型的值,那么几个颜色的平均值就这几个颜色值的和再除以颜色的数量呗?!
不是的!要计算颜色的平均值,就必须对颜色的每个通道分量计算平均值才能得到真正的颜色平均值。
如下代码用于计算几个颜色的平均值:
void averageColor(SkCanvas* canvas) {SkColor colorArr[6]{ 0xFF123456,0xFF654321,0xFF789ABC,0xFFABC789,0xFFDEF123,0xFF123DEF };SkColor4f averageColor{ SkColors::kTransparent };for (size_t i = 0; i < 6; i++){auto tempColor = SkColor4f::FromColor(colorArr[i]);averageColor.fA += tempColor.fA;averageColor.fR += tempColor.fR;averageColor.fG += tempColor.fG;averageColor.fB += tempColor.fB;}averageColor.fR /= 6;averageColor.fG /= 6;averageColor.fB /= 6;averageColor.fA /= 6;//auto color = averageColor.toSkColor();canvas->clear(averageColor);
}
上面的示例中,使用了新的颜色类型:SkColor4f
这个类型是一个对象类型,独立持有 A、R、G、B 四个通道分量的值。每个分量的值都是从 0 到 1 取值的小数。
有了这个颜色类型就可以根据不同的颜色分量计算颜色的值了。
用 SkColor4f::FromColor 方法把一个 SkColor 转型成 SkColor4f 对象。
用 SkColor4f 对象的 toSkColor 方法,把一个 SkColor4f 对象转型成 SkColor 值。
融合颜色
两个半透明颜色叠加会得到一个怎样的颜色呢?
这个颜色既不是两个颜色的平均值,也不是两个颜色的和。
最简单的办法就是创建一个仅有一个像素的画布,
然后在这个画布上先后绘制这两个半透明颜色,
最终这个点上是什么颜色,就是叠加后的颜色值,示例代码如下所示:
// #include "include/core/SkBitmap.h"void colorOverlay(SkCanvas* canvas) {SkBitmap bitmap;bitmap.allocN32Pixels(1, 1);SkCanvas tempCanvas(bitmap);tempCanvas.drawColor(0x88DD3456);tempCanvas.drawColor(0x88654321);auto result = bitmap.getColor(0, 0);canvas->clear(result);
}
这段代码有两点值得注意:
-
SkCanvas 对象是基于 SkBitmap 对象创建的 与 SkPixmap 相同,SkBitmap 对象也是一个用来描述图像的对象; 与 SkPixmap 不同的是 SkBitmap 则持有并负责管理图像的二进制数据。 所有使用 tempCanvas 绘制的内容,都将影响 bitmap 管理的像素数据。 这是迄今为止介绍的第三种创建SkCanvas对象的方法。 第一种方法是调用 SkSurface 对象的 getCanvas 方法获取的 SkCanvas 对象。 第二种方法是使用 SkCanvas 的静态方法 MakeRasterDirect ,基于像素数据创建SkCanvas对象。
-
SkBitmap 对象的 getColor 方法用于获取指定位置的像素颜色,因为bitmap 对象仅有一个像素,所以此像素的位置就是(0,0)
颜色格式化
有的时候需要把颜色格式化成字符串供用户使用。如下代码演示了如何把一个颜色值格式化成 RGBA 字符串:
// #include <format>auto color = SkColor4f::FromColor(0x99887766);
int R{ (int)(color.fR * 255) }, G{ (int)(color.fG * 255) }, B{ (int)(color.fB * 255) },A{ (int)(color.fA * 255) };
auto colorStr = std::format("RGBA: {},{},{},{}", R, G, B, A); // RGBA: 136,119,102,153
这段代码首先把一个 SkColor 值转型成 SkColor4f 对象。接着把 SkColor4f 对象的 R、G、B、A 分量从 01 的小数转型成 0255 的整数。
最后通过标准库的 std::format
把 4 个颜色分量转型成字符串。
最终得到的 colorStr 字符串的值是:RGBA: 136,119,102,153
接下来看如何把颜色值转型成HEX字符串
,代码如下所示:
// #include <format>
// #include <sstream>auto color = SkColor4f::FromColor(0x99887766);
int R{ (int)(color.fR * 255) }, G{ (int)(color.fG * 255) }, B{ (int)(color.fB * 255) },A{ (int)(color.fA * 255) };
std::stringstream ss;
ss << std::hex << ((R << 24) | (G << 16) | (B << 8) | A);;
std::wstring hex = ss.str();
std::transform(hex.begin(), hex.end(), hex.begin(), toupper);
colorStr = std::format("HEX: #{}", hex); //HEX: #88776699
这段代码使用字符串流的方式,把 SkColor4f 对象的 4 个颜色分量输入到 std::wstringstream 流中(注意分量值的移位操作)。
使用std::transform方法把生成的字符串转型成大写字符串。
最终得到的字符串的值为:HEX: #88776699
颜色空间
SkColorSpace用于表示颜色空间,颜色空间用于确定图像的颜色类型和透明度计算方式。
以SkImageInfo::MakeN32Premul(w, h)为例,它创建的 SkImageInfo 对象的颜色类型为:kBGRA_8888_SkColorType,
这个颜色类型表示每个像素由四个 8 位分量组成(R、G、B、A),每个分量的取值范围为从 0 到 255,
透明度计算方式为:kPremul_SkAlphaType,表示透明度分量预先与其他颜色分量(R、G、B)相乘。
Skia 在 include\core\SkColorType.h 头文件中定义了 SkColorType 枚举,
这个枚举类型中包含很多颜色类型,比如:kRGBA_8888_SkColorType、kBGRA_8888_SkColorType、kARGB_4444_SkColorType等。
Skia 在 include\core\SkAlphaType.h 头文件中定义了 SkAlphaType 枚举,用于表示不同的透明度分量计算方式。
如下是两种创建颜色空间的方式
sk_sp<SkColorSpace> srgbColorSpace = SkColorSpace::MakeSRGB();
sk_sp<SkColorSpace> linearSrgbColorSpace = SkColorSpace::MakeSRGBLinear();
无论使用上述代码中哪个颜色空间,示例的运行结果都没什么异样(肉眼看不出)。
如果你没能理解这部分的知识,那也并没什么大碍,等以后工作中需要用到时,再细细研究它们也不迟。
合成模式
有的时候需要在画布的同一块区域绘制不同的图像,这就需要考虑以什么合成方式绘制这些图像了,
Skia中定义了很多合成模式,如下所示(图像来源于 Skia 官网):
上图中src是源内容
,指你正在绘制的内容,dst是目标内容
,指已经绘制到画布上的内容。
当绘制几何图形时,透明的部分不会更改画布上几何图形之外的内容。这使得绘制操作更加高效,因为不需要处理透明度遮罩之外的像素。
当绘制图像数据时,透明度的处理方式可能会有所不同,因为图像数据通常包含每个像素的透明度信息。
在这种情况下,Skia 会根据 SkBlendMode
的设置来混合透明图像像素和目标像素(效率更低)。
如下代码演示了一种图像合成的方式。
void drawBlendMode(SkCanvas* canvas) {canvas->clear(0); //todo canvas->drawColor(0xFFDDDDDD);SkPaint paint;paint.setColor(0xFF00FFFF);auto rect1 = SkRect::MakeLTRB(60, h / 2 - 30, w - 60, h / 2 + 30);canvas->drawRect(rect1, paint); //目标内容paint.setColor(0xFFFFFF00);paint.setBlendMode(SkBlendMode::kSrcOut);auto rect2 = SkRect::MakeLTRB(w / 2 - 30, 60, w / 2 + 30, h - 60);canvas->drawRect(rect2, paint); //源内容
}
上述代码中 paint 对象的 setBlendMode
方法用于设置合成模式。
示例中设置的合成模式为 SkBlendMode::kSrcOut
,
此模式为将显示 源内容
不被 目标内容
覆盖的部分(覆盖的部分既不显示源内容
也不显示目标内容
)。
最终运行的效果如下图所示:
如下代码演示了如何使用 SkBlendMode::kClear 模式擦除一个圆的中心区域部分:
void drawEraser(SkCanvas* canvas) {SkPaint paint;paint.setAntiAlias(true);paint.setColor(0xFF00FFFF);auto r = std::min(w/2-60, h/2-60);canvas->drawCircle(w/2,h/2,r,paint);paint.setBlendMode(SkBlendMode::kClear);canvas->drawRect(SkRect::MakeXYWH(w / 2 - 50, h / 2 - 50,100,100), paint);
}
最终运行的效果如下图所示:
如果要开发一款画图软件,使用 SkBlendMode::kClear
来实现橡皮擦功能。
颜色是绘图的基础,也是 Skia 的重要组成部分.