线性余弦调色板
这有点像是生成艺术的内容,但主要是作者为了向自己证明,她能写一篇简短的博客文章,而不是把它变成一篇冗长的专题论文。作者是丹妮尔·纳瓦罗,文章发布于2025年9月14日。回顾在这个博客上的发文历程,作者注意到自己倾向于写长篇文章,深知这是个性格缺陷。当想了解某件事时,心理上就会有一种强烈的冲动,驱使自己深入探究细节,尽可能挖掘所有相关的具体信息,围绕这些细节整理思路,然后把这一堆复杂的内容公之于众,让那些长期忍受自己文章的读者惊恐地窥视自己内心世界的“怪诞景象”。作者知道这样做或许并不明智。反思这一弱点后,在这个美好的周日,作者给自己设定了一个挑战:能写出一篇简洁的博客文章吗?像自己这样水平一般的人,能写出一篇简短的文章,而不是把它变成一篇恐怖的专题论文吗?鉴于以往的表现,自己是否有能力做到自我克制,还真不太确定。那就看看能否成功吧。
这个想法源于迈克·程(Mike Cheng)在 [Mastodon](https://fosstodon.org/@coolbutuseless/115173701685084866) 上发布的一篇帖子,他提出了一种在 R 语言中随机生成连续调色板的简单方法。最初的灵感来自伊尼戈·基莱斯(Inigo Quilez)的一篇关于 [简单程序调色板](https://iquilezles.org/articles/palettes/) 的博客文章,其思路极其简单。假设我们有长度为 3 的向量 \(\mathbf{a}\)、\(\mathbf{b}\)、\(\mathbf{c}\) 和 \(\mathbf{d}\),它们代表四种“基础”颜色,用于生成连续的调色板。在 R 语言中,我们可以使用 `colors()` 函数选择这些基础颜色。选定后,我们可以使用以下函数定义一个平滑的调色板:\[ f(t) = \mathbf{a} + \mathbf{b} \ \cos(2 \pi(\mathbf{c} t + \mathbf{d})) \]其中,\(t\) 的取值范围是从 0 到 1。这种调色规则的优点是速度很快,因为据了解相关知识的人说,现代 CPU 和 GPU 对余弦计算进行了大量优化。不过,在作者的生成艺术作品中,速度并不是特别关心的问题,因为调色板生成在作者的代码中远不是瓶颈,而且作者比较懒。
下面是一个 R 函数,它对迈克实现的伊尼戈·基莱斯的余弦调色板做了一点小改动:
cosine_palette <- function(n, base = NULL, seed = NULL) { if (!is.null(seed)) set.seed(seed) if (is.null(base)) base <- colors(distinct = TRUE) a <- c(0.5, 0.5, 0.5) b <- (sample(base, 1) |> col2rgb() |> as.vector()) / 255 c <- (sample(base, 1) |> col2rgb() |> as.vector()) / 255 d <- (sample(base, 1) |> col2rgb() |> as.vector()) / 255 pal <- vapply( seq(0, 1, length.out = n), function(t) a + b * cos(2 * pi * (c * t + d)), double(3) ) pal[pal > 1] <- 1 rgb(t(abs(pal)))}cosine_palette(n = 16, seed = 11)[1] "#7F1616" "#6F1A17" "#362A20" "#22442F" "#8A6642" "#F18A5A" "#FFAD74" [8] "#FFCB8F" "#FFE0A9" "#FFE9C0" "#FFE6D3" "#AAD6E1" "#40BDE8" "#1F9CE9"[15] "#6377E3" "#7F54D6"这很不错,但由于作者的视觉系统不太擅长解读十六进制的 RGB 颜色代码,所以觉得用图像来展示调色板会更方便。为此,作者将使用 `shade_strip()` 函数,作者有时会用它把连续变化的调色板显示为一个色带:shade_strip <- function(cols) { withr::with_par( list(mar = c(0,0,0,0)), image( matrix(seq_along(cols), ncol = 1), col = cols, axes = FALSE) )}seeds <- 11:22seeds |> purrr::map(\(s) cosine_palette(n = 256, seed = s)) |> purrr::walk(shade_strip)在生成艺术中的应用
为了感受这些调色板在生成艺术中的效果,下面是一些使用它们创作的作品。这些作品是使用作者在几年前举办的 [代码生成艺术工作坊](../../posts/2024-12-23_art-from-code-6/) 中介绍的 `subdivision()` 系统创作的。
`subdivision()` 函数代码:
choose_rectangle <- function(blocks) { sample(nrow(blocks), 1, prob = blocks$area)}choose_break <- function(lower, upper) { round((upper - lower) * runif(1))}create_rectangles <- function(left, right, bottom, top, value) { tibble::tibble( left = left, right = right, bottom = bottom, top = top, width = right - left, height = top - bottom, area = width * height, value = value )}split_rectangle_x <- function(rectangle, new_value) { with(rectangle, { split <- choose_break(left, right) new_left <- c(left, left + split) new_right <- c(left + split, right) new_value <- c(value, new_value) create_rectangles(new_left, new_right, bottom, top, new_value) })}split_rectangle_y <- function(rectangle, new_value) { with(rectangle, { split <- choose_break(bottom, top) new_bottom <- c(bottom, bottom + split) new_top <- c(bottom + split, top) new_value <- c(value, new_value) create_rectangles(left, right, new_bottom, new_top, new_value) })}split_rectangle <- function(rectangle, value) { split_fn <- ifelse(runif(1) < .5, split_rectangle_x, split_rectangle_y) split_fn(rectangle, value)}split_block <- function(blocks, value) { old <- choose_rectangle(blocks) new <- split_rectangle(blocks[old, ], value) dplyr::bind_rows(blocks[-old, ], new)}subdivision <- function(ncol = 100, nrow = 100, nsplits = 256, border = NULL, seed = NULL) { if (!is.null(seed)) set.seed(seed) pal <- cosine_palette(n = 256, seed = seed) if (is.null(border)) border <- pal[128] rct <- create_rectangles( left = 1, right = ncol, bottom = 1, top = nrow, value = 0 ) div <- purrr::reduce( 1:nsplits, split_block, .init = rct ) plt <- div |> ggplot2::ggplot(ggplot2::aes( xmin = left, xmax = right, ymin = bottom, ymax = top, fill = value )) + ggplot2::geom_rect( show.legend = FALSE, color = border, linewidth = 1 ) + ggplot2::scale_fill_gradientn(colours = pal) + ggplot2::scale_x_continuous(expand = ggplot2::expansion(mult = .15)) + ggplot2::scale_y_continuous(expand = ggplot2::expansion(mult = .15)) + ggplot2::coord_equal() + ggplot2::theme_void() + ggplot2::theme(plot.background = ggplot2::element_rect( color = border, fill = border )) plt}seeds |> purrr::walk(\(s) plot(subdivision(seed = s)))作为第二个例子,下面是一系列基于 [利萨如曲线(Lissajous)](https://art.djnavarro.net/gallery/lissajous/) 系统创作的作品,都使用了相同的调色板:
seeds |> purrr::walk(\(s) lissajous(seed = s))复用
本文采用 [知识共享署名 4.0 国际许可协议](https://creativecommons.org/licenses/by/4.0/)。
引用
BibTeX 引用格式:
@online{navarro2025, author = {Navarro, Danielle}, title = {Linear Cosine Palettes}, date = {2025-09-14}, url = {https://blog.djnavarro.net/posts/2025-09-14_cosine-palettes/}, langid = {en}}如需引用,请按以下格式:Navarro, Danielle. 2025. “Linear Cosine Palettes.” September 14, 2025. <https://blog.djnavarro.net/posts/2025-09-14_cosine-palettes/>.[blog.djnavarro.net](https://blog.djnavarro.net)