diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index efa34905..3519dd29 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -80,6 +80,14 @@ target_sources_ifdef(CONFIG_ZMK_BACKLIGHT app PRIVATE src/behaviors/behavior_bac target_sources_ifdef(CONFIG_ZMK_BATTERY_REPORTING app PRIVATE src/events/battery_state_changed.c) target_sources_ifdef(CONFIG_ZMK_BATTERY_REPORTING app PRIVATE src/battery.c) +target_sources_ifdef(CONFIG_ZMK_ANIMATION app PRIVATE src/animation/color.c) +target_sources_ifdef(CONFIG_ZMK_ANIMATION app PRIVATE src/animation/animation_compose.c) +target_sources_ifdef(CONFIG_ZMK_ANIMATION app PRIVATE src/animation/animation_control.c) +target_sources_ifdef(CONFIG_ZMK_ANIMATION app PRIVATE src/animation/animation_ripple.c) +target_sources_ifdef(CONFIG_ZMK_ANIMATION app PRIVATE src/animation/animation_solid.c) +target_sources_ifdef(CONFIG_ZMK_ANIMATION app PRIVATE src/animation/animation.c) +target_sources_ifdef(CONFIG_ZMK_ANIMATION app PRIVATE src/behaviors/behavior_animation.c) + target_sources_ifdef(CONFIG_ZMK_SPLIT app PRIVATE src/events/split_peripheral_status_changed.c) add_subdirectory(src/split) diff --git a/app/Kconfig b/app/Kconfig index 89a128b5..3f70c986 100644 --- a/app/Kconfig +++ b/app/Kconfig @@ -305,6 +305,24 @@ config ZMK_BACKLIGHT_AUTO_OFF_USB #ZMK_BACKLIGHT endif +config ZMK_ANIMATION + bool "RGB Animations" + select LED_STRIP + +if ZMK_ANIMATION + +config ZMK_ANIMATION_FPS + int "Animation FPS" + range 1 60 + default 30 + +config ZMK_ANIMATION_PIXEL_DISTANCE + bool "Generate a lookup table for distances between pixels" + default y + +#ZMK_ANIMATION +endif + #Display/LED Options endmenu diff --git a/app/drivers/CMakeLists.txt b/app/drivers/CMakeLists.txt index 44d69ac3..cb4917ce 100644 --- a/app/drivers/CMakeLists.txt +++ b/app/drivers/CMakeLists.txt @@ -2,6 +2,7 @@ # SPDX-License-Identifier: MIT add_subdirectory_ifdef(CONFIG_ZMK_DRIVERS_GPIO gpio) +add_subdirectory_ifdef(CONFIG_ZMK_ANIUMATION animation) add_subdirectory(kscan) add_subdirectory(sensor) add_subdirectory(display) diff --git a/app/dts/animation.dtsi b/app/dts/animation.dtsi new file mode 100644 index 00000000..1d11c915 --- /dev/null +++ b/app/dts/animation.dtsi @@ -0,0 +1,13 @@ +/* + * Copyright (c) 2021 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +/ { + /omit-if-no-ref/ pixel: animation_pixel { + compatible = "zmk,animation-pixel"; + label = "PIXEL"; + #pixel-cells = <2>; + }; +}; diff --git a/app/dts/behaviors.dtsi b/app/dts/behaviors.dtsi index b3502cbb..6bf952e0 100644 --- a/app/dts/behaviors.dtsi +++ b/app/dts/behaviors.dtsi @@ -18,4 +18,5 @@ #include #include #include -#include \ No newline at end of file +#include +#include diff --git a/app/dts/behaviors/animation.dtsi b/app/dts/behaviors/animation.dtsi new file mode 100644 index 00000000..d4551528 --- /dev/null +++ b/app/dts/behaviors/animation.dtsi @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2020 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +/ { + behaviors { + /omit-if-no-ref/ animation: behavior_animation { + compatible = "zmk,behavior-animation"; + label = "ANIMATION"; + #binding-cells = <1>; + }; + }; +}; diff --git a/app/dts/bindings/animations/animation_base.yaml b/app/dts/bindings/animations/animation_base.yaml new file mode 100644 index 00000000..342ee894 --- /dev/null +++ b/app/dts/bindings/animations/animation_base.yaml @@ -0,0 +1,22 @@ +# Copyright (c) 2020, The ZMK Contributors +# SPDX-License-Identifier: MIT + +properties: + pixels: + type: array + required: true + description: | + Pixel positions to which the animation should apply. + + blending-mode: + type: string + enum: + - "normal" + - "multiply" + - "lighten" + - "darken" + - "screen" + - "subtract" + default: "normal" + description: | + Blending mode for the animation to use during render. diff --git a/app/dts/bindings/animations/zmk,animation-compose.yaml b/app/dts/bindings/animations/zmk,animation-compose.yaml new file mode 100644 index 00000000..d91f0a2a --- /dev/null +++ b/app/dts/bindings/animations/zmk,animation-compose.yaml @@ -0,0 +1,15 @@ +# Copyright (c) 2020, The ZMK Contributors +# SPDX-License-Identifier: MIT + +description: | + Higher-order animation which allows for composing other animation + drivers and using different blending modes. + +compatible: "zmk,animation-compose" + +properties: + animations: + type: phandles + required: true + description: | + Animations to be combined. diff --git a/app/dts/bindings/animations/zmk,animation-control.yaml b/app/dts/bindings/animations/zmk,animation-control.yaml new file mode 100644 index 00000000..82f049b1 --- /dev/null +++ b/app/dts/bindings/animations/zmk,animation-control.yaml @@ -0,0 +1,26 @@ +# Copyright (c) 2020, The ZMK Contributors +# SPDX-License-Identifier: MIT + +description: | + Higher-order animation which allows for controlling animation drivers + placed underneath it by turning them on and off, cycling though them, + or changing the brightness. + +compatible: "zmk,animation-control" + +properties: + label: + type: string + required: true + + animations: + type: phandles + required: true + description: | + Animations to be combined. + + brightness-steps: + type: int + default: 5 + description: | + How many brightness steps should be supported. diff --git a/app/dts/bindings/animations/zmk,animation-ripple.yaml b/app/dts/bindings/animations/zmk,animation-ripple.yaml new file mode 100644 index 00000000..df1a7eea --- /dev/null +++ b/app/dts/bindings/animations/zmk,animation-ripple.yaml @@ -0,0 +1,41 @@ +# Copyright (c) 2020, The ZMK Contributors +# SPDX-License-Identifier: MIT + +description: | + Animated 'ripples' trigered by key presses. + +compatible: "zmk,animation-ripple" + +include: animation_base.yaml + +properties: + duration: + type: int + default: 1000 + description: | + Approximate ripple travel time in milliseconds. + + color: + type: int + required: true + description: | + Ripple color in HSL format. + + buffer-size: + type: int + default: 10 + description: | + This will limit how many keystroke events the animation is able to track + at the same time. Depending on how fast you type and the animation duration + you might need to increase this number. + + ripple-width: + type: int + default: 25 + description: | + This setting determines the thickness of the ripple 'ring'. + Think of it as antialiasing. The further apart the pixels are, or if they're + spaced irregularly, the larger this number should be. Otherwise the animation + will look uneven or LEDs might not light up at all. + The effect is especially pronounced when lowering the effect duration or running + low FPS. diff --git a/app/dts/bindings/animations/zmk,animation-solid.yaml b/app/dts/bindings/animations/zmk,animation-solid.yaml new file mode 100644 index 00000000..5cca9f27 --- /dev/null +++ b/app/dts/bindings/animations/zmk,animation-solid.yaml @@ -0,0 +1,24 @@ +# Copyright (c) 2020, The ZMK Contributors +# SPDX-License-Identifier: MIT + +description: | + Animation for displaying solid colors across the entire area + +compatible: "zmk,animation-solid" + +include: animation_base.yaml + +properties: + duration: + type: int + default: 5 + description: | + Animation duration in seconds. + This is the time it takes for the animation to complete a full cycle and return to the original color. + Ignored if only a single color is given. + + colors: + type: array + required: true + description: | + The colors to cycle through during the animation in HSL format. diff --git a/app/dts/bindings/behaviors/zmk,behavior-animation.yaml b/app/dts/bindings/behaviors/zmk,behavior-animation.yaml new file mode 100644 index 00000000..0ec36fcc --- /dev/null +++ b/app/dts/bindings/behaviors/zmk,behavior-animation.yaml @@ -0,0 +1,8 @@ +# Copyright (c) 2020 The ZMK Contributors +# SPDX-License-Identifier: MIT + +description: Animation Action + +compatible: "zmk,behavior-animation" + +include: one_param.yaml diff --git a/app/dts/bindings/zmk,animation-pixel.yaml b/app/dts/bindings/zmk,animation-pixel.yaml new file mode 100644 index 00000000..2b3b5ebc --- /dev/null +++ b/app/dts/bindings/zmk,animation-pixel.yaml @@ -0,0 +1,10 @@ +# Copyright (c) 2020, The ZMK Contributors +# SPDX-License-Identifier: MIT + +description: Pixel configuration + +compatible: "zmk,animation-pixel" + +pixel-cells: + - position_x + - position_y diff --git a/app/dts/bindings/zmk,animation.yaml b/app/dts/bindings/zmk,animation.yaml new file mode 100644 index 00000000..1f82f7b4 --- /dev/null +++ b/app/dts/bindings/zmk,animation.yaml @@ -0,0 +1,36 @@ +# Copyright (c) 2020, The ZMK Contributors +# SPDX-License-Identifier: MIT + +description: Global animation configuration + +compatible: "zmk,animation" + +properties: + drivers: + type: phandles + required: true + description: | + This array should contain all driver devices responsible for illuminating animated LEDs. + The devices must implement Zephyr's LED Strip Interface. + + chain-lengths: + type: array + required: true + description: | + This field contains the number of LEDs controlled by each driver device. + + pixels: + type: phandle-array + required: true + description: | + This field contains the pixel configuration for the entire board. + The order of this array determines in what order pixels are sent to the driver device API. + If multiple driving devices are used, their chain-length property determines the size of the buffer for each device. + + key-pixels: + type: array + description: | + Use this field to specify the pixel index corresponding to each key + following the order used in your keymap. + When left unspecified, the driver assumes that for every key, the pixel has a matching id. + So for N keys, the first N pixels are exactly in the same order as keys in your keymap. diff --git a/app/include/drivers/animation.h b/app/include/drivers/animation.h new file mode 100644 index 00000000..e0ee303b --- /dev/null +++ b/app/include/drivers/animation.h @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2020 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#pragma once + +#include +#include +#include + +#include + +/** + * @file + * #brief Public API for controlling animations. + * + * This library abstracts the implementation details + * for various types of 2D animations. + */ + +struct animation_pixel { + const uint8_t position_x; + const uint8_t position_y; + + struct zmk_color_rgb value; +}; + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @typedef animation_start + * @brief Callback API for starting an animation. + * + * @see animation_start() for argument descriptions. + */ +typedef void (*animation_api_start)(const struct device *dev); + +/** + * @typedef animation_stop + * @brief Callback API for stopping an animation. + * + * @see animation_stop() for argument descriptions. + */ +typedef void (*animation_api_stop)(const struct device *dev); + +/** + * @typedef animation_render_frame + * @brief Callback API for generating the next animation frame. + * + * @see animation_render_frame() for argument descriptions. + */ +typedef void (*animation_api_render_frame)(const struct device *dev, struct animation_pixel *pixels, + size_t num_pixels); + +struct animation_api { + animation_api_start on_start; + animation_api_stop on_stop; + animation_api_render_frame render_frame; +}; + +static inline void animation_start(const struct device *dev) { + const struct animation_api *api = (const struct animation_api *)dev->api; + + return api->on_start(dev); +} + +static inline void animation_stop(const struct device *dev) { + const struct animation_api *api = (const struct animation_api *)dev->api; + + return api->on_stop(dev); +} + +static inline void animation_render_frame(const struct device *dev, struct animation_pixel *pixels, + size_t num_pixels) { + const struct animation_api *api = (const struct animation_api *)dev->api; + + return api->render_frame(dev, pixels, num_pixels); +} + +#ifdef __cplusplus +} +#endif diff --git a/app/include/dt-bindings/zmk/animation.h b/app/include/dt-bindings/zmk/animation.h new file mode 100644 index 00000000..d27162dd --- /dev/null +++ b/app/include/dt-bindings/zmk/animation.h @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2020 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +/** + * Maps HSL color settings into a single uint32_t value + * that can be cast to zmk_color_hsl. + */ +#ifdef CONFIG_BIG_ENDIAN +#define HSL(h, s, l) ((h << 16) + (s << 8) + l) +#else +#define HSL(h, s, l) (h + (s << 16) + (l << 24)) +#endif + +/** + * Animation blending modes + */ +#define BLENDING_MODE_NORMAL 0 +#define BLENDING_MODE_MULTIPLY 1 +#define BLENDING_MODE_LIGHTEN 2 +#define BLENDING_MODE_DARKEN 3 +#define BLENDING_MODE_SCREEN 4 +#define BLENDING_MODE_SUBTRACT 5 + +/** + * Animation control commands + */ +#define ANIMATION_CMD_TOGGLE 0 +#define ANIMATION_CMD_NEXT 1 +#define ANIMATION_CMD_PREVIOUS 2 +#define ANIMATION_CMD_SELECT 3 +#define ANIMATION_CMD_BRIGHTEN 4 +#define ANIMATION_CMD_DIM 5 +#define ANIMATION_CMD_NEXT_CONTROL_ZONE 6 +#define ANIMATION_CMD_PREVIOUS_CONTROL_ZONE 7 + +/** + * Generic animation command macro + */ +#define ANIMATION_CONTROL_CMD(command, zone, param) ((zone << 24) | (command << 16) | (param)) + +/** + * Animation behavior helpers + */ +#define ANIMATION_TOGGLE(zone) ANIMATION_CONTROL_CMD(ANIMATION_CMD_TOGGLE, zone, 0) +#define ANIMATION_NEXT(zone) ANIMATION_CONTROL_CMD(ANIMATION_CMD_NEXT, zone, 0) +#define ANIMATION_PREVIOUS(zone) ANIMATION_CONTROL_CMD(ANIMATION_CMD_PREVIOUS, zone, 0) +#define ANIMATION_SELECT(zone, target_animation) \ + ANIMATION_CONTROL_CMD(ANIMATION_CMD_SELECT, zone, target_animation) +#define ANIMATION_BRIGHTEN(zone) ANIMATION_CONTROL_CMD(ANIMATION_CMD_BRIGHTEN, zone, 0) +#define ANIMATION_DIM(zone) ANIMATION_CONTROL_CMD(ANIMATION_CMD_DIM, zone, 0) +#define ANIMATION_NEXT_CONTROL_ZONE ANIMATION_CONTROL_CMD(ANIMATION_CMD_NEXT_CONTROL_ZONE, 0, 0) +#define ANIMATION_PREVIOUS_CONTROL_ZONE \ + ANIMATION_CONTROL_CMD(ANIMATION_CMD_PREVIOUS_CONTROL_ZONE, 0, 0) diff --git a/app/include/dt-bindings/zmk/animation_compose.h b/app/include/dt-bindings/zmk/animation_compose.h new file mode 100644 index 00000000..7030efec --- /dev/null +++ b/app/include/dt-bindings/zmk/animation_compose.h @@ -0,0 +1,12 @@ +/* + * Copyright (c) 2020 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#define BLENDING_MODE_NORMAL 0 +#define BLENDING_MODE_MULTIPLY 1 +#define BLENDING_MODE_LIGHTEN 2 +#define BLENDING_MODE_DARKEN 3 +#define BLENDING_MODE_SCREEN 4 +#define BLENDING_MODE_SUBTRACT 5 diff --git a/app/include/zmk/animation.h b/app/include/zmk/animation.h new file mode 100644 index 00000000..add3e5f5 --- /dev/null +++ b/app/include/zmk/animation.h @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2021 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#pragma once + +#include +#include + +#define ZMK_ANIMATION_BLENDING_MODE_NORMAL 0 +#define ZMK_ANIMATION_BLENDING_MODE_MULTIPLY 1 +#define ZMK_ANIMATION_BLENDING_MODE_LIGHTEN 2 +#define ZMK_ANIMATION_BLENDING_MODE_DARKEN 3 +#define ZMK_ANIMATION_BLENDING_MODE_SCREEN 4 +#define ZMK_ANIMATION_BLENDING_MODE_SUBTRACT 5 + +struct zmk_color_rgb { + float r; + float g; + float b; +}; + +struct zmk_color_hsl { + uint16_t h; + uint8_t s; + uint8_t l; +}; + +#if DT_NODE_HAS_PROP(DT_INST(0, animation), key_position) +size_t zmk_animation_get_pixel_by_key_position(size_t key_position); +#else +static inline size_t zmk_animation_get_pixel_by_key_position(size_t key_position) { + return key_position; +} +#endif + +#if defined(CONFIG_ZMK_ANIMATION_PIXEL_DISTANCE) && (CONFIG_ZMK_ANIMATION_PIXEL_DISTANCE == 1) +uint8_t zmk_animation_get_pixel_distance(size_t pixel_idx, size_t other_pixel_idx); +#endif + +/** + * Converts color from HSL to RGB. + * + * @param hsl Color to convert + * @param rgb Converted color + */ +void zmk_hsl_to_rgb(const struct zmk_color_hsl *hsl, struct zmk_color_rgb *rgb); + +/** + * Converts the internal RGB representation into a led_rgb struct + * for use with led_strip drivers. + * + * @param rgb Color to convert + * @param led Converted color + */ +void zmk_rgb_to_led_rgb(const struct zmk_color_rgb *rgb, struct led_rgb *led); + +/** + * Returns true if two HSL colors are the same. + * + * @param a HSL color to compare + * @param b HSL color to compare + * @return True when colors share the same values + */ +bool zmk_cmp_hsl(const struct zmk_color_hsl *a, const struct zmk_color_hsl *b); + +/** + * Perform linear interpolation between HSL values of two colors + * at a given distance (step) and store the resulting value in the given pointer. + * + * @param from HSL color to interpolate + * @param to HSL color to interpolate + * @param result Resulting HSL color + * @param step Interpolation step + */ +void zmk_interpolate_hsl(const struct zmk_color_hsl *from, const struct zmk_color_hsl *to, + struct zmk_color_hsl *result, float step); + +void zmk_animation_request_frames(uint32_t frames); + +struct zmk_color_rgb __zmk_apply_blending_mode(struct zmk_color_rgb base_value, + struct zmk_color_rgb blend_value, uint8_t mode); + +static inline struct zmk_color_rgb zmk_apply_blending_mode(struct zmk_color_rgb base_value, + struct zmk_color_rgb blend_value, + uint8_t mode) { + if (mode == ZMK_ANIMATION_BLENDING_MODE_NORMAL) { + return blend_value; + } + + return __zmk_apply_blending_mode(base_value, blend_value, mode); +} diff --git a/app/include/zmk/animation/animation_control.h b/app/include/zmk/animation/animation_control.h new file mode 100644 index 00000000..a21b4d77 --- /dev/null +++ b/app/include/zmk/animation/animation_control.h @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2020 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#pragma once + +#include +#include + +/** + * Animation control commands + */ +#define ANIMATION_CMD_TOGGLE 0 +#define ANIMATION_CMD_NEXT 1 +#define ANIMATION_CMD_PREVIOUS 2 +#define ANIMATION_CMD_SELECT 3 +#define ANIMATION_CMD_BRIGHTEN 4 +#define ANIMATION_CMD_DIM 5 +#define ANIMATION_CMD_NEXT_CONTROL_ZONE 6 +#define ANIMATION_CMD_PREVIOUS_CONTROL_ZONE 7 + +int animation_control_handle_command(const struct device *dev, uint8_t command, uint8_t param); \ No newline at end of file diff --git a/app/src/animation/animation.c b/app/src/animation/animation.c new file mode 100644 index 00000000..7aed1000 --- /dev/null +++ b/app/src/animation/animation.c @@ -0,0 +1,200 @@ +/* + * Copyright (c) 2020 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#define DT_DRV_COMPAT zmk_animation + +#include +#include + +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +#define PHANDLE_TO_DEVICE(node_id, prop, idx) DEVICE_DT_GET(DT_PHANDLE_BY_IDX(node_id, prop, idx)), + +#define PHANDLE_TO_PIXEL(node_id, prop, idx) \ + { \ + .position_x = DT_PHA_BY_IDX(node_id, prop, idx, position_x), \ + .position_y = DT_PHA_BY_IDX(node_id, prop, idx, position_y), \ + }, + +/** + * LED Driver device pointers. + */ +static const struct device *drivers[] = {DT_INST_FOREACH_PROP_ELEM(0, drivers, PHANDLE_TO_DEVICE)}; + +/** + * Size of the LED driver device pointers array. + */ +static const size_t drivers_size = DT_INST_PROP_LEN(0, drivers); + +/** + * Array containing the number of LEDs handled by each device. + */ +static const size_t pixels_per_driver[] = DT_INST_PROP(0, chain_lengths); + +/** + * Pointer to the root animation + */ +static const struct device *animation_root = DEVICE_DT_GET(DT_CHOSEN(zmk_animation)); + +/** + * Pixel configuration. + */ +static struct animation_pixel pixels[] = {DT_INST_FOREACH_PROP_ELEM(0, pixels, PHANDLE_TO_PIXEL)}; + +/** + * Size of the pixels array. + */ +static const size_t pixels_size = DT_INST_PROP_LEN(0, pixels); + +/** + * Buffer for RGB values ready to be sent to the drivers. + */ +static struct led_rgb px_buffer[DT_INST_PROP_LEN(0, pixels)]; + +/** + * Counter for animation frames that have been requested but have yet to be executed. + */ +static uint32_t animation_timer_countdown = 0; + +/** + * Conditional implementation of zmk_animation_get_pixel_by_key_position + * if key-pixels is set. + */ +#if DT_INST_NODE_HAS_PROP(0, key_position) +static const uint8_t pixels_by_key_position[] = DT_INST_PROP(0, key_pixels); + +size_t zmk_animation_get_pixel_by_key_position(size_t key_position) { + return pixels_by_key_position[key_position]; +} +#endif + +#if defined(CONFIG_ZMK_ANIMATION_PIXEL_DISTANCE) && (CONFIG_ZMK_ANIMATION_PIXEL_DISTANCE == 1) + +/** + * Lookup table for distance between any two pixels. + * + * The values are stored as a triangular matrix which cuts the space requirement roughly in half. + */ +static uint8_t + pixel_distance[((DT_INST_PROP_LEN(0, pixels) + 1) * DT_INST_PROP_LEN(0, pixels)) / 2]; + +uint8_t zmk_animation_get_pixel_distance(size_t pixel_idx, size_t other_pixel_idx) { + if (pixel_idx < other_pixel_idx) { + return zmk_animation_get_pixel_distance(other_pixel_idx, pixel_idx); + } + + return pixel_distance[(((pixel_idx + 1) * pixel_idx) >> 1) + other_pixel_idx]; +} + +#endif + +static void zmk_animation_tick(struct k_work *work) { + LOG_DBG("Animation tick"); + animation_render_frame(animation_root, &pixels[0], pixels_size); + + for (size_t i = 0; i < pixels_size; ++i) { + zmk_rgb_to_led_rgb(&pixels[i].value, &px_buffer[i]); + + // Reset values for the next cycle + pixels[i].value.r = 0; + pixels[i].value.g = 0; + pixels[i].value.b = 0; + } + + size_t pixels_updated = 0; + + for (size_t i = 0; i < drivers_size; ++i) { + led_strip_update_rgb(drivers[i], &px_buffer[pixels_updated], pixels_per_driver[i]); + + pixels_updated += pixels_per_driver[i]; + } +} + +K_WORK_DEFINE(animation_work, zmk_animation_tick); + +static void zmk_animation_tick_handler(struct k_timer *timer) { + if (--animation_timer_countdown == 0) { + k_timer_stop(timer); + } + + k_work_submit(&animation_work); +} + +K_TIMER_DEFINE(animation_tick, zmk_animation_tick_handler, NULL); + +void zmk_animation_request_frames(uint32_t frames) { + if (frames <= animation_timer_countdown) { + return; + } + + if (animation_timer_countdown == 0) { + k_timer_start(&animation_tick, K_MSEC(1000 / CONFIG_ZMK_ANIMATION_FPS), + K_MSEC(1000 / CONFIG_ZMK_ANIMATION_FPS)); + } + + animation_timer_countdown = frames; +} + +static int zmk_animation_on_activity_state_changed(const zmk_event_t *event) { + const struct zmk_activity_state_changed *activity_state_event; + + if ((activity_state_event = as_zmk_activity_state_changed(event)) == NULL) { + // Event not supported. + return -ENOTSUP; + } + + switch (activity_state_event->state) { + case ZMK_ACTIVITY_ACTIVE: + animation_start(animation_root); + return 0; + case ZMK_ACTIVITY_SLEEP: + animation_stop(animation_root); + k_timer_stop(&animation_tick); + animation_timer_countdown = 0; + return 0; + default: + return 0; + } +} + +static int zmk_animation_init(const struct device *dev) { +#if defined(CONFIG_ZMK_ANIMATION_PIXEL_DISTANCE) && (CONFIG_ZMK_ANIMATION_PIXEL_DISTANCE == 1) + // Prefill the pixel distance lookup table + int k = 0; + for (size_t i = 0; i < pixels_size; ++i) { + for (size_t j = 0; j <= i; ++j) { + // Distances are normalized to fit inside 0-255 range to fit inside uint8_t + // for better space efficiency + pixel_distance[k++] = sqrt(pow(pixels[i].position_x - pixels[j].position_x, 2) + + pow(pixels[i].position_y - pixels[j].position_y, 2)) * + 255 / 360; + } + } +#endif + + LOG_INF("ZMK Animation Ready"); + + animation_start(animation_root); + + return 0; +} + +SYS_INIT(zmk_animation_init, APPLICATION, CONFIG_APPLICATION_INIT_PRIORITY); + +ZMK_LISTENER(amk_animation, zmk_animation_on_activity_state_changed); +ZMK_SUBSCRIPTION(amk_animation, zmk_activity_state_changed); diff --git a/app/src/animation/animation_compose.c b/app/src/animation/animation_compose.c new file mode 100644 index 00000000..79db7c0e --- /dev/null +++ b/app/src/animation/animation_compose.c @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2020 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#define DT_DRV_COMPAT zmk_animation_compose + +#include +#include +#include + +#include + +#include + +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +#define PHANDLE_TO_DEVICE(node_id, prop, idx) DEVICE_DT_GET(DT_PHANDLE_BY_IDX(node_id, prop, idx)), + +struct animation_compose_config { + const struct device **animations; + const size_t animations_size; +}; + +static void animation_compose_render_frame(const struct device *dev, struct animation_pixel *pixels, + size_t num_pixels) { + const struct animation_compose_config *config = dev->config; + + for (size_t i = 0; i < config->animations_size; ++i) { + animation_render_frame(config->animations[i], pixels, num_pixels); + } +} + +static void animation_compose_start(const struct device *dev) { + const struct animation_compose_config *config = dev->config; + + for (size_t i = 0; i < config->animations_size; ++i) { + animation_start(config->animations[i]); + } +} + +static void animation_compose_stop(const struct device *dev) { + const struct animation_compose_config *config = dev->config; + + for (size_t i = 0; i < config->animations_size; ++i) { + animation_stop(config->animations[i]); + } +} + +static int animation_compose_init(const struct device *dev) { return 0; } + +static const struct animation_api animation_compose_api = { + .on_start = animation_compose_start, + .on_stop = animation_compose_stop, + .render_frame = animation_compose_render_frame, +}; + +#define ANIMATION_COMPOSE_DEVICE(idx) \ + \ + static const struct device *animation_compose_##idx##_animations[] = { \ + DT_INST_FOREACH_PROP_ELEM(idx, animations, PHANDLE_TO_DEVICE)}; \ + \ + static struct animation_compose_config animation_compose_##idx##_config = { \ + .animations = animation_compose_##idx##_animations, \ + .animations_size = DT_INST_PROP_LEN(idx, animations), \ + }; \ + \ + DEVICE_DT_INST_DEFINE(idx, &animation_compose_init, NULL, NULL, \ + &animation_compose_##idx##_config, POST_KERNEL, \ + CONFIG_APPLICATION_INIT_PRIORITY, &animation_compose_api); + +DT_INST_FOREACH_STATUS_OKAY(ANIMATION_COMPOSE_DEVICE); diff --git a/app/src/animation/animation_control.c b/app/src/animation/animation_control.c new file mode 100644 index 00000000..6e298ab3 --- /dev/null +++ b/app/src/animation/animation_control.c @@ -0,0 +1,264 @@ +/* + * Copyright (c) 2020 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#define DT_DRV_COMPAT zmk_animation_control + +#include + +#include +#include +#include +#include + +#include + +#include +#include + +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +#define PHANDLE_TO_DEVICE(node_id, prop, idx) DEVICE_DT_GET(DT_PHANDLE_BY_IDX(node_id, prop, idx)), + +struct animation_control_work_context { + const struct device *animation; + struct k_work_delayable save_work; +}; + +struct animation_control_config { + const struct device **animations; + const size_t animations_size; + const uint8_t brightness_steps; + struct animation_control_work_context *work; + struct settings_handler *settings_handler; +}; + +struct animation_control_data { + bool active; + uint8_t brightness; + size_t current_animation; +}; + +static int animation_control_load_settings(const struct device *dev, const char *name, size_t len, + settings_read_cb read_cb, void *cb_arg) { +#if IS_ENABLED(CONFIG_SETTINGS) + const char *next; + int rc; + + if (settings_name_steq(name, "state", &next) && !next) { + if (len != sizeof(struct animation_control_data)) { + return -EINVAL; + } + + rc = read_cb(cb_arg, dev->data, sizeof(struct animation_control_data)); + if (rc >= 0) { + return 0; + } + + return rc; + } + + return -ENOENT; +#else + return 0; +#endif /* IS_ENABLED(CONFIG_SETTINGS) */ +} + +#if IS_ENABLED(CONFIG_SETTINGS) +static void animation_control_save_work(struct k_work *work) { + struct animation_control_work_context *ctx = + CONTAINER_OF(work, struct animation_control_work_context, save_work); + const struct device *dev = ctx->animation; + + char path[40]; + snprintf(path, 40, "%s/state", dev->name); + + settings_save_one(path, dev->data, sizeof(struct animation_control_data)); +}; + +static int animation_control_save_settings(const struct device *dev) { + const struct animation_control_config *config = dev->config; + struct animation_control_work_context *ctx = config->work; + + k_work_cancel_delayable(&ctx->save_work); + + return k_work_reschedule(&ctx->save_work, K_MSEC(CONFIG_ZMK_SETTINGS_SAVE_DEBOUNCE)); +} +#endif /* IS_ENABLED(CONFIG_SETTINGS) */ + +int animation_control_handle_command(const struct device *dev, uint8_t command, uint8_t param) { + const struct animation_control_config *config = dev->config; + struct animation_control_data *data = dev->data; + + switch (command) { + case ANIMATION_CMD_TOGGLE: + data->active = !data->active; + + if (data->active) { + animation_start(config->animations[data->current_animation]); + break; + } + + animation_stop(config->animations[data->current_animation]); + break; + case ANIMATION_CMD_NEXT: + data->current_animation++; + + if (data->current_animation == config->animations_size) { + data->current_animation = 0; + } + break; + case ANIMATION_CMD_PREVIOUS: + if (data->current_animation == 0) { + data->current_animation = config->animations_size; + } + + data->current_animation--; + break; + case ANIMATION_CMD_SELECT: + if (config->animations_size < param) { + return -ENOTSUP; + } + + data->current_animation = param; + break; + case ANIMATION_CMD_DIM: + if (data->brightness == 0) { + return 0; + } + + data->brightness--; + + if (data->brightness == 0) { + animation_stop(config->animations[data->current_animation]); + } + break; + case ANIMATION_CMD_BRIGHTEN: + if (data->brightness == config->brightness_steps) { + return 0; + } + + if (data->brightness == 0) { + animation_start(config->animations[data->current_animation]); + } + + data->brightness++; + break; + } + +#if IS_ENABLED(CONFIG_SETTINGS) + animation_control_save_settings(dev); +#endif /* IS_ENABLED(CONFIG_SETTINGS) */ + + // Force refresh + zmk_animation_request_frames(1); + + return 0; +} + +static void animation_control_render_frame(const struct device *dev, struct animation_pixel *pixels, + size_t num_pixels) { + const struct animation_control_config *config = dev->config; + const struct animation_control_data *data = dev->data; + + if (!data->active) { + return; + } + + animation_render_frame(config->animations[data->current_animation], pixels, num_pixels); + + if (data->brightness == config->brightness_steps) { + return; + } + + float brightness = (float)data->brightness / (float)config->brightness_steps; + + for (size_t i = 0; i < num_pixels; ++i) { + pixels[i].value.r *= brightness; + pixels[i].value.g *= brightness; + pixels[i].value.b *= brightness; + } +} + +static void animation_control_start(const struct device *dev) { + const struct animation_control_config *config = dev->config; + const struct animation_control_data *data = dev->data; + + if (!data->active) { + return; + } + + animation_start(config->animations[data->current_animation]); +} + +static void animation_control_stop(const struct device *dev) { + const struct animation_control_config *config = dev->config; + const struct animation_control_data *data = dev->data; + + animation_stop(config->animations[data->current_animation]); +} + +static int animation_control_init(const struct device *dev) { +#if IS_ENABLED(CONFIG_SETTINGS) + const struct animation_control_config *config = dev->config; + + settings_subsys_init(); + + settings_register(config->settings_handler); + + k_work_init_delayable(&config->work->save_work, animation_control_save_work); + + settings_load_subtree(dev->name); +#endif /* IS_ENABLED(CONFIG_SETTINGS) */ + + return 0; +} + +static const struct animation_api animation_control_api = { + .on_start = animation_control_start, + .on_stop = animation_control_stop, + .render_frame = animation_control_render_frame, +}; + +#define ANIMATION_CONTROL_DEVICE(idx) \ + \ + static const struct device *animation_control_##idx##_animations[] = { \ + DT_INST_FOREACH_PROP_ELEM(idx, animations, PHANDLE_TO_DEVICE)}; \ + \ + static struct animation_control_work_context animation_control_##idx##_work = { \ + .animation = DEVICE_DT_GET(DT_DRV_INST(idx)), \ + }; \ + \ + static int animation_control_##idx##_load_settings(const char *name, size_t len, \ + settings_read_cb read_cb, void *cb_arg) { \ + const struct device *dev = DEVICE_DT_GET(DT_DRV_INST(idx)); \ + \ + return animation_control_load_settings(dev, name, len, read_cb, cb_arg); \ + } \ + \ + static struct settings_handler animation_control_##idx##_settings_handler = { \ + .name = DT_INST_PROP(idx, label), \ + .h_set = animation_control_##idx##_load_settings, \ + }; \ + \ + static const struct animation_control_config animation_control_##idx##_config = { \ + .animations = animation_control_##idx##_animations, \ + .animations_size = DT_INST_PROP_LEN(idx, animations), \ + .brightness_steps = DT_INST_PROP(idx, brightness_steps) - 1, \ + .work = &animation_control_##idx##_work, \ + .settings_handler = &animation_control_##idx##_settings_handler, \ + }; \ + \ + static struct animation_control_data animation_control_##idx##_data = { \ + .active = true, \ + .brightness = DT_INST_PROP(idx, brightness_steps) - 1, \ + .current_animation = 0, \ + }; \ + \ + DEVICE_DT_INST_DEFINE(idx, &animation_control_init, NULL, &animation_control_##idx##_data, \ + &animation_control_##idx##_config, POST_KERNEL, \ + CONFIG_APPLICATION_INIT_PRIORITY, &animation_control_api); + +DT_INST_FOREACH_STATUS_OKAY(ANIMATION_CONTROL_DEVICE); diff --git a/app/src/animation/animation_ripple.c b/app/src/animation/animation_ripple.c new file mode 100644 index 00000000..7704d90e --- /dev/null +++ b/app/src/animation/animation_ripple.c @@ -0,0 +1,209 @@ +/* + * Copyright (c) 2020 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#define DT_DRV_COMPAT zmk_animation_ripple + +#include +#include + +#include +#include +#include + +#include + +#include +#include +#include + +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +struct animation_ripple_event { + size_t pixel_id; + uint16_t distance; + uint8_t counter; +}; + +struct animation_ripple_config { + struct zmk_color_hsl *color_hsl; + size_t *pixel_map; + size_t pixel_map_size; + size_t event_buffer_size; + uint8_t blending_mode; + uint8_t distance_per_frame; + uint8_t ripple_width; + uint8_t event_frames; +}; + +struct animation_ripple_data { + struct zmk_color_rgb color_rgb; + struct animation_ripple_event *event_buffer; + size_t events_start; + size_t events_end; + size_t num_events; + bool is_active; +}; + +static int animation_ripple_on_key_press(const struct device *dev, const zmk_event_t *event) { + const struct animation_ripple_config *config = dev->config; + struct animation_ripple_data *data = dev->data; + + const struct zmk_position_state_changed *pos_event; + + if (!data->is_active) { + return 0; + } + + if ((pos_event = as_zmk_position_state_changed(event)) == NULL) { + // Event not supported. + return -ENOTSUP; + } + + if (!pos_event->state) { + // Don't track key releases. + return 0; + } + + if (data->num_events == config->event_buffer_size) { + // Event buffer is full - new key press events are dropped. + return -ENOMEM; + } + + data->event_buffer[data->events_end].pixel_id = + zmk_animation_get_pixel_by_key_position(pos_event->position); + data->event_buffer[data->events_end].distance = 0; + data->event_buffer[data->events_end].counter = 0; + + data->events_end = (data->events_end + 1) % config->event_buffer_size; + data->num_events += 1; + + zmk_animation_request_frames(config->event_frames); + + return 0; +} + +static void animation_ripple_render_frame(const struct device *dev, struct animation_pixel *pixels, + size_t num_pixels) { + const struct animation_ripple_config *config = dev->config; + struct animation_ripple_data *data = dev->data; + + size_t *pixel_map = config->pixel_map; + + size_t i = data->events_start; + + while (i != data->events_end) { + struct animation_ripple_event *event = &data->event_buffer[i]; + + // Render all pixels for each event + for (int j = 0; j < config->pixel_map_size; ++j) { + uint8_t pixel_distance = + zmk_animation_get_pixel_distance(event->pixel_id, pixel_map[j]); + + if (config->ripple_width > abs(pixel_distance - event->distance)) { + float intensity = 1.0f - (float)abs(pixel_distance - event->distance) / + (float)config->ripple_width; + + struct zmk_color_rgb color = { + .r = intensity * data->color_rgb.r, + .g = intensity * data->color_rgb.g, + .b = intensity * data->color_rgb.b, + }; + + pixels[pixel_map[j]].value = zmk_apply_blending_mode(pixels[pixel_map[j]].value, + color, config->blending_mode); + } + } + + // Update event counter + if (event->counter < config->event_frames) { + event->distance += config->distance_per_frame; + event->counter += 1; + } else { + data->events_start = (data->events_start + 1) % config->event_buffer_size; + data->num_events -= 1; + } + + if (++i == config->event_buffer_size) { + i = 0; + } + } +} + +static void animation_ripple_start(const struct device *dev) { + struct animation_ripple_data *data = dev->data; + + data->is_active = true; +} + +static void animation_ripple_stop(const struct device *dev) { + struct animation_ripple_data *data = dev->data; + + data->is_active = false; + + // Cancel processing of any ongoing events. + data->num_events = 0; + data->events_start = 0; + data->events_end = 0; +} + +static int animation_ripple_init(const struct device *dev) { + const struct animation_ripple_config *config = dev->config; + struct animation_ripple_data *data = dev->data; + + zmk_hsl_to_rgb(config->color_hsl, &data->color_rgb); + + return 0; +} + +static const struct animation_api animation_ripple_api = { + .on_start = animation_ripple_start, + .on_stop = animation_ripple_stop, + .render_frame = animation_ripple_render_frame, +}; + +#define ANIMATION_RIPPLE_DEVICE(idx) \ + \ + static struct animation_ripple_event \ + animation_ripple_##idx##_events[DT_INST_PROP(idx, buffer_size)]; \ + \ + static struct animation_ripple_data animation_ripple_##idx##_data = { \ + .event_buffer = animation_ripple_##idx##_events, \ + .events_start = 0, \ + .events_end = 0, \ + .num_events = 0, \ + }; \ + \ + static size_t animation_ripple_##idx##_pixel_map[] = DT_INST_PROP(idx, pixels); \ + \ + static uint32_t animation_ripple_##idx##_color = DT_INST_PROP(idx, color); \ + \ + static struct animation_ripple_config animation_ripple_##idx##_config = { \ + .color_hsl = (struct zmk_color_hsl *)&animation_ripple_##idx##_color, \ + .pixel_map = &animation_ripple_##idx##_pixel_map[0], \ + .pixel_map_size = DT_INST_PROP_LEN(idx, pixels), \ + .event_buffer_size = DT_INST_PROP(idx, buffer_size), \ + .blending_mode = DT_INST_ENUM_IDX(idx, blending_mode), \ + .distance_per_frame = \ + (255 * 1000 / DT_INST_PROP(idx, duration)) / CONFIG_ZMK_ANIMATION_FPS, \ + .ripple_width = DT_INST_PROP(idx, ripple_width) / 2, \ + .event_frames = \ + 255 / ((255 * 1000 / DT_INST_PROP(idx, duration)) / CONFIG_ZMK_ANIMATION_FPS), \ + }; \ + \ + DEVICE_DT_INST_DEFINE(idx, &animation_ripple_init, NULL, &animation_ripple_##idx##_data, \ + &animation_ripple_##idx##_config, POST_KERNEL, \ + CONFIG_APPLICATION_INIT_PRIORITY, &animation_ripple_api); \ + \ + static int animation_ripple_##idx##_event_handler(const zmk_event_t *event) { \ + const struct device *dev = DEVICE_DT_GET(DT_DRV_INST(idx)); \ + \ + return animation_ripple_on_key_press(dev, event); \ + } \ + \ + ZMK_LISTENER(animation_ripple_##idx, animation_ripple_##idx##_event_handler); \ + ZMK_SUBSCRIPTION(animation_ripple_##idx, zmk_position_state_changed); + +DT_INST_FOREACH_STATUS_OKAY(ANIMATION_RIPPLE_DEVICE); diff --git a/app/src/animation/animation_solid.c b/app/src/animation/animation_solid.c new file mode 100644 index 00000000..02017b43 --- /dev/null +++ b/app/src/animation/animation_solid.c @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2020 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#define DT_DRV_COMPAT zmk_animation_solid + +#include +#include +#include + +#include + +#include + +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +struct animation_solid_config { + size_t *pixel_map; + size_t pixel_map_size; + struct zmk_color_hsl *colors; + uint8_t num_colors; + uint16_t duration; + uint16_t transition_duration; +}; + +struct animation_solid_data { + uint16_t counter; + + struct zmk_color_hsl current_hsl; + struct zmk_color_rgb current_rgb; +}; + +static void animation_solid_update_color(const struct device *dev) { + const struct animation_solid_config *config = dev->config; + struct animation_solid_data *data = dev->data; + + const size_t from = data->counter / config->transition_duration; + const size_t to = (from + 1) % config->num_colors; + + struct zmk_color_hsl next_hsl; + + zmk_interpolate_hsl(&config->colors[from], &config->colors[to], &next_hsl, + (data->counter % config->transition_duration) / + (float)config->transition_duration); + + data->current_hsl = next_hsl; + zmk_hsl_to_rgb(&data->current_hsl, &data->current_rgb); + + data->counter = (data->counter + 1) % config->duration; +} + +static void animation_solid_render_frame(const struct device *dev, struct animation_pixel *pixels, + size_t num_pixels) { + const struct animation_solid_config *config = dev->config; + struct animation_solid_data *data = dev->data; + + for (size_t i = 0; i < config->pixel_map_size; ++i) { + pixels[config->pixel_map[i]].value = data->current_rgb; + } + + if (config->num_colors == 1) { + return; + } + + // Request frames on counter reset + if (data->counter == 0) { + zmk_animation_request_frames(config->duration); + } + + animation_solid_update_color(dev); +} + +static void animation_solid_start(const struct device *dev) { zmk_animation_request_frames(1); } + +static void animation_solid_stop(const struct device *dev) { + // Nothing to do. +} + +static int animation_solid_init(const struct device *dev) { + const struct animation_solid_config *config = dev->config; + struct animation_solid_data *data = dev->data; + + data->counter = 0; + data->current_hsl = config->colors[0]; + + zmk_hsl_to_rgb(&data->current_hsl, &data->current_rgb); + + return 0; +} + +static const struct animation_api animation_solid_api = { + .on_start = animation_solid_start, + .on_stop = animation_solid_stop, + .render_frame = animation_solid_render_frame, +}; + +#define ANIMATION_SOLID_DEVICE(idx) \ + \ + static struct animation_solid_data animation_solid_##idx##_data; \ + \ + static size_t animation_ripple_##idx##_pixel_map[] = DT_INST_PROP(idx, pixels); \ + \ + static uint32_t animation_solid_##idx##_colors[] = DT_INST_PROP(idx, colors); \ + \ + static struct animation_solid_config animation_solid_##idx##_config = { \ + .pixel_map = &animation_ripple_##idx##_pixel_map[0], \ + .pixel_map_size = DT_INST_PROP_LEN(idx, pixels), \ + .colors = (struct zmk_color_hsl *)animation_solid_##idx##_colors, \ + .num_colors = DT_INST_PROP_LEN(idx, colors), \ + .duration = DT_INST_PROP(idx, duration) * CONFIG_ZMK_ANIMATION_FPS, \ + .transition_duration = (DT_INST_PROP(idx, duration) * CONFIG_ZMK_ANIMATION_FPS) / \ + DT_INST_PROP_LEN(idx, colors), \ + }; \ + \ + DEVICE_DT_INST_DEFINE(idx, &animation_solid_init, NULL, &animation_solid_##idx##_data, \ + &animation_solid_##idx##_config, POST_KERNEL, \ + CONFIG_APPLICATION_INIT_PRIORITY, &animation_solid_api); + +DT_INST_FOREACH_STATUS_OKAY(ANIMATION_SOLID_DEVICE); diff --git a/app/src/animation/color.c b/app/src/animation/color.c new file mode 100644 index 00000000..55630ec1 --- /dev/null +++ b/app/src/animation/color.c @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2020 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include + +#include + +static float fmod(float a, float b) { + float mod = a < 0 ? -a : a; + float x = b < 0 ? -b : b; + + while (mod >= x) { + mod = mod - x; + } + + return a < 0 ? -mod : mod; +} + +static float fabs(float a) { return a < 0 ? -a : a; } + +/** + * HSL chosen over HSV/HSB as it shares the same parameters with LCh or HSLuv. + * The latter color spaces could be interesting to experiment with because of their + * perceptual uniformity, but it would come at the cost of some performance. + * Using the same parameters would make it easy to toggle any such behavior + * using a single config flag. + * + * Algorithm source: https://www.tlbx.app/color-converter + */ +void zmk_hsl_to_rgb(const struct zmk_color_hsl *hsl, struct zmk_color_rgb *rgb) { + float s = (float)hsl->s / 100; + float l = (float)hsl->l / 100; + + float a = (float)hsl->h / 60; + float chroma = s * (1 - fabs(2 * l - 1)); + float x = chroma * (1 - fabs(fmod(a, 2) - 1)); + float m = l - chroma / 2; + + switch ((uint8_t)a % 6) { + case 0: + rgb->r = m + chroma; + rgb->g = m + x; + rgb->b = m; + break; + case 1: + rgb->r = m + x; + rgb->g = m + chroma; + rgb->b = m; + break; + case 2: + rgb->r = m; + rgb->g = m + chroma; + rgb->b = m + x; + break; + case 3: + rgb->r = m; + rgb->g = m + x; + rgb->b = m + chroma; + break; + case 4: + rgb->r = m + x; + rgb->g = m; + rgb->b = m + chroma; + break; + case 5: + rgb->r = m + chroma; + rgb->g = m; + rgb->b = m + x; + break; + } +} + +/** + * Converts ZMKs RGB (float) to Zephyr's led_rgb (uint8_t) format. + */ +void zmk_rgb_to_led_rgb(const struct zmk_color_rgb *rgb, struct led_rgb *led) { + led->r = rgb->r * 255; + led->g = rgb->g * 255; + led->b = rgb->b * 255; +} + +/** + * Compares two HSL colors. + */ +bool zmk_cmp_hsl(const struct zmk_color_hsl *a, const struct zmk_color_hsl *b) { + return a->h == b->h && a->s == b->s && a->l == b->l; +} + +/** + * Interpolate between two colors using the cylindrical model (HSL). + */ +void zmk_interpolate_hsl(const struct zmk_color_hsl *from, const struct zmk_color_hsl *to, + struct zmk_color_hsl *result, float step) { + int16_t hue_delta; + + hue_delta = from->h - to->h; + hue_delta = hue_delta + (180 < abs(hue_delta) ? (hue_delta < 0 ? 360 : -360) : 0); + + result->h = (uint16_t)(360 + from->h - (hue_delta * step)) % 360; + result->s = from->s - (from->s - to->s) * step; + result->l = from->l - (from->l - to->l) * step; +} + +struct zmk_color_rgb __zmk_apply_blending_mode(struct zmk_color_rgb base_value, + struct zmk_color_rgb blend_value, uint8_t mode) { + switch (mode) { + case ZMK_ANIMATION_BLENDING_MODE_MULTIPLY: + base_value.r = base_value.r * blend_value.r; + base_value.g = base_value.g * blend_value.g; + base_value.b = base_value.b * blend_value.b; + break; + case ZMK_ANIMATION_BLENDING_MODE_LIGHTEN: + base_value.r = base_value.r > blend_value.r ? base_value.r : blend_value.r; + base_value.g = base_value.g > blend_value.g ? base_value.g : blend_value.g; + base_value.b = base_value.b > blend_value.b ? base_value.b : blend_value.b; + break; + case ZMK_ANIMATION_BLENDING_MODE_DARKEN: + base_value.r = base_value.r > blend_value.r ? blend_value.r : base_value.r; + base_value.g = base_value.g > blend_value.g ? blend_value.g : base_value.g; + base_value.b = base_value.b > blend_value.b ? blend_value.b : base_value.b; + break; + case ZMK_ANIMATION_BLENDING_MODE_SCREEN: + base_value.r = base_value.r + (1.0f - base_value.r) * blend_value.r; + base_value.g = base_value.g + (1.0f - base_value.g) * blend_value.g; + base_value.b = base_value.b + (1.0f - base_value.b) * blend_value.b; + break; + case ZMK_ANIMATION_BLENDING_MODE_SUBTRACT: + base_value.r = base_value.r - base_value.r * blend_value.r; + base_value.g = base_value.g - base_value.g * blend_value.g; + base_value.b = base_value.b - base_value.b * blend_value.b; + break; + } + + return base_value; +} diff --git a/app/src/behaviors/behavior_animation.c b/app/src/behaviors/behavior_animation.c new file mode 100644 index 00000000..fa086a9c --- /dev/null +++ b/app/src/behaviors/behavior_animation.c @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2020 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include +#include +#include + +#include + +#include + +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +#define DT_DRV_COMPAT zmk_animation_control + +#define DEVICE_ADDR(idx) DEVICE_DT_GET(DT_INST(idx, zmk_animation_control)), + +/** + * Control animation instance pointers. + */ +static const struct device *control_animations[] = {DT_INST_FOREACH_STATUS_OKAY(DEVICE_ADDR)}; + +/** + * Size of control animation instance pointers array. + */ +static const uint8_t control_animations_size = sizeof(control_animations); + +/** + * Index of the current default control animation instance. + */ +static uint8_t current_zone = 0; + +#define DT_DRV_COMPAT zmk_behavior_animation + +static int +on_keymap_binding_convert_central_state_dependent_params(struct zmk_behavior_binding *binding, + struct zmk_behavior_binding_event event) { + if ((binding->param1 >> 24) == 0xff) { + binding->param1 = (current_zone << 24) | (binding->param1 & 0x00ffffff); + } + + return 0; +} + +static int on_keymap_binding_pressed(struct zmk_behavior_binding *binding, + struct zmk_behavior_binding_event event) { + uint8_t value = binding->param1 & 0xff; + uint8_t command = (binding->param1 >> 16) & 0xff; + uint8_t zone = (binding->param1 >> 24) & 0xff; + + if (command == ANIMATION_CMD_NEXT_CONTROL_ZONE) { + current_zone++; + + if (current_zone == control_animations_size) { + current_zone = 0; + } + return 0; + } + + if (command == ANIMATION_CMD_PREVIOUS_CONTROL_ZONE) { + if (current_zone == 0) { + current_zone = control_animations_size; + } + + current_zone--; + return 0; + } + + if (control_animations_size <= zone) { + return -ENOTSUP; + } + + return animation_control_handle_command(control_animations[zone], command, value); +} + +static int on_keymap_binding_released(struct zmk_behavior_binding *binding, + struct zmk_behavior_binding_event event) { + return ZMK_BEHAVIOR_OPAQUE; +} + +static int behavior_animation_init(const struct device *dev) { return 0; } + +static const struct behavior_driver_api behavior_animation_driver_api = { + .binding_convert_central_state_dependent_params = + on_keymap_binding_convert_central_state_dependent_params, + .binding_pressed = on_keymap_binding_pressed, + .binding_released = on_keymap_binding_released, +}; + +DEVICE_DT_INST_DEFINE(0, behavior_animation_init, NULL, NULL, NULL, APPLICATION, + CONFIG_KERNEL_INIT_PRIORITY_DEFAULT, &behavior_animation_driver_api); diff --git a/docs/docs/behaviors/animations.md b/docs/docs/behaviors/animations.md new file mode 100644 index 00000000..93942207 --- /dev/null +++ b/docs/docs/behaviors/animations.md @@ -0,0 +1,52 @@ +--- +title: Animation Control Behavior +sidebar_label: Animation Control +--- + +## Summary + +This page contains [Animations](../features/animations.md) behaviors supported by ZMK. + +## Animation Control Defines + +RGB actions defines are provided through the `dt-bindings/zmk/animation.h` header, +which is added at the top of the keymap file: + +``` +#include +``` + +This will allow you to reference the actions defined in this header such as +`ANIMATION_TOGGLE`. + +Here is a table describing the action for each define: + +| Define | Action | +| ------------------------------------ | ------------------------------------------------------------- | +| `ANIMATION_TOGGLE` | Toggles active animation | +| `ANIMATION_NEXT` | Selects next animation within root animation node array | +| `ANIMATION_PREVIOUS` | Selects previous animation within root animation node array | +| `ANIMATION_SELECT(x)` | Selects animation within root animation node at index `x` | +| `ANIMATION_BRIGHTEN` | Increases brightness (applied after other blending) | +| `ANIMATION_DIM` | Decreases brightness (applied after other blending) | + +## Examples + +1. Toggle active animation on/off + + ``` + &animation ANIMATION_TOGGLE(0) + ``` + +2. Switch to next animation + + ``` + &animation ANIMATION_NEXT(0) + ``` + +3. Select animation at index 3 + + ``` + &animation ANIMATION_SELECT(3) + ``` + diff --git a/docs/docs/features/animations.md b/docs/docs/features/animations.md new file mode 100644 index 00000000..02628fbd --- /dev/null +++ b/docs/docs/features/animations.md @@ -0,0 +1,226 @@ +--- +title: RGB Animations +sidebar_label: RGB Animations +--- + +ZMK supports animation effects on RGB control strips via the animation +subsystem. + +## Enabling Animation Support + +Set `CONFIG_ZMK_ANIMATION=y` to enable animation support on your keyboard. +See animation configuration for instructions on how to customize animation +support. + +If your board does not have animation support configured, see +[Adding Animation Support.](#adding-animation-support) + +## Color settings + +All colors are given in [HSL](https://www.w3schools.com/colors/colors_hsl.asp) +format. The user should specify colors in this format, although they will +be written to the LED chain driver in RGB format. + +## System Design + +This subsystem is made up of 3 key components: +- The core animations layer +- Animation drivers +- Color utilites + +### Core Animation Layer + +The core animation layer is responsible for scheduling animations, kicking +off rendering of new frames, and sending frame data to the LED strip via +Zephyr's LED Strip API. + +### Animation Drivers + +Each animation effect is implemented as a driver. The animation API exposes +the following functions: + +| Function | Purpose | +| ------------------------ | ------------------------------------- | +| `animation_start` | starts rendering the animation effect | +| `animation_stop` | ends rendering of the effect | +| `animation_render_frame` | renders a frame of the animation | + +Each driver will also likely utilize `ZMK_LISTENER` to listen for keypress +events. A driver can then request the animation subsystem render frames +using `zmk_animation_request_frames`. + + +### Color Utilities + +Color utilities are used by the animation drivers to perform color conversions. +Since colors are given in HSL format, `zmk_hsl_to_rgb` is provided to +convert user color input from devicetree to RGB data before writing to an +animation output pixel. + +## Adding Animation Support + +Adding animation support to your board will require the following devicetree +changes: +- Animation core node with `zmk,animation` compatible describing + "pixels" within animation as well as LED drivers to write output pixels to +- root animation node with `zmk,animation-control` compatible to cycle though + animations +- animation driver nodes for all animations the user would like to implement + +### Animation Core Node + +This node describes all output "pixels" on your keyboard. These pixels +correspond to individual LEDs on your LED chain. The animation subsystem +will write these pixels to your LED chain driver in the order they are +declared. + +Each pixel has an X and Y coordinate, which helps the animation +subsystem determine how effects like a ripple should be rendered on your +keyboard. X and Y coordinates can range from 0-255. + +The node also needs to have references to your LED strip driver nodes within +devicetree. Multiple LED strip nodes can be provided, but each node must have +a `chain-length` property equal to the number of pixels in the LED chain. +When multiple LED strip nodes are provided, the `chain-length` of each will be +used to determine how many pixels to write. Pixels are written sequentially, so +the pixels for the second LED driver should be declared after those written +to the first one. + +:::note + +The total number of pixels defined in your `pixel` array property should match +the sum of all `chain-length` properties for the LED strip drivers you will be +using. + +::: + +For example, here is a chain of 6 pixels, using two LED drivers: +``` +animation { + compatible = "zmk,animation"; + drivers = <&led_ctrl1 &led_ctrl2>; + pixels = <&pixel 0 0>, /* Pixel 0 is at (0,0) */ + <&pixel 21 0>, /* Pixel 1 is (0, 21) */ + <&pixel 42 0>, + <&pixel 63 0>, + <&pixel 84 0>, + <&pixel 105 0>, + <&pixel 255 255>; /* Max coordinate (255, 255) */ +} +``` + +In this case, if `led_ctrl1` has a `chain-length` of 4 and `led_ctrl2` has +a `chain-length` of 2, then the first 4 pixels will be written to `led_ctrl1`, +and the final two will be written to `led_ctrl2`. + + +## Animation Root Node + +Your animation root node should be an instance of the `zmk,animation-control` +driver. This driver allows you to cycle though other animation effects using +a keybinding. + +A simple root animation node looks like this: + +``` +root_animation: animation_0 { + compatible = "zmk,animation-control"; + label = "ANIMATION_CTRL_0"; + /* List of all animations to cycle through */ + animations = <&ripple_effect &solid_color>; +}; +``` +You should set the `zmk,animation` chosen node to this root animation: + +``` +chosen { + zmk,animation = &root_animation; +}; +``` + +Finally, you should add a keybinding to use the animation control feature +within your keymap: +``` +#include + +default_layer { + /* Basic binding, toggles animation on and off */ + bindings = <&animation ANIMATION_TOGGLE(0)>; +} +``` + +## Animation Drivers + +Animation drivers allow you to create custom effects on your keyboard, +and implement different animation zones. Zone implementation is performed +using the `pixels` property, which sets the pixels from the `pixels` property +in the core animation node that the animation effect will apply to. + +### Blending Modes + +Several animations can utilize blending effects via the +`zmk_apply_blending_mode` API. The following blending modes are supported: + +| Mode | Affect | +|----------|---------------------------------------------------------------------------| +| Multiply | Multiplies each pixel's color by the new effect's pixel color | +| Lighten | Selects the highest R, G, and B values between old and new for each pixel | +| Darken | Selects the lowest R, G, and B values between old and new for each pixel | +| Screen | Adds (1 - base {R,G,B}) * new {R,G,B} value to base {R,G,B} value | + + +### Ripple Effect + +Creates a ripple effect on your keyboard each time a key is pressed, +originating at the pixel corresponding to the key. An example of +a ripple effect node is given below. + +``` +ripple_effect: animation_1 { + compatible = "zmk,animation-ripple"; + status = "okay"; + /* Will only apply the effect to the first 4 pixels */ + pixels = <0 1 2 3>; + /* Sets how this effect will be blended with other animations */ + blending-mode = ; + /* Duration of animation in ms */ + duration = <1000>; + /* Will result in a pure white */ + color = ; + ripple-width = <50>; +}; +``` + +### Solid Color + +Creates a solid color background. This effect can be combined with other +effects such as the ripple animation to produce a static background, or +zone specific lighting + +``` +solid_color: color { + compatible = "zmk,animation-solid"; + status = "okay"; + /* All pixels will be illuminated */ + pixels = <0 1 2 3 4 5 6>; + colors = ; +}; +``` + +### Compose Animation + +Composes multiple animations together, allowing you to combine animation zones +or effects. Each animation will be rendered sequentially. If an animation has +a blending mode set, then it will be blended onto the current state of the +pixels + +``` +combine_zones: animation_1 { + compatible = "zmk,animation-compose"; + status = "okay"; + animations = < + &solid_color + &ripple_effect + >; +}; +``` diff --git a/docs/sidebars.js b/docs/sidebars.js index 43f17b41..8f8da93d 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -20,6 +20,7 @@ module.exports = { "features/backlight", "features/battery", "features/beta-testing", + "features/animations", ], Behaviors: [ "behaviors/key-press", @@ -42,6 +43,7 @@ module.exports = { "behaviors/underglow", "behaviors/backlight", "behaviors/power", + "behaviors/animations", ], Codes: [ "codes/index",