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)
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)
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)
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)
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)
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)
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)
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
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
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
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)
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