This commit is contained in:
Moritz Ruth 2024-01-08 01:21:12 +01:00
parent a57347bd78
commit a352885158
Signed by: moritzruth
GPG key ID: C9BBAB79405EE56D
7 changed files with 251 additions and 73 deletions

View file

@ -28,9 +28,9 @@ format = "svg"
[icon_packs.fad]
path = "icons/fad"
format = "svg"
global_filter = "invert"
global_filter = "invert|scale=1.2"
[icon_packs.ph]
path = "icons/ph"
format = "svg"
global_filter = "invert"
global_filter = "invert|scale=1.2"

View file

@ -1,31 +1,31 @@
[keys.1x1]
icon = "@ph/play[invert|alpha=0.5|scale=2.0]"
icon = "@apps/spotify[scale=2.0|grayscale]"
mode.vibrate.pattern = "low"
mode.media__play_pause.icon.paused = "@ph/play"
mode.media__play_pause.icon.playing = "@ph/pause"
[keys.1x2]
icon = "@fad/shuffle[invert]"
icon = "@fad/shuffle[alpha=0.6]"
mode.vibrate.pattern = "low"
mode.spotify__shuffle.icon.active = "@fad/shuffle[color=#58fc11]"
[keys.2x1]
icon = "@ph/timer[invert|scale=0.5]"
icon = "@ph/timer[color=#ff0000]"
mode.vibrate.pattern = "low"
mode.timer.durations = ["60s", "5m", "10m", "15m", "30m"]
mode.timer.vibrate_when_finished = true
mode.timer.needy = true
[keys.3x3]
icon = "@fad/thunderbolt[invert]"
icon = "@fad/thunderbolt[border=#00ff00]"
label = "Dock"
mode.vibrate.pattern = "low"
mode.home_assistant__switch.name = "switch.moritz_thunderbolt_dock"
mode.home_assistant__switch.icon.on = "@fad/thunderbolt[color=#58fc11]"
[keys.3x4]
icon = "@ph/computer-tower[invert]"
label = "Tower PC"
[keys.4x3]
icon = "@ph/computer-tower[border=#00ff00]"
label = "Tower PC unnötig lang"
mode.vibrate.pattern = "low"
mode.home_assistant__switch.name = "switch.mwin"
mode.home_assistant__switch.icon.on = "@ph/computer-tower[color=#58fc11]"

View file

@ -2,7 +2,6 @@ use color_eyre::{eyre::ContextCompat, Result};
use tiny_skia::{ColorU8, Pixmap, PremultipliedColorU8};
use crate::model::image_filter::ImageFilter;
use crate::model::rgb::RGB8Wrapper;
pub fn apply(original: &Pixmap, filter: &ImageFilter) -> Result<Pixmap> {
let mut result = if let Some(rect) = filter.crop {
@ -11,45 +10,34 @@ pub fn apply(original: &Pixmap, filter: &ImageFilter) -> Result<Pixmap> {
original.clone()
};
// scale is handled in runner::graphics::render_key
// scale, rotate and border are handled in runner::graphics::render_key
// rotate
for p in result.pixels_mut() {
if filter.invert {
*p = PremultipliedColorU8::from_rgba(p.alpha() - p.red(), p.alpha() - p.green(), p.alpha() - p.blue(), p.alpha()).unwrap();
}
if let Some(color) = &filter.color {
apply_color(&mut result, color);
}
if filter.alpha != 1.0 {
let d = p.demultiply();
*p = ColorU8::from_rgba(d.red(), d.green(), d.blue(), (d.alpha() as f32 * filter.alpha).round() as u8).premultiply();
}
if filter.alpha != 1.0 {
apply_alpha(&mut result, filter.alpha);
}
if filter.grayscale {
let a = p.alpha();
if a > 0 {
// The values are adjusted to make it look right on the Loupedeck Live display
let luminosity = (0.3 * p.red() as f32 + 0.6 * p.green() as f32 + 0.1 * p.blue() as f32).floor() as u8;
*p = ColorU8::from_rgba(luminosity, luminosity, luminosity, a).premultiply();
}
}
// grayscale
if filter.invert {
apply_invert(&mut result);
if let Some(color) = &filter.color {
let a = p.alpha();
if a > 0 {
*p = ColorU8::from_rgba(color.r, color.g, color.b, a).premultiply();
}
}
}
Ok(result)
}
fn apply_alpha(pixmap: &mut Pixmap, alpha: f32) {
for p in pixmap.pixels_mut() {
let d = p.demultiply();
*p = ColorU8::from_rgba(d.red(), d.green(), d.blue(), (d.alpha() as f32 * alpha).round() as u8).premultiply();
}
}
fn apply_invert(pixmap: &mut Pixmap) {
for p in pixmap.pixels_mut() {
*p = PremultipliedColorU8::from_rgba(p.alpha() - p.red(), p.alpha() - p.green(), p.alpha() - p.blue(), p.alpha()).unwrap();
}
}
fn apply_color(pixmap: &mut Pixmap, color: &RGB8Wrapper) {
for p in pixmap.pixels_mut() {
let a = p.alpha();
if a > 0 {
*p = ColorU8::from_rgba(color.r, color.g, color.b, a).premultiply();
}
}
}

View file

@ -9,6 +9,7 @@ use tiny_skia::{Pixmap, Transform};
use crate::model::config::{Config, IconFormat, IconPack};
use crate::model::icon_descriptor::{IconDescriptor, IconDescriptorSource};
use crate::model::rgb::RGB8Wrapper;
use crate::model::IconMap;
mod filter;
@ -17,6 +18,8 @@ mod filter;
pub struct LoadedIcon {
pub pixmap: Pixmap,
pub scale: f32,
pub clockwise_quarter_rotations: u8,
pub border: Option<RGB8Wrapper>,
}
pub fn get_used_icon_descriptors(config: &Config) -> HashSet<IconDescriptor> {
@ -83,7 +86,27 @@ pub fn load_icons(
descriptors: HashSet<IconDescriptor>,
dpi: f32,
) -> Result<HashMap<IconDescriptor, LoadedIcon>> {
let mut unfiltered_pixmap_by_source: HashMap<IconDescriptorSource, Pixmap> = HashMap::new();
let mut highest_scale_by_source: HashMap<IconDescriptorSource, f32> = HashMap::new();
for d in &descriptors {
let mut scale = d.filter.scale;
if let IconDescriptorSource::IconPack { pack_id, .. } = &d.source {
let pack = &icon_packs_by_id[pack_id];
if let Some(filter) = &pack.global_filter {
scale *= filter.scale;
}
}
if let Some(v) = highest_scale_by_source.get_mut(&d.source) {
if *v < scale {
*v = scale;
}
} else {
highest_scale_by_source.insert(d.source.clone(), scale);
}
}
let mut unfiltered_scaled_pixmap_by_source: HashMap<IconDescriptorSource, (Pixmap, f32)> = HashMap::new();
let mut icons_by_descriptor: HashMap<IconDescriptor, LoadedIcon> = HashMap::new();
let mut fonts_db = resvg::usvg::fontdb::Database::new();
fonts_db.load_system_fonts();
@ -93,29 +116,59 @@ pub fn load_icons(
continue;
}
let original_image = match unfiltered_pixmap_by_source.entry(descriptor.source.clone()) {
let (original_image, original_image_scale) = match unfiltered_scaled_pixmap_by_source.entry(descriptor.source.clone()) {
Entry::Occupied(o) => o.into_mut(),
Entry::Vacant(v) => v.insert(read_image(config_directory, icon_packs_by_id, dpi, &fonts_db, descriptor.source.clone())?),
Entry::Vacant(v) => v.insert(read_image_and_get_scale(
config_directory,
icon_packs_by_id,
dpi,
&fonts_db,
descriptor.source.clone(),
highest_scale_by_source[&descriptor.source],
)?),
};
let pixmap = filter::apply(original_image, &descriptor.filter)?;
let scale = descriptor.filter.scale;
let (pixmap, scale) = if let IconDescriptorSource::IconPack { pack_id, .. } = &descriptor.source {
let pack = &icon_packs_by_id[pack_id];
if let Some(global_filter) = &pack.global_filter {
(
filter::apply(&filter::apply(original_image, global_filter)?, &descriptor.filter)?,
descriptor.filter.scale * global_filter.scale,
)
} else {
(filter::apply(original_image, &descriptor.filter)?, descriptor.filter.scale)
}
} else {
(filter::apply(original_image, &descriptor.filter)?, descriptor.filter.scale)
};
icons_by_descriptor.insert(descriptor, LoadedIcon { pixmap, scale });
let scale = scale / *original_image_scale;
let clockwise_quarter_rotations = descriptor.filter.clockwise_quarter_rotations;
let border = descriptor.filter.border;
icons_by_descriptor.insert(
descriptor,
LoadedIcon {
pixmap,
scale,
clockwise_quarter_rotations,
border,
},
);
}
Ok(icons_by_descriptor)
}
fn read_image(
fn read_image_and_get_scale(
config_directory: &Path,
icon_packs_by_id: &HashMap<String, IconPack>,
dpi: f32,
fonts_db: &resvg::usvg::fontdb::Database,
source: IconDescriptorSource,
) -> Result<Pixmap> {
highest_scale: f32,
) -> Result<(Pixmap, f32)> {
let path = match source {
IconDescriptorSource::None => return Ok(Pixmap::new(1, 1).unwrap()),
IconDescriptorSource::None => return Ok((Pixmap::new(1, 1).unwrap(), 1.0)),
IconDescriptorSource::Path(path) => path,
IconDescriptorSource::IconPack { pack_id, icon_id } => {
let pack = icon_packs_by_id.get(&pack_id).wrap_err_with(|| format!("Unknown icon pack: @{}", pack_id))?;
@ -132,16 +185,21 @@ fn read_image(
Ok(match path.extension() {
None => return Err(eyre!("Invalid icon path: {:?}", path)),
Some(extension) => match extension.to_string_lossy().as_ref() {
"png" => Pixmap::load_png(&path).wrap_err_with(|| format!("Failed to open or decode the PNG file at {}", path.to_string_lossy()))?,
"svg" => {
read_image_from_svg(&path, dpi, fonts_db).wrap_err_with(|| format!("Failed to open or decode the SVG file at {}", path.to_string_lossy()))?
}
"png" => (
Pixmap::load_png(&path).wrap_err_with(|| format!("Failed to open or decode the PNG file at {}", path.to_string_lossy()))?,
1.0,
),
"svg" => (
read_image_from_svg(&path, dpi, fonts_db, highest_scale)
.wrap_err_with(|| format!("Failed to open or decode the SVG file at {}", path.to_string_lossy()))?,
highest_scale,
),
extension => return Err(eyre!("Invalid file extension, only *.png and *.svg are allowed: {}", extension)),
},
})
}
fn read_image_from_svg(path: &Path, dpi: f32, font_db: &resvg::usvg::fontdb::Database) -> Result<Pixmap> {
fn read_image_from_svg(path: &Path, dpi: f32, font_db: &resvg::usvg::fontdb::Database, scale: f32) -> Result<Pixmap> {
let raw_data = std::fs::read(path)?;
let tree = {
@ -162,9 +220,9 @@ fn read_image_from_svg(path: &Path, dpi: f32, font_db: &resvg::usvg::fontdb::Dat
};
let size = tree.size.to_int_size();
let mut pixmap = Pixmap::new(size.width(), size.height()).unwrap();
let mut pixmap = Pixmap::new((size.width() as f32 * scale).ceil() as u32, (size.height() as f32 * scale).ceil() as u32).unwrap();
tree.render(Transform::default(), &mut pixmap.as_mut());
tree.render(Transform::from_scale(scale, scale), &mut pixmap.as_mut());
Ok(pixmap)
}

View file

@ -17,6 +17,7 @@ pub struct ImageFilter {
pub alpha: f32,
pub grayscale: bool,
pub invert: bool,
pub border: Option<RGB8Wrapper>,
}
impl Eq for ImageFilter {
@ -37,6 +38,7 @@ const DEFAULT_IMAGE_FILTER: ImageFilter = ImageFilter {
alpha: 1.0,
grayscale: false,
invert: false,
border: None,
};
impl Default for ImageFilter {
@ -53,7 +55,7 @@ pub enum ImageFilterFromStringError {
#[error("Filter {name} can only be used once.")]
FilterUsedMoreThanOnce { name: String },
#[error("Only the following values are for rotate: -90, 90, 180, 270")]
#[error("Only the following values are allowed for rotate: -90, 90, 180, 270")]
RotationNotAllowed,
#[error("Filter {filter_name} requires a value ({filter_name}=<value>).")]
@ -169,6 +171,16 @@ impl FromStr for ImageFilter {
"alpha" => result.alpha = parse_f32_filter_value(&filter_name, use_raw_value()?, "0..<1", Box::new(|v| (0.0..1.0).contains(v)))?,
"grayscale" => result.grayscale = use_bool_value()?,
"invert" => result.invert = use_bool_value()?,
"border" => {
let raw_value = use_raw_value()?;
result.border = Some(
RGB8Wrapper::from_str(&raw_value).map_err(|_| ImageFilterFromStringError::FilterValueNotParsable {
filter_name: filter_name.to_string(),
raw_value,
})?,
)
}
_ => return Err(ImageFilterFromStringError::UnknownFilter { name: filter_name }),
};
}
@ -202,7 +214,7 @@ impl Display for ImageFilter {
f.write_str("|")?
}
f.write_fmt(format_args!("rotate={}", self.clockwise_quarter_rotations * 90))?;
f.write_fmt(format_args!("rotate={}", self.clockwise_quarter_rotations as u16 * 90))?;
is_first = false;
}
@ -236,6 +248,15 @@ impl Display for ImageFilter {
f.write_str("|")?
}
f.write_str("invert")?;
is_first = false;
}
if let Some(color) = self.color {
if !is_first {
f.write_str("|")?
}
f.write_fmt(format_args!("color={}", color))?;
// is_first = false;
}

View file

@ -1,16 +1,26 @@
use std::collections::HashMap;
use bytes::{BufMut, Bytes, BytesMut};
use tiny_skia::{BlendMode, Color, FilterQuality, IntSize, Pixmap, PixmapPaint, PremultipliedColorU8, Transform};
use resvg::usvg::tiny_skia_path::PathBuilder;
use tiny_skia::{
BlendMode, Color, FilterQuality, IntSize, LineCap, LineJoin, Paint, Pixmap, PixmapPaint, PremultipliedColorU8, Rect, Shader, Stroke, Transform,
};
use loupedeck_serial::util::Endianness;
use crate::icons::LoadedIcon;
use crate::model::icon_descriptor::{IconDescriptor, IconDescriptorSource};
use crate::runner::graphics::labels::LabelRenderer;
use crate::runner::state::Key;
pub fn render_key(key_size: IntSize, buffer_endianness: Endianness, icons: &HashMap<IconDescriptor, LoadedIcon>, state: Option<&Key>) -> Bytes {
let mut canvas = Pixmap::new(key_size.width(), key_size.height()).unwrap();
pub fn render_key(
label_renderer: &mut LabelRenderer,
key_size: IntSize,
buffer_endianness: Endianness,
icons: &HashMap<IconDescriptor, LoadedIcon>,
state: Option<&Key>,
) -> Bytes {
let mut pixmap = Pixmap::new(key_size.width(), key_size.height()).unwrap();
if let Some(state) = state {
if state.icon.source != IconDescriptorSource::None {
@ -26,20 +36,47 @@ pub fn render_key(key_size: IntSize, buffer_endianness: Endianness, icons: &Hash
quality: FilterQuality::Bicubic,
};
canvas.draw_pixmap(
(((key_size.width() - scaled_size.width()) / 2) as f32 / icon.scale).round() as i32,
(((key_size.height() - scaled_size.height()) / 2) as f32 / icon.scale).round() as i32,
pixmap.draw_pixmap(
(((key_size.width() as i32 - scaled_size.width() as i32) / 2) as f32 / icon.scale).round() as i32,
(((key_size.height() as i32 - scaled_size.height() as i32) / 2) as f32 / icon.scale).round() as i32,
icon.pixmap.as_ref(),
&PAINT,
Transform::from_scale(icon.scale, icon.scale),
Transform::from_scale(icon.scale, icon.scale).post_rotate_at(
(icon.clockwise_quarter_rotations as f32) * 90.0,
key_size.width() as f32 / 2.0,
key_size.height() as f32 / 2.0,
),
None,
);
if let Some(color) = icon.border {
let path = PathBuilder::from_rect(Rect::from_xywh(-1.0, -2.0, pixmap.width() as f32, pixmap.height() as f32).unwrap());
let paint = Paint {
shader: Shader::SolidColor(Color::from_rgba8(color.r, color.g, color.b, 255)),
..Paint::default()
};
static STROKE: Stroke = Stroke {
width: 15.0,
miter_limit: 4.0,
line_cap: LineCap::Butt,
line_join: LineJoin::Round,
dash: None,
};
pixmap.stroke_path(&path, &paint, &STROKE, Transform::identity(), None);
}
}
if !state.label.is_empty() {
label_renderer.render(&mut pixmap, &state.label);
}
} else {
canvas.fill(Color::BLACK);
pixmap.fill(Color::BLACK);
}
convert_pixels_to_rgb565(canvas.pixels(), buffer_endianness).freeze()
convert_pixels_to_rgb565(pixmap.pixels(), buffer_endianness).freeze()
}
fn convert_pixels_to_rgb565(pixels: &[PremultipliedColorU8], endianness: Endianness) -> BytesMut {
@ -61,3 +98,62 @@ fn convert_pixels_to_rgb565(pixels: &[PremultipliedColorU8], endianness: Endiann
result
}
pub mod labels {
use cosmic_text::{Align, Attrs, AttrsList, Buffer, BufferLine, FontSystem, Metrics, Shaping, SwashCache};
use tiny_skia::{Color, Paint, Pixmap, Rect, Shader, Transform};
pub struct LabelRenderer {
font_system: FontSystem,
swash_cache: SwashCache,
buffer: Buffer,
}
impl LabelRenderer {
pub fn new() -> Self {
let mut font_system = FontSystem::new();
let buffer = Buffer::new(&mut font_system, Metrics::new(11.0, 11.0));
LabelRenderer {
font_system,
swash_cache: SwashCache::new(),
buffer,
}
}
pub fn render(&mut self, pixmap: &mut Pixmap, text: &String) {
let attrs = Attrs::new();
let mut line = BufferLine::new(text, AttrsList::new(attrs), Shaping::Advanced);
line.set_align(Some(Align::Center));
self.buffer.lines.clear();
self.buffer.lines.push(line);
const PADDING: f32 = 12.0;
self.buffer.set_size(
&mut self.font_system,
pixmap.width() as f32 - PADDING * 2.0,
pixmap.height() as f32 - PADDING * 2.0,
);
self.buffer.shape_until_scroll(&mut self.font_system);
self.buffer.draw(
&mut self.font_system,
&mut self.swash_cache,
cosmic_text::Color::rgb(255, 255, 255),
|x, y, w, h, color| {
pixmap.fill_rect(
Rect::from_xywh(x as f32 + PADDING, y as f32 + PADDING, w as f32, h as f32).unwrap(),
&Paint {
shader: Shader::SolidColor(Color::from_rgba8(color.r(), color.g(), color.b(), color.a())),
..Paint::default()
},
Transform::identity(),
None,
);
},
);
}
}
}

View file

@ -1,3 +1,4 @@
use std::cell::RefCell;
use std::collections::HashMap;
use std::path::Path;
use std::sync::Arc;
@ -22,6 +23,7 @@ use crate::icons::{get_used_icon_descriptors, load_icons, LoadedIcon};
use crate::model;
use crate::model::icon_descriptor::IconDescriptor;
use crate::model::{ButtonPosition, KeyPath, KeyPosition, KnobPath};
use crate::runner::graphics::labels::LabelRenderer;
use crate::runner::graphics::render_key;
use crate::runner::state::{Key, State, StateChangeCommand};
@ -136,6 +138,7 @@ enum IoWork {
struct IoWorkerContext {
config: Arc<model::config::Config>,
icons: HashMap<IconDescriptor, LoadedIcon>,
label_renderer: RefCell<LabelRenderer>,
device: LoupedeckDevice,
state: State,
}
@ -149,7 +152,13 @@ fn do_io_work(
commands_receiver: Receiver<StateChangeCommand>,
) {
let state = create_state(&config);
let mut context = IoWorkerContext { config, icons, device, state };
let mut context = IoWorkerContext {
config,
icons,
label_renderer: RefCell::new(LabelRenderer::new()),
device,
state,
};
loop {
let a = flume::Selector::new()
@ -306,7 +315,13 @@ fn draw_key(context: &IoWorkerContext, index: u8, key: Option<&Key>) {
let key_grid = &context.device.characteristics().key_grid;
let (x, y, w, h) = key_grid.get_local_key_rect_xywh(index).unwrap();
let p = render_key(IntSize::from_wh(w as u32, h as u32).unwrap(), key_grid.display.endianness, &context.icons, key);
let p = render_key(
&mut context.label_renderer.borrow_mut(),
IntSize::from_wh(w as u32, h as u32).unwrap(),
key_grid.display.endianness,
&context.icons,
key,
);
context.device.replace_framebuffer_area_raw(&key_grid.display, x, y, w, h, p).unwrap();
}