Linear cosine palettes – Notes from a data witch
So. Looking back at my history on this blog I have noticed that I, ummmmmm, tend to write long posts. It is a character flaw of which I am acutely aware. When I want to understand a thing I feel a kind of psychological compulsion to delve too deeply into the darkness, dive into to as many of the specifics as I possibly can, organise my thoughts around those specifics, and then drag the whole cursed mess into the daylight so that my long-suffering readers can look on in horror at the grotesquerie of my inner world.
I am aware that this is perhaps unwise.
Reflecting on this as a personal weakness, I have set myself a challenge this fine Sunday: is it even possible for me to write a simple blog post? Like, is it even possible for a mediocre bitch to write a short goddamn article without turning it into some macabre monograph? Given my past form, it is not at all obvious that I’m capable of this level of self-restraint. Let’s see if I can do it?
Linear cosine palettes
The motivation came from this mastodon post by Mike Cheng proposing a simple method for randomly generating continuous colour palettes in R. The original idea comes from a blog post by Inigo Quilez on simple procedural palettes, and the idea is painfully simple. Let’s say we have length-3 vectors \(\mathbf{a}\), \(\mathbf{b}\), \(\mathbf{c}\), and \(\mathbf{d}\) representing four “base” colours from which a continous palette is to be generated. In R we could choose these base colours using the colors() function. Once these are selected we can define a smooth palette using the following function
\[<br>f(t) = \mathbf{a} + \mathbf{b} \ \cos(2 \pi(\mathbf{c} t + \mathbf{d}))<br>\]
where \(t\) is varied from 0 to 1. The nice thing about this paletting rule is that it can be very fast, especially since – I am told by people who understand such things – there are a lot of optimisations in modern CPUs and GPUs to make cosine evaluation fast. Admittedly, speed is not something I care about much in my generative art work because palette generation is not even close to being a bottleneck in my code and also I’m lazy.
Okay, so here’s an R function that implements a very minor tweak on Mike’s implementation of Inigo Quilez’ cosine palettes:
cosine_palette function(n, base = NULL, seed = NULL) {<br>if (!is.null(seed)) set.seed(seed)<br>if (is.null(base)) base colors(distinct = TRUE)<br>a c(0.5, 0.5, 0.5)<br>b (sample(base, 1) |> col2rgb() |> as.vector()) / 255<br>c (sample(base, 1) |> col2rgb() |> as.vector()) / 255<br>d (sample(base, 1) |> col2rgb() |> as.vector()) / 255<br>pal vapply(<br>seq(0, 1, length.out = n),<br>function(t) a + b * cos(2 * pi * (c * t + d)),<br>double(3)<br>pal[pal > 1] 1<br>rgb(t(abs(pal)))
cosine_palette(n = 16, seed = 11)
[1] "#7F1616" "#6F1A17" "#362A20" "#22442F" "#8A6642" "#F18A5A" "#FFAD74"<br>[8] "#FFCB8F" "#FFE0A9" "#FFE9C0" "#FFE6D3" "#AAD6E1" "#40BDE8" "#1F9CE9"<br>[15] "#6377E3" "#7F54D6"
That’s nice, but as my visual cortex is not optimised for the interpretation of hexadecimal RGB colour codes, I find it convenient to show palettes using… check notes… images? Yes. Yes, that sounds right. To that end I’ll use this shade_strip() that I sometimes use to display a continuously varying palette as a strip:
shade_strip function(cols) {<br>withr::with_par(<br>list(mar = c(0,0,0,0)),<br>image( matrix(seq_along(cols), ncol = 1), col = cols, axes = FALSE)
seeds 11:22<br>seeds |><br>purrr::map(\(s) cosine_palette(n = 256, seed = s)) |><br>purrr::walk(shade_strip)
I wanted to get a sense of how well these palettes might behave if applied in a generative art system, so I chose 12 sequential seeds. The sequence starts at seed = 11 because I happened to like the first piece that was generated using that palette, but apart from that minor intervention I haven’t tried to “hack” the seed to bias the outputs.
Application to generative art
To get a feel for how these palettes behave when used in generative art, here are some pieces created using them. These pieces are created using the subdivision() system that I wrote about as part of the art from code workshop I gave a few years ago.
Code for subdivision()<br>choose_rectangle function(blocks) {<br>sample(nrow(blocks), 1, prob = blocks$area)
choose_break function(lower, upper) {<br>round((upper - lower) * runif(1))
create_rectangles function(left, right, bottom, top, value) {<br>tibble::tibble(<br>left = left,<br>right = right,<br>bottom = bottom,<br>top = top,<br>width = right - left,<br>height = top - bottom,<br>area = width * height,<br>value = value
split_rectangle_x function(rectangle, new_value) {<br>with(rectangle, {<br>split choose_break(left, right)<br>new_left c(left, left + split)<br>new_right c(left + split, right)<br>new_value c(value, new_value)<br>create_rectangles(new_left, new_right, bottom, top, new_value)<br>})
split_rectangle_y function(rectangle, new_value) {<br>with(rectangle, {<br>split choose_break(bottom, top)<br>new_bottom c(bottom, bottom + split)<br>new_top c(bottom + split, top)<br>new_value c(value,...