273 lines
9.7 KiB
Python
Executable File
273 lines
9.7 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("-m", "--mode",
|
|
action="store",
|
|
dest="mode",
|
|
default="base16",
|
|
help="what to generate ({})".format(", ".join(modes)))
|
|
parser.add_option("-p", "--preview",
|
|
action="store_true",
|
|
dest="showPreview",
|
|
default=False,
|
|
help="generate a preview image")
|
|
parser.add_option("-n", "--name",
|
|
action="store",
|
|
dest="name",
|
|
default="acid16",
|
|
help="scheme name")
|
|
parser.add_option("-a", "--author",
|
|
action="store",
|
|
dest="author",
|
|
default="acid16",
|
|
help="author name")
|
|
parser.add_option("-b", "--hue-blend",
|
|
action="store",
|
|
dest="rainbowBlend",
|
|
default=0.0,
|
|
help="mix ratio between hues and background")
|
|
parser.add_option("-e", "--equiluminant",
|
|
action="store_true",
|
|
dest="equiluminant",
|
|
default=0.0,
|
|
help="use the same luminance for last hues")
|
|
|
|
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]))
|
|
|
|
bgHue, bgSat, bgLum = parseTuple(args[0])
|
|
|
|
accentHue, accentSat = parseTuple(args[1])
|
|
|
|
foreHue, foreSat = parseTuple(args[2]) if len(args) > 2 else (0.0, 0.0)
|
|
|
|
rainbowBlend = floatOrDie(options.rainbowBlend, "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 luminance(col):
|
|
return col[0]*0.2126+col[1]*0.7152+col[2]*0.0722
|
|
|
|
def hue(col):
|
|
return colorsys.rgb_to_hls(*col)[0]
|
|
|
|
def saturation(col):
|
|
return colorsys.rgb_to_hls(*col)[2]
|
|
|
|
def contrast(a, b):
|
|
lo, hi = min(a, b), max(a, b)
|
|
return (hi + 0.05) / (lo + 0.05)
|
|
|
|
def getContrast(lum, ratio):
|
|
hi = ratio*lum + (ratio - 1)*0.05
|
|
lo = (lum + 0.05 - ratio * 0.05) / ratio
|
|
return hi if hi < 0.99 or lo < 0.0 else lo
|
|
|
|
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 foregroundContrast():
|
|
return getContrast(bgLum, 7)
|
|
|
|
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, rainbowBlend) for hue in hues]
|
|
|
|
rainLums = [luminance(col) for col in rainbow]
|
|
hiLum, loLum = max(rainLums), min(rainLums)
|
|
minLum, maxLum = sorted((getContrast(bgLum, minCon), getContrast(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)
|
|
say("generated by acid16 - https://git.lain.church/whut/acid16")
|
|
say("--- settings ---")
|
|
say("background HCL: {} {} {}".format(bgHue, bgSat, bgLum))
|
|
say("accent HC : {} {}".format(accentHue, accentSat))
|
|
say("foreground HC : {} {}".format(foreHue, foreSat))
|
|
say("hue blend : {}".format(rainbowBlend))
|
|
if options.equiluminant:
|
|
say("equiluminant hues")
|
|
|
|
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 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 = foregroundContrast()
|
|
foreground = makeColor(foreHue, foreLum, foreSat)
|
|
commentLum = getContrast(bgLum, 3)
|
|
comment = makeColor(accentHue, commentLum, accentSat)
|
|
accentLum = getContrast(foreLum, 4.5)
|
|
accent = makeColor(accentHue, accentLum, accentSat)
|
|
|
|
foreDark = makeColor(foreHue, (commentLum + foreLum) * 0.5, foreSat)
|
|
backLighter = makeColor(bgHue, (bgLum + accentLum) * 0.5, bgSat)
|
|
|
|
# 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, backLighter, accent, comment, foreDark, 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[6], 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, "'Dark' foreground", foreDark, backLighter)
|
|
im.draw.line((0, h-42, w, h-42), usable(foreground), 2)
|
|
|
|
im.drawPalette(cols)
|
|
|
|
def xresourcesGenerate():
|
|
background = makeBackground()
|
|
foreLum = foregroundContrast()
|
|
foreground = makeColor(foreHue, foreLum, foreSat)
|
|
cursor = makeColor(accentHue, getContrast(foreLum, 3), accentSat)
|
|
|
|
rainHues = [RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN]
|
|
|
|
hiCon, loCon = (4.5, 7.0), (3.0, 4.5)
|
|
|
|
#hiRainbow = genRainbow(rainHues, *hiCon)
|
|
loRainbow = genRainbow(rainHues, *loCon)
|
|
hiRainbow = [makeColor(hue(c), getContrast(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(getContrast(bgLum, 3), getContrast(bgLum, 4.5))
|
|
|
|
greys = [makeColor(0, lum, 0) for lum in sorted((firstGrey, getContrast(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()
|