Color Palette Generator — Ruby source
Generate harmonious color palettes - complementary, analogous, triadic, tetradic, and monochromatic - from any base color. Export to CSS, Tailwind, or JSON.
This is the Ruby implementation — the same logic the interactive tool runs, in a shareable, citable form.
# palette-generator — harmonious palettes from a base color.
# Ruby 3.1+, stdlib only. CosmoDev polyglot port of src/lib/colorPalette.ts.
# All inputs clamped; nothing raises — bad hex falls back to black.
module PaletteGenerator
HEX_RE = /\A[0-9a-fA-F]+\z/.freeze
module_function
# Parse #rgb / #rrggbb (leading # optional); fallback black.
def hex_to_rgb(hex)
h = hex.to_s.strip.sub(/\A#/, '')
return [0, 0, 0] unless (h.length == 3 || h.length == 6) && HEX_RE.match?(h)
h = h.chars.map { |c| c * 2 }.join if h.length == 3
[h[0, 2].to_i(16), h[2, 2].to_i(16), h[4, 2].to_i(16)]
end
# Round half up (TS Math.round), then clamp to [0, 255].
def clamp_byte(n) = n.round(half: :up).clamp(0, 255)
def rgb_to_hex(r, g, b) = format('#%02x%02x%02x', clamp_byte(r), clamp_byte(g), clamp_byte(b))
# hex -> HSL. h in [0,360), s/l in [0,100]; achromatic gives h = 0.
def hex_to_hsl(hex)
r, g, b = hex_to_rgb(hex).map { |c| c / 255.0 }
mx = [r, g, b].max
mn = [r, g, b].min
l = (mx + mn) / 2
h = 0.0
s = 0.0
if mx != mn
d = mx - mn
s = l > 0.5 ? d / (2 - mx - mn) : d / (mx + mn)
h = mx == r ? (g - b) / d + (g < b ? 6 : 0) : mx == g ? (b - r) / d + 2 : (r - g) / d + 4
h /= 6
end
[h * 360, s * 100, l * 100]
end
# HSL -> hex. Ruby % is a true modulo, so the wrap needs no double-mod.
def hsl_to_hex(h, s, l)
big = h % 360
sat = s.clamp(0.0, 100.0) / 100
lig = l.clamp(0.0, 100.0) / 100
c = (1 - (2 * lig - 1).abs) * sat
x = c * (1 - ((big / 60) % 2 - 1).abs)
m = lig - c / 2
# sextant table: channel weights per 60-degree hue sector
r, g, b = [[c, x, 0.0], [x, c, 0.0], [0.0, c, x], [0.0, x, c], [x, 0.0, c], [c, 0.0, x]][(big / 60).floor]
rgb_to_hex((r + m) * 255, (g + m) * 255, (b + m) * 255)
end
# Generate a harmony. Counts: complement 2, split-complement 3, analogous 3,
# triadic 3, tetradic 4; count steers only monochromatic.
def generate_palette(base_hex, scheme, count = 5)
h, s, l = hex_to_hsl(base_hex)
rot = ->(deg) { hsl_to_hex(h + deg, s, l) }
base = hsl_to_hex(h, s, l)
case scheme
when 'complement' then [base, rot.call(180)]
when 'split-complement' then [base, rot.call(150), rot.call(210)]
when 'analogous' then [rot.call(-30), base, rot.call(30)]
when 'triadic' then [base, rot.call(120), rot.call(240)]
when 'tetradic' then [base, rot.call(90), rot.call(180), rot.call(270)]
when 'monochromatic'
n = [1, count.floor].max
lo = [10.0, l - 32].max
hi = [90.0, l + 32].min
(0...n).map { |i| hsl_to_hex(h, s, n == 1 ? l : lo + (hi - lo) * i / (n - 1)) }
else [base]
end
end
# n shades / tints — RGB lerp toward black / white; i runs 1..n so the base
# color itself is never returned, only intermediate steps.
def shades(base_hex, n) = mix(base_hex, n) { |c, f| c * (1 - f) }
def tints(base_hex, n) = mix(base_hex, n) { |c, f| c + (255 - c) * f }
def mix(base_hex, n)
r, g, b = hex_to_rgb(base_hex)
steps = [1, n.floor].max
(1..steps).map { |i| f = i.to_f / (steps + 1); rgb_to_hex(yield(r, f), yield(g, f), yield(b, f)) }
end
end
Also available in 13 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →