Skip to content

Commit 289b7d9

Browse files
Change pack() to an instance method. Update examples.
1 parent a288ed4 commit 289b7d9

File tree

4 files changed

+174
-184
lines changed

4 files changed

+174
-184
lines changed

adafruit_fancyled.py

Lines changed: 93 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,11 @@
4545
class CRGB(object):
4646
"""Color stored in Red, Green, Blue color space.
4747
48-
One of two ways: separate red, gren, blue values (either as integers (0 to 255
49-
range) or floats (0.0 to 1.0 range), either type is 'clamped' to valid range and
50-
stored internally in the normalized (float) format), OR can accept a CHSV color as
51-
input, which will be converted and stored in RGB format.
48+
One of two ways: separate red, gren, blue values (either as integers
49+
(0 to 255 range) or floats (0.0 to 1.0 range), either type is
50+
'clamped' to valid range and stored internally in the normalized
51+
(float) format), OR can accept a CHSV color as input, which will be
52+
converted and stored in RGB format.
5253
5354
Following statements are equivalent - all return red:
5455
@@ -108,23 +109,34 @@ def __repr__(self):
108109
def __str__(self):
109110
return "(%s, %s, %s)" % (self.red, self.green, self.blue)
110111

112+
def pack(self):
113+
"""'Pack' a `CRGB` color into a 24-bit RGB integer.
114+
115+
:returns: 24-bit integer a la ``0x00RRGGBB``.
116+
"""
117+
118+
return ((denormalize(self.red) << 16) |
119+
(denormalize(self.green) << 8) |
120+
(denormalize(self.blue)))
121+
111122

112123
class CHSV(object):
113124
"""Color stored in Hue, Saturation, Value color space.
114125
115-
Accepts hue as float (any range) or integer (0-256 -> 0.0-1.0) with no clamping
116-
performed (hue can 'wrap around'), saturation and value as float (0.0 to 1.0) or
117-
integer (0 to 255), both are clamped and stored internally in the normalized (float)
118-
format. Latter two are optional, can pass juse hue and saturation/value will
119-
default to 1.0.
126+
Accepts hue as float (any range) or integer (0-256 -> 0.0-1.0) with
127+
no clamping performed (hue can 'wrap around'), saturation and value
128+
as float (0.0 to 1.0) or integer (0 to 255), both are clamped and
129+
stored internally in the normalized (float) format. Latter two are
130+
optional, can pass juse hue and saturation/value will default to 1.0.
120131
121-
Unlike `CRGB` (which can take a `CHSV` as input), there's currently no equivalent
122-
RGB-to-HSV conversion, mostly because it's a bit like trying to reverse a hash...
123-
there may be multiple HSV solutions for a given RGB input.
132+
Unlike `CRGB` (which can take a `CHSV` as input), there's currently
133+
no equivalent RGB-to-HSV conversion, mostly because it's a bit like
134+
trying to reverse a hash...there may be multiple HSV solutions for a
135+
given RGB input.
124136
125-
This might be OK as long as conversion precedence is documented, but otherwise (and
126-
maybe still) could cause confusion as certain HSV->RGB->HSV translations won't have
127-
the same input and output. Hmmm.
137+
This might be OK as long as conversion precedence is documented,
138+
but otherwise (and maybe still) could cause confusion as certain
139+
HSV->RGB->HSV translations won't have the same input and output.
128140
"""
129141

130142
def __init__(self, h, s=1.0, v=1.0):
@@ -147,6 +159,15 @@ def __repr__(self):
147159
def __str__(self):
148160
return "(%s, %s, %s)" % (self.hue, self.saturation, self.value)
149161

162+
def pack(self):
163+
"""'Pack' a `CHSV` color into a 24-bit RGB integer.
164+
165+
:returns: 24-bit integer a la ``0x00RRGGBB``.
166+
"""
167+
168+
# Convert CHSV to CRGB, return packed result
169+
return CRGB(self).pack()
170+
150171

151172
def clamp(val, lower, upper):
152173
"""Constrain value within a numeric range (inclusive).
@@ -157,7 +178,10 @@ def clamp(val, lower, upper):
157178
def normalize(val, inplace=False):
158179
"""Convert 8-bit (0 to 255) value to normalized (0.0 to 1.0) value.
159180
160-
Accepts integer, 0 to 255 range (input is clamped) or a list or tuple of integers. In list case, 'inplace' can be used to control whether the original list is modified (True) or a new list is generated and returned (False).
181+
Accepts integer, 0 to 255 range (input is clamped) or a list or tuple
182+
of integers. In list case, 'inplace' can be used to control whether
183+
the original list is modified (True) or a new list is generated and
184+
returned (False).
161185
162186
Returns float, 0.0 to 1.0 range, or list of floats (or None if inplace).
163187
"""
@@ -180,11 +204,12 @@ def normalize(val, inplace=False):
180204
def denormalize(val, inplace=False):
181205
"""Convert normalized (0.0 to 1.0) value to 8-bit (0 to 255) value
182206
183-
Accepts float, 0.0 to 1.0 range or a list or tuple of floats. In list case,
184-
'inplace' can be used to control whether the original list is modified (True) or a
185-
new list is generated and returned (False).
207+
Accepts float, 0.0 to 1.0 range or a list or tuple of floats. In
208+
list case, 'inplace' can be used to control whether the original list
209+
is modified (True) or a new list is generated and returned (False).
186210
187-
Returns integer, 0 to 255 range, or list of integers (or None if inplace).
211+
Returns integer, 0 to 255 range, or list of integers (or None if
212+
inplace).
188213
"""
189214

190215
# 'Denormalizing' math varies slightly from normalize(). This is on
@@ -206,22 +231,6 @@ def denormalize(val, inplace=False):
206231
return [denormalize(n) for n in val]
207232

208233

209-
def pack(val):
210-
"""'Pack' a `CRGB` or `CHSV` color into a 24-bit RGB integer.
211-
212-
:param obj val: `CRGB` or `CHSV` color.
213-
:returns: 24-bit integer a la ``0x00RRGGBB``.
214-
"""
215-
216-
# Convert CHSV input to CRGB if needed
217-
if isinstance(val, CHSV):
218-
val = CRGB(val)
219-
220-
return ((denormalize(val.red) << 16) |
221-
(denormalize(val.green) << 8) |
222-
(denormalize(val.blue)))
223-
224-
225234
def unpack(val):
226235
"""'Unpack' a 24-bit color into a `CRGB` instance.
227236
@@ -239,8 +248,9 @@ def unpack(val):
239248

240249

241250
def mix(color1, color2, weight2=0.5):
242-
"""Blend between two colors using given ratio. Accepts two colors (each may be `CRGB`,
243-
`CHSV` or packed integer), and weighting (0.0 to 1.0) of second color.
251+
"""Blend between two colors using given ratio. Accepts two colors (each
252+
may be `CRGB`, `CHSV` or packed integer), and weighting (0.0 to 1.0)
253+
of second color.
244254
245255
:returns: `CRGB` color in most cases, `CHSV` if both inputs are `CHSV`.
246256
"""
@@ -283,29 +293,32 @@ def mix(color1, color2, weight2=0.5):
283293
(color1.blue * weight1 + color2.blue * weight2))
284294

285295

286-
GFACTOR = 2.5 # Default gamma-correction factor for function below
296+
GFACTOR = 2.7 # Default gamma-correction factor for function below
287297

288298
def gamma_adjust(val, gamma_value=None, brightness=1.0, inplace=False):
289299
"""Provides gamma adjustment for single values, `CRGB` and `CHSV` types
290300
and lists of any of these.
291301
292302
Works in one of three ways:
293-
1. Accepts a single normalized level (0.0 to 1.0) and optional gamma-adjustment
294-
factor (float usu. > 1.0, default if unspecified is GFACTOR) and
295-
brightness (float 0.0 to 1.0, default is 1.0). Returns a single normalized gamma-corrected brightness level (0.0 to 1.0).
296-
2. Accepts a single `CRGB` or `CHSV` type, optional single gamma factor OR a
297-
(R,G,B) gamma tuple (3 values usu. > 1.0), optional single
298-
brightness factor OR a (R,G,B) brightness tuple. The input tuples
299-
are RGB even when a `CHSV` color is passed. Returns a normalized
300-
gamma-corrected `CRGB` type (NOT `CHSV`!).
301-
3. Accept a list or tuple of normalized levels, `CRGB` or `CHSV` types (and
302-
optional gamma and brightness levels or tuples applied to all). Returns a list
303-
of gamma-corrected values or `CRGB` types (NOT `CHSV`!).
303+
1. Accepts a single normalized level (0.0 to 1.0) and optional
304+
gamma-adjustment factor (float usu. > 1.0, default if
305+
unspecified is GFACTOR) and brightness (float 0.0 to 1.0,
306+
default is 1.0). Returns a single normalized gamma-corrected
307+
brightness level (0.0 to 1.0).
308+
2. Accepts a single `CRGB` or `CHSV` type, optional single gamma
309+
factor OR a (R,G,B) gamma tuple (3 values usu. > 1.0), optional
310+
single brightness factor OR a (R,G,B) brightness tuple. The
311+
input tuples are RGB even when a `CHSV` color is passed. Returns
312+
a normalized gamma-corrected `CRGB` type (NOT `CHSV`!).
313+
3. Accept a list or tuple of normalized levels, `CRGB` or `CHSV`
314+
types (and optional gamma and brightness levels or tuples
315+
applied to all). Returns a list of gamma-corrected values or
316+
`CRGB` types (NOT `CHSV`!).
304317
305318
In cases 2 and 3, if the input is a list (NOT a tuple!), the 'inplace'
306319
flag determines whether a new tuple/list is calculated and returned,
307-
or the existing value is modified in-place. By default this is 'False'.
308-
If you try to inplace-modify a tuple, an exception is raised.
320+
or the existing value is modified in-place. By default this is
321+
'False'. If you try to inplace-modify a tuple, an exception is raised.
309322
310323
In cases 2 and 3, there is NO return value if 'inplace' is True --
311324
the original values are modified.
@@ -338,14 +351,18 @@ def gamma_adjust(val, gamma_value=None, brightness=1.0, inplace=False):
338351
gamma_red, gamma_green, gamma_blue = GFACTOR, GFACTOR, GFACTOR
339352
elif isinstance(gamma_value, float):
340353
# Single gamma value provided, apply to R,G,B
341-
gamma_red, gamma_green, gamma_blue = gamma_value, gamma_value, gamma_value
354+
gamma_red, gamma_green, gamma_blue = (
355+
gamma_value, gamma_value, gamma_value)
342356
else:
343-
gamma_red, gamma_green, gamma_blue = gamma_value[0], gamma_value[1], gamma_value[2]
357+
gamma_red, gamma_green, gamma_blue = (
358+
gamma_value[0], gamma_value[1], gamma_value[2])
344359
if isinstance(brightness, float):
345360
# Single brightness value provided, apply to R,G,B
346-
brightness_red, brightness_green, brightness_blue = brightness, brightness, brightness
361+
brightness_red, brightness_green, brightness_blue = (
362+
brightness, brightness, brightness)
347363
else:
348-
brightness_red, brightness_green, brightness_blue = brightness[0], brightness[1], brightness[2]
364+
brightness_red, brightness_green, brightness_blue = (
365+
brightness[0], brightness[1], brightness[2])
349366
if inplace:
350367
for i, x in enumerate(val):
351368
if isinstance(x, CHSV):
@@ -369,14 +386,18 @@ def gamma_adjust(val, gamma_value=None, brightness=1.0, inplace=False):
369386
gamma_red, gamma_green, gamma_blue = GFACTOR, GFACTOR, GFACTOR
370387
elif isinstance(gamma_value, float):
371388
# Single gamma value provided, apply to R,G,B
372-
gamma_red, gamma_green, gamma_blue = gamma_value, gamma_value, gamma_value
389+
gamma_red, gamma_green, gamma_blue = (
390+
gamma_value, gamma_value, gamma_value)
373391
else:
374-
gamma_red, gamma_green, gamma_blue = gamma_value[0], gamma_value[1], gamma_value[2]
392+
gamma_red, gamma_green, gamma_blue = (
393+
gamma_value[0], gamma_value[1], gamma_value[2])
375394
if isinstance(brightness, float):
376395
# Single brightness value provided, apply to R,G,B
377-
brightness_red, brightness_green, brightness_blue = brightness, brightness, brightness
396+
brightness_red, brightness_green, brightness_blue = (
397+
brightness, brightness, brightness)
378398
else:
379-
brightness_red, brightness_green, brightness_blue = brightness[0], brightness[1], brightness[2]
399+
brightness_red, brightness_green, brightness_blue = (
400+
brightness[0], brightness[1], brightness[2])
380401

381402
if isinstance(val, CHSV):
382403
val = CRGB(val)
@@ -397,24 +418,24 @@ def palette_lookup(palette, position):
397418

398419
position %= 1.0 # Wrap palette position in 0.0 to <1.0 range
399420

400-
weight2 = position * len(palette) # Scale position to palette length
401-
idx = int(floor(weight2)) # Index of 'lower' color (0 to len-1)
402-
weight2 -= idx # Weighting of 'upper' color
421+
weight2 = position * len(palette) # Scale position to palette length
422+
idx = int(floor(weight2)) # Index of 'lower' color (0 to len-1)
423+
weight2 -= idx # Weighting of 'upper' color
403424

404-
color1 = palette[idx] # Fetch 'lower' color
405-
idx = (idx + 1) % len(palette) # Get index of 'upper' color
406-
color2 = palette[idx] # Fetch 'upper' color
425+
color1 = palette[idx] # Fetch 'lower' color
426+
idx = (idx + 1) % len(palette) # Get index of 'upper' color
427+
color2 = palette[idx] # Fetch 'upper' color
407428

408429
return mix(color1, color2, weight2)
409430

410431

411432
def expand_gradient(gradient, length):
412433
"""Convert gradient palette into standard equal-interval palette.
413434
414-
:param sequence gradient: List or tuple of of 2-element lists/tuples containing
415-
position (0.0 to 1.0) and color (packed int, CRGB or CHSV). It's OK if the
416-
list/tuple elements are either lists OR tuples, but don't mix and match lists and
417-
tuples -- use all one or the other.
435+
:param sequence gradient: List or tuple of of 2-element lists/tuples
436+
containing position (0.0 to 1.0) and color (packed int, CRGB or CHSV).
437+
It's OK if the list/tuple elements are either lists OR tuples, but
438+
don't mix and match lists and tuples -- use all one or the other.
418439
419440
:returns: CRGB list, can be used with palette_lookup() function.
420441
"""
@@ -443,7 +464,8 @@ def expand_gradient(gradient, length):
443464
if pos <= x[0]:
444465
above = -1 - n
445466

446-
r = gradient[above][0] - gradient[below][0] # Range between below, above
467+
# Range between below, above
468+
r = gradient[above][0] - gradient[below][0]
447469
if r <= 0:
448470
newlist.append(gradient[below][1]) # Use 'below' color only
449471
else:

examples/cpx_helper_example.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
""" FancyLED example for Circuit Playground Express using fastled_helpers
2+
"""
3+
4+
from adafruit_circuitplayground.express import cpx
5+
import adafruit_fancyled as fancy
6+
import fastled_helpers as helper
7+
8+
cpx.pixels.auto_write = False # Refresh pixels only when we say
9+
10+
# A dynamic gradient palette is a compact way of representing a palette with
11+
# non-equal spacing between elements. This one's a blackbody palette with a
12+
# longer red 'tail'. The helper functions let us declare this as a list of
13+
# bytes, so they're easier to copy over from existing FastLED projects.
14+
heatmap_gp = bytes([
15+
0, 255, 255, 255, # White
16+
64, 255, 255, 0, # Yellow
17+
128, 255, 0, 0, # Red
18+
255, 0, 0, 0]) # Black
19+
20+
# Convert the gradient palette into a normal palette w/16 elements:
21+
palette = helper.loadDynamicGradientPalette(heatmap_gp, 16)
22+
23+
offset = 0 # Positional offset into color palette to get it to 'spin'
24+
25+
while True:
26+
for i in range(10):
27+
# Load each pixel's color from the palette. FastLED uses 16-step
28+
# in-between blending...so for a 16-color palette, there's 256
29+
# steps total. With 10 pixels, multiply the pixel index by 25.5
30+
# (and add our offset) to get FastLED-style palette position.
31+
color = helper.ColorFromPalette(palette, int(offset + i * 25.5),
32+
blend=True)
33+
# Apply gamma using the FastLED helper syntax
34+
color = helper.applyGamma_video(color)
35+
# 'Pack' color and assign to NeoPixel #i
36+
cpx.pixels[i] = color.pack()
37+
cpx.pixels.show()
38+
39+
offset += 8 # Bigger number = faster spin

examples/cpx_rotate.py

Lines changed: 19 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -4,59 +4,26 @@
44
from adafruit_circuitplayground.express import cpx
55
import adafruit_fancyled as fancy
66

7-
# Function names are kept the same as FastLED examples, which normally
8-
# upsets pylint. Disable name-checking so this passes muster.
9-
# pylint: disable=invalid-name
7+
cpx.pixels.auto_write = False # Refresh pixels only when we say
8+
cpx.pixels.brightness = 1.0 # We'll use FancyLED's brightness controls
109

11-
# A dynamic gradient palette is a compact representation of a color palette
12-
# that lists only the key points (specific positions and colors), which are
13-
# later interpolated to produce a full 'normal' color palette.
14-
# This one happens to be a blackbody spectrum, it ranges from black to red
15-
# to yellow to white...
16-
BLACKBODY = bytes([
17-
0, 0, 0, 0,
18-
85, 255, 0, 0,
19-
170, 255, 255, 0,
20-
255, 255, 255, 255])
10+
# Declare a 4-element color palette, this one happens to be a
11+
# 'blackbody' palette -- good for heat maps and firey effects.
12+
palette = [fancy.CRGB(1.0, 1.0, 1.0), # White
13+
fancy.CRGB(1.0, 1.0, 0), # Yellow
14+
fancy.CRGB(1.0, 0, 0), # Red
15+
fancy.CRGB(0,0,0)] # Black
2116

22-
# Here's where we convert the dynamic gradient palette to a full normal
23-
# palette. First we need a list to hold the resulting palette...it can be
24-
# filled with nonsense but the list length needs to match the desired
25-
# palette length (in FastLED, after which FancyLED is modeled, color
26-
# palettes always have 16, 32 or 256 entries...we can actually use whatever
27-
# length we want in CircuitPython, but for the sake of consistency, let's
28-
# make it a 16-element palette...
29-
PALETTE = [0] * 16
30-
fancy.loadDynamicGradientPalette(BLACKBODY, PALETTE)
31-
32-
# The dynamic gradient step is optional...some projects will just specify
33-
# a whole color palette directly on their own, not expand it from bytes.
34-
35-
def FillLEDsFromPaletteColors(palette, offset):
36-
""" This function fills the Circuit Playground Express NeoPixels from a
37-
color palette plus an offset to allow us to 'spin' the colors. In
38-
FancyLED (a la FastLED), palette indices are multiples of 16 (e.g.
39-
first palette entry is index 0, second is index 16, third is 32, etc)
40-
and indices between these values will interpolate color between the
41-
two nearest palette entries.
42-
"""
43-
44-
for i in range(10):
45-
# This looks up the color in the palette, scaling from
46-
# 'palette space' (16 colors * 16 interp position = 256)
47-
# to 'pixel space' (10 NeoPixels):
48-
color = fancy.ColorFromPalette(palette, int(i * len(palette) * 16 / 10 + offset), 255, True)
49-
# Gamma correction gives more sensible-looking colors
50-
cpx.pixels[i] = fancy.applyGamma_video(color)
51-
52-
# This is an offset (0-255) into the color palette to get it to 'spin'
53-
ADJUST = 0
17+
offset = 0 # Positional offset into color palette to get it to 'spin'
18+
levels = (0.25, 0.3, 0.15) # Color balance / brightness for gamma function
5419

5520
while True:
56-
FillLEDsFromPaletteColors(PALETTE, ADJUST)
57-
ADJUST += 4 # Bigger number = faster spin
58-
if ADJUST >= 256:
59-
ADJUST -= 256
60-
61-
62-
# pylint: enable=invalid-name
21+
for i in range(10):
22+
# Load each pixel's color from the palette using an offset, run it
23+
# through the gamma function, pack RGB value and assign to pixel.
24+
color = fancy.palette_lookup(palette, offset + i / 10)
25+
color = fancy.gamma_adjust(color, brightness=levels)
26+
cpx.pixels[i] = color.pack()
27+
cpx.pixels.show()
28+
29+
offset += 0.033 # Bigger number = faster spin

0 commit comments

Comments
 (0)