This commit is contained in:
Kuba Birecki 2023-09-12 11:52:16 +03:00 committed by GitHub
commit 92d0c27c6e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 1890 additions and 1 deletions

View file

@ -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/events/battery_state_changed.c)
target_sources_ifdef(CONFIG_ZMK_BATTERY_REPORTING app PRIVATE src/battery.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) target_sources_ifdef(CONFIG_ZMK_SPLIT app PRIVATE src/events/split_peripheral_status_changed.c)
add_subdirectory(src/split) add_subdirectory(src/split)

View file

@ -305,6 +305,24 @@ config ZMK_BACKLIGHT_AUTO_OFF_USB
#ZMK_BACKLIGHT #ZMK_BACKLIGHT
endif 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 #Display/LED Options
endmenu endmenu

View file

@ -2,6 +2,7 @@
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
add_subdirectory_ifdef(CONFIG_ZMK_DRIVERS_GPIO gpio) add_subdirectory_ifdef(CONFIG_ZMK_DRIVERS_GPIO gpio)
add_subdirectory_ifdef(CONFIG_ZMK_ANIUMATION animation)
add_subdirectory(kscan) add_subdirectory(kscan)
add_subdirectory(sensor) add_subdirectory(sensor)
add_subdirectory(display) add_subdirectory(display)

13
app/dts/animation.dtsi Normal file
View file

@ -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>;
};
};

View file

@ -19,3 +19,4 @@
#include <behaviors/key_repeat.dtsi> #include <behaviors/key_repeat.dtsi>
#include <behaviors/backlight.dtsi> #include <behaviors/backlight.dtsi>
#include <behaviors/macros.dtsi> #include <behaviors/macros.dtsi>
#include <behaviors/animation.dtsi>

View file

@ -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>;
};
};
};

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

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

View file

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

View file

@ -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.

View file

@ -0,0 +1,86 @@
/*
* Copyright (c) 2020 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#pragma once
#include <zephyr/types.h>
#include <zephyr/device.h>
#include <zephyr/drivers/led_strip.h>
#include <zmk/animation.h>
/**
* @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

View file

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

View file

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

View file

@ -0,0 +1,94 @@
/*
* Copyright (c) 2021 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#pragma once
#include <zephyr/device.h>
#include <zephyr/drivers/led_strip.h>
#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);
}

View file

@ -0,0 +1,24 @@
/*
* Copyright (c) 2020 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#pragma once
#include <zephyr/device.h>
#include <zephyr/kernel.h>
/**
* 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);

View file

@ -0,0 +1,200 @@
/*
* Copyright (c) 2020 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#define DT_DRV_COMPAT zmk_animation
#include <stdlib.h>
#include <math.h>
#include <zephyr/device.h>
#include <zephyr/drivers/led_strip.h>
#include <zephyr/init.h>
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
#include <drivers/animation.h>
#include <zmk/animation.h>
#include <zmk/event_manager.h>
#include <zmk/events/activity_state_changed.h>
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);

View file

@ -0,0 +1,73 @@
/*
* Copyright (c) 2020 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#define DT_DRV_COMPAT zmk_animation_compose
#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <zephyr/logging/log.h>
#include <drivers/animation.h>
#include <zmk/animation.h>
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);

View file

@ -0,0 +1,264 @@
/*
* Copyright (c) 2020 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#define DT_DRV_COMPAT zmk_animation_control
#include <stdio.h>
#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <zephyr/logging/log.h>
#include <zephyr/settings/settings.h>
#include <drivers/animation.h>
#include <zmk/animation.h>
#include <zmk/animation/animation_control.h>
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);

View file

@ -0,0 +1,209 @@
/*
* Copyright (c) 2020 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#define DT_DRV_COMPAT zmk_animation_ripple
#include <stdlib.h>
#include <math.h>
#include <zephyr/device.h>
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
#include <drivers/animation.h>
#include <zmk/animation.h>
#include <zmk/event_manager.h>
#include <zmk/events/position_state_changed.h>
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);

View file

@ -0,0 +1,121 @@
/*
* Copyright (c) 2020 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#define DT_DRV_COMPAT zmk_animation_solid
#include <zephyr/device.h>
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
#include <drivers/animation.h>
#include <zmk/animation.h>
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);

138
app/src/animation/color.c Normal file
View file

@ -0,0 +1,138 @@
/*
* Copyright (c) 2020 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#include <stdlib.h>
#include <zmk/animation.h>
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;
}

View file

@ -0,0 +1,94 @@
/*
* Copyright (c) 2020 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#include <zephyr/device.h>
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
#include <drivers/behavior.h>
#include <zmk/animation/animation_control.h>
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);

View file

@ -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 <dt-bindings/zmk/animation.h>
```
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)
```

View file

@ -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 <dt-bindings/zmk/animation.h>
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 = <BLENDING_MODE_NORMAL>;
/* Duration of animation in ms */
duration = <1000>;
/* Will result in a pure white */
color = <HSL(0, 0, 100)>;
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 = <HSL(236, 30, 50)>;
};
```
### 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
>;
};
```

View file

@ -20,6 +20,7 @@ module.exports = {
"features/backlight", "features/backlight",
"features/battery", "features/battery",
"features/beta-testing", "features/beta-testing",
"features/animations",
], ],
Behaviors: [ Behaviors: [
"behaviors/key-press", "behaviors/key-press",
@ -42,6 +43,7 @@ module.exports = {
"behaviors/underglow", "behaviors/underglow",
"behaviors/backlight", "behaviors/backlight",
"behaviors/power", "behaviors/power",
"behaviors/animations",
], ],
Codes: [ Codes: [
"codes/index", "codes/index",