Cope 2.5.0
My personal "standard library" of all the generally useful code I've written for various projects over the years
Loading...
Searching...
No Matches
colors.py
1""" Functions for working with colors """
2__version__ = '0.0.0'
3
4import math
5from typing import Tuple, Literal
6from .named_colors import named_colors
7from .misc import translate
8from colorsys import *
9
10
11# These functions comprise the color algorithm for _matchJSON()
12def rgb2html(r, g, b):
13 return f'#{r:02x}{g:02x}{b:02x}'
14
15def html2rgb(html: str) -> tuple:
16 hex_color = html.lstrip('#')
17 rgb = tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
18 return rgb
19
20# TODO make this output rgb
21def generate_colors(amt, s=1, v=1, offset=0):
22 """ Generate `amt` number of colors evenly spaced around the color wheel
23 with a given saturation and value
24 """
25 amt += 1
26 return [toHtml(*map(lambda c: round(c*255), colorsys.hsv_to_rgb(*((offset + ((1/amt) * (i + 1))) % 1.001, s, v)))) for i in range(amt-1)]
27
28# TODO: Make this input any color and output rgb
29def furthest_colors(html, amt=5, v_bias=0, s_bias=0):
30 """ Gets the `amt` number of colors evenly spaced around the color wheel from the given color
31 `v_bias` and `s_bias` are between 0-1 and offset the colors
32 """
33 amt += 1
34 h, s, v = colorsys.rgb_to_hsv(*map(lambda c: c/255, toRgb(html)))
35
36 return [toHtml(*map(lambda c: round(c*255), colorsys.hsv_to_rgb(*((h + ((1/amt) * (i + 1))) % 1.001, (s+s_bias) % 1.001, (v+v_bias) % 1.001)))) for i in range(amt-1)]
37
38
39# TODO: tests
40def complimentary_color(*color, rtn:Literal['html', 'rgb', 'rgba', 'opengl', 'hsv', 'hls', 'yiq']='rgb'):
41 """ Returns the color opposite it on the color wheel, with the same saturation and value. """
42 h, s, v = parse_color(*color, rtn='hsv')
43
44 return parse_color(map(lambda i: i*255, hsv_to_rgb((h + .5) % 1.0001, s, v)), rtn=rtn)
45
46
47def distinct_color(n: int) -> tuple:
48 # Credit to chatGPT
49 # First, ensure if it's 0, we return black
50 if n == 0:
51 return 0, 0, 0
52 angle = n * 137.508
53 r = int(math.sin(angle) * 127 + 128)
54 g = int(math.sin(angle + 2 * math.pi / 3) * 127 + 128)
55 b = int(math.sin(angle + 4 * math.pi / 3) * 127 + 128)
56 return r, g, b
57
58# TODO: change how inputs between 0-1 are handled
59# TODO: add input support for hsv, hls, and yiq
60# TODO: add input & output support for hsv, hls, and yiq with 0-255 instead of 0-1
61def parse_color(*args, rtn:Literal['html', 'rgb', 'rgba', 'opengl', 'hsv', 'hls', 'yiq']='rgb', **kwargs) -> 'type(rtn)':
62 """ One color function to rule them all!
63 Parses a color, however you care to pass it, and returns it however you like.
64
65 Input Schemes:
66 * Positional parameters
67 * parse_color(255, 255, 255, 255)
68 * parse_color(255, 255, 255)
69 * tuple or list
70 * parse_color((255, 255, 255, 255))
71 * parse_color((255, 255, 255))
72 * Keyword parameters/dict
73 * parse_color(r=255, g=255, b=255, a=255)
74 * parse_color({'r': 255, 'g': 255, 'b': 255, 'a': 255})
75 * parse_color(red=255, green=255, blue=255, alpha=255)
76 * parse_color({'red': 255, 'green': 255, 'blue': 255, 'alpha': 255})
77 * OpenGL style colors (between -1 and 1)
78 * parse_color(1., 1., 1.)
79 * parse_color((1., 1., 1.))
80 * Hex colors
81 * parse_color('#FFFFFF')
82 * parse_color('#ffffff')
83 * Named colors
84 * parse_color('white')
85 * "Random" distinct colors
86 * parse_color(1)
87 * Anything that has r, g, b attributes or red, green, blue attributes (callable or not)
88 * parse_color(QColor(255, 255, 255))
89
90 Return Schemes:
91 * 'html'
92 * #FFFFFF
93 * 'rgb'
94 * (255, 255, 255)
95 * 'rgba'
96 * (255, 255, 255, 255)
97 * 'opengl'
98 * (1., 1., 1., 1.)
99 * 'hsv'
100 * (0., 0., 1.)
101 * 'hls'
102 * (0., 1., 0.)
103 * 'yiq'
104 * (1., 0., 0.)
105
106 If `a` is not provided, but it is selected to be returned, it defaults to the max (255, usually)
107
108 NOTE: Don't pass OpenGL colors as dicts or as keyword arguements. It will interpret them as
109 RGBA parameters.
110 """
111 assert len(args) or len(kwargs)
112 a = 255
113 r = None
114 g = None
115 b = None
116
117 if len(args):
118 if isinstance(args[0], str):
119 # We've been given a hex string
120 if args[0].startswith('#'):
121 r, g, b = [int(html.lstrip('#')[i:i+2], 16) for i in (0, 2, 4)]
122 # We've been given the name of a color
123 else:
124 if (r, g, b := named_colors.get(args[0].lower())) is None:
125 raise TypeError(f'{args[0]} is not a recognized color name')
126
127 # We've been given a list of values
128 elif isinstance(args[0], (tuple, list)):
129 if len(args[0]) not in (3, 4):
130 raise TypeError(f'Incorrect number ({len(args[0])}) of color parameters given. Please give either 3 or 4 parameters')
131 else:
132 # We've been given an OpenGL color
133 if all([isinstance(i, float) and i >= -1 and i <= 1 for i in args[0]]):
134 if len(args[0]) == 3:
135 r, g, b = [translate(i, -1, 1, 0, 255) for i in args[0]]
136 else:
137 r, g, b, a = [translate(i, -1, 1, 0, 255) for i in args[0]]
138
139 # Otherwise, interpret them as ints between 0 and 255
140 if len(args[0]) == 3:
141 r, g, b = args[0]
142 else:
143 r, g, b, a = args[0]
144
145 # We've been given a single integer
146 elif isinstance(args[0], (int, float)) and len(args) == 1:
147 r, g, b = distinct_color(int(args[0]))
148
149 # We've been given a dict, for some reason
150 elif isinstance(args[0], dict) and len(args) == 1:
151 kwargs.update(args[0])
152
153 # We've been given something with color attributes
154 elif hasattr(args[0], 'red') and hasattr(args[0], 'green') and hasattr(args[0], 'blue'):
155 if hasattr(args[0], 'alpha'):
156 if hasattr(args[0].red, '__call__') and hasattr(args[0].green, '__call__') and hasattr(args[0].blue, '__call__') and hasattr(args[0].alpha, '__call__'):
157 r, g, b, a = r.red(), r.green(), r.blue(), r.alpha()
158 else:
159 r, g, b, a = r.red, r.green, r.blue, r.alpha
160
161 else:
162 if hasattr(args[0].red, '__call__') and hasattr(args[0].green, '__call__') and hasattr(args[0].blue, '__call__'):
163 r, g, b = r.red(), r.green(), r.blue()
164 else:
165 r, g, b = r.red, r.green, r.blue
166
167 # Try other color attributes
168 elif hasattr(args[0], 'r') and hasattr(args[0], 'g') and hasattr(args[0], 'b'):
169 if hasattr(args[0], 'a'):
170 if hasattr(args[0].r, '__call__') and hasattr(args[0].g, '__call__') and hasattr(args[0].b, '__call__') and hasattr(args[0].a, '__call__'):
171 r, g, b, a = r.r(), r.g(), r.b(), r.a()
172 else:
173 r, g, b, a = r.r, r.g, r.b, r.a
174 else:
175 if hasattr(args[0].r, '__call__') and hasattr(args[0].g, '__call__') and hasattr(args[0].b, '__call__'):
176 r, g, b = r.r(), r.g(), r.b()
177 else:
178 r, g, b = r.r, r.g, r.b
179
180 # We've been given seperate color parameters
181 elif len(args) in (3, 4):
182 assert isinstance(args[0], (int, float)) and isinstance(args[1], (int, float)) and isinstance(args[2], (int, float))
183
184 # We've been given an OpenGL color
185 if all([isinstance(i, float) and i >= -1 and i <= 1 for i in args]):
186 if len(args) == 3:
187 r, g, b = [translate(i, -1, 1, 0, 255) for i in args]
188 else:
189 r, g, b, a = [translate(i, -1, 1, 0, 255) for i in args]
190
191 # Otherwise, interpret them as ints between 0 and 255
192 if len(args) == 3:
193 r, g, b = args
194 else:
195 r, g, b, a = args
196 elif len(args) not in (3, 4):
197 raise TypeError(f'Incorrect number ({len(args)}) of color parameters given. Please give either 3 or 4 parameters')
198
199 # Now handle keyword args. If a keyword arg is specified, it should override other methods
200 r = r if ((_r := (kwargs.get('r') or kwargs.get('red'))) is None) else _r
201 g = g if ((_g := (kwargs.get('g') or kwargs.get('green'))) is None) else _g
202 b = b if ((_b := (kwargs.get('b') or kwargs.get('blue'))) is None) else _b
203 a = a if ((_a := (kwargs.get('a') or kwargs.get('alpha'))) is None) else _a
204
205 # _r, _g, _b, _a = (
206 # (kwargs.get('r') or kwargs.get('red')),
207 # (kwargs.get('g') or kwargs.get('green')),
208 # (kwargs.get('b') or kwargs.get('blue')),
209 # (kwargs.get('a') or kwargs.get('alpha')),
210 # )
211 # We've been given an OpenGL color
212 # if all([isinstance(i, float) and i >= -1 and i <= 1 for i in colors]):
213 # # if all(colors[:3]):
214 # r = r if _r is None else r
215 # r = r if _r is None else r
216 # if colors[3] is None:
217 # r, g, b = [translate(i, -1, 1, 0, 255) for i in colors]
218 # else:
219 # r, g, b, a = [translate(i, -1, 1, 0, 255) for i in colors]
220
221
222 if r is None or b is None or g is None:
223 # We're not sure how to interpret the parameters given
224 raise TypeError(f'Unsure how to interpret the parameters given (or no parameters were given)')
225
226 # Make sure they're the right type
227 r, g, b, a = [int(i) for i in (r, g, b, a)]
228
229 # ─── NOW WE RETURN ──────────────────────────────────────────────────────────────
230 # We now have r, g, b and a (255 by default) between 0 and 255
231 match rtn.lower():
232 case 'html':
233 return f'#{r:02x}{g:02x}{b:02x}'
234 case 'rgb':
235 return r, g, b
236 case 'rgba':
237 return r, g, b, a
238 case 'hsv':
239 return rgb_to_hsv(*[translate(i, 0, 255, 0, 1) for i in (r, g, b)])
240 case 'hls':
241 return rgb_to_hls(*[translate(i, 0, 255, 0, 1) for i in (r, g, b)])
242 case 'yiq':
243 return rgb_to_yiq(*[translate(i, 0, 255, 0, 1) for i in (r, g, b)])
244 case 'opengl':
245 # Does OpenGL include alpha?
246 # return [translate(i, 0, 255, -1, 1) for i in (r, g, b)]
247 return [translate(i, 0, 255, -1, 1) for i in (r, g, b, a)]
248 case _:
249 raise TypeError(f"Invalid return type given. Options are `html`, `rgb`, `rgba`, `opengl`")
250
251def darken(amt, *args) -> Tuple['r', 'g', 'b']:
252 """ Returns the given color, but darkened. Make amount negative to lighten
253 NOTE: If you pass alpha to this function it will still work, but it
254 won't return it.
255 NOTE: If you pass OpenGL colors to this, it will work, but `amt` still
256 has to be within 0-255
257 """
258 return tuple(i - amt for i in parse_color(*args))
259
260def darken(amt, *args) -> Tuple['r', 'g', 'b']:
261 """ Returns the given color, but lightened. Make amount negative to darken
262 NOTE: If you pass alpha to this function it will still work, but it
263 won't return it.
264 NOTE: If you pass OpenGL colors to this, it will work, but `amt` still
265 has to be within 0-255
266 """
267 return tuple(i + amt for i in parse_color(*args))
268
269def invert_color(*args, **kwargs) -> Tuple['r', 'g', 'b']:
270 """ Inverts a color.
271 NOTE: If you pass alpha to this function it will still work, but it
272 won't return it.
273 """
274 return tuple(255 - i for i in parse_color(*args, rtn, **kwargs))
Tuple[ 'r', 'g', 'b'] darken(amt, *args)
Returns the given color, but darkened.
Definition: colors.py:251
def generate_colors(amt, s=1, v=1, offset=0)
Generate amt number of colors evenly spaced around the color wheel with a given saturation and value.
Definition: colors.py:21
def furthest_colors(html, amt=5, v_bias=0, s_bias=0)
Gets the amt number of colors evenly spaced around the color wheel from the given color v_bias and s_...
Definition: colors.py:29
def complimentary_color(*color, Literal['html', 'rgb', 'rgba', 'opengl', 'hsv', 'hls', 'yiq'] rtn='rgb')
Returns the color opposite it on the color wheel, with the same saturation and value.
Definition: colors.py:40
Tuple[ 'r', 'g', 'b'] invert_color(*args, **kwargs)
Inverts a color.
Definition: colors.py:269