一个用于在 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 中,并提供从这些颜色转换为各种颜色空间的方法。目前支持的颜色空间有
LuvLCh
,这是 CIE-L*u*v* 颜色空间的圆柱变换。与上面的 HCL 类似:H° 在 [0..360] 范围内,C* 几乎在 [0..1] 范围内,L* 与 CIE-L*u*v* 中相同。对于有意义的颜色空间(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
文件中
.package(url: "https://github.com/mojzesh/swift-colorful.git", from: "1.0.0")
B) 在 Xcode 中,只需使用
File -> Add Packages...
https://github.com/mojzesh/swift-colorful.git
:Search or Enter Package URL
然后可以通过以下方式使用该软件包
import Colorful
从 CLI 运行演示项目
swift run DemoColorBlend-macOS
swift run DemoColorDist-macOS
swift run DemoColorGens-macOS
swift run DemoColorSort-macOS
swift run DemoGradientGen-macOS
swift run DemoPaletteGens-macOS
运行测试
swift test
运行基准测试
swift run Benchmark-macOS
使用不同的源空间创建美丽的蓝色
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()
在 RGB 颜色空间中,颜色之间的欧几里得距离不对应于视觉/感知距离。这意味着在 RGB 空间中具有相同距离的两对颜色可能看起来相差甚远。CIE-L*a*b*、CIE-L*u*v* 和 CIE-L*C*h° 颜色空间解决了这个问题。因此,您应该只在这些空间中的任何一个中比较颜色。(请注意,CIE-L*a*b* 和 CIE-L*C*h° 中的距离是相同的,因为它是相同的空间,但采用圆柱坐标系)
顶部显示的两种颜色看起来比底部显示的两种颜色差异更大。尽管如此,在 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
,并且已被稍微更准确但成本更高的 DistanceCIE94
和 DistanceCIEDE2000
取代。
请注意,提供 AlmostEqualRgb
主要用于(单元)测试目的。仅在您真正知道自己在做什么时才使用它。它会吃掉您的猫。
混合与距离高度相关,因为它基本上“穿过”颜色空间,因此如果颜色空间很好地映射了距离,则过渡是“平滑的”。
Colorful 带有 RGB、HSV 和任何 LAB 空间中的混合函数。当然,您更希望使用 LAB 空间的混合函数,因为这些空间可以很好地映射距离,但以防万一,这里有一个示例向您展示了如何在各种空间中完成混合(#fdffcc
到 #242a42
)
您看到的是 HSV 非常糟糕:它添加了一些绿色,而绿色根本不存在于原始颜色中!RGB 好得多,但它保持浅色状态的时间有点太长。LUV 和 LAB 都达到了正确的亮度,但 LAB 的颜色更多一些。HCL 的工作方式与 HSV 相同(都是圆柱插值),但它做得正确,因为没有出现绿色,并且亮度以线性方式变化。
虽然这看起来都不错,但您需要知道一件事:在任何 CIE 颜色空间中进行插值时,您可能会得到无效的 RGB 颜色!如果起始颜色和结束颜色是用户输入或随机的,这一点很重要。一个发生这种情况的示例是在 #eeef61
和 #1e3140
之间混合时
您可以通过调用 IsValid
方法来测试颜色是否是有效的 RGB 颜色,并且实际上,对底部偏红的颜色调用 IsValid 将返回 false。一种“修复”此问题的方法是调用 Clamped
获得接近无效颜色的有效颜色,Clamped
始终返回附近的有效颜色。这样做,我们得到了以下令人满意的结果
以下是创建上述三张图像的代码;可以在 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 空间中生成的华丽渐变
有时需要生成随机颜色。您可以简单地通过生成具有随机值的颜色来自己完成此操作。通过将随机值限制在小于 [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° 空间的常规函数。下图显示了顶部两行中的暖色和底部两行中的快乐色。在这些颜色中,第一个是常规的,第二个是快速的。
不要忘记初始化随机种子!您可以在 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
成员,它是一个布尔函数,接受三个浮点数作为参数:l
、a
和 b
。对于位于您想要的区域内的颜色,此函数应返回 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
中),按照它们呈现的顺序排列,即从上到下:Warm
、FastWarm
、Happy
、FastHappy
、Soft
、SoftEx(isbrowny)
。所有这些都包含一些随机性,因此 YMMV。
同样,用于生成上述图像的代码可在 Sources/Demos/PaletteGens/main.swift 中找到。
排序颜色不是一个明确定义的操作。例如,如果较暗的颜色应该在较浅的颜色之前,则 {深蓝色、深红色、浅蓝色、浅红色} 已经排序,但如果较长波长的颜色应该在较短波长的颜色之前,则需要重新排序为 {深红色、浅红色、深蓝色、浅蓝色}。
Go-Colorful 的 Sorted
函数对颜色列表进行排序,以最小化相邻颜色之间的平均距离,包括最后一个和第一个之间的距离。(Sorted
不一定找到真正的最小值,只是一个相当接近的近似值。)由 Sources/Demos/ColorSort/main.swift 绘制的下图说明了 Sorted
的行为
第一行表示输入:512 个随机选择的颜色切片。第二行显示在 CIE-L*C*h° 空间中排序的颜色,首先按亮度 (L) 排序,然后按色相角 (h) 排序,最后按色度 (C) 排序。请注意,分散注意力的细条纹渗透到颜色中。使用任何颜色空间和任何通道排序都会产生类似的细条纹图案。图像的第三行是使用 Go-Colorful 的 Sorted
函数排序的。虽然颜色似乎没有任何特定的顺序,但该序列至少看起来比按通道排序的序列更平滑。
有两种方法用于转换 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)
答:您可能提供了错误范围内的值。例如,RGB 值应介于 0 和 1 之间,而不是介于 0 和 255 之间。请归一化您的颜色。
它们看起来像这样
答:您可能正在尝试生成和显示 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,以及我的回答,两者都包含代码和漂亮的图片。另请注意,上面在 “混合颜色”部分中对此进行了一些介绍。
答:是的,它们很慢。此库的目标是正确性、可读性和模块化;它的编写并非以速度为目标。很大一部分速度慢来自于这些转换通过使用幂的 LinearRgb
。我通过使用泰勒近似实现了一个名为 FastLinearRgb
的 LinearRgb
的快速近似。该近似速度大约快 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) 中跟踪这些函数的推导过程。这是显示近似质量的主要图
通过在许多地方使用 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。