swift-colorful

一个用于在 Swift 中处理颜色的库。支持 Swift 5.6 及更高版本。

此库由 Artur Torun (又名 Mojzesh) 从 Golang 移植到 Swift

原始库名为 go-colorful,由 Lucas Beyer 编写,可以在这里找到:https://github.com/lucasb-eyer/go-colorful

注意: 以下大部分文本和示例均从 README.md 文件复制而来:https://github.com/lucasb-eyer/go-colorful

为什么?

我制作了这个库的 Swift 移植版本,因为我正在将我的 3D 图形引擎从 Go + Vulkan 迁移到 Swift + Metal。我喜欢这个库,并对其精湛的工艺印象深刻,而且我真的认为它在许多应用中都很有用。我找不到任何 Swift 中的库可以媲美它。我花了一天时间移植了 80% 的代码,然后又花了一个星期移植了剩下的部分(其中排序是最棘手的部分)。与原始实现相比,唯一缺少的是对 HEX 颜色编码/解码的支持,其中输入/输出是 SQL 数据库或 JSON 处理器。但是您仍然可以基于 web HEX RGB 字符串创建颜色(支持短格式和完整格式)。

这是 go-colorful 的原始作者所说的

我热爱游戏。我制作游戏。我热爱细节,并沉迷于细节。在开发 Memory Which Does Not Suck 时,就出现了一个这样的细节,当时我们希望服务器为玩家分配随机颜色。有时两个玩家得到的颜色非常相似,这让我感到困扰。就在当天晚上,I want hue 登上了 HackerNews 的头版,并向我展示了如何正确地做到这一点™。最后但并非最不重要的一点是,go 中没有可用的颜色空间库。Colorful 正是做到了这一点,并实现了 Go 的 color.Color 接口。

是什么?

Go-Colorful 将颜色存储在 RGB 中,并提供从这些颜色转换为各种颜色空间的方法。目前支持的颜色空间有

对于有意义的颜色空间(XYZ、Lab、Luv、HCl),D65 默认用作参考白点,但也提供了使用您自己的参考白点的方法。

坐标几乎在某个范围内意味着通常是这样,但对于非常明亮的颜色,并且取决于参考白点,它可能会稍微超出此范围。例如,#0000ff 的 C* 为 1.338。

提供了单元测试。

很好,但这有什么用呢?

那么我应该使用哪个颜色空间呢?

这取决于您想做什么。我认为 I want hue 的人们说得很有道理,RGB 适合屏幕产生颜色的方式,CIE L*a*b* 适合人类感知颜色的方式,而 HCL 适合人类思考颜色的方式。

每当您要使用 HSV 时,最好选择 CIE-L*C*h°。对于固定的亮度 L* 和色度 C* 值,色相角 h° 会旋转通过具有相同感知亮度和强度的颜色。

如何使用?

安装

安装库非常简单:A) 从 CLI,将以下代码片段添加到您的 Package.swift 文件中

B) 在 Xcode 中,只需使用

然后可以通过以下方式使用该软件包

import Colorful

从 CLI 运行演示项目

运行测试

运行基准测试

基本用法

使用不同的源空间创建美丽的蓝色

import Colorful

// Any of the following should be the same
var c = Color(R: 0.313725, G: 0.478431, B: 0.721569)
do {
    c = try Color.Hex("#517AB8")
} catch let error {
    print("\(error)")
}

c = Color.Hsv(H: 216.0,    S:  0.56,     V:  0.722)
c = Color.Xyz(x: 0.189165, y:  0.190837, z:  0.480248)
c = Color.Xyy(x: 0.219895, y:  0.221839, Y:  0.190837)
c = Color.Lab(l: 0.507850, a:  0.040585, b: -0.370945)
c = Color.Luv(l: 0.507849, u: -0.194172, v: -0.567924)
c = Color.Hcl(h: 276.2440, c:  0.373160, l:  0.507849)

let (r, g, b) = c.Values()
print(String(format: "RGB values: %f, %f, %f", r, g, b))

let (r255, g255, b255) = c.RGB255()
print(String(format: "RGB values: %i, %i, %i", r255, g255, b255))

然后将此颜色转换回各种颜色空间

let hex := c.Hex()
let (h, s, v) = c.Hsv()
let (x, y, z) = c.Xyz()
let (x, y, Y) = c.Xyy()
let (l, a, b) = c.Lab()
let (l, u, v) = c.Luv()
let (h, c, l) = c.Hcl()

该库试图使函数命名约定尽可能接近原始 go-colorful 实现。

比较颜色

在 RGB 颜色空间中,颜色之间的欧几里得距离对应于视觉/感知距离。这意味着在 RGB 空间中具有相同距离的两对颜色可能看起来相差甚远。CIE-L*a*b*、CIE-L*u*v* 和 CIE-L*C*h° 颜色空间解决了这个问题。因此,您应该只在这些空间中的任何一个中比较颜色。(请注意,CIE-L*a*b* 和 CIE-L*C*h° 中的距离是相同的,因为它是相同的空间,但采用圆柱坐标系)

Color distance comparison

顶部显示的两种颜色看起来比底部显示的两种颜色差异更大。尽管如此,在 RGB 空间中,它们的距离是相同的。这是一个小示例程序,显示了顶部两种颜色和底部两种颜色在 RGB、CIE-L*a*b* 和 CIE-L*u*v* 空间中的距离。您可以在 Sources/Demos/ColorDist/main.swift 中找到它。

import Foundation

import Colorful

let c1a = Color(R: 150.0 / 255.0, G: 10.0  / 255.0, B: 150.0 / 255.0)
let c1b = Color(R: 53.0  / 255.0, G: 10.0  / 255.0, B: 150.0 / 255.0)
let c2a = Color(R: 10.0  / 255.0, G: 150.0 / 255.0, B: 50.0  / 255.0)
let c2b = Color(R: 99.9  / 255.0, G: 150.0 / 255.0, B: 10.0  / 255.0)

print(String(format: "DistanceRgb:       c1: %.17f\tand c2: %.17f", c1a.DistanceRgb(c1b), c2a.DistanceRgb(c2b)))
print(String(format: "DistanceLab:       c1: %.17f\tand c2: %.17f", c1a.DistanceLab(c1b), c2a.DistanceLab(c2b)))
print(String(format: "DistanceLuv:       c1: %.17f\tand c2: %.17f", c1a.DistanceLuv(c1b), c2a.DistanceLuv(c2b)))
print(String(format: "DistanceCIE76:     c1: %.17f\tand c2: %.17f", c1a.DistanceCIE76(c1b), c2a.DistanceCIE76(c2b)))
print(String(format: "DistanceCIE94:     c1: %.17f\tand c2: %.17f", c1a.DistanceCIE94(c1b), c2a.DistanceCIE94(c2b)))
print(String(format: "DistanceCIEDE2000: c1: %.17f\tand c2: %.17f", c1a.DistanceCIEDE2000(c1b), c2a.DistanceCIEDE2000(c2b)))

运行上面的程序表明您应该始终首选任何 CIE 距离

$ swift run DemoColorDist-macOS
Building for debugging...
[3/3] Linking DemoColorDist-macOS
Build complete! (0.38s)
DistanceRgb:       c1: 0.38039215686274508      and c2: 0.38587139311711588
DistanceLab:       c1: 0.32042638877384100      and c2: 0.24395387956805947
DistanceLuv:       c1: 0.51331140446133072      and c2: 0.25683706276058060
DistanceCIE76:     c1: 0.32042638877384100      and c2: 0.24395387956805947
DistanceCIE94:     c1: 0.19795102869625045      and c2: 0.12206359588444481
DistanceCIEDE2000: c1: 0.17271531164545623      and c2: 0.10664280425514172

它还表明 DistanceLab 更正式的名称是 DistanceCIE76,并且已被稍微更准确但成本更高的 DistanceCIE94DistanceCIEDE2000 取代。

请注意,提供 AlmostEqualRgb 主要用于(单元)测试目的。仅在您真正知道自己在做什么时才使用它。它会吃掉您的猫。

混合颜色

混合与距离高度相关,因为它基本上“穿过”颜色空间,因此如果颜色空间很好地映射了距离,则过渡是“平滑的”。

Colorful 带有 RGB、HSV 和任何 LAB 空间中的混合函数。当然,您更希望使用 LAB 空间的混合函数,因为这些空间可以很好地映射距离,但以防万一,这里有一个示例向您展示了如何在各种空间中完成混合(#fdffcc#242a42

Blending colors in different spaces.

您看到的是 HSV 非常糟糕:它添加了一些绿色,而绿色根本不存在于原始颜色中!RGB 好得多,但它保持浅色状态的时间有点太长。LUV 和 LAB 都达到了正确的亮度,但 LAB 的颜色更多一些。HCL 的工作方式与 HSV 相同(都是圆柱插值),但它做得正确,因为没有出现绿色,并且亮度以线性方式变化。

虽然这看起来都不错,但您需要知道一件事:在任何 CIE 颜色空间中进行插值时,您可能会得到无效的 RGB 颜色!如果起始颜色和结束颜色是用户输入或随机的,这一点很重要。一个发生这种情况的示例是在 #eeef61#1e3140 之间混合时

Invalid RGB colors may crop up when blending in CIE spaces.

您可以通过调用 IsValid 方法来测试颜色是否是有效的 RGB 颜色,并且实际上,对底部偏红的颜色调用 IsValid 将返回 false。一种“修复”此问题的方法是调用 Clamped 获得接近无效颜色的有效颜色,Clamped 始终返回附近的有效颜色。这样做,我们得到了以下令人满意的结果

Fixing invalid RGB colors by clamping them to the valid range.

以下是创建上述三张图像的代码;可以在 Sources/Demos/ColorBlend/main.swift 中找到它

import Foundation
import AppKit

import Colorful
import DemoShared

let blocks = 10
let blockw = 40

let (image, imageRep) = createImageRep(NSSize(width: blocks*blockw, height: 200))

let c1 = try! Color.Hex("#fdffcc")
let c2 = try! Color.Hex("#242a42")

// Use these colors to get invalid RGB in the gradient.
// let c1 = try! Color.Hex("#EEEF61")
// let c2 = try! Color.Hex("#1E3140")

var col = Color()
for i in 0..<blocks {
    col = c1.BlendHsv(c2: c2, t: Float64(i)/Float64(blocks-1))
    drawRect(imageRep: imageRep, rect: NSRect(x: i*blockw, y: 0, width: blockw, height: blockw), color: col)
    col = c1.BlendLuv(c2: c2, t: Float64(i)/Float64(blocks-1))
    drawRect(imageRep: imageRep, rect: NSRect(x: i*blockw, y: 40, width: blockw, height: blockw), color: col)
    col = c1.BlendRgb(c2: c2, t: Float64(i)/Float64(blocks-1))
    drawRect(imageRep: imageRep, rect: NSRect(x: i*blockw, y: 80, width: blockw, height: blockw), color: col)
    col = c1.BlendLab(c2: c2, t: Float64(i)/Float64(blocks-1))
    drawRect(imageRep: imageRep, rect: NSRect(x: i*blockw, y: 120, width: blockw, height: blockw), color: col)
    col = c1.BlendHcl(c2: c2, t: Float64(i)/Float64(blocks-1))
    drawRect(imageRep: imageRep, rect: NSRect(x: i*blockw, y: 160, width: blockw, height: blockw), color: col)

    // This can be used to "fix" invalid colors in the gradient.
    // col = c1.BlendHcl(c2: c2, t: Float64(i)/Float64(blocks-1)).Clamped()
    // drawRect(imageRep: imageRep, rect: NSRect(x: i*blockw,y: 160,width: blockw, height: blockw), color: col)
}

_ = savePNG(image: image, path: "Sources/Demos/ColorBlend/colorblend.png")

生成颜色渐变

混合颜色非常常见的原因是创建渐变。在 Sources/Demos/GradientGen/main.swift 中有一个示例程序;它没有使用任何以前的示例代码中没有使用过的 API,所以我不会费心在这里粘贴代码。只需看看它在 HCL 空间中生成的华丽渐变

"Spectral" colorbrewer gradient in HCL space.

获取随机颜色

有时需要生成随机颜色。您可以简单地通过生成具有随机值的颜色来自己完成此操作。通过将随机值限制在小于 [0..1] 的范围内,并使用 CIE-H*C*l° 或 HSV 等空间,您可以生成颜色的随机色调或亮度的随机颜色

let random_blue =  Color.Hcl(h: 180.0+randomFloat64()*50.0, c: 0.2+randomFloat64()*0.8, l: 0.3+randomFloat64()*0.7)
let random_dark =  Color.Hcl(h: randomFloat64()*360.0,      c: randomFloat64(),         l: randomFloat64()*0.4)
let random_light = Color.Hcl(h: randomFloat64()*360.0,      c: randomFloat64(),         l: 0.6+randomFloat64()*0.4)

由于获取随机的“温暖”和“快乐”的颜色是一项非常常见的任务,因此有一些辅助函数

WarmColor()
HappyColor()
FastWarmColor()
FastHappyColor()

Fast 为前缀的函数速度更快,但一致性较差,因为它们使用 HSV 空间,而不是使用 CIE-L*C*h° 空间的常规函数。下图显示了顶部两行中的暖色和底部两行中的快乐色。在这些颜色中,第一个是常规的,第二个是快速的。

Warm, fast warm, happy and fast happy random colors, respectively.

不要忘记初始化随机种子!您可以在 Sources/Demos/ColorGens/main.swift 中查看用于生成此图片的代码。

获取随机调色板

一旦您需要生成多个随机颜色,您可能希望它们是可区分的。与一个与我的蓝色几乎相同的对手对战并不有趣。这就是随机调色板可以提供帮助的地方。

这些调色板是使用一种算法生成的,该算法确保调色板上的所有颜色都尽可能可区分。同样,有一个 Fast 方法在 HSV 中工作,并且感知均匀性较差 - 以及一个在 CIE 空间中工作的非 Fast 方法。有关 SoftPalette 的更多理论,请查看 I want hue。再一次,有一个 Happy 和一个 Warm 版本,它们会执行您期望的操作,但现在还有一个额外的 Soft 版本,它更可配置:您可以对颜色空间进行约束以获得具有某种感觉的颜色。

让我们首先从简单的方法开始:它们所需要的就是要生成的颜色数量,例如,可以是玩家数量。它们返回一个 colorful.Color 对象数组

do {
    warm = try WarmPalette(10)
} catch {}

let fwarm = FastWarmPalette(10)

do {
    happy = try HappyPalette(10)
} catch {}

let fhappy = FastHappyPalette(10)

do {
    soft = try SoftPalette(10)
} catch {}

请注意,如果您请求的颜色过多,非快速方法可能会失败。让我们继续介绍高级方法,即 SoftPaletteEx。除了颜色计数之外,此函数还接受一个 SoftPaletteSettings 对象作为参数。这里有趣的部分是它的 CheckColor 成员,它是一个布尔函数,接受三个浮点数作为参数:lab。对于位于您想要的区域内的颜色,此函数应返回 true,否则返回 false。其他成员是 Iteration,它应该在 [5..100] 范围内,其中较高值意味着速度较慢但调色板更精确,以及 ManySamples,如果您的 CheckColor 约束拒绝了颜色空间的大部分,您应该将其设置为 true

例如,要创建 10 个棕色调色板,您可以像这样调用它

func isbrowny(l: Float64, a: Float64, b: Float64) -> Bool {
	let (h, c, L) = Color.LabToHcl(L: l, a: a, b: b)
	return 10.0 < h && h < 50.0 && 0.1 < c && c < 0.5 && L < 0.5
}
// Since the above function is pretty restrictive, we set ManySamples to true.
do {
    brownies = try SoftPaletteEx(colorsCount: colors, settings: SoftPaletteSettings(checkColorFn: isbrowny, iterations: 50, manySamples: true))
} catch {}

下图显示了由所有这些方法生成的调色板(源代码在 Sources/Demos/PaletteGens/main.swift 中),按照它们呈现的顺序排列,即从上到下:WarmFastWarmHappyFastHappySoftSoftEx(isbrowny)。所有这些都包含一些随机性,因此 YMMV。

All example palettes

同样,用于生成上述图像的代码可在 Sources/Demos/PaletteGens/main.swift 中找到。

排序颜色

排序颜色不是一个明确定义的操作。例如,如果较暗的颜色应该在较浅的颜色之前,则 {深蓝色、深红色、浅蓝色、浅红色} 已经排序,但如果较长波长的颜色应该在较短波长的颜色之前,则需要重新排序为 {深红色、浅红色、深蓝色、浅蓝色}。

Go-Colorful 的 Sorted 函数对颜色列表进行排序,以最小化相邻颜色之间的平均距离,包括最后一个和第一个之间的距离。(Sorted 不一定找到真正的最小值,只是一个相当接近的近似值。)由 Sources/Demos/ColorSort/main.swift 绘制的下图说明了 Sorted 的行为

Sorting colors

第一行表示输入:512 个随机选择的颜色切片。第二行显示在 CIE-L*C*h° 空间中排序的颜色,首先按亮度 (L) 排序,然后按色相角 (h) 排序,最后按色度 (C) 排序。请注意,分散注意力的细条纹渗透到颜色中。使用任何颜色空间和任何通道排序都会产生类似的细条纹图案。图像的第三行是使用 Go-Colorful 的 Sorted 函数排序的。虽然颜色似乎没有任何特定的顺序,但该序列至少看起来比按通道排序的序列更平滑。

使用线性 RGB 进行计算

有两种方法用于转换 RGB⟷线性 RGB:一种快速且几乎精确的方法,以及一种缓慢且精确的方法。

let (r, g, b) = try Color.Hex("#FF0000").FastLinearRgb()

TODO:更多描述。

想要使用其他参考点?

let c = Color.LabWhiteRef(l: 0.507850, a: 0.040585, b: -0.370945, wref: D50)
let (l, a, b) = c.LabWhiteRef(wref: D50)

从数据库读取和写入颜色

FAQ

问:我得到的所有值都乱七八糟的!你的库太烂了!

答:您可能提供了错误范围内的值。例如,RGB 值应介于 0 和 1 之间,而不是介于 0 和 255 之间。请归一化您的颜色。

问:Lab/Luv/HCl 似乎坏了!你的库太烂了!

它们看起来像这样

答:您可能正在尝试生成和显示 RGB 无法表示的颜色,因此监视器也无法显示。当您尝试转换时,例如 Color.Hcl(h: 190.0, c: 1.0, l: 1.0).RGB255(),您要求 RGB 值为 (-2105.254 300.680 286.185),这显然不存在,并且 RGB255 函数只是将这些数字强制转换为 uint8,从而创建环绕,并且看起来像一个完全损坏的渐变。您想要做的是要么使用更合理的实际存在于 RGB 中的颜色值,要么只是 Clamp() 将结果颜色限制为最接近的现有颜色,并承担后果:Hcl(h: 190.0, c: 1.0, l: 1.0).Clamp().RGB255()。它看起来会像这样

这是一个深入探讨此问题的 issue,以及我的回答,两者都包含代码和漂亮的图片。另请注意,上面在 “混合颜色”部分中对此进行了一些介绍。

问:在紧密循环中,转换为 Lab/Luv/HCl/... 很慢!

答:是的,它们很慢。此库的目标是正确性、可读性和模块化;它的编写并非以速度为目标。很大一部分速度慢来自于这些转换通过使用幂的 LinearRgb。我通过使用泰勒近似实现了一个名为 FastLinearRgbLinearRgb 的快速近似。该近似速度大约快 5 倍,精度约为 0.5%,主要警告是,如果输入值超出 0-1 范围,精度会急剧下降。您可以在转换中按如下方式使用它们

let (R, G, B) = c1.LinearRgb()
let (x, y, z) = Color.LinearRgbToXyz(r: R, g: G, b: B)
let (l, a, b) = Color.XyzToLab(x: x, y: y, z: z)

如果您需要更快版本的 Distance*Blend* 来利用这种快速近似,请随时实现它们并打开 pull-request,我很乐意接受。

可以在 [这个 Jupyter notebook](doc/LinearRGB Approximations.ipynb) 中跟踪这些函数的推导过程。这是显示近似质量的主要图

approximation quality

通过在许多地方使用 SIMD 指令可以获得更高的速度。您还可以通过近似完整的转换函数来获得特定转换的更高速度,但这超出了此库的范围。感谢 @ZirconiumX 启动了这项调查,请参阅 issue #18 了解详细信息。

谁?

此库由 Artur Torun (又名 Mojzesh) 从 Golang 移植到 Swift

原始 go-colorful 库由 Lucas Beyer 开发,并由 Bastien Dejean (@baskerville)、Phil Kulak (@pkulak)、Christian Muehlhaeuser (@muesli) 和 Scott Pakin (@spakin) 贡献。

已知问题

许可证

此仓库在 MIT 许可证下,有关详细信息,请参阅 LICENSE