new Color class replacing bare hex strings, WCAG contrast ratio and relative luminance functions, allow automatic selection of more ideal contrasting colors

This commit is contained in:
Xevion
2021-01-20 23:24:48 -06:00
parent c3bb1f1fee
commit fd7cf3bdec
3 changed files with 125 additions and 14 deletions

View File

@@ -1,3 +1,7 @@
from typing import List
import webcolors
HEADER_LENGTH = 10
@@ -24,17 +28,118 @@ class Requests:
GET_HISTORY = 'GET_HISTORY' # Send a short history of the chat to the client
class Color():
"""
Describes a basic RGB color.
"""
def __init__(self, name: str, hex: str) -> None:
self.name, self.hex = name, hex
self.rgb = webcolors.hex_to_rgb(hex)
def __repr__(self) -> str:
return f'Color({self.name})'
@property
def id(self) -> str:
return '_'.join(self.name.split()).upper()
def relative_luminance(self) -> float:
"""
Calculates the relative luminance of a given color, according to WCAG guidelines.
https://www.w3.org/TR/WCAG20/#relativeluminancedef
:return: The relative luminance of this color.
"""
r, g, b = self.rgb.red / 255, self.rgb.green / 255, self.rgb.blue / 255
r = r / 12.92 if r <= 0.03928 else ((r + 0.055) / 1.055) ** 2.4
g = g / 12.92 if g <= 0.03928 else ((g + 0.055) / 1.055) ** 2.4
b = b / 12.92 if b <= 0.03928 else ((b + 0.055) / 1.055) ** 2.4
return 0.2126 * r + 0.7152 * g + 0.0722 * b
def contrast_ratio(self, other: 'Color') -> float:
"""
Calculates the relative contrast between two colors in a ratio from 1:1 to 21:1, according to WCAG guidelines.
https://www.w3.org/TR/WCAG20/#contrast-ratiodef
:param other: The darker color.
:return:
"""
l1 = self.relative_luminance()
l2 = other.relative_luminance()
return (l1 + 0.05) / (l2 + 0.05)
class Colors:
"""
Stores hexadecimal color codes of popular colors, with names.
"""
ALL_NAMES = ['Aqua', 'Azure', 'Beige', 'Black', 'Blue', 'Brown', 'Cyan', 'Dark Blue', 'Dark Cyan', 'Dark Grey',
'Dark Green', 'Dark Khaki', 'Dark Magenta', 'Dark Olive Green', 'Dark Orange', ' Dark Orchid', 'Dark Red', 'Dark Salmon',
'Dark Violet', 'Fuchsia', 'Gold', 'Green', 'Indigo', 'Khaki', 'Light Blue', 'Light Cyan',
'Light Green', 'Light Grey', 'Light Pink', 'Light Yellow', 'Lime', 'Magenta', 'Maroon', 'Navy',
'Olive', 'Orange', 'Pink', 'Purple', 'Violet', 'Red', 'Silver', 'White', 'Yellow']
AQUA = Color("Aqua", "#00ffff")
AZURE = Color("Azure", "#f0ffff")
BEIGE = Color("Beige", "#f5f5dc")
BLACK = Color("Black", "#000000")
BLUE = Color("Blue", "#0000ff")
BROWN = Color("Brown", "#a52a2a")
CYAN = Color("Cyan", "#00ffff")
DARKBLUE = Color("Dark Blue", "#00008b")
DARKCYAN = Color("Dark Cyan", "#008b8b")
DARKGREY = Color("Dark Grey", "#a9a9a9")
DARKGREEN = Color("Dark Green", "#006400")
DARKKHAKI = Color("Dark Khaki", "#bdb76b")
DARKMAGENTA = Color("Dark Magenta", "#8b008b")
DARKOLIVEGREEN = Color("Dark Olive Green", "#556b2f")
DARKORANGE = Color("Dark Orange", "#ff8c00")
DARKORCHID = Color("Dark Orchid", "#9932cc")
DARKRED = Color("Dark Red", "#8b0000")
DARKSALMON = Color("Dark Salmon", "#e9967a")
DARKVIOLET = Color("Dark Violet", "#9400d3")
FUCHSIA = Color("Fuchsia", "#ff00ff")
GOLD = Color("Gold", "#ffd700")
GREEN = Color("Green", "#008000")
INDIGO = Color("Indigo", "#4b0082")
KHAKI = Color("Khaki", "#f0e68c")
LIGHTBLUE = Color("Light Blue", "#add8e6")
LIGHTCYAN = Color("Light Cyan", "#e0ffff")
LIGHTGREEN = Color("Light Green", "#90ee90")
LIGHTGREY = Color("Light Grey", "#d3d3d3")
LIGHTPINK = Color("Light Pink", "#ffb6c1")
LIGHTYELLOW = Color("Light Yellow", "#ffffe0")
LIME = Color("Lime", "#00ff00")
MAGENTA = Color("Magenta", "#ff00ff")
MAROON = Color("Maroon", "#800000")
NAVY = Color("Navy", "#000080")
OLIVE = Color("Olive", "#808000")
ORANGE = Color("Orange", "#ffa500")
PINK = Color("Pink", "#ffc0cb")
PURPLE = Color("Purple", "#800080")
VIOLET = Color("Violet", "#800080")
RED = Color("Red", "#ff0000")
SILVER = Color("Silver", "#c0c0c0")
WHITE = Color("White", "#ffffff")
YELLOW = Color("Yellow", "#ffff00")
ALL = [AQUA, AZURE, BEIGE, BLACK, BLUE, BROWN, CYAN, DARKBLUE, DARKCYAN, DARKGREY, DARKGREEN, DARKKHAKI,
DARKMAGENTA, DARKOLIVEGREEN, DARKORANGE, DARKORCHID, DARKRED, DARKSALMON, DARKVIOLET, FUCHSIA, GOLD, GREEN,
INDIGO, KHAKI, LIGHTBLUE, LIGHTCYAN, LIGHTGREEN, LIGHTGREY, LIGHTPINK, LIGHTYELLOW, LIME, MAGENTA, MAROON,
NAVY, OLIVE, ORANGE, PINK, PURPLE, VIOLET, RED, SILVER, WHITE, YELLOW, ]
NAVY, OLIVE, ORANGE, PINK, PURPLE, VIOLET, RED, SILVER, WHITE, YELLOW]
@staticmethod
def has_contrast(ratio: float, background: Color = WHITE) -> List[Color]:
"""
Returns a list of Color objects with contrast ratios above the given minimum.
:param ratio: The minimum contrast ratio each color must adhere to.
:param background: Defaults to White, the background (lighter) for the other contrasting colors to be compared to.
:return: A list of Color objects with contrast ratios above the given minimum.
"""
above_ratio = []
for color in Colors.ALL:
contrast = background.contrast_ratio(color)
if contrast >= ratio:
above_ratio.append(color)
return above_ratio

View File

@@ -70,14 +70,20 @@ class CommandHandler:
logger.error(f'Could not process client {self.client.nickname}\'s command request.', exc_info=e)
return 'A fatal error occurred while trying to process this command.'
def reroll(self) -> str:
def reroll(self, minimum_contrast: float = 4.65) -> str:
"""
Randomly change the client's color to a different color.
"""
newColor = random.choice(constants.Colors.ALL)
newColorName = constants.Colors.ALL_NAMES[constants.Colors.ALL.index(newColor)]
i = 0
newColor = self.client.color
choices = constants.Colors.has_contrast(float(minimum_contrast))
while i < 50 and newColor == self.client.color:
newColor = random.choice(choices)
self.client.color = newColor
return f'Changed your color to {newColorName} ({newColor})'
contrast = round(constants.Colors.WHITE.contrast_ratio(newColor), 1)
return f'Changed your color to {newColor.name} ({newColor.hex}/{contrast})'
def help(self, command: str) -> Optional[str]:
"""

View File

@@ -42,7 +42,7 @@ class Client:
self.conn.send(helpers.prepare_json(
{
'type': constants.Types.USER_LIST,
'users': [{'nickname': other.nickname, 'color': other.color} for other in self.all_clients]
'users': [{'nickname': other.nickname, 'color': other.color.hex} for other in self.all_clients]
}
))
@@ -69,13 +69,13 @@ class Client:
def send_message(self, message: str) -> None:
"""Sends a string message as the server to this client."""
self.conn.send(helpers.prepare_message(
nickname='Server', message=message, color=constants.Colors.BLACK
nickname='Server', message=message, color=constants.Colors.BLACK.hex
))
def broadcast_message(self, message: str) -> None:
"""Sends a string message to all connected clients as the Server."""
prepared = helpers.prepare_message(
nickname='Server', message=message, color=constants.Colors.BLACK
nickname='Server', message=message, color=constants.Colors.BLACK.hex
)
for client in self.all_clients:
client.send(prepared)
@@ -99,7 +99,7 @@ class Client:
self.broadcast(helpers.prepare_message(
nickname=self.nickname,
message=data['content'],
color=self.color
color=self.color.hex
))
command = data['content'].strip()