Generate color schemes from a background color, accent color, and optional foreground color
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

316 lines
12KB

  1. #!/usr/bin/env python3
  2. import colorsys, optparse
  3. from PIL import Image, ImageDraw, ImageFont
  4. modes = ["base16","xresources"]
  5. RED, YELLOW, GREEN, CYAN, BLUE, MAGENTA = 0, 60, 120, 200, 240, 300
  6. blendTargets = ["background","foreground","accent"]
  7. parser = optparse.OptionParser(usage="usage: %prog [options] background accent [foreground]")
  8. parser.add_option("-a", "--author",
  9. action="store",
  10. dest="author",
  11. default="acid16",
  12. help="author name")
  13. parser.add_option("-b", "--hue-blend",
  14. action="store",
  15. dest="hueBlend",
  16. default=0.0,
  17. help="mix ratio between hues and background")
  18. parser.add_option("-B", "--hue-blend-target",
  19. action="store",
  20. dest="hueBlendTarget",
  21. default="background",
  22. help="what to blend the hues with: background (default), foreground, accent")
  23. parser.add_option("-c", "--contrast",
  24. action="store",
  25. dest="foreContrast",
  26. default=7.0,
  27. help="contrast between foreground and background (default 7)")
  28. parser.add_option("-e", "--equiluminant",
  29. action="store_true",
  30. dest="equiluminant",
  31. default=0.0,
  32. help="use the same luminance for last hues")
  33. parser.add_option("-m", "--mode",
  34. action="store",
  35. dest="mode",
  36. default="base16",
  37. help="what to generate ({})".format(", ".join(modes)))
  38. parser.add_option("-n", "--name",
  39. action="store",
  40. dest="name",
  41. default="acid16",
  42. help="scheme name")
  43. parser.add_option("-p", "--preview",
  44. action="store_true",
  45. dest="showPreview",
  46. default=False,
  47. help="generate a preview image")
  48. parser.add_option("--hsl",
  49. action="store_true",
  50. dest="hsl",
  51. default=0.0,
  52. help="treat background color as gamma-encoded HSL")
  53. base16Options = optparse.OptionGroup(parser, "Base16 options")
  54. base16Options.add_option("-l", "--light-brown",
  55. action="store_true",
  56. dest="lightBrown",
  57. default=0.0,
  58. help="don't darken last color")
  59. parser.add_option_group(base16Options)
  60. (options, args) = parser.parse_args()
  61. def dieIf(cond, message):
  62. cond and parser.error(message)
  63. dieIf(options.mode not in modes, "unknown mode \"{}\"".format(options.mode))
  64. dieIf(len(args) < 2, "need background and accent colors minimum")
  65. dieIf(options.hueBlendTarget not in blendTargets, "unknown hue blend target \"{}\"".format(options.hueBlendTarget))
  66. def parseTuple(st):
  67. return tuple(float(x) for x in st.split(","))
  68. def floatOrDie(fl, errSt):
  69. try:
  70. return float(fl)
  71. except:
  72. parser.error(errSt)
  73. class ImageHandler:
  74. def __init__(self, color):
  75. self.font = ImageFont.truetype("LiberationMono-Regular.ttf", 20)
  76. self.image = Image.new("RGB", (640, 480), usable(color))
  77. self.draw = ImageDraw.Draw(self.image)
  78. def size(self):
  79. return self.image.size
  80. def desc(self, t, a, b):
  81. return "{} (contrast {:.1f})".format(t, contrast(luminance(a), luminance(b)))
  82. def text(self, x, y, t, col, other):
  83. self.draw.text((x, y), self.desc(t, col, other), usable(col), self.font)
  84. def show(self):
  85. self.image.show()
  86. def drawPalette(self, cols):
  87. w, h = self.size()
  88. for i in range(len(cols)):
  89. im.draw.rectangle((i*40,h-40,i*40+39,h), usable(cols[i]))
  90. def toLinear(col):
  91. return tuple(x/12.92 if x < 0.04045 else ((x+0.055)/1.055)**2.4 for x in col)
  92. def toSRGB(col):
  93. return tuple(x*12.92 if x < 0.0031308 else 1.055*x**(1/2.4) - 0.055 for x in col)
  94. def luminance(col):
  95. return col[0]*0.2126+col[1]*0.7152+col[2]*0.0722
  96. foreContrast = floatOrDie(options.foreContrast, "foreground contrast must be a number")
  97. bgHue, bgSat, origLum = parseTuple(args[0])
  98. bgLum = luminance(toLinear(colorsys.hls_to_rgb(bgHue, origLum, bgSat))) if options.hsl else origLum
  99. accentHue, accentSat = parseTuple(args[1])
  100. foreHue, foreSat = parseTuple(args[2]) if len(args) > 2 else (0.0, 0.0)
  101. hueBlend = floatOrDie(options.hueBlend, "hue blend must be a number")
  102. def lerp(a, b, t):
  103. return a + (b - a) * t
  104. def collapse(l):
  105. return [y for x in l for y in x]
  106. def reposition(a, b, t, c, d):
  107. return lerp(c, d, (t - a) / (b - a))
  108. def mixColors(a, b, t):
  109. return tuple(lerp(a[i], b[i], t) for i in range(3))
  110. def hue(col):
  111. return colorsys.rgb_to_hls(*col)[0]
  112. def saturation(col):
  113. return colorsys.rgb_to_hls(*col)[2]
  114. def contrast(a, b):
  115. return (max(a, b) + 0.05) / (min(a, b) + 0.05)
  116. def lumFromContrast(lum, ratio):
  117. hi = ratio*lum + (ratio - 1)*0.05
  118. lo = (lum + 0.05 - ratio * 0.05) / ratio
  119. if hi == min(1.0, hi): return hi
  120. if lo == max(0.0, lo): return lo
  121. hiCon = 1.05 / (lum + 0.05)
  122. loCon = (lum + 0.05) / 0.05
  123. return hi if hiCon > loCon else lo
  124. def clamp(x):
  125. return min(1.0, max(x, 0.0))
  126. def makeColor(hue, lum, sat = 1.0):
  127. orig = luminance(colorsys.hls_to_rgb(hue, 0.5, sat))
  128. 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))
  129. def makeBackground():
  130. return makeColor(bgHue, bgLum, bgSat)
  131. def foregroundLum(bg = bgLum):
  132. return lumFromContrast(bg, foreContrast)
  133. CONSTRAIN_LOWER = False
  134. CONSTRAIN_UPPER = True
  135. def constrain(orig, compare, limit, mode=CONSTRAIN_UPPER):
  136. comp = lambda a, b: a > b if mode else a < b
  137. return lumFromContrast(compare, limit) if comp(contrast(orig, compare), limit) else orig
  138. def genRainbow(hues, minCon, maxCon):
  139. target = {"background": (bgHue,bgSat),
  140. "foreground": (foreHue, foreSat),
  141. "accent": (accentHue, accentSat)}[options.hueBlendTarget]
  142. backFull = colorsys.hls_to_rgb(target[0], 0.5, target[1])
  143. rainbow = [mixColors(colorsys.hls_to_rgb(hue/360.0, 0.5, 1.0), backFull, hueBlend) for hue in hues]
  144. rainLums = [luminance(col) for col in rainbow]
  145. hiLum, loLum = max(rainLums), min(rainLums)
  146. minLum, maxLum = sorted((lumFromContrast(bgLum, minCon), lumFromContrast(bgLum, minCon if options.equiluminant else maxCon )))
  147. maxLum = min(1.0, maxLum)
  148. darkenBrown = (options.equiluminant or options.lightBrown)
  149. rainHLS = [colorsys.rgb_to_hls(*col) for col in rainbow]
  150. 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))]
  151. def printBanner(commentChar):
  152. say = lambda st: print(commentChar+" "+st)
  153. sayIf = lambda cond, st: say(st) if cond else None
  154. say("generated by acid16 - https://git.lain.church/whut/acid16")
  155. say("--- settings ---")
  156. say("background {}: {} {} {}".format("HSL" if options.hsl else "HCL", bgHue, bgSat, origLum))
  157. say("accent HC : {} {}".format(accentHue, accentSat))
  158. say("foreground HC : {} {}".format(foreHue, foreSat))
  159. sayIf(hueBlend > 0.0, "hue blend : {}, with {}".format(hueBlend, options.hueBlendTarget))
  160. sayIf(foreContrast != 7, "fg contrast : {}".format(foreContrast))
  161. sayIf(options.equiluminant, "equiluminant hues")
  162. sayIf(options.hsl, "using gamma-encoded HSL")
  163. def usable(col):
  164. return tuple(int(x*255+0.5) for x in toSRGB(col))
  165. def toHex(col):
  166. return "".join(["{:02x}".format(x) for x in usable(col)])
  167. def base16Generate():
  168. background = makeBackground()
  169. foreLum = clamp(foregroundLum())
  170. foreCon = contrast(foreLum, bgLum)
  171. foreground = makeColor(foreHue, foreLum, foreSat)
  172. commentLum = lumFromContrast(bgLum, max(4.5, 1.0 + foreCon / 2.0))
  173. comment = makeColor(accentHue, commentLum, accentSat)
  174. accentLum = constrain(lumFromContrast(foreLum, 4.5), bgLum, 2.0)
  175. accent = makeColor(accentHue, accentLum, accentSat)
  176. statusBgLum = lumFromContrast(bgLum, 1.25)
  177. backStatus = makeColor(bgHue, statusBgLum, bgSat)
  178. foreStatus = makeColor(foreHue, lumFromContrast(statusBgLum, 7.0), foreSat)
  179. # supposedly these two are rarely used so i don't care
  180. lastTwo = [lerp(foreLum, 1.0 if foreLum > bgLum else 0.0, (x + 1) / 3) for x in range(2)]
  181. foreLight = makeColor(foreHue, lastTwo[0], foreSat)
  182. backLight = makeColor(bgHue, lastTwo[1], bgSat)
  183. rainbow = genRainbow([RED, 23, YELLOW, GREEN, CYAN, BLUE, MAGENTA, 15], 4.5, 7.0)
  184. cols = [background, backStatus, accent, comment, foreStatus, foreground, foreLight, backLight] + rainbow
  185. printBanner("#")
  186. if options.lightBrown:
  187. print("# not darkening last color (brown)")
  188. print("\nscheme: \"{}\"".format(options.name))
  189. print("author: \"{}\"".format(options.author))
  190. for i in range(len(cols)):
  191. print("base{:02X}: \"{}\"".format(i, toHex(cols[i])))
  192. return cols
  193. def base16Preview(im, cols):
  194. background, foreground, accent, comment, backLighter, foreDark, rainbow = cols[0], cols[5], cols[2], cols[3], cols[1], cols[4], cols[8:]
  195. w, h = im.size()
  196. im.draw.rectangle((2, 2, w-2, 32), usable(accent))
  197. im.text(5, 5, "Foreground on highlight", foreground, accent)
  198. im.text(5, 45, "Foreground on background", foreground, background)
  199. im.text(5, 85, "# Comment color", comment, background)
  200. for i in range(len(rainbow)):
  201. im.draw.rectangle((4, 120+i*35, 84, 145+i*35), usable(rainbow[i]))
  202. im.text(90, 123+i*35, "Colored text", rainbow[i], background)
  203. im.draw.rectangle((0, h-80, w, h-40), usable(backLighter))
  204. im.text(5, h-75, "Status foreground", foreDark, backLighter)
  205. im.draw.line((0, h-42, w, h-42), usable(foreground), 2)
  206. im.drawPalette(cols)
  207. def xresourcesGenerate():
  208. background = makeBackground()
  209. foreLum = clamp(foregroundLum())
  210. foreground = makeColor(foreHue, foreLum, foreSat)
  211. cursor = makeColor(accentHue, constrain(lumFromContrast(foreLum, 3), bgLum, 2.0), accentSat)
  212. rainHues = [RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN]
  213. hiCon, loCon = (4.5, 7.0), (3.0, 4.5)
  214. loRainbow = genRainbow(rainHues, *loCon)
  215. hiRainbow = [makeColor(hue(c), lumFromContrast(luminance(c), 3) if foreLum > bgLum else luminance(c) / 3, saturation(c)) for c in loRainbow]
  216. darkRainbow, lightRainbow = (loRainbow, hiRainbow) if foreLum > bgLum else (hiRainbow, loRainbow)
  217. firstGrey = min(lumFromContrast(bgLum, 3), lumFromContrast(bgLum, 4.5))
  218. greys = [makeColor(0, lum, 0) for lum in sorted((firstGrey, lumFromContrast(firstGrey, 3)))]
  219. minLum, maxLum = sorted((bgLum, foreLum))
  220. black = makeColor(0, minLum / 2.0, 0)
  221. white = makeColor(0, (1.0 + maxLum) / 2.0, 0)
  222. cols = [black] + darkRainbow + [greys[1], greys[0]] + lightRainbow + [white]
  223. print("*foreground: #"+toHex(foreground))
  224. print("*background: #"+toHex(background))
  225. print("*cursorColor: #"+toHex(cursor))
  226. for i in range(len(cols)):
  227. print("*color{}: #{}".format(i, toHex(cols[i])))
  228. return [background, foreground, cursor] + cols
  229. def xresourcesPreview(im, cols):
  230. background, foreground, cursor = cols[:3]
  231. cols = cols[3:]
  232. w, h = im.size()
  233. im.text(5, 5, "Foreground on background", foreground, background)
  234. im.draw.rectangle((2, 27, w-2, 52), usable(cursor))
  235. im.text(5, 30, "Foreground on cursor color", foreground, cursor)
  236. for i in range(len(cols)):
  237. x, y = (i // 8) * 300, (i % 8) * 30
  238. im.text(20+x, 80+y, "Color", cols[i], background)
  239. im.drawPalette(cols)
  240. cols = globals()[options.mode+"Generate"]()
  241. if options.showPreview:
  242. im = ImageHandler(cols[0])
  243. globals()[options.mode+"Preview"](im, cols)
  244. im.show()