316 lines
12 KiB
Python
Executable File
316 lines
12 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
|
|
blendTargets = ["background","foreground","accent"]
|
|
|
|
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("-B", "--hue-blend-target",
|
|
action="store",
|
|
dest="hueBlendTarget",
|
|
default="background",
|
|
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()
|
|
|
|
def dieIf(cond, message):
|
|
cond and parser.error(message)
|
|
|
|
dieIf(options.mode not in modes, "unknown mode \"{}\"".format(options.mode))
|
|
dieIf(len(args) < 2, "need background and accent colors minimum")
|
|
dieIf(options.hueBlendTarget not in blendTargets, "unknown hue blend target \"{}\"".format(options.hueBlendTarget))
|
|
|
|
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, "hue 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):
|
|
target = {"background": (bgHue,bgSat),
|
|
"foreground": (foreHue, foreSat),
|
|
"accent": (accentHue, accentSat)}[options.hueBlendTarget]
|
|
backFull = colorsys.hls_to_rgb(target[0], 0.5, target[1])
|
|
|
|
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))
|
|
sayIf(hueBlend > 0.0, "hue blend : {}, with {}".format(hueBlend, options.hueBlendTarget))
|
|
sayIf(foreContrast != 7, "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()
|