|
|
@@ -0,0 +1,132 @@ |
|
|
|
#!/usr/bin/env python3 |
|
|
|
import colorsys, optparse, collections |
|
|
|
from PIL import Image, ImageDraw, ImageFont |
|
|
|
|
|
|
|
parser = optparse.OptionParser(usage="usage: %prog [options] background accent [foreground]") |
|
|
|
parser.add_option("-p", "--preview", |
|
|
|
action="store_true", |
|
|
|
dest="showPreview", |
|
|
|
default=False, |
|
|
|
help="show preview") |
|
|
|
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("-r", "--rainbow-blend", |
|
|
|
action="store", |
|
|
|
dest="rainbowBlend", |
|
|
|
default=0.0, |
|
|
|
help="mix ratio between last 8 colors' hues and background") |
|
|
|
|
|
|
|
(options, args) = parser.parse_args() |
|
|
|
if len(args) < 2: |
|
|
|
parser.error("need background and accent colors minimum") |
|
|
|
|
|
|
|
def parseTuple(st): |
|
|
|
return tuple(float(x) for x in st.split(",")) |
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
try: |
|
|
|
rainbowBlend = float(options.rainbowBlend) |
|
|
|
except: |
|
|
|
parser.error("rainbow blend must be a number") |
|
|
|
|
|
|
|
def lerp(a, b, t): |
|
|
|
return a + (b - a) * t |
|
|
|
|
|
|
|
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 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 < 1.0 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 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) for x in toSRGB(col)) |
|
|
|
|
|
|
|
def toHex(col): |
|
|
|
return "".join(["{:02x}".format(x) for x in usable(col)]) |
|
|
|
|
|
|
|
background = makeColor(bgHue, bgLum, bgSat) |
|
|
|
foreLum = getContrast(bgLum, 7) |
|
|
|
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) |
|
|
|
|
|
|
|
rainHues = [0, 20, 55, 120, 180, 240, 280, 330] |
|
|
|
|
|
|
|
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 rainHues] |
|
|
|
rainbow = [makeColor(colorsys.rgb_to_hls(*col)[0], getContrast(bgLum, 3.5), 1.0) for col in rainbow] |
|
|
|
|
|
|
|
cols = [background, backLighter, accent, comment, foreDark, foreground, foreLight, backLight] + rainbow |
|
|
|
|
|
|
|
print("scheme: \"{}\"".format(options.name)) |
|
|
|
print("author: \"{}\"".format(options.author)) |
|
|
|
|
|
|
|
for i in range(len(cols)): |
|
|
|
print("base{:02X}: \"{}\"".format(i, toHex(cols[i]))) |
|
|
|
|
|
|
|
if options.showPreview: |
|
|
|
font = ImageFont.truetype("/usr/share/fonts/TTF/LiberationMono-Regular.ttf", 20) |
|
|
|
|
|
|
|
im = Image.new("RGB", (640, 640), usable(background)) |
|
|
|
draw = ImageDraw.Draw(im) |
|
|
|
|
|
|
|
draw.rectangle((2, 2, 638, 32), usable(accent)) |
|
|
|
draw.text((5, 5), "Foreground (contrast {:.1f}:1 w/ highlight)".format(contrast(luminance(foreground), luminance(accent))), usable(foreground), font) |
|
|
|
draw.text((5, 45), "Foreground (contrast {:.1f}:1 w/ background)".format(contrast(luminance(foreground), luminance(background))), usable(foreground), font) |
|
|
|
draw.text((5, 85), "# Comment color (contrast {:.1f}:1)".format(contrast(luminance(comment), luminance(background))), usable(comment), font) |
|
|
|
|
|
|
|
for i in range(len(rainbow)): |
|
|
|
draw.rectangle((4, 120+i*40, 84, 150+i*40), usable(rainbow[i])) |
|
|
|
draw.text((90, 125+i*40), "Text (contrast 3.5:1)".format(contrast(luminance(rainbow[i]), luminance(background))), usable(rainbow[i]), font) |
|
|
|
|
|
|
|
draw.rectangle((0, 560, 640, 600), usable(backLighter)) |
|
|
|
draw.text((5, 565), "'Dark' foreground (contrast {:.2f}:1)".format(contrast(luminance(backLighter), luminance(foreDark))), usable(foreDark), font) |
|
|
|
draw.line((0, 598, 640, 598), usable(foreground), 2) |
|
|
|
|
|
|
|
for i in range(len(cols)): |
|
|
|
draw.rectangle((i*40,600,i*40+39,639), usable(cols[i])) |
|
|
|
|
|
|
|
im.show() |