Browse Source

Greatly improved contrast detection and color selection

* Colors are now converted to (hue, lightness, saturation) for
measuring brightness instead of doing my janky average
* Comments are properly selected to be contrasty instead of just being
another dark color
* Foreground colors try to be unique but will resort to randomly
picking from the contrasty pool if there are no free colors
Macoy Madson 5 years ago
  1. 131
  2. 20


@ -2,8 +2,11 @@
# MIT License
import random
import colorsys
This script generates a "base16" color theme intended for code syntax highlighting.
Base16 Style (from
base00 - Default Background
base01 - Lighter Background (Used for status bars)
@ -23,10 +26,32 @@ Base16 Style (from
base0F - Deprecated, Opening/Closing Embedded Language Tags, e.g. <?php ?>
# TODO: Add other options for templates (take as a command line argument)
outputTemplateFilename = 'emacs-base16-theme-template.el'
outputFilename = 'base16-my-auto-theme.el'
# TODO: Make these into modes that auto-set these constraints
# TODO: Make contrast ratio mode which meets accessibility guidelines (see
## Background
# Make sure the background is darker than this (for dark themes). In HSL Lightness (0-1)
maximumBackgroundBrightness = 0.29
## Foreground contrasts (i.e. text color HSL lightness - background color HSL lightness)
# These are relative values instead of ratios because you can't figure a ratio on a black background
minimumCommentTextContrast = 0.3
minimumTextContrast = 0.43
maximumTextContrast = 0.65
## Debugging
debugColorsVerbose = False
## Internal constants
# The index for the darkest background color. This is important to make sure contrast is high enough
# between the darkest background color and the darkest text
@ -37,17 +62,31 @@ class Base16Color:
self.color = None
self.selectionFunction = selectionFunction
Color utility functions
def rgbColorFromStringHex(colorStringHex):
# From
return tuple(int(colorStringHex.strip('#')[i:i+2], 16) for i in (0, 2 ,4))
# TODO: There's probably some fancier way to do this which will give better results (e.g. convert to HSV)
def getColorAverageBrightness(color):
def rgb256ToHls(color):
rgbColor = color
if type(color) == str:
rgbColor = rgbColorFromStringHex(color)
normalizedColor = []
for component in rgbColor:
normalizedColor.append(component / 256)
return colorsys.rgb_to_hls(normalizedColor[0], normalizedColor[1], normalizedColor[2])
def getColorBrightness(color):
hsvColor = rgb256ToHls(color)
return sum(rgbColor) / len(rgbColor)
return hsvColor[1]
def colorHasBeenUsed(base16Colors, color):
for base16Color in base16Colors:
@ -55,29 +94,45 @@ def colorHasBeenUsed(base16Colors, color):
return True
return False
def isColorWithinContrastRange(color, backgroundColor, minimumContrast, maximumContrast):
colorBrightness = getColorBrightness(color)
backgroundBrightness = getColorBrightness(backgroundColor)
contrast = colorBrightness - backgroundBrightness
if debugColorsVerbose:
print('Color {} brightness {} background {} brightness {} difference {}'
.format(color, colorBrightness, backgroundColor, backgroundBrightness, contrast))
return (contrast >= minimumContrast and contrast <= maximumContrast)
Selection heuristics
# Pick darkest, most grey color for background. If the color is already taken, pick the next unique darkest
def pickDarkestGreyestColorUnique(base16Colors, currentBase16Color, colorPool):
bestColor = None
bestColorAverage = 10000
bestColorBrightness = 10000
for color in colorPool:
rgbColorAverage = getColorAverageBrightness(color)
if rgbColorAverage < bestColorAverage and not colorHasBeenUsed(base16Colors, color):
rgbColorBrightness = getColorBrightness(color)
if rgbColorBrightness < bestColorBrightness and not colorHasBeenUsed(base16Colors, color):
bestColor = color
bestColorAverage = rgbColorAverage
bestColorBrightness = rgbColorBrightness
return bestColor
# Selects the darkest color which meets the contrast requirements and which hasn't been used yet
def pickDarkestHighContrastColorUnique(base16Colors, currentBase16Color, colorPool):
minimumDarkContrast = 56
viableColors = []
for color in colorPool:
if (getColorAverageBrightness(color)
- getColorAverageBrightness(base16Colors[BACKGROUND_COLOR_INDEX].color) > minimumDarkContrast):
if isColorWithinContrastRange(color, base16Colors[BACKGROUND_COLOR_INDEX].color,
minimumCommentTextContrast, maximumTextContrast):
viableColors = sorted(viableColors,
key=lambda color: getColorAverageBrightness(color), reverse=True)
key=lambda color: getColorBrightness(color), reverse=False)
# We've sorted in order of brightness; pick the darkest one which is unique
bestColor = None
@ -90,23 +145,34 @@ def pickDarkestHighContrastColorUnique(base16Colors, currentBase16Color, colorPo
# Pick high contrast foreground
# High contrast = a minimum brightness difference between this and the brightest background)
def pickHighContrastBrightColorRandom(base16Colors, currentBase16Color, colorPool):
minimumBrightContrast = 92
def pickHighContrastBrightColorUniqueOrRandom(base16Colors, currentBase16Color, colorPool):
viableColors = []
for color in colorPool:
if (getColorAverageBrightness(color)
- getColorAverageBrightness(base16Colors[BACKGROUND_COLOR_INDEX].color) > minimumBrightContrast):
if isColorWithinContrastRange(color, base16Colors[BACKGROUND_COLOR_INDEX].color,
minimumTextContrast, maximumTextContrast):
if not viableColors:
return None
return random.choice(viableColors)
# Prefer a color which is unique
bestColor = None
for color in viableColors:
if not colorHasBeenUsed(base16Colors, color):
bestColor = color
return bestColor if bestColor else random.choice(viableColors)
def pickRandomColor(base16Colors, currentBase16Color, colorPool):
return random.choice(colorPool)
def main():
colorsFile = open('colors.txt', 'r')
colorsLines = colorsFile.readlines()
@ -121,31 +187,31 @@ def main():
# base02 - Selection Background
Base16Color('base02', pickDarkestGreyestColorUnique),
# base03 - Comments, Invisibles, Line Highlighting
Base16Color('base03', pickDarkestHighContrastColorUnique),#pickDarkestGreyestColorUnique),
Base16Color('base03', pickDarkestHighContrastColorUnique),
# base04 - Dark Foreground (Used for status bars)
Base16Color('base04', pickDarkestGreyestColorUnique),
# base05 - Default Foreground, Caret, Delimiters, Operators
Base16Color('base05', pickDarkestGreyestColorUnique),
Base16Color('base05', pickDarkestHighContrastColorUnique),
# base06 - Light Foreground (Not often used)
Base16Color('base06', pickDarkestGreyestColorUnique),
# base07 - Light Background (Not often used)
Base16Color('base07', pickDarkestGreyestColorUnique),
# base08 - Variables, XML Tags, Markup Link Text, Markup Lists, Diff Deleted
Base16Color('base08', pickHighContrastBrightColorRandom),
Base16Color('base08', pickHighContrastBrightColorUniqueOrRandom),
# base09 - Integers, Boolean, Constants, XML Attributes, Markup Link Url
Base16Color('base09', pickHighContrastBrightColorRandom),
Base16Color('base09', pickHighContrastBrightColorUniqueOrRandom),
# base0A - Classes, Markup Bold, Search Text Background
Base16Color('base0A', pickHighContrastBrightColorRandom),
Base16Color('base0A', pickHighContrastBrightColorUniqueOrRandom),
# base0B - Strings, Inherited Class, Markup Code, Diff Inserted
Base16Color('base0B', pickHighContrastBrightColorRandom),
Base16Color('base0B', pickHighContrastBrightColorUniqueOrRandom),
# base0C - Support, Regular Expressions, Escape Characters, Markup Quotes
Base16Color('base0C', pickHighContrastBrightColorRandom),
Base16Color('base0C', pickHighContrastBrightColorUniqueOrRandom),
# base0D - Functions, Methods, Attribute IDs, Headings
Base16Color('base0D', pickHighContrastBrightColorRandom),
Base16Color('base0D', pickHighContrastBrightColorUniqueOrRandom),
# base0E - Keywords, Storage, Selector, Markup Italic, Diff Changed
Base16Color('base0E', pickHighContrastBrightColorRandom),
Base16Color('base0E', pickHighContrastBrightColorUniqueOrRandom),
# base0F - Deprecated, Opening/Closing Embedded Language Tags, e.g. <?php ?>
Base16Color('base0F', pickHighContrastBrightColorRandom)]
Base16Color('base0F', pickHighContrastBrightColorUniqueOrRandom)]
# For testing
colorPool = ['#001b8c', '#0a126b', '#010e44', '#772e51', '#ca4733', '#381f4d', '#814174',
@ -178,6 +244,13 @@ def main():
print('Selected {} for {}'.format(base16Colors[i].color,
# Ensure backgrounds are dark enough
# backgroundColor = base16Colors[BACKGROUND_COLOR_INDEX]
# for i in [0, 1, 2]:
# color = base16Colors[i].color
# if getColorBrightness
# Output selected colors
outputTemplateFile = open(outputTemplateFilename, 'r')
outputTemplate = ''.join(outputTemplateFile.readlines())
@ -195,7 +268,7 @@ def main():
outputFile = open(outputFilename, 'w')
print('Wrote {} using template {}', outputFilename, outputTemplateFilename)
print('Wrote {} using template {}'.format(outputFilename, outputTemplateFilename))
if __name__ == '__main__':


@ -0,0 +1,20 @@
* Left off
** Make it so hawaii.jpg and georgia.jpg look good (don't let background get too colorful?)
*** blade2.jpg makes it seem like I need some background brightness clamping system
* To Do
** TODO Test selections with more images
** TODO Emacs interface doesn't update correctly (status bars), which doesn't give an accurate picture for those colors
** TODO Check contrast better (i.e. with some fancy algorithm)
** TESTING Make sure base03 comments has a high enough contrast
** TODO Green and red tint for diff colors? Either way, establish relationship between those two (and maybe others) ensuring they are different
*** Relationships
**** base08, base0B, base0E should be unique between eachother (diff colors)
**** Always have base08 variables be darker than 0A classes, 0D functions, and 0E keywords?
**** Ensure no other foreground text values are the same as base03 comments?
** DONE Contrast ratio between text colors shouldn't be too great (try georgia.jpg for a bad result)
** TODO Take template to use as a command line argument
** TODO Support different heuristic modes (e.g. light theme)
* Done
** DONE Figure out how to update base16 theme colors without having to reload emacs (just unset and set in customize-themes?)
** DONE Consider making a maximum contrast to avoid starkly bright text
*** Test maximum brightness code