#!/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()