Generate color schemes from a background color, accent color, and optional foreground color
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

316 lignes
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()