/ adafruit_fancyled / adafruit_fancyled.py
adafruit_fancyled.py
1 # The MIT License (MIT) 2 # 3 # Copyright (c) 2017 PaintYourDragon for Adafruit Industries 4 # 5 # Permission is hereby granted, free of charge, to any person obtaining a copy 6 # of this software and associated documentation files (the "Software"), to deal 7 # in the Software without restriction, including without limitation the rights 8 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 # copies of the Software, and to permit persons to whom the Software is 10 # furnished to do so, subject to the following conditions: 11 # 12 # The above copyright notice and this permission notice shall be included in 13 # all copies or substantial portions of the Software. 14 # 15 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 # THE SOFTWARE. 22 """ 23 `adafruit_fancyled.adafruit_fancyled` 24 ==================================================== 25 26 FancyLED is a CircuitPython library to assist in creating buttery smooth LED animation. 27 It's loosely inspired by the FastLED library for Arduino, and in fact we have a "helper" 28 library using similar function names to assist with porting of existing Arduino FastLED 29 projects to CircuitPython. 30 31 * Author(s): PaintYourDragon 32 """ 33 34 # imports 35 36 __version__ = "0.0.0-auto.0" 37 __repo__ = "https://github.com/Adafruit/Adafruit_CircuitPython_FancyLED.git" 38 39 from math import floor 40 41 42 # FancyLED provides color- and palette-related utilities for LED projects, 43 # offering a buttery smooth look instead of the usual 8-bit-like "blip blip" 44 # effects often seen with LEDs. It's loosely inspired by, but NOT a drop-in 45 # replacement for, the FastLED library for Arduino. 46 47 48 class CRGB: 49 """Color stored in Red, Green, Blue color space. 50 51 One of two ways: separate red, gren, blue values (either as integers 52 (0 to 255 range) or floats (0.0 to 1.0 range), either type is 53 'clamped' to valid range and stored internally in the normalized 54 (float) format), OR can accept a CHSV color as input, which will be 55 converted and stored in RGB format. 56 57 Following statements are equivalent - all return red: 58 59 .. code-block:: python 60 61 c = CRGB(255, 0, 0) 62 c = CRGB(1.0, 0.0, 0.0) 63 c = CRGB(CHSV(0.0, 1.0, 1.0)) 64 """ 65 66 def __init__(self, red, green=0.0, blue=0.0): 67 # pylint: disable=too-many-branches 68 if isinstance(red, CHSV): 69 # If first/only argument is a CHSV type, perform HSV to RGB 70 # conversion. 71 hsv = red # 'red' is CHSV, this is just more readable 72 hue = hsv.hue * 6.0 # Hue circle = 0.0 to 6.0 73 sxt = floor(hue) # Sextant index is next-lower integer of hue 74 frac = hue - sxt # Fraction-within-sextant is 0.0 to <1.0 75 sxt = int(sxt) % 6 # mod6 the sextant so it's always 0 to 5 76 77 if sxt == 0: # Red to <yellow 78 r, g, b = 1.0, frac, 0.0 79 elif sxt == 1: # Yellow to <green 80 r, g, b = 1.0 - frac, 1.0, 0.0 81 elif sxt == 2: # Green to <cyan 82 r, g, b = 0.0, 1.0, frac 83 elif sxt == 3: # Cyan to <blue 84 r, g, b = 0.0, 1.0 - frac, 1.0 85 elif sxt == 4: # Blue to <magenta 86 r, g, b = frac, 0.0, 1.0 87 else: # Magenta to <red 88 r, g, b = 1.0, 0.0, 1.0 - frac 89 90 invsat = 1.0 - hsv.saturation # Inverse-of-saturation 91 92 self.red = ((r * hsv.saturation) + invsat) * hsv.value 93 self.green = ((g * hsv.saturation) + invsat) * hsv.value 94 self.blue = ((b * hsv.saturation) + invsat) * hsv.value 95 else: 96 # Red, green, blue arguments (normalized floats OR integers) 97 # TODO(tannewt): Factor this out into a helper function 98 if isinstance(red, float): 99 self.red = clamp(red, 0.0, 1.0) 100 else: 101 self.red = normalize(red) 102 if isinstance(green, float): 103 self.green = clamp(green, 0.0, 1.0) 104 else: 105 self.green = normalize(green) 106 if isinstance(blue, float): 107 self.blue = clamp(blue, 0.0, 1.0) 108 else: 109 self.blue = normalize(blue) 110 111 def __repr__(self): # pylint: disable=invalid-repr-returned 112 return (self.red, self.green, self.blue) 113 114 def __str__(self): 115 return "(%s, %s, %s)" % (self.red, self.green, self.blue) 116 117 def __len__(self): 118 """Retrieve total number of color-parts available.""" 119 return 3 120 121 def __getitem__(self, key): 122 """Retrieve red, green or blue value as iterable.""" 123 if key == 0: 124 return self.red 125 if key == 1: 126 return self.green 127 if key == 2: 128 return self.blue 129 raise IndexError 130 131 def pack(self): 132 """'Pack' a `CRGB` color into a 24-bit RGB integer. 133 134 :returns: 24-bit integer a la ``0x00RRGGBB``. 135 """ 136 137 return ( 138 (denormalize(self.red) << 16) 139 | (denormalize(self.green) << 8) 140 | (denormalize(self.blue)) 141 ) 142 143 144 class CHSV: 145 """Color stored in Hue, Saturation, Value color space. 146 147 Accepts hue as float (any range) or integer (0-256 -> 0.0-1.0) with 148 no clamping performed (hue can 'wrap around'), saturation and value 149 as float (0.0 to 1.0) or integer (0 to 255), both are clamped and 150 stored internally in the normalized (float) format. Latter two are 151 optional, can pass juse hue and saturation/value will default to 1.0. 152 153 Unlike `CRGB` (which can take a `CHSV` as input), there's currently 154 no equivalent RGB-to-HSV conversion, mostly because it's a bit like 155 trying to reverse a hash...there may be multiple HSV solutions for a 156 given RGB input. 157 158 This might be OK as long as conversion precedence is documented, 159 but otherwise (and maybe still) could cause confusion as certain 160 HSV->RGB->HSV translations won't have the same input and output. 161 """ 162 163 def __init__(self, h, s=1.0, v=1.0): 164 if isinstance(h, float): 165 self.hue = h # Don't clamp! Hue can wrap around forever. 166 else: 167 self.hue = float(h) / 256.0 168 if isinstance(s, float): 169 self.saturation = clamp(s, 0.0, 1.0) 170 else: 171 self.saturation = normalize(s) 172 if isinstance(v, float): 173 self.value = clamp(v, 0.0, 1.0) 174 else: 175 self.value = normalize(v) 176 177 def __repr__(self): # pylint: disable=invalid-repr-returned 178 return (self.hue, self.saturation, self.value) 179 180 def __str__(self): 181 return "(%s, %s, %s)" % (self.hue, self.saturation, self.value) 182 183 def __len__(self): 184 """Retrieve total number of 'color-parts' available.""" 185 return 3 186 187 def __getitem__(self, key): 188 """Retrieve hue, saturation or value as iterable.""" 189 if key == 0: 190 return self.hue 191 if key == 1: 192 return self.saturation 193 if key == 2: 194 return self.value 195 raise IndexError 196 197 def pack(self): 198 """'Pack' a `CHSV` color into a 24-bit RGB integer. 199 200 :returns: 24-bit integer a la ``0x00RRGGBB``. 201 """ 202 203 # Convert CHSV to CRGB, return packed result 204 return CRGB(self).pack() 205 206 207 def clamp(val, lower, upper): 208 """Constrain value within a numeric range (inclusive). 209 """ 210 return max(lower, min(val, upper)) 211 212 213 def normalize(val, inplace=False): 214 """Convert 8-bit (0 to 255) value to normalized (0.0 to 1.0) value. 215 216 Accepts integer, 0 to 255 range (input is clamped) or a list or tuple 217 of integers. In list case, 'inplace' can be used to control whether 218 the original list is modified (True) or a new list is generated and 219 returned (False). 220 221 Returns float, 0.0 to 1.0 range, or list of floats (or None if inplace). 222 """ 223 224 if isinstance(val, int): 225 # Divide by 255 (not 256) so maximum level is 1.0. 226 return clamp(val, 0, 255) / 255.0 227 228 # If not int, is assumed list or tuple. 229 if inplace: 230 # Modify list in-place (OK for lists, NOT tuples, no check made) 231 for i, n in enumerate(val): 232 val[i] = normalize(n) 233 return None 234 235 # Generate new list 236 return [normalize(n) for n in val] 237 238 239 def denormalize(val, inplace=False): 240 """Convert normalized (0.0 to 1.0) value to 8-bit (0 to 255) value 241 242 Accepts float, 0.0 to 1.0 range or a list or tuple of floats. In 243 list case, 'inplace' can be used to control whether the original list 244 is modified (True) or a new list is generated and returned (False). 245 246 Returns integer, 0 to 255 range, or list of integers (or None if 247 inplace). 248 """ 249 250 # 'Denormalizing' math varies slightly from normalize(). This is on 251 # purpose. Multiply by 256 (NOT 255) and clip. This ensures that all 252 # fractional values fall into the correct 'buckets' -- e.g. 0.999 253 # should return 255, not 254 -- and that the buckets are all equal- 254 # sized (usu. method of adding 0.5 before int() would miss this). 255 if isinstance(val, float): 256 return clamp(int(val * 256.0), 0, 255) 257 258 # If not int, is assumed list or tuple. 259 if inplace: 260 # Modify the list in-place (OK for lists, NOT tuples, no check made) 261 for i, n in enumerate(val): 262 val[i] = denormalize(n) 263 return None 264 265 # Generate new list 266 return [denormalize(n) for n in val] 267 268 269 def unpack(val): 270 """'Unpack' a 24-bit color into a `CRGB` instance. 271 272 :param int val: 24-bit integer a la ``0x00RRGGBB``. 273 :returns: CRGB color. 274 :rtype: CRGB 275 """ 276 277 # See notes in normalize() for math explanation. Large constants here 278 # avoid the usual shift-right step, e.g. 16711680.0 is 255 * 256 * 256, 279 # so we can just mask out the red and divide by this for 0.0 to 1.0. 280 return CRGB( 281 (val & 0xFF0000) / 16711680.0, # Red 282 (val & 0x00FF00) / 65280.0, # Green 283 (val & 0x0000FF) / 255.0, 284 ) # Blue 285 286 287 def mix(color1, color2, weight2=0.5): 288 """Blend between two colors using given ratio. Accepts two colors (each 289 may be `CRGB`, `CHSV` or packed integer), and weighting (0.0 to 1.0) 290 of second color. 291 292 :returns: `CRGB` color in most cases, `CHSV` if both inputs are `CHSV`. 293 """ 294 295 clamp(weight2, 0.0, 1.0) 296 weight1 = 1.0 - weight2 297 298 if isinstance(color1, CHSV): 299 if isinstance(color2, CHSV): 300 # Both colors are CHSV -- interpolate in HSV color space 301 # because of the way hue can cross the unit boundary... 302 # e.g. if the hues are 0.25 and 0.75, the center point is 303 # 0.5 (cyan)...but if you want hues to wrap the other way 304 # (with red at the center), you can have hues of 1.25 and 0.75. 305 hue = color1.hue + ((color2.hue - color1.hue) * weight2) 306 sat = color1.saturation * weight1 + color2.saturation * weight2 307 val = color1.value * weight1 + color2.value * weight2 308 return CHSV(hue, sat, val) 309 # Else color1 is HSV, color2 is RGB. Convert color1 to RGB 310 # before doing interpolation in RGB space. 311 color1 = CRGB(color1) 312 # If color2 is a packed integer, convert to CRGB instance. 313 if isinstance(color2, int): 314 color2 = unpack(color2) 315 else: 316 if isinstance(color2, CHSV): 317 # color1 is RGB, color2 is HSV. Convert color2 to RGB 318 # before interpolating in RGB space. 319 color2 = CRGB(color2) 320 elif isinstance(color2, int): 321 # If color2 is a packed integer, convert to CRGB instance. 322 color2 = unpack(color2) 323 # If color1 is a packed integer, convert to CRGB instance. 324 if isinstance(color1, int): 325 color1 = unpack(color1) 326 327 # Interpolate and return as CRGB type 328 return CRGB( 329 (color1.red * weight1 + color2.red * weight2), 330 (color1.green * weight1 + color2.green * weight2), 331 (color1.blue * weight1 + color2.blue * weight2), 332 ) 333 334 335 GFACTOR = 2.7 # Default gamma-correction factor for function below 336 337 338 def gamma_adjust(val, gamma_value=None, brightness=1.0, inplace=False): 339 """Provides gamma adjustment for single values, `CRGB` and `CHSV` types 340 and lists of any of these. 341 342 Works in one of three ways: 343 1. Accepts a single normalized level (0.0 to 1.0) and optional 344 gamma-adjustment factor (float usu. > 1.0, default if 345 unspecified is GFACTOR) and brightness (float 0.0 to 1.0, 346 default is 1.0). Returns a single normalized gamma-corrected 347 brightness level (0.0 to 1.0). 348 2. Accepts a single `CRGB` or `CHSV` type, optional single gamma 349 factor OR a (R,G,B) gamma tuple (3 values usu. > 1.0), optional 350 single brightness factor OR a (R,G,B) brightness tuple. The 351 input tuples are RGB even when a `CHSV` color is passed. Returns 352 a normalized gamma-corrected `CRGB` type (NOT `CHSV`!). 353 3. Accept a list or tuple of normalized levels, `CRGB` or `CHSV` 354 types (and optional gamma and brightness levels or tuples 355 applied to all). Returns a list of gamma-corrected values or 356 `CRGB` types (NOT `CHSV`!). 357 358 In cases 2 and 3, if the input is a list (NOT a tuple!), the 'inplace' 359 flag determines whether a new tuple/list is calculated and returned, 360 or the existing value is modified in-place. By default this is 361 'False'. If you try to inplace-modify a tuple, an exception is raised. 362 363 In cases 2 and 3, there is NO return value if 'inplace' is True -- 364 the original values are modified. 365 """ 366 # pylint: disable=too-many-branches 367 368 if isinstance(val, float): 369 # Input value appears to be a single float 370 if gamma_value is None: 371 gamma_value = GFACTOR 372 return pow(val, gamma_value) * brightness 373 374 if isinstance(val, (list, tuple)): 375 # List or tuple of values 376 if isinstance(val[0], float): 377 # Input appears to be a list of floats 378 if gamma_value is None: 379 gamma_value = GFACTOR 380 if inplace: 381 for i, x in enumerate(val): 382 val[i] = pow(val[i], gamma_value) * brightness 383 return None 384 newlist = [] 385 for x in val: 386 newlist.append(pow(x, gamma_value) * brightness) 387 return newlist 388 # List of CRGB or CHSV...we'll get back to that in a moment... 389 # but first determine gamma-correction factors for R,G,B: 390 if gamma_value is None: 391 # No gamma specified, use default 392 gamma_red, gamma_green, gamma_blue = GFACTOR, GFACTOR, GFACTOR 393 elif isinstance(gamma_value, float): 394 # Single gamma value provided, apply to R,G,B 395 gamma_red, gamma_green, gamma_blue = (gamma_value, gamma_value, gamma_value) 396 else: 397 gamma_red, gamma_green, gamma_blue = ( 398 gamma_value[0], 399 gamma_value[1], 400 gamma_value[2], 401 ) 402 if isinstance(brightness, float): 403 # Single brightness value provided, apply to R,G,B 404 brightness_red, brightness_green, brightness_blue = ( 405 brightness, 406 brightness, 407 brightness, 408 ) 409 else: 410 brightness_red, brightness_green, brightness_blue = ( 411 brightness[0], 412 brightness[1], 413 brightness[2], 414 ) 415 if inplace: 416 for i, x in enumerate(val): 417 if isinstance(x, CHSV): 418 x = CRGB(x) 419 val[i] = CRGB( 420 pow(x.red, gamma_red) * brightness_red, 421 pow(x.green, gamma_green) * brightness_green, 422 pow(x.blue, gamma_blue) * brightness_blue, 423 ) 424 return None 425 newlist = [] 426 for x in val: 427 if isinstance(x, CHSV): 428 x = CRGB(x) 429 newlist.append( 430 CRGB( 431 pow(x.red, gamma_red) * brightness_red, 432 pow(x.green, gamma_green) * brightness_green, 433 pow(x.blue, gamma_blue) * brightness_blue, 434 ) 435 ) 436 return newlist 437 438 # Single CRGB or CHSV value 439 if gamma_value is None: 440 # No gamma specified, use default 441 gamma_red, gamma_green, gamma_blue = GFACTOR, GFACTOR, GFACTOR 442 elif isinstance(gamma_value, float): 443 # Single gamma value provided, apply to R,G,B 444 gamma_red, gamma_green, gamma_blue = (gamma_value, gamma_value, gamma_value) 445 else: 446 gamma_red, gamma_green, gamma_blue = ( 447 gamma_value[0], 448 gamma_value[1], 449 gamma_value[2], 450 ) 451 if isinstance(brightness, float): 452 # Single brightness value provided, apply to R,G,B 453 brightness_red, brightness_green, brightness_blue = ( 454 brightness, 455 brightness, 456 brightness, 457 ) 458 else: 459 brightness_red, brightness_green, brightness_blue = ( 460 brightness[0], 461 brightness[1], 462 brightness[2], 463 ) 464 465 if isinstance(val, CHSV): 466 val = CRGB(val) 467 468 return CRGB( 469 pow(val.red, gamma_red) * brightness_red, 470 pow(val.green, gamma_green) * brightness_green, 471 pow(val.blue, gamma_blue) * brightness_blue, 472 ) 473 474 475 def palette_lookup(palette, position): 476 """Fetch color from color palette, with interpolation. 477 478 :param palette: color palette (list of CRGB, CHSV and/or packed integers) 479 :param float position: palette position (0.0 to 1.0, wraps around). 480 481 :returns: `CRGB` or `CHSV` instance, no gamma correction applied. 482 """ 483 484 position %= 1.0 # Wrap palette position in 0.0 to <1.0 range 485 486 weight2 = position * len(palette) # Scale position to palette length 487 idx = int(floor(weight2)) # Index of 'lower' color (0 to len-1) 488 weight2 -= idx # Weighting of 'upper' color 489 490 color1 = palette[idx] # Fetch 'lower' color 491 idx = (idx + 1) % len(palette) # Get index of 'upper' color 492 color2 = palette[idx] # Fetch 'upper' color 493 494 return mix(color1, color2, weight2) 495 496 497 def expand_gradient(gradient, length): 498 """Convert gradient palette into standard equal-interval palette. 499 500 :param sequence gradient: List or tuple of of 2-element lists/tuples 501 containing position (0.0 to 1.0) and color (packed int, CRGB or CHSV). 502 It's OK if the list/tuple elements are either lists OR tuples, but 503 don't mix and match lists and tuples -- use all one or the other. 504 505 :returns: CRGB list, can be used with palette_lookup() function. 506 """ 507 508 gradient = sorted(gradient) # Sort list by position values 509 least = gradient[0][0] # Lowest position value (ostensibly 0.0) 510 most = gradient[-1][0] # Highest position value (ostensibly 1.0) 511 newlist = [] 512 513 for i in range(length): 514 pos = i / float(length - 1) # 0.0 to 1.0 in 'length' steps 515 # Determine indices in list of item 'below' and 'above' pos 516 if pos <= least: 517 # Off bottom of list - use lowest index 518 below, above = 0, 0 519 elif pos >= most: 520 # Off top of list - use highest index 521 below, above = -1, -1 522 else: 523 # Seek position between two items in list 524 below, above = 0, -1 525 for n, x in enumerate(gradient): 526 if pos >= x[0]: 527 below = n 528 for n, x in enumerate(gradient[-1:0:-1]): 529 if pos <= x[0]: 530 above = -1 - n 531 532 # Range between below, above 533 r = gradient[above][0] - gradient[below][0] 534 if r <= 0: 535 newlist.append(gradient[below][1]) # Use 'below' color only 536 else: 537 weight2 = (pos - gradient[below][0]) / r # Weight of 'above' color 538 color1 = gradient[below][1] 539 color2 = gradient[above][1] 540 # Interpolate and add to list 541 newlist.append(mix(color1, color2, weight2)) 542 543 return newlist