diff --git a/AutoBase16Theme.py b/AutoBase16Theme.py index 6283c7f..34f3204 100644 --- a/AutoBase16Theme.py +++ b/AutoBase16Theme.py @@ -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. """ +""" + +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) > minimumDarkContrast): + if isColorWithinContrastRange(color, base16Colors[BACKGROUND_COLOR_INDEX].color, + minimumCommentTextContrast, maximumTextContrast): viableColors.append(color) 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): 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', 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. - 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, 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() diff --git a/Planning.org b/Planning.org new file mode 100644 index 0000000..c0f0b88 --- /dev/null +++ b/Planning.org @@ -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