对比色对于字体着色很有用,但是,我发现很难确保调色板中的颜色始终保持令人满意的对比度。一些看上去很酷的调色板在某些情况下会出现相邻token对比度消失的情况,这是我非常讨厌的。
完美对比色序列可以通过使用单位根来构建。考虑HSL色轮上的点 r\theta ,其中 r \in \mathbb {C} \land 0 < |r| < 1 是极径(决定饱和度和亮度),\theta 是极角(决定色调)。设 r = e ^{ i \phi_1 } ,\theta = \omega t + \phi_2 。然后,如果 \omega = \frac{q}{p} ,其中 p 为素数,q \in \mathbb{N} \land 0 < q < p ,我们可以确保对于任意连续的 \frac{p}{q} 种颜色,对比度不小于 \frac{q}{p} 。这里 \phi_1 和 \phi_2 是种子参数,用于保存调色板的配置。
(require 'color)
(defun hsl-color-wheel (theta r i)
"Pick color from a color wheel, TEHTA ∈ [0, 2π], R, I ∈ [0, 1]."
(let* ((2pi (* 2 float-pi))
(hue (/ (- theta (* 2pi (floor (/ theta 2pi)))) 2pi)))
(apply #'color-rgb-to-hex
(nconc (color-hsl-to-rgb hue i r) '(2)))))
(defun perfect-palette (p q &optional phi-1 phi-2)
"Calculate perfect pallete with P, Q, PHI-1 and PHI-2.
P, Q are integers, PHI-1, PHI-2 ∈ [0, 2π].
Return a list of hex strings,"
(let ((phi-1 (or phi-1 0))
(phi-2 (or phi-2 0))
(2pi (* 2 float-pi))
(omega (/ (* 2 float-pi q) p)))
(mapcar
(lambda (x)
(let ((normal-x (* (/ (float x) p) 2pi)))
(hsl-color-wheel (+ (* omega x) phi-2)
(/ (1+ (sin phi-1)) 2)
(/ (1+ (cos phi-1)) 2))))
(number-sequence 0 (1- p) 1))))
;; example:
(perfect-palette 7 3 (/ float-pi 4) (/ float-pi 6))
在最初的设计里我考虑给决定饱和度和亮度的参数 \phi_1 再加一个微扰,但是欠缺稳定性。冯诺依曼说过,四个参数可以画大象了,我们还是只保留三个参数(注意到 p/q 实际上是一个参数)好了。
假设有一个语法,它有两个规则A B C和A C B,那么,如果我们分配颜色像(A . "#00f") (B. "#f00") (C . "#00f"),当出现满足第二条规则A C B的推导时,A C就出现了对比度消失的问题。然而,如果这个语法中只有第一条规则A B C,这个方案就没有那么糟糕了。因此,在许多情况下,p 确实可以被缩小。那么,在进行词法着色的时候,给定一个词法规则,是否存在一个高效的算法能算出最小的 p 呢?
12 个赞
虽然看不懂,但是生成的颜色确实不错。留给以后绘图着色用。
7 个赞
这很有趣。他使用的是不均匀的CIELAB颜色空间,该颜色空间专门用来模拟人的色彩感知,因此在CIELAB空间中,在任一切面上构建正多边形就可以产生相应的调色板,这一点和我的算法是相似的。我在圆柱型的HSL空间中使用球极投影变换把圆柱变成了球,仅此而已。
对的,把问题转换到另外一个域上可能可以节省一些数学上的处理。
不能。这个调色板其实是用来生成对比色循环序列的。
但是你可以试试下面的函数:
(defun monochrome (theta i &optional phi)
"Generate monochrome pallete from HSL color wheel.
TEHTA, PHI ∈ [0, 2π], I is an integer."
(let ((phi (or phi (/ float-pi 2)))
(2pi (* 2 float-pi)))
(mapcar
(lambda (x)
(let ((r (/ (float x) i)))
(hsl-color-wheel theta
(- 1 (* r (/ (1+ (sin phi)) 2)))
(- 1 (* r (/ (1+ (cos phi)) 2))))))
(number-sequence 0 (1- i) 1))))
(monochrome (/ float-pi 4) 8 (/ float-pi 3))
1 个赞
我一时半会没有理解你的算法中“q”如何起作用。我能理解的问题是要给任意有可能相邻的词涂上不同颜色,最少要多少种颜色。这个问题只需分析语法,构造一个图,图中的点代表词,将所有可能相邻的词用边相连;然后在这个图上求解图着色问题(Graph Coloring Problem)即可。但是图着色问题是NP完全的,仅这一点就可以说目前没有“高效”的算法。
q相当于在基频上乘了一个倍数,可以通过设置q增大hsl空间中两个相邻点的间距。最好选择与p互素的q,否则会出现重复。
而且这个问题并不对应一般的图着色问题,词法规则构成一个ATN,而不是平凡的图。
事实上,判定 p 有没有可计算的上界,我觉得都成问题
Kana
2025 年9 月 1 日 17:36
11
顺着 CIELAB 翻到了 HSLuv 这么个东西,似乎是稍微扩展了 CIELUV ,让可以用类似 HSL 的方式来使用。MELPA 上也有一个 hsluv 包,试用了一下感觉亮度的确统一了一些?(不知道自动反色的亮度依据是怎么计算的)但感觉 HSLuv 饱和度整体偏低就是了
简单修改后的代码
(require 'color)
(require 'hsluv)
(defun perfect-palette (p q &optional hue-start s l luv)
"Calculate perfect pallete with P, Q, and init params.
P, Q are integers, with P being a prime number and Q < P/2. Return a
list of hex strings."
(let ((hue-start (or hue-start 0))
(s (or s 0))
(l (or l 0))
(omega (/ (float q) p)))
(mapconcat
(lambda (x)
(let ((x (* x omega)))
(if luv
(hsluv-hsluv-to-hex (list (* 360 (mod x 1.0)) s l))
(apply #'color-rgb-to-hex
(append (color-hsl-to-rgb
(mod x 1.0) (/ s 100.0) (/ l 100.0))
'(2))))))
(number-sequence 0 (1- p) 1) " ")))
(concat
(perfect-palette 7 3 0 50 50)
"\n"
(perfect-palette 7 3 0 50 50 t))
另外还在某篇文献 里找到这张图:
“HLS中非感知均匀性的例子。左:三种亮度(L)相等的颜色。中间:两对色调(H)距离相等的颜色。右:两对亮度相等的颜色。”
(虽然这篇文献主要是生成同色调的色阶就是了)
3 个赞
反色(inverse color)其实就是直接对RGB值取反(比如,如果每个通道占8位,就用255减去对应的通道值)。你说的可能是补色(complementary color),补色是取色相圆上的对称点,相对反色而言,使用互补色显得更匀称。设计LAB空间本身是为了让XYZ空间中距离近的颜色显得更有区分度,而不是让距离远的颜色显得匀称。
我又重新思考了一下我的这个算法,想到了一个可能的解释:因为它把饱和度和亮度绑定到一个圆圈上的行为,似乎无形中执行了一次近似的gamma校正操作。注意到,正弦函数长得很像一个二次函数,而gamma校正本身正是用2.2作为幂次。这导致它产生的色彩看起来非常的……亮丽。hsluv则是已经对亮度进行了拉伸,此时得到的是一个亮度是长轴的椭圆,因为绕椭圆转动时长轴方向衰减快,导致gamma校正被抵消,看起来就没那么亮了。