Skip to content

Commit 1e20b41

Browse files
Initial commit
1 parent c692244 commit 1e20b41

File tree

3 files changed

+276
-1
lines changed

3 files changed

+276
-1
lines changed

README.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,19 @@
11
# Adafruit_CircuitPython_FancyLED
2-
Helper functions to assist with porting FastLED Arduino projects to CircuitPython
2+
Helper functions to assist with porting FastLED Arduino projects to CircuitPython. This is NOT a complete port of FastLED, just a small subset of functions to get things moving. Also, as it's implemented in Python rather than C, it's not fast. The output of some functions (e.g. hsv2rgb_spectrum) may yield different results from FastLED. Aim here is just to have equivalent (ish) functions for the most oft-used calls, keeping the same names.
3+
4+
Currently-implemented functions include:
5+
6+
* applyGamma_video()
7+
* napplyGamma_video()
8+
* fill_gradient_rgb()
9+
* loadDynamicGradientPalette()
10+
* ColorFromPalette()
11+
* hsv2rgb_spectrum()
12+
13+
### Roadmap
14+
15+
* Always fit on Circuit Playground Express (.mpy format is fine) -- don't bloat up the code with every feature to the point that it only works on the highest-end boards! Just the most vital stuff.
16+
* Fix bugs as they're found.
17+
* Add more functions as they're needed (except where this would violate first item)
18+
* Improve performance where possible.
19+
* Improve compatibility with FastLED output where possible.

examples/cpx_rotate.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Simple fancyled example for Circuit Playground Express
2+
3+
from adafruit_circuitplayground.express import cpx
4+
import fancyled
5+
6+
# A dynamic gradient palette is a compact representation of a color palette
7+
# that lists only the key points (specific positions and colors), which are
8+
# later interpolated to produce a full 'normal' color palette.
9+
# This one happens to be a blackbody spectrum, it ranges from black to red
10+
# to yellow to white...
11+
blackbody = bytes([
12+
0, 0, 0, 0,
13+
85, 255, 0, 0,
14+
170, 255, 255, 0,
15+
255, 255, 255, 255 ])
16+
17+
# Here's where we convert the dynamic gradient palette to a full normal
18+
# palette. First we need a list to hold the resulting palette...it can be
19+
# filled with nonsense but the list length needs to match the desired
20+
# palette length (in FastLED, after which fancyled is modeled, color
21+
# palettes always have 16, 32 or 256 entries...we can actually use whatever
22+
# length we want in CircuitPython, but for the sake of consistency, let's
23+
# make it a 16-element palette...
24+
palette = [0] * 16
25+
fancyled.loadDynamicGradientPalette(blackbody, palette)
26+
27+
# The dynamic gradient step is optional...some projects will just specify
28+
# a whole color palette directly on their own, not expand it from bytes.
29+
30+
# This function fills the Circuit Playground Express NeoPixels from a
31+
# color palette plus an offset to allow us to 'spin' the colors. In
32+
# fancyled (a la FastLED), palette indices are multiples of 16 (e.g.
33+
# first palette entry is index 0, second is index 16, third is 32, etc)
34+
# and indices between these values will interpolate color between the
35+
# two nearest palette entries.
36+
def FillLEDsFromPaletteColors(palette, offset):
37+
for i in range(10):
38+
# This looks up the color in the palette, scaling from
39+
# 'palette space' (16 colors * 16 interp position = 256)
40+
# to 'pixel space' (10 NeoPixels):
41+
c = fancyled.ColorFromPalette(palette,
42+
int(i * len(palette) * 16 / 10 + offset), 255, True)
43+
# Gamma correction gives more sensible-looking colors
44+
cpx.pixels[i] = fancyled.applyGamma_video(c)
45+
46+
# This is an offset (0-255) into the color palette to get it to 'spin'
47+
adjust = 0
48+
49+
while True:
50+
FillLEDsFromPaletteColors(palette, adjust)
51+
adjust += 4 # Bigger number = faster spin
52+
if adjust >= 256: adjust -= 256

fancyled.py

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
# fancyled is sort of a mild CircuitPython interpretation of a subset
2+
# of the FastLED library for Arduino. This is mainly to assist with
3+
# porting of existing Arduino projects to CircuitPython.
4+
# It is NOT fast. Whereas FastLED does a lot of bit-level numerical
5+
# tricks for performance, we don't really have that luxury in Python,
6+
# and the aim here is just to have equivalent (ish) functions for the
7+
# most oft-used calls.
8+
9+
from math import pow
10+
11+
gfactor = 2.5 # Default gamma-correction factor for function below
12+
13+
# This approximates various invocations of FastLED's many-ways-overloaded
14+
# applyGamma_video() function.
15+
# ACCEPTS: One of three ways:
16+
# 1. A single brightness level (0-255) and optional gamma-correction
17+
# factor (float usu. > 1.0, default if unspecified is 2.5).
18+
# 2. A single RGB tuple (3 values 0-255) and optional gamma factor
19+
# or separate R, G, B gamma values.
20+
# 3. A list of RGB tuples (and optional gamma(s)).
21+
# In the tuple/list cases, the 'inPlace' flag determines whether
22+
# a new tuple/list is calculated and returned, or the existing
23+
# value is modified in-place. By default this is 'False'.
24+
# Can also use the napplyGamma_video() function to more directly
25+
# approximate FastLED syntax/behavior.
26+
# RETURNS: Corresponding to above cases:
27+
# 1. Single gamma-corrected brightness level (0-255).
28+
# 2. A gamma-corrected RGB tuple (3 values 0-255).
29+
# 3. A list of gamma-corrected RGB tuples (ea. 3 values 0-255).
30+
# In the tuple/list cases, there is NO return value if 'inPlace'
31+
# is true -- the original values are modified.
32+
def applyGamma_video(n, gR=gfactor, gG=None, gB=None, inPlace=False):
33+
if isinstance(n, int):
34+
# Input appears to be a single integer
35+
result = int(pow(n / 255.0, gR) * 255.0 + 0.5)
36+
# Never gamma-adjust a positive number down to zero
37+
if result == 0 and n > 0: result = 1
38+
return result
39+
else:
40+
# Might be an RGB tuple, or a list of tuples, but
41+
# isinstance() doesn't seem to distinguish...so try
42+
# treating it as a list first, and if that fails,
43+
# fall back on the RGB tuple case.
44+
try:
45+
if inPlace:
46+
for i in range(len(n)):
47+
n[i] = applyGamma_video(n[i],
48+
gR, gG, gB)
49+
else:
50+
newlist = []
51+
for i in n:
52+
newlist += applyGamma_video(i,
53+
gR, gG, gB)
54+
return newlist
55+
except TypeError:
56+
if gG is None: gG = gR
57+
if gB is None: gB = gR
58+
if inPlace:
59+
n[0] = applyGamma_video(n[0], gR)
60+
n[1] = applyGamma_video(n[1], gG)
61+
n[2] = applyGamma_video(n[2], gB)
62+
else:
63+
return [
64+
applyGamma_video(n[0], gR),
65+
applyGamma_video(n[1], gG),
66+
applyGamma_video(n[2], gB) ]
67+
68+
69+
# In-place version of above (to match FastLED function name)
70+
# This is for RGB tuples and tuple lists (not the above's integer case)
71+
def napplyGamma_video(n, gR=gfactor, gG=None, gB=None):
72+
return applyGamma_video(n, gR=gfactor, gG=None, gB=None, inPlace=True)
73+
74+
75+
# Sort-of-approximation of FastLED full_gradient_rgb() function.
76+
# Fills subsection of palette with RGB color range.
77+
# ACCEPTS: Palette to modify (must be a preallocated list with a suitable
78+
# number of elements), index of first entry to fill, RGB color of
79+
# first entry (as RGB tuple), index of last entry to fill, RGB
80+
# color of last entry (as RGB tuple).
81+
# RETURNS: Nothing; palette list is modified in-place.
82+
def fill_gradient_rgb(pal, startpos, startcolor, endpos, endcolor):
83+
if endpos < startpos:
84+
startpos , endpos = endpos , startpos
85+
startcolor, endcolor = endcolor, startcolor
86+
87+
dist = endpos - startpos
88+
if dist == 0:
89+
pal[startpos] = startcolor
90+
return
91+
92+
deltaR = endcolor[0] - startcolor[0]
93+
deltaG = endcolor[1] - startcolor[1]
94+
deltaB = endcolor[2] - startcolor[2]
95+
96+
for i in range(dist + 1):
97+
scale = i / dist
98+
pal[int(startpos + i)] = [
99+
int(startcolor[0] + deltaR * scale),
100+
int(startcolor[1] + deltaG * scale),
101+
int(startcolor[2] + deltaB * scale) ]
102+
103+
104+
# Kindasorta like FastLED's loadDynamicGradientPalette() function, with
105+
# some gotchas.
106+
# ACCEPTS: Gradient palette data as a 'bytes' type (makes it easier to copy
107+
# over gradient palettes from existing FastLED Arduino sketches)...
108+
# each palette entry is four bytes: a relative position (0-255)
109+
# within the overall resulting palette (whatever its size), and
110+
# 3 values for R, G and B...and a destination palette list which
111+
# must be preallocated to the desired length (e.g. 16, 32 or 256
112+
# elements if following FastLED conventions, but can be other
113+
# lengths if needed, the palette lookup function doesn't care).
114+
# RETURNS: Nothing; palette list data is modified in-place.
115+
def loadDynamicGradientPalette(src, dst):
116+
palettemaxindex = len(dst) - 1 # Index of last entry in dst list
117+
startpos = 0
118+
startcolor = [ src[1], src[2], src[3] ]
119+
n = 4
120+
while True:
121+
endpos = src[n] * palettemaxindex / 255
122+
endcolor = [ src[n+1], src[n+2], src[n+3] ]
123+
fill_gradient_rgb(dst, startpos, startcolor, endpos, endcolor)
124+
if src[n] >= 255: break # Done!
125+
n += 4
126+
startpos = endpos
127+
startcolor = endcolor
128+
129+
130+
# Approximates the FastLED ColorFromPalette() function
131+
# ACCEPTS: color palette (list of ints or tuples), palette index (x16) +
132+
# blend factor of next index (0-15) -- e.g. pass 32 to retrieve palette
133+
# index 2, or 40 for an interpolated value between palette index 2 and 3,
134+
# optional brightness (0-255), optiona blend flag (True/False)
135+
# RETURNS: single RGB tuple (3 values 0-255, no gamma correction)
136+
def ColorFromPalette(pal, index, brightness=255, blend=False):
137+
lo4 = index & 0xF # 0-15 blend factor to next palette entry
138+
hi4 = (index >> 4) % len(pal)
139+
c = pal[hi4]
140+
hi4 = (hi4 + 1) % len(pal)
141+
if isinstance(pal[0], int):
142+
# Color palette is in packed integer format
143+
r1 = (c >> 16) & 0xFF
144+
g1 = (c >> 8) & 0xFF
145+
b1 = c & 0xFF
146+
c = pal[hi4]
147+
r2 = (c >> 16) & 0xFF
148+
g2 = (c >> 8) & 0xFF
149+
b2 = c & 0xFF
150+
else:
151+
# Color palette is in RGB tuple format
152+
r1 = c[0]
153+
g1 = c[1]
154+
b1 = c[2]
155+
c = pal[hi4]
156+
r2 = c[0]
157+
g2 = c[1]
158+
b2 = c[2]
159+
160+
if blend and lo4 > 0:
161+
a2 = (lo4 * 0x11) + 1 # upper weighting 1-256
162+
a1 = 257 - a2 # lower weighting 1-256
163+
if brightness == 255:
164+
return [(r1 * a1 + r2 * a2) >> 8,
165+
(g1 * a1 + g2 * a2) >> 8,
166+
(b1 * a1 + b2 * a2) >> 8]
167+
else:
168+
brightness += 1 # 1-256
169+
return [(r1 * a1 + r2 * a2) * brightness >> 16,
170+
(g1 * a1 + g2 * a2) * brightness >> 16,
171+
(b1 * a1 + b2 * a2) * brightness >> 16]
172+
else:
173+
if brightness == 255:
174+
return [ r1, g1, b1 ]
175+
else:
176+
brightness += 1
177+
return [(r1 * brightness) >> 8,
178+
(g1 * brightness) >> 8,
179+
(b1 * brightness) >> 8]
180+
181+
182+
# This is named the same thing as FastLED's simpler HSV to RGB function
183+
# (spectrum, vs rainbow) but implementation is a bit different for the
184+
# sake of getting something running (adapted from some NeoPixel code).
185+
# ACCEPTS: HSV color as a 3-element tuple [hue, saturation, value], each
186+
# in the range 0 to 255.
187+
# RETURNS: RGB color as a 3-element tuple [R, G, B]
188+
def hsv2rgb_spectrum(hsv):
189+
h = hsv[0] * 6.0 / 256.0 # 0.0 to <6.0
190+
s = int(h) # Sextant number; 0 to 5
191+
n = int((h - s) * 255) # 0-254 within sextant (NOT 255!)
192+
193+
if s == 0: r, g, b = 255 , n , 0 # R to Y
194+
elif s == 1: r, g, b = 254-n, 255 , 0 # Y to G
195+
elif s == 2: r, g, b = 0 , 255 , n # G to C
196+
elif s == 3: r, g, b = 0 , 254-n, 255 # C to B
197+
elif s == 4: r, g, b = n , 0 , 255 # B to M
198+
else: r, g, b = 255 , 0 , 254-n # M to R
199+
200+
v1 = 1 + hsv[2] # value 1 to 256; allows >>8 instead of /255
201+
s1 = 1 + hsv[1] # saturation 1 to 256; same reason
202+
s2 = 255 - hsv[1] # 255 to 0
203+
204+
return [ (((((r * s1) >> 8) + s2) * v1) >> 8) & 0xFF,
205+
(((((g * s1) >> 8) + s2) * v1) >> 8) & 0xFF,
206+
(((((b * s1) >> 8) + s2) * v1) >> 8) & 0xFF ]

0 commit comments

Comments
 (0)