Repository: ManiacalLabs/BiblioPixel
Pull request #73 (2016-12-18 11:23:16)

New util subdirectory;  split up animation.py.
I didn't try to be fancy here, and you might well decide that there are two many small files in `animation/` and later want to glue them together, but as this is, it's pretty understandable and uncontroversial.

rec: Tried the "request review" feature just to see what it did.  :-D
rec: Ah, thought I'd routed out all uses of d!  I'll put it back, moment pls.

If you recall, you had some complex thingie for `d` which did exactly what `AttributeDict` does.  I gave it that name because `d` is a pathologically bad name for a class!
rec: I just removed that commit and rebased - no sweat!
adammhaile: @rec - LOL, yeah, I'm lazy so I like short names so I can type them faster :P Though my main concern is that d() did more than AttributeDict, for example iterrating through the dict and converting everything below it into the same. Otherwise. myDict.name might work but myDict.name.last would have to be myDict.name['last']. Or am I missing something and AttributeDict will do the same?
rec: Well, what was there before Feb 21 https://github.com/ManiacalLabs/BiblioPixel/commit/9542735e41135e7e830e252f318f84197fcbf31b didn't do that right.  :-D  It only did it for two levels!

I can easily add that change separately - very reasonable ide... but that's independent of this change.
adammhaile: Oh yeah... didn't realize that d() had basically been gone forever. Whoops.
+1 to all of this. Will merge unless you've got more.

On Sun, Dec 18, 2016 at 11:21 AM, Tom Swirly <notifications@github.com>
wrote:

> Well, what was there before Feb 21 9542735
> <https://github.com/ManiacalLabs/BiblioPixel/commit/9542735e41135e7e830e252f318f84197fcbf31b>
> didn't do that right. :-D It only did it for two levels!
>
> I can easily add that change separately - very reasonable ide... but
> that's independent of this change.
>
> —
> You are receiving this because you commented.
> Reply to this email directly, view it on GitHub
> <https://github.com/ManiacalLabs/BiblioPixel/pull/73#issuecomment-267830366>,
> or mute the thread
> <https://github.com/notifications/unsubscribe-auth/AA6a6jpb6ylFRTZkmui55nEJ9BVYLNYDks5rJV2agaJpZM4LQGsJ>
> .
>

rec: I did change the meaning before, however!

Overall, either `d` or `AttributeDict` are both broken because if you assign something after it's created, you get bad results.

Let me fix that - in a separate change...

Good to go!

Patch:
diff --git a/bibliopixel/animation.py b/bibliopixel/animation.py
deleted file mode 100644
index 853b88d..0000000
--- a/bibliopixel/animation.py
+++ /dev/null
@@ -1,484 +0,0 @@
-import threading, time
-
-from . led import LEDMatrix, LEDStrip, LEDCircle
-from . import colors, log
-from . util import AttributeDict
-
-
-class animThread(threading.Thread):
-
-    def __init__(self, anim, args):
-        super(animThread, self).__init__()
-        self.setDaemon(True)
-        self._anim = anim
-        self._args = args
-
-    def run(self):
-        log.debug("Starting thread...")
-        self._anim._run(**self._args)
-        log.debug("Thread Complete")
-
-
-class BaseAnimation(object):
-
-    def __init__(self, led):
-        self._led = led
-        self.animComplete = False
-        self._step = 0
-        self._timeRef = 0
-        self._internalDelay = None
-        self._threaded = False
-        self._thread = None
-        self._callback = None
-        self._stopEvent = threading.Event()
-        self._stopEvent.clear()
-        self._led._threadedAnim = False
-        self._free_run = False
-
-    def _msTime(self):
-        return time.time() * 1000.0
-
-    def preRun(self, amt=1):
-        self._led.all_off()
-
-    def preStep(self, amt=1):
-        pass
-
-    def postStep(self, amt=1):
-        pass
-
-    def step(self, amt=1):
-        raise RuntimeError("Base class step() called. This shouldn't happen")
-
-    def stopThread(self, wait=False):
-        if self._thread:
-            self._stopEvent.set()
-
-            if wait:
-                self._thread.join()
-
-    def __enter__(self):
-        return self
-
-    def __exit__(self, type, value, traceback):
-        self.stopThread(wait=True)
-        self._led.all_off()
-        self._led.push_to_driver()
-        self._led.waitForUpdate()
-
-    def cleanup(self):
-        return self.__exit__(None, None, None)
-
-    def stopped(self):
-        return not (self._thread and self._thread.isAlive())
-
-    def _run(self, amt, fps, sleep, max_steps, untilComplete, max_cycles, seconds):
-        self.preRun()
-
-        # calculate sleep time base on desired Frames per Second
-        if fps:
-            sleep = int(1000 / fps)
-
-        if seconds is not None:
-            max_steps = int((seconds * 1000) / sleep)
-
-        initSleep = sleep
-
-        self._step = 0
-        cur_step = 0
-        cycle_count = 0
-        self.animComplete = False
-
-        while (not self._stopEvent.isSet() and
-               ((max_steps == 0 and not untilComplete) or
-                (max_steps > 0 and cur_step < max_steps) or
-                (max_steps == 0 and untilComplete and not self.animComplete))):
-
-            self._timeRef = self._msTime()
-
-            start = self._msTime()
-            if hasattr(self, "_input_dev"):
-                self._keys = self._input_dev.getKeys()
-            self.preStep(amt)
-            self.step(amt)
-            self.postStep(amt)
-            mid = self._msTime()
-
-            if self._free_run:
-                sleep = None
-            elif self._internalDelay:
-                sleep = self._internalDelay
-            elif initSleep:
-                sleep = initSleep
-
-            self._led._frameGenTime = int(mid - start)
-            self._led._frameTotalTime = sleep
-
-            self._led.push_to_driver()
-            now = self._msTime()
-
-            if self.animComplete and max_cycles > 0:
-                if cycle_count < max_cycles - 1:
-                    cycle_count += 1
-                    self.animComplete = False
-
-            stepTime = int(mid - start)
-            if self._led._threadedUpdate:
-                updateTime = int(self._led.lastThreadedUpdate())
-                totalTime = updateTime
-            else:
-                updateTime = int(now - mid)
-                totalTime = stepTime + updateTime
-
-            if self._led._threadedUpdate:
-                log.debug(
-                    "Frame: %sms / Update Max: %sms", stepTime, updateTime)
-            else:
-                log.debug("%sms/%sfps / Frame: %sms / Update: %sms",
-                          totalTime, int(1000 / max(totalTime, 1)), stepTime, updateTime)
-
-            if sleep:
-                diff = (self._msTime() - self._timeRef)
-                t = max(0, (sleep - diff) / 1000.0)
-                if t == 0:
-                    log.warning(
-                        "Frame-time of %dms set, but took %dms!", sleep, diff)
-                if self._threaded:
-                    self._stopEvent.wait(t)
-                else:
-                    time.sleep(t)
-            cur_step += 1
-
-        self.__exit__(None, None, None)
-
-        if self._callback:
-            self._callback(self)
-
-    def run(self, amt=1, fps=None, sleep=None, max_steps=0, untilComplete=False, max_cycles=0, threaded=False, joinThread=False, callback=None, seconds=None):
-
-        self._led._threadedAnim = self._threaded = threaded
-        if self._threaded:
-            self._stopEvent.clear()
-        self._callback = callback
-
-        if self._threaded:
-            args = {}
-            l = locals()
-            run_params = ["amt", "fps", "sleep",
-                          "max_steps", "untilComplete", "max_cycles", "seconds"]
-            for p in run_params:
-                if p in l:
-                    args[p] = l[p]
-
-            self._thread = animThread(self, args)
-            self._thread.start()
-            if joinThread:
-                self._thread.join()
-        else:
-            self._run(amt, fps, sleep, max_steps, untilComplete, max_cycles, seconds)
-
-    RUN_PARAMS = [{
-        "id": "amt",
-        "label": "Step Amount",
-        "type": "int",
-                "min": 1,
-                "default": 1,
-                "help": "Amount to step animation by on each frame. May not be used on some animations."
-    }, {
-        "id": "fps",
-        "label": "Framerate",
-        "type": "int",
-                "default": 30,
-                "min": 1,
-                "help": "Framerate at which to run animation."
-    }, {
-        "id": "seconds",
-        "label": "Run Seconds",
-        "type": "int",
-                "default": None,
-                "min": 0,
-                "help": "Number of seconds to run animation for, based on framerate."
-    }, {
-        "id": "max_steps",
-        "label": "Max Frames",
-        "type": "int",
-                "min": 0,
-                "default": 0,
-                "help": "Total frames to run before stopping."
-    }, {
-        "id": "untilComplete",
-        "label": "Until Complete",
-        "type": "bool",
-                "default": False,
-                "help": "Run until animation marks itself as complete. If supported."
-    }, {
-        "id": "max_cycles",
-        "label": "Max Cycles",
-        "type": "int",
-                "min": 1,
-                "default": 1,
-                "help": "If Until Complete is set, animation will repeat this many times."
-    }, ]
-
-
-class OffAnim(BaseAnimation):
-
-    def __init__(self, led, timeout=10):
-        super(OffAnim, self).__init__(led)
-        self._internalDelay = timeout * 1000
-
-    def step(self, amt=1):
-        self._led.all_off()
-
-
-class AnimationQueue(BaseAnimation):
-
-    def __init__(self, led, anims=None):
-        super(AnimationQueue, self).__init__(led)
-        self.anims = anims or []
-        self.curAnim = None
-        self.animIndex = 0
-        self._internalDelay = 0  # never wait
-        self.fps = None
-        self.untilComplete = False
-
-    # overriding to handle all the animations
-    def stopThread(self, wait=False):
-        for a, r in self.anims:
-            # a bit of a hack. they aren't threaded, but stops them anyway
-            a._stopEvent.set()
-        super(AnimationQueue, self).stopThread(wait)
-
-    def addAnim(self, anim, amt=1, fps=None, max_steps=0, untilComplete=False, max_cycles=0, seconds=None):
-        a = (
-            anim,
-            {
-                "amt": amt,
-                "fps": fps,
-                "max_steps": max_steps,
-                "untilComplete": untilComplete,
-                "max_cycles": max_cycles,
-                "seconds": seconds
-            }
-        )
-        self.anims.append(a)
-
-    def preRun(self, amt=1):
-        if len(self.anims) == 0:
-            raise Exception("Must provide at least one animation.")
-        self.animIndex = -1
-
-    def run(self, amt=1, fps=None, sleep=None, max_steps=0, untilComplete=False, max_cycles=0, threaded=False, joinThread=False, callback=None, seconds=None):
-        self.fps = fps
-        self.untilComplete = untilComplete
-        super(AnimationQueue, self).run(amt=1, fps=None, sleep=None, max_steps=0, untilComplete=untilComplete,
-                                        max_cycles=0, threaded=threaded, joinThread=joinThread, callback=callback, seconds=seconds)
-
-    def step(self, amt=1):
-        self.animIndex += 1
-        if self.animIndex >= len(self.anims):
-            if self.untilComplete:
-                self.animComplete = True
-            else:
-                self.animIndex = 0
-
-        if not self.animComplete:
-            self.curAnim = self.anims[self.animIndex]
-
-            anim, run = self.curAnim
-            run.update(threaded=False, joinThread=False, callback=None)
-
-            run['fps'] = run.get('fps') or self.fps
-            anim.run(**(run))
-
-    RUN_PARAMS = [{
-        "id": "fps",
-        "label": "Default Framerate",
-        "type": "int",
-                "default": None,
-                "min": 1,
-                "help": "Default framerate to run all animations in queue."
-    }, {
-        "id": "untilComplete",
-        "label": "Until Complete",
-        "type": "bool",
-                "default": False,
-                "help": "Run until animation marks itself as complete. If supported."
-    }]
-
-
-class BaseStripAnim(BaseAnimation):
-
-    def __init__(self, led, start=0, end=-1):
-        super(BaseStripAnim, self).__init__(led)
-
-        if not isinstance(led, LEDStrip):
-            raise RuntimeError("Must use LEDStrip with Strip Animations!")
-
-        self._start = max(start, 0)
-        self._end = end
-        if self._end < 0 or self._end >= self._led.numLEDs:
-            self._end = self._led.numLEDs - 1
-
-        self._size = self._end - self._start + 1
-
-
-class BaseMatrixAnim(BaseAnimation):
-
-    def __init__(self, led, width=0, height=0, startX=0, startY=0):
-        super(BaseMatrixAnim, self).__init__(led)
-        if not isinstance(led, LEDMatrix):
-            raise RuntimeError("Must use LEDMatrix with Matrix Animations!")
-
-        self.width = width or led.width
-        self.height = height or led.height
-
-        self.startX = startX
-        self.startY = startY
-
-
-class BaseGameAnim(BaseMatrixAnim):
-
-    def __init__(self, led, inputDev):
-        super(BaseGameAnim, self).__init__(led)
-        self._input_dev = inputDev
-        self._keys = None
-        self._lastKeys = None
-        self._speedStep = 0
-        self._speeds = {}
-        self._keyfuncs = {}
-
-    def _exit(self, type, value, traceback):
-        if hasattr(self._input_dev, 'setLightsOff'):
-            self._input_dev.setLightsOff(5)
-        self._input_dev.close()
-
-    def setSpeed(self, name, speed):
-        self._speeds[name] = speed
-
-    def getSpeed(self, name):
-        return self._speeds.get(name)
-
-    def _checkSpeed(self, speed):
-        return not (self._speedStep % speed)
-
-    def checkSpeed(self, name):
-        return name in self._speeds and self._checkSpeed(self._speeds[name])
-
-    def addKeyFunc(self, key, func, speed=1, hold=True):
-        if not isinstance(key, list):
-            key = [key]
-        for k in key:
-            self._keyfuncs[k] = AttributeDict({
-                "func": func,
-                "speed": speed,
-                "hold": hold,
-                "last": False,
-                "inter": False
-            })
-
-    def handleKeys(self):
-        kf = self._keyfuncs
-        for key in self._keys:
-            val = self._keys[key]
-            if key in kf:
-                cfg = kf[key]
-                speedPass = self._checkSpeed(cfg.speed)
-
-                if cfg.hold:
-                    if speedPass:
-                        if (val or cfg.inter):
-                            cfg.func()
-                        else:
-                            cfg.inter = cfg.last = val
-                elif speedPass:
-                    if (val or cfg.inter) and not cfg.last:
-                        cfg.func()
-                    cfg.inter = cfg.last = val
-                else:
-                    cfg.inter |= val
-        self._lastKeys = self._keys
-
-    def preStep(self, amt):
-        pass
-
-    def postStep(self, amt):
-        self._speedStep += 1
-
-
-class BaseCircleAnim(BaseAnimation):
-
-    def __init__(self, led):
-        super(BaseCircleAnim, self).__init__(led)
-
-        if not isinstance(led, LEDCircle):
-            raise RuntimeError("Must use LEDCircle with Circle Animations!")
-
-        self.rings = led.rings
-        self.ringCount = led.ringCount
-        self.lastRing = led.lastRing
-        self.ringSteps = led.ringSteps
-
-
-class StripChannelTest(BaseStripAnim):
-
-    def __init__(self, led):
-        super(StripChannelTest, self).__init__(led)
-        self._internalDelay = 500
-        self.colors = [colors.Red, colors.Green, colors.Blue, colors.White]
-
-    def step(self, amt=1):
-
-        self._led.set(0, colors.Red)
-        self._led.set(1, colors.Green)
-        self._led.set(2, colors.Green)
-        self._led.set(3, colors.Blue)
-        self._led.set(4, colors.Blue)
-        self._led.set(5, colors.Blue)
-
-        color = self._step % 4
-        self._led.fill(self.colors[color], 7, 9)
-
-        self._step += 1
-
-
-class MatrixChannelTest(BaseMatrixAnim):
-
-    def __init__(self, led):
-        super(MatrixChannelTest, self).__init__(led, 0, 0)
-        self._internalDelay = 500
-        self.colors = [colors.Red, colors.Green, colors.Blue, colors.White]
-
-    def step(self, amt=1):
-
-        self._led.drawLine(0, 0, 0, self.height - 1, colors.Red)
-        self._led.drawLine(1, 0, 1, self.height - 1, colors.Green)
-        self._led.drawLine(2, 0, 2, self.height - 1, colors.Green)
-        self._led.drawLine(3, 0, 3, self.height - 1, colors.Blue)
-        self._led.drawLine(4, 0, 4, self.height - 1, colors.Blue)
-        self._led.drawLine(5, 0, 5, self.height - 1, colors.Blue)
-
-        color = self._step % 4
-        self._led.fillRect(7, 0, 3, self.height, self.colors[color])
-
-        self._step += 1
-
-
-class MatrixCalibrationTest(BaseMatrixAnim):
-
-    def __init__(self, led):
-        super(MatrixCalibrationTest, self).__init__(led, 0, 0)
-        self._internalDelay = 500
-        self.colors = [colors.Red, colors.Green, colors.Green,
-                       colors.Blue, colors.Blue, colors.Blue]
-
-    def step(self, amt=1):
-        self._led.all_off()
-        i = self._step % self.width
-        for x in range(i + 1):
-            c = self.colors[x % len(self.colors)]
-            self._led.drawLine(x, 0, x, i, c)
-
-        self.animComplete = (i == (self.width - 1))
-
-        self._step += 1
diff --git a/bibliopixel/animation/__init__.py b/bibliopixel/animation/__init__.py
new file mode 100644
index 0000000..41fb740
--- /dev/null
+++ b/bibliopixel/animation/__init__.py
@@ -0,0 +1,6 @@
+from . circle import BaseCircleAnim
+from . game import BaseGameAnim
+from . off import OffAnim
+from . queue import AnimationQueue
+from . strip import BaseStripAnim
+from . tests import StripChannelTest, MatrixChannelTest, MatrixCalibrationTest
diff --git a/bibliopixel/animation/base.py b/bibliopixel/animation/base.py
new file mode 100644
index 0000000..a3c7587
--- /dev/null
+++ b/bibliopixel/animation/base.py
@@ -0,0 +1,219 @@
+import threading, time
+
+from .. import log
+
+
+class AnimThread(threading.Thread):
+
+    def __init__(self, anim, args):
+        super().__init__()
+        self.setDaemon(True)
+        self._anim = anim
+        self._args = args
+
+    def run(self):
+        log.debug("Starting thread...")
+        self._anim._run(**self._args)
+        log.debug("Thread Complete")
+
+
+class BaseAnimation(object):
+
+    def __init__(self, led):
+        self._led = led
+        self.animComplete = False
+        self._step = 0
+        self._timeRef = 0
+        self._internalDelay = None
+        self._threaded = False
+        self._thread = None
+        self._callback = None
+        self._stopEvent = threading.Event()
+        self._stopEvent.clear()
+        self._led._threadedAnim = False
+        self._free_run = False
+
+    def _msTime(self):
+        return time.time() * 1000.0
+
+    def preRun(self, amt=1):
+        self._led.all_off()
+
+    def preStep(self, amt=1):
+        pass
+
+    def postStep(self, amt=1):
+        pass
+
+    def step(self, amt=1):
+        raise RuntimeError("Base class step() called. This shouldn't happen")
+
+    def stopThread(self, wait=False):
+        if self._thread:
+            self._stopEvent.set()
+
+            if wait:
+                self._thread.join()
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, type, value, traceback):
+        self.stopThread(wait=True)
+        self._led.all_off()
+        self._led.push_to_driver()
+        self._led.waitForUpdate()
+
+    def cleanup(self):
+        return self.__exit__(None, None, None)
+
+    def stopped(self):
+        return not (self._thread and self._thread.isAlive())
+
+    def _run(self, amt, fps, sleep, max_steps, untilComplete, max_cycles, seconds):
+        self.preRun()
+
+        # calculate sleep time base on desired Frames per Second
+        if fps:
+            sleep = int(1000 / fps)
+
+        if seconds is not None:
+            max_steps = int((seconds * 1000) / sleep)
+
+        initSleep = sleep
+
+        self._step = 0
+        cur_step = 0
+        cycle_count = 0
+        self.animComplete = False
+
+        while (not self._stopEvent.isSet() and
+               ((max_steps == 0 and not untilComplete) or
+                (max_steps > 0 and cur_step < max_steps) or
+                (max_steps == 0 and untilComplete and not self.animComplete))):
+
+            self._timeRef = self._msTime()
+
+            start = self._msTime()
+            if hasattr(self, "_input_dev"):
+                self._keys = self._input_dev.getKeys()
+            self.preStep(amt)
+            self.step(amt)
+            self.postStep(amt)
+            mid = self._msTime()
+
+            if self._free_run:
+                sleep = None
+            elif self._internalDelay:
+                sleep = self._internalDelay
+            elif initSleep:
+                sleep = initSleep
+
+            self._led._frameGenTime = int(mid - start)
+            self._led._frameTotalTime = sleep
+
+            self._led.push_to_driver()
+            now = self._msTime()
+
+            if self.animComplete and max_cycles > 0:
+                if cycle_count < max_cycles - 1:
+                    cycle_count += 1
+                    self.animComplete = False
+
+            stepTime = int(mid - start)
+            if self._led._threadedUpdate:
+                updateTime = int(self._led.lastThreadedUpdate())
+                totalTime = updateTime
+            else:
+                updateTime = int(now - mid)
+                totalTime = stepTime + updateTime
+
+            if self._led._threadedUpdate:
+                log.debug(
+                    "Frame: %sms / Update Max: %sms", stepTime, updateTime)
+            else:
+                log.debug("%sms/%sfps / Frame: %sms / Update: %sms",
+                          totalTime, int(1000 / max(totalTime, 1)), stepTime, updateTime)
+
+            if sleep:
+                diff = (self._msTime() - self._timeRef)
+                t = max(0, (sleep - diff) / 1000.0)
+                if t == 0:
+                    log.warning(
+                        "Frame-time of %dms set, but took %dms!", sleep, diff)
+                if self._threaded:
+                    self._stopEvent.wait(t)
+                else:
+                    time.sleep(t)
+            cur_step += 1
+
+        self.__exit__(None, None, None)
+
+        if self._callback:
+            self._callback(self)
+
+    def run(self, amt=1, fps=None, sleep=None, max_steps=0, untilComplete=False, max_cycles=0, threaded=False, joinThread=False, callback=None, seconds=None):
+
+        self._led._threadedAnim = self._threaded = threaded
+        if self._threaded:
+            self._stopEvent.clear()
+        self._callback = callback
+
+        if self._threaded:
+            args = {}
+            l = locals()
+            run_params = ["amt", "fps", "sleep",
+                          "max_steps", "untilComplete", "max_cycles", "seconds"]
+            for p in run_params:
+                if p in l:
+                    args[p] = l[p]
+
+            self._thread = AnimThread(self, args)
+            self._thread.start()
+            if joinThread:
+                self._thread.join()
+        else:
+            self._run(amt, fps, sleep, max_steps, untilComplete, max_cycles, seconds)
+
+    RUN_PARAMS = [{
+        "id": "amt",
+        "label": "Step Amount",
+        "type": "int",
+                "min": 1,
+                "default": 1,
+                "help": "Amount to step animation by on each frame. May not be used on some animations."
+    }, {
+        "id": "fps",
+        "label": "Framerate",
+        "type": "int",
+                "default": 30,
+                "min": 1,
+                "help": "Framerate at which to run animation."
+    }, {
+        "id": "seconds",
+        "label": "Run Seconds",
+        "type": "int",
+                "default": None,
+                "min": 0,
+                "help": "Number of seconds to run animation for, based on framerate."
+    }, {
+        "id": "max_steps",
+        "label": "Max Frames",
+        "type": "int",
+                "min": 0,
+                "default": 0,
+                "help": "Total frames to run before stopping."
+    }, {
+        "id": "untilComplete",
+        "label": "Until Complete",
+        "type": "bool",
+                "default": False,
+                "help": "Run until animation marks itself as complete. If supported."
+    }, {
+        "id": "max_cycles",
+        "label": "Max Cycles",
+        "type": "int",
+                "min": 1,
+                "default": 1,
+                "help": "If Until Complete is set, animation will repeat this many times."
+    }, ]
diff --git a/bibliopixel/animation/circle.py b/bibliopixel/animation/circle.py
new file mode 100644
index 0000000..ec6baad
--- /dev/null
+++ b/bibliopixel/animation/circle.py
@@ -0,0 +1,16 @@
+from . base import BaseAnimation
+from .. led import LEDCircle
+
+
+class BaseCircleAnim(BaseAnimation):
+
+    def __init__(self, led):
+        super().__init__(led)
+
+        if not isinstance(led, LEDCircle):
+            raise RuntimeError("Must use LEDCircle with Circle Animations!")
+
+        self.rings = led.rings
+        self.ringCount = led.ringCount
+        self.lastRing = led.lastRing
+        self.ringSteps = led.ringSteps
diff --git a/bibliopixel/animation/game.py b/bibliopixel/animation/game.py
new file mode 100644
index 0000000..cb350bb
--- /dev/null
+++ b/bibliopixel/animation/game.py
@@ -0,0 +1,71 @@
+from . matrix import BaseMatrixAnim
+from .. util import AttributeDict
+
+
+class BaseGameAnim(BaseMatrixAnim):
+
+    def __init__(self, led, inputDev):
+        super().__init__(led)
+        self._input_dev = inputDev
+        self._keys = None
+        self._lastKeys = None
+        self._speedStep = 0
+        self._speeds = {}
+        self._keyfuncs = {}
+
+    def _exit(self, type, value, traceback):
+        if hasattr(self._input_dev, 'setLightsOff'):
+            self._input_dev.setLightsOff(5)
+        self._input_dev.close()
+
+    def setSpeed(self, name, speed):
+        self._speeds[name] = speed
+
+    def getSpeed(self, name):
+        return self._speeds.get(name)
+
+    def _checkSpeed(self, speed):
+        return not (self._speedStep % speed)
+
+    def checkSpeed(self, name):
+        return name in self._speeds and self._checkSpeed(self._speeds[name])
+
+    def addKeyFunc(self, key, func, speed=1, hold=True):
+        if not isinstance(key, list):
+            key = [key]
+        for k in key:
+            self._keyfuncs[k] = AttributeDict({
+                "func": func,
+                "speed": speed,
+                "hold": hold,
+                "last": False,
+                "inter": False
+            })
+
+    def handleKeys(self):
+        kf = self._keyfuncs
+        for key in self._keys:
+            val = self._keys[key]
+            if key in kf:
+                cfg = kf[key]
+                speedPass = self._checkSpeed(cfg.speed)
+
+                if cfg.hold:
+                    if speedPass:
+                        if (val or cfg.inter):
+                            cfg.func()
+                        else:
+                            cfg.inter = cfg.last = val
+                elif speedPass:
+                    if (val or cfg.inter) and not cfg.last:
+                        cfg.func()
+                    cfg.inter = cfg.last = val
+                else:
+                    cfg.inter |= val
+        self._lastKeys = self._keys
+
+    def preStep(self, amt):
+        pass
+
+    def postStep(self, amt):
+        self._speedStep += 1
diff --git a/bibliopixel/animation/matrix.py b/bibliopixel/animation/matrix.py
new file mode 100644
index 0000000..b304386
--- /dev/null
+++ b/bibliopixel/animation/matrix.py
@@ -0,0 +1,16 @@
+from . base import BaseAnimation
+from .. led import LEDMatrix
+
+
+class BaseMatrixAnim(BaseAnimation):
+
+    def __init__(self, led, width=0, height=0, startX=0, startY=0):
+        super().__init__(led)
+        if not isinstance(led, LEDMatrix):
+            raise RuntimeError("Must use LEDMatrix with Matrix Animations!")
+
+        self.width = width or led.width
+        self.height = height or led.height
+
+        self.startX = startX
+        self.startY = startY
diff --git a/bibliopixel/animation/off.py b/bibliopixel/animation/off.py
new file mode 100644
index 0000000..066c3d3
--- /dev/null
+++ b/bibliopixel/animation/off.py
@@ -0,0 +1,12 @@
+from . base import BaseAnimation
+
+
+class OffAnim(BaseAnimation):
+    """A trivial animation that turns all LEDs off."""
+
+    def __init__(self, led, timeout=10):
+        super().__init__(led)
+        self._internalDelay = timeout * 1000
+
+    def step(self, amt=1):
+        self._led.all_off()
diff --git a/bibliopixel/animation/queue.py b/bibliopixel/animation/queue.py
new file mode 100644
index 0000000..4ea1f68
--- /dev/null
+++ b/bibliopixel/animation/queue.py
@@ -0,0 +1,82 @@
+from . base import BaseAnimation
+
+
+class AnimationQueue(BaseAnimation):
+
+    def __init__(self, led, anims=None):
+        super().__init__(led)
+        self.anims = anims or []
+        self.curAnim = None
+        self.animIndex = 0
+        self._internalDelay = 0  # never wait
+        self.fps = None
+        self.untilComplete = False
+
+    # overriding to handle all the animations
+    def stopThread(self, wait=False):
+        for a, r in self.anims:
+            # a bit of a hack. they aren't threaded, but stops them anyway
+            a._stopEvent.set()
+        super().stopThread(wait)
+
+    def addAnim(self, anim, amt=1, fps=None, max_steps=0, untilComplete=False,
+                max_cycles=0, seconds=None):
+        a = (
+            anim,
+            {
+                "amt": amt,
+                "fps": fps,
+                "max_steps": max_steps,
+                "untilComplete": untilComplete,
+                "max_cycles": max_cycles,
+                "seconds": seconds
+            }
+        )
+        self.anims.append(a)
+
+    def preRun(self, amt=1):
+        if len(self.anims) == 0:
+            raise Exception("Must provide at least one animation.")
+        self.animIndex = -1
+
+    def run(self, amt=1, fps=None, sleep=None, max_steps=0, untilComplete=False,
+            max_cycles=0, threaded=False, joinThread=False, callback=None,
+            seconds=None):
+        self.fps = fps
+        self.untilComplete = untilComplete
+        super().run(amt=1, fps=None, sleep=None, max_steps=0,
+                    untilComplete=untilComplete,
+                    max_cycles=0, threaded=threaded, joinThread=joinThread,
+                    callback=callback, seconds=seconds)
+
+    def step(self, amt=1):
+        self.animIndex += 1
+        if self.animIndex >= len(self.anims):
+            if self.untilComplete:
+                self.animComplete = True
+            else:
+                self.animIndex = 0
+
+        if not self.animComplete:
+            self.curAnim = self.anims[self.animIndex]
+
+            anim, run = self.curAnim
+            run.update(threaded=False, joinThread=False, callback=None)
+
+            run['fps'] = run.get('fps') or self.fps
+            anim.run(**(run))
+
+    RUN_PARAMS = [{
+        'id': 'fps',
+        'label': 'Default Framerate',
+        'type': 'int',
+        'default': None,
+        'min': 1,
+        'help': 'Default framerate to run all animations in queue.'
+    }, {
+        'id': 'untilComplete',
+        'label': 'Until Complete',
+        'type': 'bool',
+        'default': False,
+        'help': 'Run until animation marks itself as complete. If supported.'
+    }]
diff --git a/bibliopixel/animation/strip.py b/bibliopixel/animation/strip.py
new file mode 100644
index 0000000..5f362ba
--- /dev/null
+++ b/bibliopixel/animation/strip.py
@@ -0,0 +1,18 @@
+from . base import BaseAnimation
+from .. led import LEDStrip
+
+
+class BaseStripAnim(BaseAnimation):
+
+    def __init__(self, led, start=0, end=-1):
+        super().__init__(led)
+
+        if not isinstance(led, LEDStrip):
+            raise RuntimeError("Must use LEDStrip with Strip Animations!")
+
+        self._start = max(start, 0)
+        self._end = end
+        if self._end < 0 or self._end >= self._led.numLEDs:
+            self._end = self._led.numLEDs - 1
+
+        self._size = self._end - self._start + 1
diff --git a/bibliopixel/colors.py b/bibliopixel/colors.py
deleted file mode 100644
index a4bf280..0000000
--- a/bibliopixel/colors.py
+++ /dev/null
@@ -1,484 +0,0 @@
-# Functions to convert HSV colors to RGB colors lovingly ported from FastLED
-#
-#  The basically fall into two groups: spectra, and rainbows.
-#  Spectra and rainbows are not the same thing.  Wikipedia has a good
-#  illustration here
-#   http://upload.wikimedia.org//wikipedia//commons//f//f6//Prism_compare_rainbow_01.png
-#  from this article
-#   http://en.wikipedia.org//wiki//Rainbow#Number_of_colours_in_spectrum_or_rainbow
-#  that shows a 'spectrum' and a 'rainbow' side by side.  Among other
-#  differences, you'll see that a 'rainbow' has much more yellow than
-#  a plain spectrum.  "Classic" LED color washes are spectrum based, and
-#  usually show very little yellow.
-#
-#  Wikipedia's page on HSV color space, with pseudocode for conversion
-#  to RGB color space
-#   http://en.wikipedia.org//wiki//HSL_and_HSV
-#  Note that their conversion algorithm, which is (naturally) very popular
-#  is in the "maximum brightness at any given hue" style, vs the "uniform
-#  brightness for all hues" style.
-#
-#  You can't have both; either purple is the same brightness as red, e.g
-#    red = #FF0000 and purple = #800080 -> same "total light" output
-#  OR purple is 'as bright as it can be', e.g.
-#    red = #FF0000 and purple = #FF00FF -> purple is much brighter than red.
-#  The colorspace conversions here try to keep the apparent brightness
-#  constant even as the hue varies.
-#
-#  Adafruit's "Wheel" function, discussed here
-#   http://forums.adafruit.com//viewtopic.php?f=47&t=22483
-#  is also of the "constant apparent brightness" variety.
-#
-# More details here: https://github.com//FastLED//FastLED//wiki//FastLED-HSV-Colors
-
-from __future__ import division
-import colorsys
-import types
-import util
-
-
-def color_scale(color, level):
-    """Scale RGB tuple by level, 0 - 255"""
-    return tuple([(i * level) >> 8 for i in list(color)])
-
-
-def color_blend(a, b):
-    """Performs a Screen blend on RGB color tuples, a and b"""
-    return (255 - (((255 - a[0]) * (255 - b[0])) >> 8), 255 - (((255 - a[1]) * (255 - b[1])) >> 8), 255 - (((255 - a[2]) * (255 - b[2])) >> 8))
-
-
-def gamma_correct(color, gamma):
-    # DEPRECATED
-    """Applies a gamma correction to an RGB color tuple"""
-    return (gamma[color[0]], gamma[color[1]], gamma[color[2]])
-
-
-def hsv2rgb_raw(hsv):
-    """
-    Converts an HSV tuple to RGB. Intended for internal use.
-    You should use hsv2rgb_spectrum or hsv2rgb_rainbow instead.
-    """
-
-    HSV_SECTION_3 = 0x40
-
-    h, s, v = hsv
-
-    # The brightness floor is minimum number that all of
-    # R, G, and B will be set to.
-    invsat = 255 - s
-    brightness_floor = (v * invsat) // 256
-
-    # The color amplitude is the maximum amount of R, G, and B
-    # that will be added on top of the brightness_floor to
-    # create the specific hue desired.
-    color_amplitude = v - brightness_floor
-
-    # figure out which section of the hue wheel we're in,
-    # and how far offset we are within that section
-    section = h // HSV_SECTION_3  # 0..2
-    offset = h % HSV_SECTION_3  # 0..63
-
-    rampup = offset
-    rampdown = (HSV_SECTION_3 - 1) - offset
-
-    # compute color-amplitude-scaled-down versions of rampup and rampdown
-    rampup_amp_adj = (rampup * color_amplitude) // (256 // 4)
-    rampdown_amp_adj = (rampdown * color_amplitude) // (256 // 4)
-
-    # add brightness_floor offset to everything
-    rampup_adj_with_floor = rampup_amp_adj + brightness_floor
-    rampdown_adj_with_floor = rampdown_amp_adj + brightness_floor
-
-    r, g, b = (0, 0, 0)
-
-    if section:
-        if section == 1:
-            # section 1: 0x40..0x7F
-            r = brightness_floor
-            g = rampdown_adj_with_floor
-            b = rampup_adj_with_floor
-        else:
-            # section 2; 0x80..0xBF
-            r = rampup_adj_with_floor
-            g = brightness_floor
-            b = rampdown_adj_with_floor
-    else:
-        # section 0: 0x00..0x3F
-        r = rampdown_adj_with_floor
-        g = rampup_adj_with_floor
-        b = brightness_floor
-
-    return (r, g, b)
-
-
-def hsv2rgb_spectrum(hsv):
-    """Generates RGB values from  HSV values in line with a typical light spectrum"""
-    h, s, v = hsv
-    return hsv2rgb_raw(((h * 192) >> 8, s, v))
-
-
-def _nscale8x3_video(r, g, b, scale):
-    """Internal Use Only"""
-    nonzeroscale = 0
-    if scale != 0:
-        nonzeroscale = 1
-    if r != 0:
-        r = ((r * scale) >> 8) + nonzeroscale
-    if g != 0:
-        g = ((g * scale) >> 8) + nonzeroscale
-    if b != 0:
-        b = ((b * scale) >> 8) + nonzeroscale
-    return (r, g, b)
-
-
-def _scale8_video_LEAVING_R1_DIRTY(i, scale):
-    """Internal Use Only"""
-    nonzeroscale = 0
-    if scale != 0:
-        nonzeroscale = 1
-    if i != 0:
-        i = ((i * scale) >> 8) + nonzeroscale
-    return i
-
-
-def hsv2rgb_rainbow(hsv):
-    """Generates RGB values from HSV that have an even visual distribution.
-       Be careful as this method is only have as fast as hsv2rgb_spectrum.
-    """
-
-    h, s, v = hsv
-    offset = h & 0x1F  # 0..31
-    offset8 = offset * 8
-    third = (offset8 * (256 // 3)) >> 8
-    r, g, b = (0, 0, 0)
-
-    if not (h & 0x80):
-        if not (h & 0x40):
-            if not (h & 0x20):
-                r = 255 - third
-                g = third
-                b = 0
-            else:
-                r = 171
-                g = 85 + third
-                b = 0x00
-        else:
-            if not (h & 0x20):
-                twothirds = (third << 1)
-                r = 171 - twothirds
-                g = 171 + third
-                b = 0
-            else:
-                r = 0
-                g = 255 - third
-                b = third
-    else:
-        if not (h & 0x40):
-            if not (h & 0x20):
-                r = 0x00
-                twothirds = (third << 1)
-                g = 171 - twothirds
-                b = 85 + twothirds
-            else:
-                r = third
-                g = 0
-                b = 255 - third
-        else:
-            if not (h & 0x20):
-                r = 85 + third
-                g = 0
-                b = 171 - third
-            else:
-                r = 171 + third
-                g = 0x00
-                b = 85 - third
-
-    if s != 255:
-        r, g, b = _nscale8x3_video(r, g, b, s)
-        desat = 255 - s
-        desat = (desat * desat) >> 8
-        brightness_floor = desat
-        r = r + brightness_floor
-        g = g + brightness_floor
-        b = b + brightness_floor
-
-    if v != 255:
-        v = _scale8_video_LEAVING_R1_DIRTY(v, v)
-        r, g, b = _nscale8x3_video(r, g, b, v)
-
-    return (r, g, b)
-
-
-def hsv2rgb_360(hsv):
-    """
-    Python default hsv to rgb conversion for when hue values 0-359 are preferred.
-    Due to requiring float math, this method is slower than hsv2rgb_rainbow and hsv2rgb_spectrum.
-    """
-
-    h, s, v = hsv
-
-    r, g, b = colorsys.hsv_to_rgb(h // 360.0, s, v)
-    return (int(r * 255.0), int(g * 255.0), int(b * 255.0))
-
-
-# pre-generated spectrums for the sake of speed
-hue_raw = [hsv2rgb_raw((hue, 255, 255)) for hue in range(256)]
-hue_rainbow = [hsv2rgb_rainbow((hue, 255, 255)) for hue in range(256)]
-hue_spectrum = [hsv2rgb_spectrum((hue, 255, 255)) for hue in range(256)]
-hue_360 = [hsv2rgb_360((hue, 1.0, 1.0)) for hue in range(360)]
-
-
-def hue2rgb_raw(hue):
-    if hue >= 0 or hue < 256:
-        return hue_raw[hue]
-    else:
-        raise ValueError("hue must be between 0 and 255")
-
-
-def hue2rgb_rainbow(hue):
-    if hue >= 0 or hue < 256:
-        return hue_rainbow[hue]
-    else:
-        raise ValueError("hue must be between 0 and 255")
-
-
-def hue2rgb_spectrum(hue):
-    if hue >= 0 or hue < 256:
-        return hue_spectrum[hue]
-    else:
-        raise ValueError("hue must be between 0 and 255")
-
-
-def hue2rgb_360(hue):
-    if hue >= 0 or hue < 360:
-        return hue_360[hue]
-    else:
-        raise ValueError("hue must be between 0 and 359")
-
-
-hsv2rgb = hsv2rgb_rainbow
-hue2rgb = hue2rgb_rainbow
-
-
-def hex2rgb(hex):
-    """Helper for converting RGB and RGBA hex values to Color"""
-    hex = hex.strip('#')
-    if len(hex) == 6:
-        split = (hex[0:2], hex[2:4], hex[4:6])
-    else:
-        raise ValueError('Must pass in a 6 character hex value!')
-
-    r, g, b = [int(x, 16) for x in split]
-
-    return (r, g, b)
-
-
-WHEEL_MAX = 384
-
-
-def _gen_wheel():
-    for p in range(385):
-        if p < 128:
-            r = 127 - p % 128
-            g = p % 128
-            b = 0
-        elif p < 256:
-            g = 127 - p % 128
-            b = p % 128
-            r = 0
-        else:
-            b = 127 - p % 128
-            r = p % 128
-            g = 0
-
-    return (r, g, b)
-
-
-_wheel = _gen_wheel()
-
-
-def wheel_color(position):
-    """Get color from wheel value (0 - 384).
-    Provided for those used to using it from Adafruit libraries
-    """
-    if position < 0:
-        position = 0
-    if position > 384:
-        position = 384
-
-    return _wheel[position]
-
-
-def hue_gradient(start, stop, steps):
-    assert (start >= 0 and start < 256) and (
-        stop >= 0 and stop < 256), "hue must be between 0 and 255"
-    flip = False
-    if start > stop:
-        start, stop = stop, start
-        flip = True
-
-    stops = util.even_dist(start, stop, steps)
-
-    if flip:
-        stops = stops[::-1]
-
-    return stops
-
-
-def hue_helper(pos, length, cycle_step):
-    return hue2rgb(((pos * 255 // length) + cycle_step) % 255)
-
-
-def hue_helper360(pos, length, cycle_step):
-    return hue2rgb_360(((pos * 360 // length) + cycle_step) % 360)
-
-
-def wheel_helper(pos, length, cycle_step):
-    """Helper for wheel_color that distributes colors over length and allows shifting position"""
-    return wheel_color(((pos * WHEEL_MAX // length) + cycle_step) % WHEEL_MAX)
-
-
-Off = (0, 0, 0)
-Blue = (0, 0, 255)
-Pink = (255, 192, 203)
-Honeydew = (240, 255, 240)
-Purple = (128, 0, 128)
-Fuchsia = (255, 0, 255)
-LawnGreen = (124, 252, 0)
-AliceBlue = (240, 248, 255)
-Crimson = (220, 20, 60)
-White = (255, 255, 255)
-NavajoWhite = (255, 222, 173)
-Cornsilk = (255, 248, 220)
-Bisque = (255, 228, 196)
-PaleGreen = (152, 251, 152)
-Brown = (165, 42, 42)
-DarkTurquoise = (0, 206, 209)
-DarkGreen = (0, 100, 0)
-MediumOrchid = (186, 85, 211)
-Chocolate = (210, 105, 30)
-PapayaWhip = (255, 239, 213)
-Olive = (128, 128, 0)
-DarkSalmon = (233, 150, 122)
-PeachPuff = (255, 218, 185)
-Plum = (221, 160, 221)
-DarkGoldenrod = (184, 134, 11)
-MintCream = (245, 255, 250)
-CornflowerBlue = (100, 149, 237)
-HotPink = (255, 105, 180)
-DarkBlue = (0, 0, 139)
-LimeGreen = (50, 205, 50)
-DeepSkyBlue = (0, 191, 255)
-DarkKhaki = (189, 183, 107)
-LightGrey = (211, 211, 211)
-Yellow = (255, 255, 0)
-LightSalmon = (255, 160, 122)
-MistyRose = (255, 228, 225)
-SandyBrown = (244, 164, 96)
-DeepPink = (255, 20, 147)
-Magenta = (255, 0, 255)
-Amethyst = (153, 102, 204)
-DarkCyan = (0, 139, 139)
-GreenYellow = (173, 255, 47)
-DarkOrchid = (153, 50, 204)
-OliveDrab = (107, 142, 35)
-Chartreuse = (127, 255, 0)
-Peru = (205, 133, 63)
-Orange = (255, 165, 0)
-Red = (255, 0, 0)
-Wheat = (245, 222, 179)
-LightCyan = (224, 255, 255)
-LightSeaGreen = (32, 178, 170)
-BlueViolet = (138, 43, 226)
-Cyan = (0, 255, 255)
-MediumPurple = (147, 112, 219)
-MidnightBlue = (25, 25, 112)
-Gainsboro = (220, 220, 220)
-PaleTurquoise = (175, 238, 238)
-PaleGoldenrod = (238, 232, 170)
-Gray = (128, 128, 128)
-MediumSeaGreen = (60, 179, 113)
-Moccasin = (255, 228, 181)
-Ivory = (255, 255, 240)
-SlateBlue = (106, 90, 205)
-Green = (0, 255, 0)
-Green_HTML = (0, 128, 0)
-DarkSlateBlue = (72, 61, 139)
-Teal = (0, 128, 128)
-Azure = (240, 255, 255)
-LightSteelBlue = (176, 196, 222)
-Tan = (210, 180, 140)
-AntiqueWhite = (250, 235, 215)
-WhiteSmoke = (245, 245, 245)
-GhostWhite = (248, 248, 255)
-MediumTurquoise = (72, 209, 204)
-FloralWhite = (255, 250, 240)
-LavenderBlush = (255, 240, 245)
-SeaGreen = (46, 139, 87)
-Lavender = (230, 230, 250)
-BlanchedAlmond = (255, 235, 205)
-DarkOliveGreen = (85, 107, 47)
-DarkSeaGreen = (143, 188, 143)
-Violet = (238, 130, 238)
-Navy = (0, 0, 128)
-Beige = (245, 245, 220)
-SaddleBrown = (139, 69, 19)
-IndianRed = (205, 92, 92)
-Snow = (255, 250, 250)
-SteelBlue = (70, 130, 180)
-MediumSlateBlue = (123, 104, 238)
-Black = (0, 0, 0)
-LightBlue = (173, 216, 230)
-Turquoise = (64, 224, 208)
-MediumVioletRed = (199, 21, 133)
-DarkViolet = (148, 0, 211)
-DarkGray = (169, 169, 169)
-Salmon = (250, 128, 114)
-DarkMagenta = (139, 0, 139)
-Tomato = (255, 99, 71)
-SkyBlue = (135, 206, 235)
-Goldenrod = (218, 165, 32)
-MediumSpringGreen = (0, 250, 154)
-DodgerBlue = (30, 144, 255)
-Aqua = (0, 255, 255)
-ForestGreen = (34, 139, 34)
-DarkRed = (139, 0, 0)
-SlateGray = (112, 128, 144)
-Indigo = (75, 0, 130)
-CadetBlue = (95, 158, 160)
-LightYellow = (255, 255, 224)
-DarkOrange = (255, 140, 0)
-PowderBlue = (176, 224, 230)
-RoyalBlue = (65, 105, 225)
-Sienna = (160, 82, 45)
-Thistle = (216, 191, 216)
-Lime = (0, 255, 0)
-Seashell = (255, 245, 238)
-LemonChiffon = (255, 250, 205)
-LightSkyBlue = (135, 206, 250)
-YellowGreen = (154, 205, 50)
-Plaid = (204, 85, 51)
-Aquamarine = (127, 255, 212)
-LightCoral = (240, 128, 128)
-DarkSlateGray = (47, 79, 79)
-Coral = (255, 127, 80)
-Khaki = (240, 230, 140)
-BurlyWood = (222, 184, 135)
-LightGoldenrodYellow = (250, 250, 210)
-MediumBlue = (0, 0, 205)
-LightSlateGray = (119, 136, 153)
-RosyBrown = (188, 143, 143)
-Silver = (192, 192, 192)
-PaleVioletRed = (219, 112, 147)
-FireBrick = (178, 34, 34)
-SpringGreen = (0, 255, 127)
-LightGreen = (144, 238, 144)
-Linen = (250, 240, 230)
-OrangeRed = (255, 69, 0)
-DimGray = (105, 105, 105)
-Maroon = (128, 0, 0)
-LightPink = (255, 182, 193)
-MediumAquamarine = (102, 205, 170)
-Gold = (255, 215, 0)
-Orchid = (218, 112, 214)
-OldLace = (253, 245, 230)
diff --git a/bibliopixel/colors/__init__.py b/bibliopixel/colors/__init__.py
new file mode 100644
index 0000000..329987f
--- /dev/null
+++ b/bibliopixel/colors/__init__.py
@@ -0,0 +1,198 @@
+"""Functions to convert HSV colors to RGB colors lovingly ported from FastLED
+
+The basically fall into two groups: spectra, and rainbows.
+
+Spectra and rainbows are not the same thing.  Wikipedia has a good
+illustration here
+
+ http://upload.wikimedia.org/wikipedia/commons/f/f6/Prism_compare_rainbow_01.png
+
+from this article
+
+  http://en.wikipedia.org/wiki/Rainbow#Number_of_colours_in_spectrum_or_rainbow
+
+that shows a 'spectrum' and a 'rainbow' side by side.  Among other
+differences, you'll see that a 'rainbow' has much more yellow than
+a plain spectrum.  "Classic" LED color washes are spectrum based, and
+usually show very little yellow.
+
+Wikipedia's page on HSV color space, with pseudocode for conversion
+to RGB color space
+
+  http://en.wikipedia.org/wiki/HSL_and_HSV
+
+Note that their conversion algorithm, which is (naturally) very popular
+is in the "maximum brightness at any given hue" style, vs the "uniform
+brightness for all hues" style.
+
+You can't have both; either purple is the same brightness as red, e.g
+
+  red = #FF0000 and purple = #800080 -> same "total light" output
+
+OR purple is 'as bright as it can be', e.g.
+
+  red = #FF0000 and purple = #FF00FF -> purple is much brighter than red.
+
+The colorspace conversions here try to keep the apparent brightness
+constant even as the hue varies.
+
+Adafruit's "Wheel" function, discussed here
+
+  http://forums.adafruit.com/viewtopic.php?f=47&t=22483
+
+is also of the "constant apparent brightness" variety.
+
+More details here: https://github.com/FastLED/FastLED/wiki/FastLED-HSV-Colors
+"""
+
+from . arithmetic import color_blend, color_scale
+from . hue import hsv2rgb_spectrum, hsv2rgb_rainbow, hsv2rgb_360
+from . hue import hsv2rgb, hue2rgb, hex2rgb, hue_gradient
+from . hue import hue_helper, hue_helper360
+from . wheel import wheel_color, wheel_helper
+
+
+Off = (0, 0, 0)
+Blue = (0, 0, 255)
+Pink = (255, 192, 203)
+Honeydew = (240, 255, 240)
+Purple = (128, 0, 128)
+Fuchsia = (255, 0, 255)
+LawnGreen = (124, 252, 0)
+AliceBlue = (240, 248, 255)
+Crimson = (220, 20, 60)
+White = (255, 255, 255)
+NavajoWhite = (255, 222, 173)
+Cornsilk = (255, 248, 220)
+Bisque = (255, 228, 196)
+PaleGreen = (152, 251, 152)
+Brown = (165, 42, 42)
+DarkTurquoise = (0, 206, 209)
+DarkGreen = (0, 100, 0)
+MediumOrchid = (186, 85, 211)
+Chocolate = (210, 105, 30)
+PapayaWhip = (255, 239, 213)
+Olive = (128, 128, 0)
+DarkSalmon = (233, 150, 122)
+PeachPuff = (255, 218, 185)
+Plum = (221, 160, 221)
+DarkGoldenrod = (184, 134, 11)
+MintCream = (245, 255, 250)
+CornflowerBlue = (100, 149, 237)
+HotPink = (255, 105, 180)
+DarkBlue = (0, 0, 139)
+LimeGreen = (50, 205, 50)
+DeepSkyBlue = (0, 191, 255)
+DarkKhaki = (189, 183, 107)
+LightGrey = (211, 211, 211)
+Yellow = (255, 255, 0)
+LightSalmon = (255, 160, 122)
+MistyRose = (255, 228, 225)
+SandyBrown = (244, 164, 96)
+DeepPink = (255, 20, 147)
+Magenta = (255, 0, 255)
+Amethyst = (153, 102, 204)
+DarkCyan = (0, 139, 139)
+GreenYellow = (173, 255, 47)
+DarkOrchid = (153, 50, 204)
+OliveDrab = (107, 142, 35)
+Chartreuse = (127, 255, 0)
+Peru = (205, 133, 63)
+Orange = (255, 165, 0)
+Red = (255, 0, 0)
+Wheat = (245, 222, 179)
+LightCyan = (224, 255, 255)
+LightSeaGreen = (32, 178, 170)
+BlueViolet = (138, 43, 226)
+Cyan = (0, 255, 255)
+MediumPurple = (147, 112, 219)
+MidnightBlue = (25, 25, 112)
+Gainsboro = (220, 220, 220)
+PaleTurquoise = (175, 238, 238)
+PaleGoldenrod = (238, 232, 170)
+Gray = (128, 128, 128)
+MediumSeaGreen = (60, 179, 113)
+Moccasin = (255, 228, 181)
+Ivory = (255, 255, 240)
+SlateBlue = (106, 90, 205)
+Green = (0, 255, 0)
+Green_HTML = (0, 128, 0)
+DarkSlateBlue = (72, 61, 139)
+Teal = (0, 128, 128)
+Azure = (240, 255, 255)
+LightSteelBlue = (176, 196, 222)
+Tan = (210, 180, 140)
+AntiqueWhite = (250, 235, 215)
+WhiteSmoke = (245, 245, 245)
+GhostWhite = (248, 248, 255)
+MediumTurquoise = (72, 209, 204)
+FloralWhite = (255, 250, 240)
+LavenderBlush = (255, 240, 245)
+SeaGreen = (46, 139, 87)
+Lavender = (230, 230, 250)
+BlanchedAlmond = (255, 235, 205)
+DarkOliveGreen = (85, 107, 47)
+DarkSeaGreen = (143, 188, 143)
+Violet = (238, 130, 238)
+Navy = (0, 0, 128)
+Beige = (245, 245, 220)
+SaddleBrown = (139, 69, 19)
+IndianRed = (205, 92, 92)
+Snow = (255, 250, 250)
+SteelBlue = (70, 130, 180)
+MediumSlateBlue = (123, 104, 238)
+Black = (0, 0, 0)
+LightBlue = (173, 216, 230)
+Turquoise = (64, 224, 208)
+MediumVioletRed = (199, 21, 133)
+DarkViolet = (148, 0, 211)
+DarkGray = (169, 169, 169)
+Salmon = (250, 128, 114)
+DarkMagenta = (139, 0, 139)
+Tomato = (255, 99, 71)
+SkyBlue = (135, 206, 235)
+Goldenrod = (218, 165, 32)
+MediumSpringGreen = (0, 250, 154)
+DodgerBlue = (30, 144, 255)
+Aqua = (0, 255, 255)
+ForestGreen = (34, 139, 34)
+DarkRed = (139, 0, 0)
+SlateGray = (112, 128, 144)
+Indigo = (75, 0, 130)
+CadetBlue = (95, 158, 160)
+LightYellow = (255, 255, 224)
+DarkOrange = (255, 140, 0)
+PowderBlue = (176, 224, 230)
+RoyalBlue = (65, 105, 225)
+Sienna = (160, 82, 45)
+Thistle = (216, 191, 216)
+Lime = (0, 255, 0)
+Seashell = (255, 245, 238)
+LemonChiffon = (255, 250, 205)
+LightSkyBlue = (135, 206, 250)
+YellowGreen = (154, 205, 50)
+Plaid = (204, 85, 51)
+Aquamarine = (127, 255, 212)
+LightCoral = (240, 128, 128)
+DarkSlateGray = (47, 79, 79)
+Coral = (255, 127, 80)
+Khaki = (240, 230, 140)
+BurlyWood = (222, 184, 135)
+LightGoldenrodYellow = (250, 250, 210)
+MediumBlue = (0, 0, 205)
+LightSlateGray = (119, 136, 153)
+RosyBrown = (188, 143, 143)
+Silver = (192, 192, 192)
+PaleVioletRed = (219, 112, 147)
+FireBrick = (178, 34, 34)
+SpringGreen = (0, 255, 127)
+LightGreen = (144, 238, 144)
+Linen = (250, 240, 230)
+OrangeRed = (255, 69, 0)
+DimGray = (105, 105, 105)
+Maroon = (128, 0, 0)
+LightPink = (255, 182, 193)
+MediumAquamarine = (102, 205, 170)
+Gold = (255, 215, 0)
+Orchid = (218, 112, 214)
+OldLace = (253, 245, 230)
diff --git a/bibliopixel/colors/arithmetic.py b/bibliopixel/colors/arithmetic.py
new file mode 100644
index 0000000..c858443
--- /dev/null
+++ b/bibliopixel/colors/arithmetic.py
@@ -0,0 +1,10 @@
+def color_blend(a, b):
+    """Performs a Screen blend on RGB color tuples, a and b"""
+    return (255 - (((255 - a[0]) * (255 - b[0])) >> 8),
+            255 - (((255 - a[1]) * (255 - b[1])) >> 8),
+            255 - (((255 - a[2]) * (255 - b[2])) >> 8))
+
+
+def color_scale(color, level):
+    """Scale RGB tuple by level, 0 - 255"""
+    return tuple([(i * level) >> 8 for i in list(color)])
diff --git a/bibliopixel/colors/hue.py b/bibliopixel/colors/hue.py
new file mode 100644
index 0000000..da1722c
--- /dev/null
+++ b/bibliopixel/colors/hue.py
@@ -0,0 +1,242 @@
+from .. import util
+import colorsys
+
+
+def hsv2rgb_raw(hsv):
+    """
+    Converts an HSV tuple to RGB. Intended for internal use.
+    You should use hsv2rgb_spectrum or hsv2rgb_rainbow instead.
+    """
+
+    HSV_SECTION_3 = 0x40
+
+    h, s, v = hsv
+
+    # The brightness floor is minimum number that all of
+    # R, G, and B will be set to.
+    invsat = 255 - s
+    brightness_floor = (v * invsat) // 256
+
+    # The color amplitude is the maximum amount of R, G, and B
+    # that will be added on top of the brightness_floor to
+    # create the specific hue desired.
+    color_amplitude = v - brightness_floor
+
+    # figure out which section of the hue wheel we're in,
+    # and how far offset we are within that section
+    section = h // HSV_SECTION_3  # 0..2
+    offset = h % HSV_SECTION_3  # 0..63
+
+    rampup = offset
+    rampdown = (HSV_SECTION_3 - 1) - offset
+
+    # compute color-amplitude-scaled-down versions of rampup and rampdown
+    rampup_amp_adj = (rampup * color_amplitude) // (256 // 4)
+    rampdown_amp_adj = (rampdown * color_amplitude) // (256 // 4)
+
+    # add brightness_floor offset to everything
+    rampup_adj_with_floor = rampup_amp_adj + brightness_floor
+    rampdown_adj_with_floor = rampdown_amp_adj + brightness_floor
+
+    r, g, b = (0, 0, 0)
+
+    if section:
+        if section == 1:
+            # section 1: 0x40..0x7F
+            r = brightness_floor
+            g = rampdown_adj_with_floor
+            b = rampup_adj_with_floor
+        else:
+            # section 2; 0x80..0xBF
+            r = rampup_adj_with_floor
+            g = brightness_floor
+            b = rampdown_adj_with_floor
+    else:
+        # section 0: 0x00..0x3F
+        r = rampdown_adj_with_floor
+        g = rampup_adj_with_floor
+        b = brightness_floor
+
+    return (r, g, b)
+
+
+def hsv2rgb_spectrum(hsv):
+    """Generates RGB values from HSV values in line with a typical light
+    spectrum."""
+    h, s, v = hsv
+    return hsv2rgb_raw(((h * 192) >> 8, s, v))
+
+
+def hsv2rgb_rainbow(hsv):
+    """Generates RGB values from HSV that have an even visual
+    distribution.  Be careful as this method is only have as fast as
+    hsv2rgb_spectrum."""
+
+    def nscale8x3_video(r, g, b, scale):
+        nonzeroscale = 0
+        if scale != 0:
+            nonzeroscale = 1
+        if r != 0:
+            r = ((r * scale) >> 8) + nonzeroscale
+        if g != 0:
+            g = ((g * scale) >> 8) + nonzeroscale
+        if b != 0:
+            b = ((b * scale) >> 8) + nonzeroscale
+        return (r, g, b)
+
+    def scale8_video_LEAVING_R1_DIRTY(i, scale):
+        nonzeroscale = 0
+        if scale != 0:
+            nonzeroscale = 1
+        if i != 0:
+            i = ((i * scale) >> 8) + nonzeroscale
+        return i
+
+    h, s, v = hsv
+    offset = h & 0x1F  # 0..31
+    offset8 = offset * 8
+    third = (offset8 * (256 // 3)) >> 8
+    r, g, b = (0, 0, 0)
+
+    if not (h & 0x80):
+        if not (h & 0x40):
+            if not (h & 0x20):
+                r = 255 - third
+                g = third
+                b = 0
+            else:
+                r = 171
+                g = 85 + third
+                b = 0x00
+        else:
+            if not (h & 0x20):
+                twothirds = (third << 1)
+                r = 171 - twothirds
+                g = 171 + third
+                b = 0
+            else:
+                r = 0
+                g = 255 - third
+                b = third
+    else:
+        if not (h & 0x40):
+            if not (h & 0x20):
+                r = 0x00
+                twothirds = (third << 1)
+                g = 171 - twothirds
+                b = 85 + twothirds
+            else:
+                r = third
+                g = 0
+                b = 255 - third
+        else:
+            if not (h & 0x20):
+                r = 85 + third
+                g = 0
+                b = 171 - third
+            else:
+                r = 171 + third
+                g = 0x00
+                b = 85 - third
+
+    if s != 255:
+        r, g, b = nscale8x3_video(r, g, b, s)
+        desat = 255 - s
+        desat = (desat * desat) >> 8
+        brightness_floor = desat
+        r = r + brightness_floor
+        g = g + brightness_floor
+        b = b + brightness_floor
+
+    if v != 255:
+        v = scale8_video_LEAVING_R1_DIRTY(v, v)
+        r, g, b = nscale8x3_video(r, g, b, v)
+
+    return (r, g, b)
+
+
+def hsv2rgb_360(hsv):
+    """Python default hsv to rgb conversion for when hue values in the
+    range 0-359 are preferred.  Due to requiring float math, this method
+    is slower than hsv2rgb_rainbow and hsv2rgb_spectrum."""
+
+    h, s, v = hsv
+
+    r, g, b = colorsys.hsv_to_rgb(h // 360.0, s, v)
+    return (int(r * 255.0), int(g * 255.0), int(b * 255.0))
+
+
+# pre-generated spectra for the sake of speed
+HUE_RAW = [hsv2rgb_raw((hue, 255, 255)) for hue in range(256)]
+HUE_RAINBOW = [hsv2rgb_rainbow((hue, 255, 255)) for hue in range(256)]
+HUE_SPECTRUM = [hsv2rgb_spectrum((hue, 255, 255)) for hue in range(256)]
+HUE_360 = [hsv2rgb_360((hue, 1.0, 1.0)) for hue in range(360)]
+
+
+def hue2rgb_raw(hue):
+    if hue >= 0 or hue < 256:
+        return HUE_RAW[hue]
+    else:
+        raise ValueError("hue must be between 0 and 255")
+
+
+def hue2rgb_rainbow(hue):
+    if hue >= 0 or hue < 256:
+        return HUE_RAINBOW[hue]
+    else:
+        raise ValueError("hue must be between 0 and 255")
+
+
+def hue2rgb_spectrum(hue):
+    if hue >= 0 or hue < 256:
+        return HUE_SPECTRUM[hue]
+    else:
+        raise ValueError("hue must be between 0 and 255")
+
+
+def hue2rgb_360(hue):
+    if hue >= 0 or hue < 360:
+        return HUE_360[hue]
+    else:
+        raise ValueError("hue must be between 0 and 359")
+
+
+hsv2rgb = hsv2rgb_rainbow
+hue2rgb = hue2rgb_rainbow
+
+
+def hex2rgb(hex):
+    """Helper for converting RGB and RGBA hex values to Color"""
+    hex = hex.strip('#')
+    if len(hex) == 6:
+        split = (hex[0:2], hex[2:4], hex[4:6])
+    else:
+        raise ValueError('Must pass in a 6 character hex value!')
+
+    r, g, b = [int(x, 16) for x in split]
+
+    return (r, g, b)
+
+
+def hue_gradient(start, stop, steps):
+    assert (start >= 0 and start < 256) and (
+        stop >= 0 and stop < 256), "hue must be between 0 and 255"
+    flip = False
+    if start > stop:
+        start, stop = stop, start
+        flip = True
+
+    stops = util.even_dist(start, stop, steps)
+
+    if flip:
+        stops = stops[::-1]
+
+    return stops
+
+
+def hue_helper(pos, length, cycle_step):
+    return hue2rgb(((pos * 255 // length) + cycle_step) % 255)
+
+
+def hue_helper360(pos, length, cycle_step):
+    return hue2rgb_360(((pos * 360 // length) + cycle_step) % 360)
diff --git a/bibliopixel/colors/wheel.py b/bibliopixel/colors/wheel.py
new file mode 100644
index 0000000..d4e4462
--- /dev/null
+++ b/bibliopixel/colors/wheel.py
@@ -0,0 +1,40 @@
+WHEEL_MAX = 384
+
+
+def _gen_wheel():
+    for p in range(385):
+        if p < 128:
+            r = 127 - p % 128
+            g = p % 128
+            b = 0
+        elif p < 256:
+            g = 127 - p % 128
+            b = p % 128
+            r = 0
+        else:
+            b = 127 - p % 128
+            r = p % 128
+            g = 0
+
+    return r, g, b
+
+
+_WHEEL = _gen_wheel()
+
+
+def wheel_color(position):
+    """Get color from wheel value (0 - 384).
+    Provided for those used to using it from Adafruit libraries
+    """
+    if position < 0:
+        position = 0
+    if position > 384:
+        position = 384
+
+    return _WHEEL[position]
+
+
+def wheel_helper(pos, length, cycle_step):
+    """Helper for wheel_color that distributes colors over length and
+    allows shifting position."""
+    return wheel_color(((pos * WHEEL_MAX // length) + cycle_step) % WHEEL_MAX)
diff --git a/bibliopixel/led/base.py b/bibliopixel/led/base.py
index 87bc443..a0e8a2b 100644
--- a/bibliopixel/led/base.py
+++ b/bibliopixel/led/base.py
@@ -1,6 +1,6 @@
 import time
 
-from .. import colors, timedata, update_thread
+from .. import colors, timedata, util
 
 
 class LEDBase(object):
@@ -31,7 +31,7 @@ class LEDBase(object):
         self._threadedUpdate = threadedUpdate
 
         if self._threadedUpdate:
-            self._updateThread = update_thread.UpdateThread(self.drivers)
+            self._updateThread = util.UpdateThread(self.drivers)
             self._updateThread.start()
 
         self._waitingBrightness = False
diff --git a/bibliopixel/led/matrix.py b/bibliopixel/led/matrix.py
index 0e4d51f..7b7dd8e 100644
--- a/bibliopixel/led/matrix.py
+++ b/bibliopixel/led/matrix.py
@@ -1,7 +1,6 @@
 import math, threading, time
 
-from .. import colors, font, matrix, timedata, update_thread
-
+from .. import colors, font, matrix, timedata
 from . base import LEDBase
 
 
diff --git a/bibliopixel/led/pov.py b/bibliopixel/led/pov.py
index 43bbb1a..8294ec8 100644
--- a/bibliopixel/led/pov.py
+++ b/bibliopixel/led/pov.py
@@ -1,7 +1,5 @@
 import math, threading, time
 
-# from .. import colors, font, matrix, timedata, update_thread
-
 from . matrix import LEDMatrix, MatrixRotation
 
 
diff --git a/bibliopixel/util/__init__.py b/bibliopixel/util/__init__.py
new file mode 100644
index 0000000..c618c96
--- /dev/null
+++ b/bibliopixel/util/__init__.py
@@ -0,0 +1,2 @@
+from . util import generate_header, AttributeDict, even_dist
+from . update_thread import UpdateThread


Test patch:
diff --git a/bibliopixel/animation/tests.py b/bibliopixel/animation/tests.py
new file mode 100644
index 0000000..ead5c17
--- /dev/null
+++ b/bibliopixel/animation/tests.py
@@ -0,0 +1,67 @@
+from . strip import BaseStripAnim
+from . matrix import BaseMatrixAnim
+from .. import colors
+
+
+class StripChannelTest(BaseStripAnim):
+
+    def __init__(self, led):
+        super().__init__(led)
+        self._internalDelay = 500
+        self.colors = [colors.Red, colors.Green, colors.Blue, colors.White]
+
+    def step(self, amt=1):
+
+        self._led.set(0, colors.Red)
+        self._led.set(1, colors.Green)
+        self._led.set(2, colors.Green)
+        self._led.set(3, colors.Blue)
+        self._led.set(4, colors.Blue)
+        self._led.set(5, colors.Blue)
+
+        color = self._step % 4
+        self._led.fill(self.colors[color], 7, 9)
+
+        self._step += 1
+
+
+class MatrixChannelTest(BaseMatrixAnim):
+
+    def __init__(self, led):
+        super().__init__(led, 0, 0)
+        self._internalDelay = 500
+        self.colors = [colors.Red, colors.Green, colors.Blue, colors.White]
+
+    def step(self, amt=1):
+
+        self._led.drawLine(0, 0, 0, self.height - 1, colors.Red)
+        self._led.drawLine(1, 0, 1, self.height - 1, colors.Green)
+        self._led.drawLine(2, 0, 2, self.height - 1, colors.Green)
+        self._led.drawLine(3, 0, 3, self.height - 1, colors.Blue)
+        self._led.drawLine(4, 0, 4, self.height - 1, colors.Blue)
+        self._led.drawLine(5, 0, 5, self.height - 1, colors.Blue)
+
+        color = self._step % 4
+        self._led.fillRect(7, 0, 3, self.height, self.colors[color])
+
+        self._step += 1
+
+
+class MatrixCalibrationTest(BaseMatrixAnim):
+
+    def __init__(self, led):
+        super().__init__(led, 0, 0)
+        self._internalDelay = 500
+        self.colors = [colors.Red, colors.Green, colors.Green,
+                       colors.Blue, colors.Blue, colors.Blue]
+
+    def step(self, amt=1):
+        self._led.all_off()
+        i = self._step % self.width
+        for x in range(i + 1):
+            c = self.colors[x % len(self.colors)]
+            self._led.drawLine(x, 0, x, i, c)
+
+        self.animComplete = (i == (self.width - 1))
+
+        self._step += 1
diff --git a/bibliopixel/compose_events.py b/bibliopixel/util/compose_events.py
similarity index 100%
rename from bibliopixel/compose_events.py
rename to bibliopixel/util/compose_events.py
diff --git a/bibliopixel/double_buffer.py b/bibliopixel/util/double_buffer.py
similarity index 100%
rename from bibliopixel/double_buffer.py
rename to bibliopixel/util/double_buffer.py
diff --git a/bibliopixel/task_thread.py b/bibliopixel/util/task_thread.py
similarity index 100%
rename from bibliopixel/task_thread.py
rename to bibliopixel/util/task_thread.py
diff --git a/bibliopixel/update_thread.py b/bibliopixel/util/update_thread.py
similarity index 100%
rename from bibliopixel/update_thread.py
rename to bibliopixel/util/update_thread.py
diff --git a/bibliopixel/util.py b/bibliopixel/util/util.py
similarity index 100%
rename from bibliopixel/util.py
rename to bibliopixel/util/util.py
diff --git a/tests/compose_events_test.py b/tests/compose_events_test.py
index 5a791e5..8ca6751 100644
--- a/tests/compose_events_test.py
+++ b/tests/compose_events_test.py
@@ -1,6 +1,6 @@
 import threading, unittest
 
-from bibliopixel import compose_events
+from bibliopixel.util import compose_events
 
 
 class ComposeEventTest(unittest.TestCase):
diff --git a/tests/double_buffer_test.py b/tests/double_buffer_test.py
index 2b7cf58..64bf665 100644
--- a/tests/double_buffer_test.py
+++ b/tests/double_buffer_test.py
@@ -1,6 +1,6 @@
 import random, threading, time, unittest
 
-from bibliopixel import double_buffer
+from bibliopixel.util import double_buffer
 
 
 class DoubleBufferTest(unittest.TestCase):