acid16/acid16.py
2022-07-07 21:52:00 -05:00

304 lines
11 KiB
Python
Executable File

#!/usr/bin/env python3
import colorsys, optparse
from PIL import Image, ImageDraw, ImageFont
modes = ["base16","xresources"]
RED, YELLOW, GREEN, CYAN, BLUE, MAGENTA = 0, 60, 120, 200, 240, 300
parser = optparse.OptionParser(usage="usage: %prog [options] background accent [foreground]")
parser.add_option("-a", "--author",
action="store",
dest="author",
default="acid16",
help="author name")
parser.add_option("-b", "--hue-blend",
action="store",
dest="hueBlend",
default=0.0,
help="mix ratio between hues and background")
parser.add_option("-c", "--contrast",
action="store",
dest="foreContrast",
default=7.0,
help="contrast between foreground and background (default 7)")
parser.add_option("-e", "--equiluminant",
action="store_true",
dest="equiluminant",
default=0.0,
help="use the same luminance for last hues")
parser.add_option("-m", "--mode",
action="store",
dest="mode",
default="base16",
help="what to generate ({})".format(", ".join(modes)))
parser.add_option("-n", "--name",
action="store",
dest="name",
default="acid16",
help="scheme name")
parser.add_option("-p", "--preview",
action="store_true",
dest="showPreview",
default=False,
help="generate a preview image")
parser.add_option("--hsl",
action="store_true",
dest="hsl",
default=0.0,
help="treat background color as gamma-encoded HSL")
base16Options = optparse.OptionGroup(parser, "Base16 options")
base16Options.add_option("-l", "--light-brown",
action="store_true",
dest="lightBrown",
default=0.0,
help="don't darken last color")
parser.add_option_group(base16Options)
(options, args) = parser.parse_args()
if options.mode not in modes:
parser.error("unknown mode {}".format(options.mode))
if len(args) < 2:
parser.error("need background and accent colors minimum")
def parseTuple(st):
return tuple(float(x) for x in st.split(","))
def floatOrDie(fl, errSt):
try:
return float(fl)
except:
parser.error(errSt)
class ImageHandler:
def __init__(self, color):
self.font = ImageFont.truetype("LiberationMono-Regular.ttf", 20)
self.image = Image.new("RGB", (640, 480), usable(color))
self.draw = ImageDraw.Draw(self.image)
def size(self):
return self.image.size
def desc(self, t, a, b):
return "{} (contrast {:.1f})".format(t, contrast(luminance(a), luminance(b)))
def text(self, x, y, t, col, other):
self.draw.text((x, y), self.desc(t, col, other), usable(col), self.font)
def show(self):
self.image.show()
def drawPalette(self, cols):
w, h = self.size()
for i in range(len(cols)):
im.draw.rectangle((i*40,h-40,i*40+39,h), usable(cols[i]))
def toLinear(col):
return tuple(x/12.92 if x < 0.04045 else ((x+0.055)/1.055)**2.4 for x in col)
def toSRGB(col):
return tuple(x*12.92 if x < 0.0031308 else 1.055*x**(1/2.4) - 0.055 for x in col)
def luminance(col):
return col[0]*0.2126+col[1]*0.7152+col[2]*0.0722
foreContrast = floatOrDie(options.foreContrast, "foreground contrast must be a number")
bgHue, bgSat, origLum = parseTuple(args[0])
bgLum = luminance(toLinear(colorsys.hls_to_rgb(bgHue, origLum, bgSat))) if options.hsl else origLum
accentHue, accentSat = parseTuple(args[1])
foreHue, foreSat = parseTuple(args[2]) if len(args) > 2 else (0.0, 0.0)
hueBlend = floatOrDie(options.hueBlend, "rainbow blend must be a number")
def lerp(a, b, t):
return a + (b - a) * t
def collapse(l):
return [y for x in l for y in x]
def reposition(a, b, t, c, d):
return lerp(c, d, (t - a) / (b - a))
def mixColors(a, b, t):
return tuple(lerp(a[i], b[i], t) for i in range(3))
def hue(col):
return colorsys.rgb_to_hls(*col)[0]
def saturation(col):
return colorsys.rgb_to_hls(*col)[2]
def contrast(a, b):
return (max(a, b) + 0.05) / (min(a, b) + 0.05)
def lumFromContrast(lum, ratio):
hi = ratio*lum + (ratio - 1)*0.05
lo = (lum + 0.05 - ratio * 0.05) / ratio
if hi == min(1.0, hi): return hi
if lo == max(0.0, lo): return lo
hiCon = 1.05 / (lum + 0.05)
loCon = (lum + 0.05) / 0.05
return hi if hiCon > loCon else lo
def clamp(x):
return min(1.0, max(x, 0.0))
def makeColor(hue, lum, sat = 1.0):
orig = luminance(colorsys.hls_to_rgb(hue, 0.5, sat))
return tuple(min(1.0, x) for x in colorsys.hls_to_rgb(hue, 0.5 * (lum / orig) if lum < orig else (0.5 + 0.5 * ((lum - orig) / (1.0 - orig))), sat))
def makeBackground():
return makeColor(bgHue, bgLum, bgSat)
def foregroundLum(bg = bgLum):
return lumFromContrast(bg, foreContrast)
CONSTRAIN_LOWER = False
CONSTRAIN_UPPER = True
def constrain(orig, compare, limit, mode=CONSTRAIN_UPPER):
comp = lambda a, b: a > b if mode else a < b
return lumFromContrast(compare, limit) if comp(contrast(orig, compare), limit) else orig
def genRainbow(hues, minCon, maxCon):
backFull = colorsys.hls_to_rgb(bgHue, 0.5, bgSat)
rainbow = [mixColors(colorsys.hls_to_rgb(hue/360.0, 0.5, 1.0), backFull, hueBlend) for hue in hues]
rainLums = [luminance(col) for col in rainbow]
hiLum, loLum = max(rainLums), min(rainLums)
minLum, maxLum = sorted((lumFromContrast(bgLum, minCon), lumFromContrast(bgLum, minCon if options.equiluminant else maxCon )))
maxLum = min(1.0, maxLum)
darkenBrown = (options.equiluminant or options.lightBrown)
rainHLS = [colorsys.rgb_to_hls(*col) for col in rainbow]
return [makeColor(rainHLS[c][0], reposition(loLum, hiLum+0.00001, rainLums[c], minLum, maxLum) if c < 7 or darkenBrown else minLum, rainHLS[c][2] / (1 if c < 7 else 1.5)) for c in range(len(rainbow))]
def printBanner(commentChar):
say = lambda st: print(commentChar+" "+st)
sayIf = lambda cond, st: say(st) if cond else None
say("generated by acid16 - https://git.lain.church/whut/acid16")
say("--- settings ---")
say("background {}: {} {} {}".format("HSL" if options.hsl else "HCL", bgHue, bgSat, origLum))
say("accent HC : {} {}".format(accentHue, accentSat))
say("foreground HC : {} {}".format(foreHue, foreSat))
say("hue blend : {}".format(hueBlend))
say("fg contrast : {}".format(foreContrast))
sayIf(options.equiluminant, "equiluminant hues")
sayIf(options.hsl, "using gamma-encoded HSL")
def usable(col):
return tuple(int(x*255+0.5) for x in toSRGB(col))
def toHex(col):
return "".join(["{:02x}".format(x) for x in usable(col)])
def base16Generate():
background = makeBackground()
foreLum = clamp(foregroundLum())
foreCon = contrast(foreLum, bgLum)
foreground = makeColor(foreHue, foreLum, foreSat)
commentLum = lumFromContrast(bgLum, max(4.5, 1.0 + foreCon / 2.0))
comment = makeColor(accentHue, commentLum, accentSat)
accentLum = constrain(lumFromContrast(foreLum, 4.5), bgLum, 2.0)
accent = makeColor(accentHue, accentLum, accentSat)
statusBgLum = lumFromContrast(bgLum, 1.25)
backStatus = makeColor(bgHue, statusBgLum, bgSat)
foreStatus = makeColor(foreHue, lumFromContrast(statusBgLum, 7.0), foreSat)
# supposedly these two are rarely used so i don't care
lastTwo = [lerp(foreLum, 1.0 if foreLum > bgLum else 0.0, (x + 1) / 3) for x in range(2)]
foreLight = makeColor(foreHue, lastTwo[0], foreSat)
backLight = makeColor(bgHue, lastTwo[1], bgSat)
rainbow = genRainbow([RED, 23, YELLOW, GREEN, CYAN, BLUE, MAGENTA, 15], 4.5, 7.0)
cols = [background, backStatus, accent, comment, foreStatus, foreground, foreLight, backLight] + rainbow
printBanner("#")
if options.lightBrown:
print("# not darkening last color (brown)")
print("\nscheme: \"{}\"".format(options.name))
print("author: \"{}\"".format(options.author))
for i in range(len(cols)):
print("base{:02X}: \"{}\"".format(i, toHex(cols[i])))
return cols
def base16Preview(im, cols):
background, foreground, accent, comment, backLighter, foreDark, rainbow = cols[0], cols[5], cols[2], cols[3], cols[1], cols[4], cols[8:]
w, h = im.size()
im.draw.rectangle((2, 2, w-2, 32), usable(accent))
im.text(5, 5, "Foreground on highlight", foreground, accent)
im.text(5, 45, "Foreground on background", foreground, background)
im.text(5, 85, "# Comment color", comment, background)
for i in range(len(rainbow)):
im.draw.rectangle((4, 120+i*35, 84, 145+i*35), usable(rainbow[i]))
im.text(90, 123+i*35, "Colored text", rainbow[i], background)
im.draw.rectangle((0, h-80, w, h-40), usable(backLighter))
im.text(5, h-75, "Status foreground", foreDark, backLighter)
im.draw.line((0, h-42, w, h-42), usable(foreground), 2)
im.drawPalette(cols)
def xresourcesGenerate():
background = makeBackground()
foreLum = clamp(foregroundLum())
foreground = makeColor(foreHue, foreLum, foreSat)
cursor = makeColor(accentHue, constrain(lumFromContrast(foreLum, 3), bgLum, 2.0), accentSat)
rainHues = [RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN]
hiCon, loCon = (4.5, 7.0), (3.0, 4.5)
loRainbow = genRainbow(rainHues, *loCon)
hiRainbow = [makeColor(hue(c), lumFromContrast(luminance(c), 3) if foreLum > bgLum else luminance(c) / 3, saturation(c)) for c in loRainbow]
darkRainbow, lightRainbow = (loRainbow, hiRainbow) if foreLum > bgLum else (hiRainbow, loRainbow)
firstGrey = min(lumFromContrast(bgLum, 3), lumFromContrast(bgLum, 4.5))
greys = [makeColor(0, lum, 0) for lum in sorted((firstGrey, lumFromContrast(firstGrey, 3)))]
minLum, maxLum = sorted((bgLum, foreLum))
black = makeColor(0, minLum / 2.0, 0)
white = makeColor(0, (1.0 + maxLum) / 2.0, 0)
cols = [black] + darkRainbow + [greys[1], greys[0]] + lightRainbow + [white]
print("*foreground: #"+toHex(foreground))
print("*background: #"+toHex(background))
print("*cursorColor: #"+toHex(cursor))
for i in range(len(cols)):
print("*color{}: #{}".format(i, toHex(cols[i])))
return [background, foreground, cursor] + cols
def xresourcesPreview(im, cols):
background, foreground, cursor = cols[:3]
cols = cols[3:]
w, h = im.size()
im.text(5, 5, "Foreground on background", foreground, background)
im.draw.rectangle((2, 27, w-2, 52), usable(cursor))
im.text(5, 30, "Foreground on cursor color", foreground, cursor)
for i in range(len(cols)):
x, y = (i // 8) * 300, (i % 8) * 30
im.text(20+x, 80+y, "Color", cols[i], background)
im.drawPalette(cols)
cols = globals()[options.mode+"Generate"]()
if options.showPreview:
im = ImageHandler(cols[0])
globals()[options.mode+"Preview"](im, cols)
im.show()