colorsluagradient256color

Lua - xterm 256 colors gradient scripting


This has been bothering me for a bit, and I'm not sure there is an answer out there. I know there are modules such as Love2D that accomplish gradients, which I'm guessing uses RGB coloring. However, I'm needing to find something quite similar using xterm 256 colors, but I cannot seem to find a gradient map anywhere to assist with this.

My guess is that I'll have to create a "nearest to RGB color" and create a gradient from that, matching the corresponding RBG to the nearest xterm match, but to be quite honest, I don't even know where to begin with this. I know there's a "convert xterm to RGB hex" script in Python (located here), but as I don't know Python, I don't know how to convert that to Lua.

Ultimately, what I want to do is be able to turn text into a rainbow gradient, more or less. I currently have a function to return xterm colors, but it's completely random, and the output can be a bit harsh to read. Here's what I have for that code. The @x stands for "convert to xterm color", and it is followed by a three digit code (001 to 255), followed by the text.

function rainbow(text)
    local rtext = ""
    local xcolor = 1
    local sbyte = 1
    for i = 1, #text do
        math.randomseed(os.time() * xcolor * sbyte)
        sbyte = string.byte(i)
        xcolor = math.random(1, 255)
        rtext = rtext .. "@x" .. string.rep("0", 3 - string.len(xcolor)) .. xcolor .. text:sub(i,i)
    end
    return rtext
end

So, for example, print(rainbow("Test")) would result in:

@x211T@x069e@x154s@x177t

Obviously, this is not a gradient, and is not what I want to end up with. Is what I want possible, or is it a lost cause?

Edit
I know the limitiations of 256 colors, and I know there's not a whole lot of wiggle room. As was pointed out in the comment, there'd be a lot of the same color matching, so I'd get a string of the same colors. That's fine with me, really. No matter how many actual transitions it makes, I'd like for it to closely simulate a gradient.

What would be nice is if I were able to at least create color groups properly without having to poll the charts. What I may wind up having to do, I guess, is create a table of "compatible colors schemes" and work with that, unless someone has a better idea.


Solution

  • Define nearest_term256_color_index function:

    local abs, min, max, floor = math.abs, math.min, math.max, math.floor
    local levels = {[0] = 0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff}
    
    local function index_0_5(value) -- value = color component 0..255
       return floor(max((value - 35) / 40, value / 58))
    end
    
    local function nearest_16_231(r, g, b)   -- r, g, b = 0..255
       -- returns color_index_from_16_to_231, appr_r, appr_g, appr_b
       r, g, b = index_0_5(r), index_0_5(g), index_0_5(b)
       return 16 + 36 * r + 6 * g + b, levels[r], levels[g], levels[b]
    end
    
    local function nearest_232_255(r, g, b)  -- r, g, b = 0..255
       local gray = (3 * r + 10 * g + b) / 14
       -- this is a rational approximation for well-known formula
       -- gray = 0.2126 * r + 0.7152 * g + 0.0722 * b
       local index = min(23, max(0, floor((gray - 3) / 10)))
       gray = 8 + index * 10
       return 232 + index, gray, gray, gray
    end
    
    local function color_distance(r1, g1, b1, r2, g2, b2)
       return abs(r1 - r2) + abs(g1 - g2) + abs(b1 - b2)
    end
    
    local function nearest_term256_color_index(r, g, b)   -- r, g, b = 0..255
       local idx1, r1, g1, b1 = nearest_16_231(r, g, b)
       local idx2, r2, g2, b2 = nearest_232_255(r, g, b)
       local dist1 = color_distance(r, g, b, r1, g1, b1)
       local dist2 = color_distance(r, g, b, r2, g2, b2)
       return dist1 < dist2 and idx1 or idx2
    end
    

    Define generate_gradient function which inserts @x... in your text:

    local unpack, tonumber = table.unpack or unpack, tonumber
    
    local function convert_color_to_table(rrggbb)
       if type(rrggbb) == "string" then
          local r, g, b = rrggbb:match"(%x%x)(%x%x)(%x%x)"
          return {tonumber(r, 16), tonumber(g, 16), tonumber(b, 16)}
       else
          return rrggbb
       end
    end
    
    local function round(x)
       return floor(x + 0.5)
    end
    
    local function generate_gradient(text, first_color, last_color)
       local r, g, b = unpack(convert_color_to_table(first_color))
       local dr, dg, db = unpack(convert_color_to_table(last_color))
       local char_pattern = "[^\128-\191][\128-\191]*"
       local n = max(1, select(2, text:gsub(char_pattern, "")) - 1)
       dr, dg, db = (dr - r)/n, (dg - g)/n, (db - b)/n
       local result = ""
       for c in text:gmatch(char_pattern) do
          result = result..("@x%03d"):format(nearest_term256_color_index(
             round(r), round(g), round(b)))..c
          r, g, b = r + dr, g + dg, b + db
       end
       return result
    end
    

    Test it inside terminal:

    local function print_with_colors(str)
       print(
          str:gsub("@x(%d%d%d)",
             function(color_idx)
                return "\27[38;5;"..color_idx.."m"
             end)
          .."\27[0m"
       )
    end
    
    local str = "Gradient"
    local blue, red = {0, 0, 255}, "#FF0000"
    str = generate_gradient(str, blue, red)  -- gradient from blue to red
    print(str)
    print_with_colors(str)
    

    rainbow() is not a gradient, it would be a chain of several gradients.