Repository: ManiacalLabs/BiblioPixel
Pull request #998 (2018-10-07 15:25:59)

New Palette.autoscale
Add this feature and document it at the same time!

Autoscale requires changes in the animations and cannot be used in BPA until this is released.


Patch:
diff --git a/bibliopixel/util/colors/palette.py b/bibliopixel/util/colors/palette.py
index 5181244..a081ca3 100644
--- a/bibliopixel/util/colors/palette.py
+++ b/bibliopixel/util/colors/palette.py
@@ -11,7 +11,7 @@ class Palette(list):
     """
 
     def __init__(self, colors=(), continuous=False, serpentine=False, scale=1,
-                 offset=0):
+                 offset=0, autoscale=False):
         """
         Arguments:
             colors: an iterable of colors
@@ -23,11 +23,16 @@ class Palette(list):
               other iteration, giving a back-and-forth effect.  If False,
               palette colors always restart on each iteration
 
-            scale: scales the incoming index ``i``.  As ``i`` moves from 0
+            scale: Scales the incoming index ``i``.  As ``i`` moves from 0
               to ``len(colors) - 1``, the whole palette repeats itself
               ``self.scale`` times
 
             offset: offset to the incoming index ``i``, applied after scaling
+
+            autoscale: If True, automatically rescale the Palette size to
+              match the length of the output.  ``autoscale`` happens before
+              ``scale``, so the two work well together to give banding or
+              striping effects across your display
         """
         super().__init__(colors)
         if not self:
@@ -37,50 +42,74 @@ class Palette(list):
         self.serpentine = serpentine
         self.scale = scale
         self.offset = offset
+        self.autoscale = autoscale
 
-    def get(self, position):
+    def get(self, position, length=None):
         """
-        Return a color interpolated from the Palette. ``i`` may be any integer
-        or floating point number.
+        Return a color interpolated from the Palette.
 
-        In the case where continuous=False, serpentine=False, scale=1 and
-        offset=0, this is exactly the same as plain old [] indexing, but with a
-        wrap-around.  The parameters affect this result as documented in the
-        constructor.
-        """
-        pos = self._position(position * self.scale + self.offset)
-        index = int(pos)
-        fade = pos - index
-        if not fade:
-            return self[index]
+        In the case where continuous=False, serpentine=False, scale=1,
+        autoscale=False, and offset=0, this is exactly the same as plain old []
+        indexing, but with a wrap-around.
 
-        r1, g1, b1 = self[index]
-        r2, g2, b2 = self[(index + 1) % len(self)]
-        dr, dg, db = r2 - r1, g2 - g1, b2 - b1
+        The constructor parameters affect this result as documented in the
+        constructor.
 
-        return r1 + fade * dr, g1 + fade * dg, b1 + fade * db
+        Arguments:
+           ``position``:
+             May be any integer or floating point number
 
-    def _position(self, i):
+           ``length``:
+             The length of the output color_list.  If None, use the length of
+             the palette itself.  If autoscale=True, ``length`` is used to scale
+             the palette to match the output.
+        """
         n = len(self)
         if n == 1:
-            return 0
+            return self[0]
+
+        pos = position
+
+        if length and self.autoscale:
+            pos *= len(self)
+            pos /= length
+
+        pos *= self.scale
+        pos += self.offset
 
         if not self.continuous:
             if not self.serpentine:
-                return int(i % n)
+                return self[int(pos % n)]
 
             # We want a color sequence of length 2n-2
             # e.g. for n=5: a b c d | e d c b | a b c d ...
             m = (2 * n) - 2
-            index = i % m
-            return int(index if index < n else m - index)
+            pos %= m
+            if pos < n:
+                return self[int(pos)]
+            else:
+                return self[int(m - pos)]
+
+        if self.serpentine:
+            pos %= (2 * n)
+            if pos > n:
+                pos = (2 * n) - pos
+        else:
+            pos %= n
 
-        if not self.serpentine:
-            index = i % n
+        # p is a number in [0, n): scale it to be in [0, n-1)
+        pos *= n - 1
+        pos /= n
+
+        index = int(pos)
+        fade = pos - index
+        if not fade:
+            return self[index]
         else:
-            index = i % (2 * n)
-            if index > n:
-                index = (2 * n) - index
+            print(' -> fade', fade)
 
-        # This is a number in [0, n): scale it to be in [0, n-1)
-        return index * (n - 1) / n
+        r1, g1, b1 = self[index]
+        r2, g2, b2 = self[(index + 1) % len(self)]
+        dr, dg, db = r2 - r1, g2 - g1, b2 - b1
+
+        return r1 + fade * dr, g1 + fade * dg, b1 + fade * db
diff --git a/doc/tutorial/Palettes.rst b/doc/tutorial/Palettes.rst
index 7cc2e19..3bda67f 100644
--- a/doc/tutorial/Palettes.rst
+++ b/doc/tutorial/Palettes.rst
@@ -74,18 +74,20 @@ The list of Built-In Palettes is in the source code
 Fields in a Palette
 ====================================
 
-You can experiment with these Fields and not break anything.
-
-You can also attach them to a Control (described later in this Tutorial) and
-change them in real-time from an external source.
+There are quite a few Fields below.  They're all optional but you can
+experiment with them and not break anything, and you can get interesting
+results that way.
 
+You can also attach Palette Fields to a Control (described later in this
+Tutorial) and change the values in real-time from an external source for
+more excitement.
 
 ``colors`` (default ``[Black]``)
   A list of colors, or a string of color names separated with commas.
-  Colors can be string names, hex strings like #FFFFFF or 0xE8E0E8,
-  or triplets of integers like [255, 0, 255].  It can also be a name
-  of a Built-in or User Palette, in which case its Fields are copied
-  and then possibly overwritten by Fields in this Palette.
+  Each color can be a string name, a hex string like #FFFFFF or 0xE8E0E8,
+  or a triplet of integers like [255, 0, 255].  ``colors`` can also be the name
+  of a Built-in or User Palette, in which case its Fields are copied and then
+  possibly overwritten by Fields in this Palette.
 
 ``continuous`` (default ``False``)
   If ``True``, interpolate linearly between colors; otherwise
@@ -104,14 +106,10 @@ change them in real-time from an external source.
 ``offset``
   Offset added to the incoming index ``i``.  The offset is applied after scaling
 
-
-Reusing Palettes
-========================
-
-Named Palettes can be re-used a ``palette`` field of an Animation, or in the
-``palettes`` section of the Project.
-
-
+``autoscale``
+  If True, automatically rescale the Palette size to match the length of the
+  output length. ``autoscale`` happens before ``scale``, so the two work well
+  together to give banding or striping effects across your display
 
 ----
 


Test patch:
diff --git a/test/bibliopixel/util/colors/palette_test.py b/test/bibliopixel/util/colors/palette_test.py
index c3ae328..a22640f 100644
--- a/test/bibliopixel/util/colors/palette_test.py
+++ b/test/bibliopixel/util/colors/palette_test.py
@@ -188,3 +188,15 @@ class PaletteTest(unittest.TestCase):
         # result2 = [tuple(float(i) for i in c) for c in result]
         # print('', *result2, sep=',\n            ')
         self.assertEqual(expected, result)
+
+    def test_autoscale(self):
+        colors = [Red, Green, Blue, White]
+        p = Palette(colors, autoscale=True)
+
+        expected = [Red, Red, Green, Green, Blue, Blue, White, White, Red, Red]
+        result = [p.get(32 * i, 256) for i in range(10)]
+        print(*result, sep='\n')
+        self.assertEqual(expected, result)
+
+        result = [p.get(Fraction(i) / 2) for i in range(10)]
+        self.assertEqual(expected, result)