完美的调色板算法

对比色对于字体着色很有用,但是,我发现很难确保调色板中的颜色始终保持令人满意的对比度。一些看上去很酷的调色板在某些情况下会出现相邻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 CA C B,那么,如果我们分配颜色像(A . "#00f") (B. "#f00") (C . "#00f"),当出现满足第二条规则A C B的推导时,A C就出现了对比度消失的问题。然而,如果这个语法中只有第一条规则A B C,这个方案就没有那么糟糕了。因此,在许多情况下,p 确实可以被缩小。那么,在进行词法着色的时候,给定一个词法规则,是否存在一个高效的算法能算出最小的 p 呢?

9 个赞

虽然看不懂,但是生成的颜色确实不错。留给以后绘图着色用。

4 个赞

我有个朋友以前做过相关研究,可以参考一下

2 个赞

这很有趣。他使用的是不均匀的CIELAB颜色空间,该颜色空间专门用来模拟人的色彩感知,因此在CIELAB空间中,在任一切面上构建正多边形就可以产生相应的调色板,这一点和我的算法是相似的。我在圆柱型的HSL空间中使用球极投影变换把圆柱变成了球,仅此而已。

对的,把问题转换到另外一个域上可能可以节省一些数学上的处理。

能用这个生成monochrome调色板吗?

不能。这个调色板其实是用来生成对比色循环序列的。

但是你可以试试下面的函数:

(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 有没有可计算的上界,我觉得都成问题

顺着 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))

另外还在某篇文献里找到这张图:

Details are in the caption following the image

“HLS中非感知均匀性的例子。左:三种亮度(L)相等的颜色。中间:两对色调(H)距离相等的颜色。右:两对亮度相等的颜色。”

(虽然这篇文献主要是生成同色调的色阶就是了)

2 个赞