@ -2,8 +2,11 @@
# MIT License
# https://github.com/makuto/auto-base16-theme
import random
import colorsys
"""
This script generates a " base16 " color theme intended for code syntax highlighting .
Base16 Style ( from https : / / github . com / chriskempson / base16 / blob / master / styling . md ) :
base00 - Default Background
base01 - Lighter Background ( Used for status bars )
@ -23,10 +26,32 @@ Base16 Style (from https://github.com/chriskempson/base16/blob/master/styling.md
base0F - Deprecated , Opening / Closing Embedded Language Tags , e . g . < ? php ? >
"""
"""
Configuration
"""
# 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 https://webaim.org/resources/contrastchecker/)?
## 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
BACKGROUND_COLOR_INDEX = 0
@ -37,17 +62,31 @@ class Base16Color:
self . color = None
self . selectionFunction = selectionFunction
"""
Color utility functions
"""
def rgbColorFromStringHex ( colorStringHex ) :
# From https://stackoverflow.com/questions/29643352/converting-hex-to-rgb-value-in-python
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 ) > minimumDark Contrast) :
if isColorWithinContrastRange ( color , base16Colors [ BACKGROUND_COLOR_INDEX ] . color ,
minimumCommentTextContrast , maximumText Contrast ) :
viableColors . append ( color )
viableColors = sorted ( viableColors ,
key = lambda color : getColorAverage Brightness ( color ) , reverse = Tru e)
key = lambda color : getColorBrightness ( color ) , reverse = Fals e)
# 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 ) > minimumBrigh tContrast) :
if isColorWithinContrastRange ( color , base16Colors [ BACKGROUND_COLOR_INDEX ] . color ,
minimumTextContrast , maximumTex tContrast ) :
viableColors . append ( color )
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
break
return bestColor if bestColor else random . choice ( viableColors )
def pickRandomColor ( base16Colors , currentBase16Color , colorPool ) :
return random . choice ( colorPool )
"""
Procedure
"""
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 ' , pickDarkestGreye stColorUnique ) ,
Base16Color ( ' base05 ' , pickDarkestHighContra stColorUnique ) ,
# 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 ' , pickHighContrastBrightColorUniqueOr Random ) ,
# base09 - Integers, Boolean, Constants, XML Attributes, Markup Link Url
Base16Color ( ' base09 ' , pickHighContrastBrightColorRandom ) ,
Base16Color ( ' base09 ' , pickHighContrastBrightColorUniqueOr Random ) ,
# base0A - Classes, Markup Bold, Search Text Background
Base16Color ( ' base0A ' , pickHighContrastBrightColorRandom ) ,
Base16Color ( ' base0A ' , pickHighContrastBrightColorUniqueOr Random ) ,
# base0B - Strings, Inherited Class, Markup Code, Diff Inserted
Base16Color ( ' base0B ' , pickHighContrastBrightColorRandom ) ,
Base16Color ( ' base0B ' , pickHighContrastBrightColorUniqueOr Random ) ,
# base0C - Support, Regular Expressions, Escape Characters, Markup Quotes
Base16Color ( ' base0C ' , pickHighContrastBrightColorRandom ) ,
Base16Color ( ' base0C ' , pickHighContrastBrightColorUniqueOr Random ) ,
# base0D - Functions, Methods, Attribute IDs, Headings
Base16Color ( ' base0D ' , pickHighContrastBrightColorRandom ) ,
Base16Color ( ' base0D ' , pickHighContrastBrightColorUniqueOr Random ) ,
# base0E - Keywords, Storage, Selector, Markup Italic, Diff Changed
Base16Color ( ' base0E ' , pickHighContrastBrightColorRandom ) ,
Base16Color ( ' base0E ' , pickHighContrastBrightColorUniqueOr Random ) ,
# base0F - Deprecated, Opening/Closing Embedded Language Tags, e.g. <?php ?>
Base16Color ( ' base0F ' , pickHighContrastBrightColorRandom ) ]
Base16Color ( ' base0F ' , pickHighContrastBrightColorUniqueOr Random ) ]
# For testing
colorPool = [ ' #001b8c ' , ' #0a126b ' , ' #010e44 ' , ' #772e51 ' , ' #ca4733 ' , ' #381f4d ' , ' #814174 ' ,
@ -178,6 +244,13 @@ def main():
print ( ' Selected {} for {} ' . format ( base16Colors [ i ] . color , base16Color . name ) )
# 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 ' )
outputFile . write ( outputText )
outputFile . close ( )
print ( ' Wrote {} using template {} ' , outputFilename , outputTemplateFilename )
print ( ' Wrote {} using template {} ' . format ( outputFilename , outputTemplateFilename ) )
if __name__ == ' __main__ ' :
main ( )