Ejemplo n.º 1
0
    class RippleSolidFX(BaseFX):
        description = Unicode('Ripple effect on a solid background')
        color = ColorTrait(default_value='green').tag(config=True)
        speed = Int(default_value=3, min=1, max=8).tag(config=True)

        def apply(self) -> bool:
            return self._fxmod.set_effect(FX.RIPPLE_SOLID, 0x01,
                                          self.speed * 10, self.color)
Ejemplo n.º 2
0
    class RippleFX(BaseFX):
        description = Unicode('Ripple effect when keys are pressed')
        color = ColorTrait(default_value='green').tag(config=True)
        speed = Int(default_value=3, min=1, max=8).tag(config=True)

        def apply(self) -> bool:
            return self._fxmod.set_effect(FX.RIPPLE, 0x01, self.speed * 10,
                                          self.color)
Ejemplo n.º 3
0
    class MorphFX(BaseFX):
        description = Unicode('Morphing colors when keys are pressed')
        color = ColorTrait(default_value='magenta').tag(config=True)
        base_color = ColorTrait(default_value='darkblue').tag(config=True)
        speed = Int(default_value=2, min=1, max=4).tag(config=True)

        def apply(self) -> bool:
            """
            A "morphing" color effect when keys are pressed

            :param color: The color when keys are pressed (defaults to magenta)
            :param base_color: The base color for the effect (defaults to blue)
            :param speed: Speed of the sweep
            :param preset: Predefined color pair to use. Invalid with color/base_color.

            :return: True if successful
            """
            return self._fxmod.set_effect(FX.MORPH, 0x04, self.speed,
                                          self.color, self.base_color)
Ejemplo n.º 4
0
    class SweepFX(BaseFX):
        description = Unicode('Colors sweep across the device')
        color = ColorTrait(default_value='green').tag(config=True)
        base_color = ColorTrait().tag(config=True)
        speed = Int(default_value=15, min=1, max=30).tag(config=True)
        direction = UseEnumCaseless(enum_class=Direction, \
                default_value=Direction.RIGHT).tag(config=True)

        def apply(self) -> bool:
            """
            Produces colors which sweep across the device

            :param color: The color to sweep with (defaults to light blue)
            :param base_color: The base color for the effect (defaults to black)
            :param direction: The direction for the sweep
            :param speed: Speed of the sweep
            :param preset: Predefined color pair to use. Invalid with color/base_color.

            :return: True if successful
            """
            return self._fxmod.set_effect(FX.SWEEP, self.direction, self.speed,
                                          self.base_color, self.color)
Ejemplo n.º 5
0
    class StaticFX(BaseFX):
        description = Unicode('Static color')
        color = ColorTrait(default_value='green').tag(config=True)

        def apply(self) -> bool:
            """
            Sets lighting to a static color

            :param color: The color to apply

            :return: True if successful
            """
            return self._fxmod.set_effect(FX.STATIC, self.color)
Ejemplo n.º 6
0
    class ReactiveFX(BaseFX):
        description = Unicode('Keys light up when pressed')
        color = ColorTrait(default_value='skyblue').tag(config=True)
        speed = Int(1, min=1, max=4).tag(config=True)

        def apply(self) -> bool:
            """
            Lights up keys when they are pressed

            :param color: Color of lighting when keys are pressed
            :param speed: Speed (1-4) at which the keys should fade out

            :return: True if successful
            """
            return self._fxmod.set_effect(FX.REACTIVE, self.speed, self.color)
Ejemplo n.º 7
0
    class FireFX(BaseFX):
        description = Unicode('Keys on fire')
        color = ColorTrait(default_value='red').tag(config=True)
        speed = Int(default_value=0x40, min=0x10, max=0x80).tag(config=True)

        def apply(self) -> bool:
            """
            Animated fire!

            :param color: Color scheme of the fire
            :param speed: Speed of the fire

            :return: True if successful
            """
            return self._fxmod.set_effect(FX.FIRE, 0x01, self.speed,
                                          self.color)
Ejemplo n.º 8
0
    class StaticFX(BaseFX):
        description = Unicode("Static color")
        color = ColorTrait(default_value='green').tag(config=True)

        def apply(self) -> bool:
            """
            Sets lighting to a static color

            :param color: The color to apply

            :return: True if successful
            """
            bits = EffectBits()
            bits.on = True
            with self._driver.device_open():
                if self._driver._set_rgb(self.color):
                    return self._driver._set_led_mode(bits)

            return False
Ejemplo n.º 9
0
    def __init__(self, driver, led_type: LEDType, *args, **kwargs):
        super(LED, self).__init__(*args, **kwargs)
        self._driver = driver
        self._led_type = led_type
        self._logger = driver.logger
        self.led_type = led_type
        self._restoring = True
        self._refreshing = False
        self._dirty = True

        # dynamic traits, since they are normally class-level
        brightness = Float(min=0.0, max=100.0, default_value=80.0,
                           allow_none=False).tag(config=True)
        color = ColorTrait(default_value=led_type.default_color,
                           allow_none=False).tag(config=led_type.rgb)
        mode = UseEnumCaseless(enum_class=LEDMode, default_value=LEDMode.STATIC,
                               allow_none=False).tag(config=led_type.has_modes)

        self.add_traits(color=color, mode=mode, brightness=brightness)

        self._restoring = False
Ejemplo n.º 10
0
class Reaction(Renderer):
    """
    Reaction creates a two tone animation effect based on the 'react' fx
    Config Options:
     - background_color: the static color when no keys are active
     - color: the color the key will change to when pressed
     - speed: (1 - 9) how fast the keys will change back. 9 is the fastest.
    """
    meta = RendererMeta('Reaction', 'Keys change color when pressed',
                        'Ryan Burns', '1.0')

    # Configuration options
    speed = Int(default_value=DEFAULT_SPEED, min=1,
                max=MAX_SPEED).tag(config=True)
    background_color = ColorTrait().tag(config=True)
    color = ColorTrait().tag(config=True)

    def __init__(self, *args, **kwargs):
        super(Reaction, self).__init__(*args, **kwargs)
        self.fps = 30

        # It seems like observers can be called before __init__
        # How is this possible?
        if not hasattr(self, 'init_speed'):
            self._set_speed(DEFAULT_SPEED)

        if not hasattr(self, 'init_colors'):
            self._set_colors("#000000", "#FFFFFF")

    @observe('speed')
    def _change_speed(self, change):
        """
        responds to speed changes made by the user
        """
        self.init_speed = True
        self._set_speed(change.new)

    def _set_speed(self, value):
        expire = MAX_SPEED + 1 - value
        self.key_expire_time = expire * EXPIRE_TIME_FACTOR

    def _set_colors(self, bg_color, color):
        self._gradient = ColorUtils.color_scheme(color=color,
                                                 base_color=bg_color,
                                                 steps=100)
        self._gradient_count = len(self._gradient)

    def _process_events(self, layer, events):
        """
        process events and assign a color to each one
        """
        if self._gradient is None:
            return None

        for event in events:
            if REACT_COLOR_KEY not in events:
                # percent_complete appears to go from 1 to 0.
                # perhaps it should be renamed percent_remaining?
                idx = int(self._gradient_count -
                          (event.percent_complete * self._gradient_count))
                if event.percent_complete <= 0.15:
                    # TODO: Is there a better way to know if this will be
                    # the last event for this key press?
                    event.data[REACT_COLOR_KEY] = self.background_color
                else:
                    event.data[REACT_COLOR_KEY] = self._gradient[idx]

            self._react_keys(layer, event)

    def _react_keys(self, layer, event):
        """
        updates all the coordinates for an event with the appropriate color
        """
        if event.coords is None or len(event.coords) == 0:
            self.logger.error('No coordinates available: %s', event)
            return

        react_color = event.data[REACT_COLOR_KEY]
        for coord in event.coords:
            layer.put(coord.y, coord.x, react_color)

    async def draw(self, layer, timestamp):
        """
        Draw the next layer
        """
        # Yield until the queue becomes active
        events = await self.get_input_events()

        if len(events) > 0:
            self._process_events(layer, events)
            return True
        return False

    @observe('background_color', 'color')
    def _update_colors(self, change=None):
        """
        responds to color changes made by the user
        """
        with self.hold_trait_notifications():
            if change.new is None:
                return

            self.init_colors = True
            bg_color = self.background_color
            if bg_color == (0, 0, 0, 1):
                bg_color = None
            self._set_colors(bg_color, self.color)

    def init(self, frame) -> bool:
        if not self.has_key_input:
            return False
        return True
Ejemplo n.º 11
0
class Renderer(HasTraits, object):
    """
    Base class for custom effects renderers.
    """

    # traits
    meta = RendererMeta('_unknown_', 'Unimplemented', 'Unknown', '0')

    fps = Float(min=0.0, max=MAX_FPS,
                default_value=DEFAULT_FPS).tag(config=True)
    blend_mode = DefaultCaselessStrEnum(BlendOp.get_modes(),
                                        default_value='screen',
                                        allow_none=False).tag(config=True)
    opacity = Float(min=0.0, max=1.0, default_value=1.0).tag(config=True)
    background_color = ColorTrait().tag(config=True)

    height = WriteOnceInt()
    width = WriteOnceInt()
    zindex = Int(default_value=-1)
    running = Bool(False)

    def __init__(self, driver, *args, **kwargs):
        self._avail_q = asyncio.Queue(maxsize=NUM_BUFFERS)
        self._active_q = asyncio.Queue(maxsize=NUM_BUFFERS)

        self.running = False

        self.width = driver.width
        self.height = driver.height

        self._tick = Ticker(1 / DEFAULT_FPS)

        self._input_queue = None
        if hasattr(driver,
                   'input_manager') and driver.input_manager is not None:
            self._input_queue = InputQueue(driver)

        self._logger = Log.get('uchroma.%s.%d' %
                               (self.__class__.__name__, self.zindex))
        super(Renderer, self).__init__(*args, **kwargs)

    @observe('zindex')
    def _z_changed(self, change):
        if change.old == change.new and change.new >= 0:
            return

        self._logger = Log.get('uchroma.%s.%d' %
                               (self.__class__.__name__, change.new))

    def init(self, frame) -> bool:
        """
        Invoked by AnimationLoop when the effect is activated. At this
        point, the traits will have been set. An implementation
        should perform any final setup here.

        :param frame: The frame instance being configured

        :return: True if the renderer was configured
        """
        return False

    def finish(self, frame):
        """
        Invoked by AnimationLoop when the effect is deactivated.
        An implementation should perform cleanup tasks here.

        :param frame: The frame instance being shut down
        """
        pass

    @abstractmethod
    async def draw(self, layer: Layer, timestamp: float) -> bool:
        """
        Coroutine called by AnimationLoop when a new frame needs
        to be drawn. If nothing should be drawn (such as if keyboard
        input is needed), then the implementation should yield until
        ready.

        :param layer: Layer to draw
        :param timestamp: The timestamp of this frame

        :return: True if the frame has been drawn
        """
        return False

    @property
    def has_key_input(self) -> bool:
        """
        True if the device is capable of producing key events
        """
        return self._input_queue is not None

    @property
    def key_expire_time(self) -> float:
        """
        Gets the duration (in seconds) that key events will remain
        available.
        """
        return self._input_queue.expire_time

    @key_expire_time.setter
    def key_expire_time(self, expire_time: float):
        """
        Set the duration (in seconds) that key events should remain
        in the queue for. This allows the renderer to act on groups
        of key events over time. If zero, events are not kept after
        being dequeued.
        """
        self._input_queue.expire_time = expire_time

    async def get_input_events(self):
        """
        Gets input events, yielding until at least one event is
        available. If expiration is not enabled, this returns
        a single item. Otherwise a list of all unexpired events
        is returned.
        """
        if not self.has_key_input or not self._input_queue.attach():
            raise ValueError('Input events are not supported for this device')

        events = await self._input_queue.get_events()
        return events

    @observe('fps')
    def _fps_changed(self, change):
        self._tick.interval = 1 / self.fps

    @property
    def logger(self):
        """
        The logger for this instance
        """
        return self._logger

    def _free_layer(self, layer):
        """
        Clear the layer and return it to the queue

        Called by AnimationLoop after a layer is replaced on the
        active list. Implementations should not call this directly.
        """
        layer.lock(False)
        layer.clear()
        self._avail_q.put_nowait(layer)

    async def _run(self):
        """
        Coroutine which dequeues buffers for drawing and queues them
        to the AnimationLoop when drawing is done.
        """
        if self.running:
            return

        self.running = True

        while self.running:
            async with self._tick:
                # get a buffer, blocking if necessary
                layer = await self._avail_q.get()
                layer.background_color = self.background_color
                layer.blend_mode = self.blend_mode
                layer.opacity = self.opacity

                try:
                    # draw the layer
                    status = await self.draw(layer,
                                             asyncio.get_event_loop().time())
                except Exception as err:
                    self.logger.exception(
                        "Exception in renderer, exiting now!", exc_info=err)
                    self.logger.error('Renderer traits: %s',
                                      self._trait_values)
                    break

                if not self.running:
                    break

                # submit for composition
                if status:
                    layer.lock(True)
                    await self._active_q.put(layer)

        await self._stop()

    def _flush(self):
        if self.running:
            return
        for qlen in range(0, self._avail_q.qsize()):
            self._avail_q.get_nowait()
        for qlen in range(0, self._active_q.qsize()):
            self._active_q.get_nowait()

    async def _stop(self):
        if not self.running:
            return

        self.running = False

        self._flush()

        if self.has_key_input:
            await self._input_queue.detach()

        self.logger.info("Renderer stopped: z=%d", self.zindex)
Ejemplo n.º 12
0
class Ripple(Renderer):

    # meta
    meta = RendererMeta('Ripples', 'Ripples of color when keys are pressed',
                        'Steve Kondik', '1.0')

    # configurable traits
    ripple_width = Int(default_value=DEFAULT_WIDTH, min=1,
                       max=5).tag(config=True)
    speed = Int(default_value=DEFAULT_SPEED, min=1, max=9).tag(config=True)
    preset = ColorPresetTrait(ColorScheme, default_value=None).tag(config=True)
    random = Bool(True).tag(config=True)
    color = ColorTrait().tag(config=True)

    def __init__(self, *args, **kwargs):

        super(Ripple, self).__init__(*args, **kwargs)

        self._generator = ColorUtils.rainbow_generator()
        self._max_distance = None
        self.key_expire_time = DEFAULT_SPEED * EXPIRE_TIME_FACTOR

        self.fps = 30

    @observe('speed')
    def _set_speed(self, change):
        self.key_expire_time = change.new * EXPIRE_TIME_FACTOR

    def _process_events(self, events):
        if self._generator is None:
            return None

        for event in events:
            if COLOR_KEY not in event.data:
                event.data[COLOR_KEY] = next(self._generator)

    @staticmethod
    def _ease(n):
        n = clamp(n, 0.0, 1.0)
        n = 2 * n
        if n < 1:
            return 0.5 * n**5

        n = n - 2
        return 0.5 * (n**5 + 2)

    def _draw_circles(self, layer, radius, event):
        width = self.ripple_width
        if COLOR_KEY not in event.data:
            return

        if event.coords is None or len(event.coords) == 0:
            self.logger.error('No coordinates available: %s', event)
            return

        if SCHEME_KEY in event.data:
            colors = event.data[SCHEME_KEY]
        else:
            color = event.data[COLOR_KEY]
            if width > 1:
                colors = ColorUtils.color_scheme(color=color,
                                                 base_color=color,
                                                 steps=width)
            else:
                colors = [color]
            event.data[SCHEME_KEY] = colors

        for circle_num in range(width - 1, -1, -1):
            if radius - circle_num < 0:
                continue

            rad = radius - circle_num
            a = Ripple._ease(1.0 - (rad / self._max_distance))
            cc = (*colors[circle_num].rgb, colors[circle_num].alpha * a)

            for coord in event.coords:
                layer.ellipse(coord.y, coord.x, rad / 1.33, rad, color=cc)

    async def draw(self, layer, timestamp):
        """
        Draw the next layer
        """

        # Yield until the queue becomes active
        events = await self.get_input_events()

        if len(events) > 0:
            self._process_events(events)

            # paint circles in descending timestamp order (oldest first)
            events = sorted(events, key=operator.itemgetter(0), reverse=True)

            for event in events:
                distance = 1.0 - event.percent_complete
                if distance < 1.0:
                    radius = self._max_distance * distance

                    self._draw_circles(layer, radius, event)

            return True

        return False

    @observe('preset', 'color', 'background_color', 'random')
    def _update_colors(self, change=None):
        with self.hold_trait_notifications():
            if change.new is None:
                return

            if change.name == 'preset':
                self.color = 'black'
                self.random = False
                self._generator = ColorUtils.color_generator(
                    list(change.new.value))
            elif change.name == 'random' and change.new:
                self.preset = None
                self.color = 'black'
                self._generator = ColorUtils.rainbow_generator()
            else:
                self.preset = None
                self.random = False
                base_color = self.background_color
                if base_color == (0, 0, 0, 1):
                    base_color = None
                self._generator = ColorUtils.scheme_generator(
                    color=self.color, base_color=base_color)

    def init(self, frame) -> bool:

        if not self.has_key_input:
            return False

        self._max_distance = math.hypot(frame.width, frame.height)

        return True