diff --git a/Cargo.lock b/Cargo.lock index 239188a..50be929 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -188,9 +188,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.4.12" +version = "4.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcfab8ba68f3668e89f6ff60f5b205cea56aa7b769451a59f34b8682f51c056d" +checksum = "52bdc885e4cacc7f7c9eedc1ef6da641603180c783c41a15c264944deeaab642" dependencies = [ "clap_builder", "clap_derive", @@ -217,7 +217,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.48", ] [[package]] @@ -328,7 +328,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.43", + "syn 2.0.48", ] [[package]] @@ -339,7 +339,7 @@ checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" dependencies = [ "darling_core", "quote", - "syn 2.0.43", + "syn 2.0.48", ] [[package]] @@ -379,9 +379,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.3.10" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eb30d70a07a3b04884d2677f06bec33509dc67ca60d92949e5535352d3191dc" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" dependencies = [ "powerfmt", "serde", @@ -417,7 +417,7 @@ checksum = "44600091ce205df4f8b661e98617d49c37b2dd609e449ec82b0fb5d7b33e2eeb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.48", ] [[package]] @@ -437,7 +437,7 @@ checksum = "0d28318a75d4aead5c4db25382e8ef717932d0346600cacae6357eb5941bc5ff" dependencies = [ "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.48", ] [[package]] @@ -458,7 +458,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.48", ] [[package]] @@ -669,9 +669,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.58" +version = "0.1.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" +checksum = "b6a67363e2aa4443928ce15e57ebae94fd8949958fd1223c4cfc0cd473ad7539" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1019,18 +1019,18 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "proc-macro2" -version = "1.0.71" +version = "1.0.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75cb1540fadbd5b8fbccc4dddad2734eba435053f725621c070711a14bb5f4b8" +checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.33" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] @@ -1228,29 +1228,29 @@ checksum = "b97ed7a9823b74f99c7742f5336af7be5ecd3eeafcb1507d1fa93347b1d589b0" [[package]] name = "serde" -version = "1.0.193" +version = "1.0.195" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" +checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.193" +version = "1.0.195" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" +checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.48", ] [[package]] name = "serde_json" -version = "1.0.108" +version = "1.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" +checksum = "176e46fa42316f18edd598015a5166857fc835ec732f5215eac6b7bdbf0a84f4" dependencies = [ "itoa", "ryu", @@ -1302,7 +1302,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.48", ] [[package]] @@ -1426,9 +1426,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.43" +version = "2.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee659fb5f3d355364e1f3e5bc10fb82068efbf824a1e9d1c9504244a6469ad53" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" dependencies = [ "proc-macro2", "quote", @@ -1470,7 +1470,7 @@ checksum = "e7fbe9b594d6568a6a1443250a7e67d80b74e1e96f6d1715e1e21cc1888291d3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.48", ] [[package]] @@ -1574,7 +1574,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.48", ] [[package]] @@ -1843,7 +1843,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.48", "wasm-bindgen-shared", ] @@ -1865,7 +1865,7 @@ checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" dependencies = [ "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.48", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -1915,11 +1915,11 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-core" -version = "0.51.1" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.48.5", + "windows-targets 0.52.0", ] [[package]] @@ -2047,9 +2047,9 @@ checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" [[package]] name = "winnow" -version = "0.5.31" +version = "0.5.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97a4882e6b134d6c28953a387571f1acdd3496830d5e36c5e3a1075580ea641c" +checksum = "b7520bbdec7211caa7c4e682eb1fbe07abe20cee6756b6e00f537c82c11816aa" dependencies = [ "memchr", ] diff --git a/deckster/examples/full/key-pages/default.toml b/deckster/examples/full/key-pages/default.toml index 0eccbb0..0dd1268 100644 --- a/deckster/examples/full/key-pages/default.toml +++ b/deckster/examples/full/key-pages/default.toml @@ -1,30 +1,30 @@ [keys.1x1] -icon = "@ph/play[alpha=0.8]" +icon = "@ph/play[invert|alpha=0.5|scale=2.0]" 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" +icon = "@fad/shuffle[invert]" mode.vibrate.pattern = "low" mode.spotify__shuffle.icon.active = "@fad/shuffle[color=#58fc11]" [keys.2x1] -icon = "@ph/timer" +icon = "@ph/timer[invert|scale=0.5]" 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" +icon = "@fad/thunderbolt[invert]" 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" +icon = "@ph/computer-tower[invert]" label = "Tower PC" mode.vibrate.pattern = "low" mode.home_assistant__switch.name = "switch.mwin" diff --git a/deckster/src/icons/filter.rs b/deckster/src/icons/filter.rs index 61457ca..15a3f79 100644 --- a/deckster/src/icons/filter.rs +++ b/deckster/src/icons/filter.rs @@ -1,14 +1,55 @@ use color_eyre::{eyre::ContextCompat, Result}; -use tiny_skia::Pixmap; +use tiny_skia::{ColorU8, Pixmap, PremultipliedColorU8}; use crate::model::image_filter::ImageFilter; +use crate::model::rgb::RGB8Wrapper; -pub fn apply_filter(original: &Pixmap, filter: &ImageFilter) -> Result { +pub fn apply(original: &Pixmap, filter: &ImageFilter) -> Result { let mut result = if let Some(rect) = filter.crop { original.clone_rect(*rect).wrap_err_with(|| format!("Invalid crop rect: {}", rect))? } else { original.clone() }; + // scale is handled in runner::graphics::render_key + + // rotate + + if let Some(color) = &filter.color { + apply_color(&mut result, color); + } + + if filter.alpha != 1.0 { + apply_alpha(&mut result, filter.alpha); + } + + // grayscale + + if filter.invert { + apply_invert(&mut result); + } + 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(); + } + } +} diff --git a/deckster/src/icons/mod.rs b/deckster/src/icons/mod.rs index 60270b3..d8e2266 100644 --- a/deckster/src/icons/mod.rs +++ b/deckster/src/icons/mod.rs @@ -7,7 +7,6 @@ use color_eyre::Result; use resvg::usvg::{TextRendering, TreeParsing, TreeTextToPath}; use tiny_skia::{Pixmap, Transform}; -use crate::icons::filter::apply_filter; use crate::model::config::{Config, IconFormat, IconPack}; use crate::model::icon_descriptor::{IconDescriptor, IconDescriptorSource}; use crate::model::IconMap; @@ -99,7 +98,7 @@ pub fn load_icons( Entry::Vacant(v) => v.insert(read_image(config_directory, icon_packs_by_id, dpi, &fonts_db, descriptor.source.clone())?), }; - let pixmap = apply_filter(original_image, &descriptor.filter)?; + let pixmap = filter::apply(original_image, &descriptor.filter)?; let scale = descriptor.filter.scale; icons_by_descriptor.insert(descriptor, LoadedIcon { pixmap, scale }); diff --git a/deckster/src/model/image_filter.rs b/deckster/src/model/image_filter.rs index 7a1a366..85e77e6 100644 --- a/deckster/src/model/image_filter.rs +++ b/deckster/src/model/image_filter.rs @@ -15,7 +15,6 @@ pub struct ImageFilter { pub clockwise_quarter_rotations: u8, pub color: Option, pub alpha: f32, - pub blur: f32, pub grayscale: bool, pub invert: bool, } @@ -36,7 +35,6 @@ const DEFAULT_IMAGE_FILTER: ImageFilter = ImageFilter { crop: None, color: None, alpha: 1.0, - blur: 0.0, grayscale: false, invert: false, }; @@ -169,7 +167,6 @@ 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)))?, - "blur" => result.blur = parse_f32_filter_value(&filter_name, use_raw_value()?, "0..=10", Box::new(|v| (0.0..=10.0).contains(v)))?, "grayscale" => result.grayscale = use_bool_value()?, "invert" => result.invert = use_bool_value()?, _ => return Err(ImageFilterFromStringError::UnknownFilter { name: filter_name }), @@ -226,14 +223,6 @@ impl Display for ImageFilter { is_first = false; } - if self.blur != DEFAULT_IMAGE_FILTER.blur { - if !is_first { - f.write_str("|")? - } - f.write_fmt(format_args!("blur={}", self.blur))?; - is_first = false; - } - if self.grayscale { if !is_first { f.write_str("|")? diff --git a/deckster/src/runner/graphics.rs b/deckster/src/runner/graphics.rs index 4634b61..0131af4 100644 --- a/deckster/src/runner/graphics.rs +++ b/deckster/src/runner/graphics.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use bytes::{BufMut, Bytes, BytesMut}; -use tiny_skia::{Color, IntSize, Pixmap, PremultipliedColorU8}; +use tiny_skia::{BlendMode, Color, FilterQuality, IntSize, Pixmap, PixmapPaint, PremultipliedColorU8, Transform}; use loupedeck_serial::util::Endianness; @@ -9,8 +9,8 @@ use crate::icons::LoadedIcon; use crate::model::icon_descriptor::{IconDescriptor, IconDescriptorSource}; use crate::runner::state::Key; -pub fn render_key(key_size: (u16, u16), buffer_endianness: Endianness, icons: &HashMap, state: Option<&Key>) -> Bytes { - let mut canvas = Pixmap::new(key_size.0 as u32, key_size.1 as u32).unwrap(); +pub fn render_key(key_size: IntSize, buffer_endianness: Endianness, icons: &HashMap, state: Option<&Key>) -> Bytes { + let mut canvas = Pixmap::new(key_size.width(), key_size.height()).unwrap(); if let Some(state) = state { if state.icon.source != IconDescriptorSource::None { @@ -20,11 +20,24 @@ pub fn render_key(key_size: (u16, u16), buffer_endianness: Endianness, icons: &H .scale_by(icon.scale) .unwrap(); - //canvas.draw_pixmap(0, 0, icon.pixmap.as_ref(), &PixmapPaint::default(), Transform::from_scale(icon.scale, icon.scale), None); + static PAINT: PixmapPaint = PixmapPaint { + opacity: 1.0, + blend_mode: BlendMode::SourceOver, + 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, + icon.pixmap.as_ref(), + &PAINT, + Transform::from_scale(icon.scale, icon.scale), + None, + ); } } else { + canvas.fill(Color::BLACK); } - canvas.fill(Color::WHITE); convert_pixels_to_rgb565(canvas.pixels(), buffer_endianness).freeze() } diff --git a/deckster/src/runner/mod.rs b/deckster/src/runner/mod.rs index 2831f90..ee21ff6 100644 --- a/deckster/src/runner/mod.rs +++ b/deckster/src/runner/mod.rs @@ -2,14 +2,16 @@ use std::collections::HashMap; use std::path::Path; use std::sync::Arc; use std::thread; +use std::time::Instant; use color_eyre::eyre::{ContextCompat, WrapErr}; use color_eyre::Result; use enum_map::EnumMap; use enum_ordinalize::Ordinalize; use flume::{Receiver, Sender}; -use log::{debug, trace}; +use log::{debug, info, trace}; use rgb::RGB8; +use tiny_skia::IntSize; use loupedeck_serial::characteristics::{LoupedeckButton, LoupedeckDeviceKeyGridCharacteristics}; use loupedeck_serial::commands::VibrationPattern; @@ -28,18 +30,26 @@ mod state; pub async fn start(config_directory: &Path, config: model::config::Config) -> Result<()> { let config = Arc::new(config); - let device = LoupedeckDevice::discover()? - .first() - .wrap_err("No device connected.")? - .connect() - .wrap_err("Connecting to the device failed.")?; + info!("Discovering devices…"); + + let available_devices = LoupedeckDevice::discover()?; + let available_device = available_devices.first().wrap_err("No device connected.")?; + + info!("Connecting to the device…"); + let device = available_device.connect().wrap_err("Connecting to the device failed.")?; + + info!("Connected"); let key_grid = &device.characteristics().key_grid; let used_icon_descriptors = get_used_icon_descriptors(&config); - let icons = load_icons(config_directory, &config.icon_packs, used_icon_descriptors, key_grid.display.dpi)?; - device.set_brightness(0.0); + let start_time = Instant::now(); + info!("Loading icons…"); + let icons = load_icons(config_directory, &config.icon_packs, used_icon_descriptors, key_grid.display.dpi)?; + info!("Finished loading {} icon(s) in {}ms", icons.len(), start_time.elapsed().as_millis()); + + device.set_brightness(0.5); device.vibrate(VibrationPattern::RiseFall); let events_receiver = device.events(); @@ -59,6 +69,7 @@ pub async fn start(config_directory: &Path, config: model::config::Config) -> Re .spawn(move || do_io_work(cloned_config, icons, device, events_receiver, cloned_commands_sender, commands_receiver)) .wrap_err("Could not spawn the worker thread")?; + info!("Ready."); io_worker_thread.join().unwrap(); Ok(()) @@ -191,14 +202,6 @@ fn handle_event(context: &IoWorkerContext, commands_sender: &Sender) { 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((w, h), key_grid.display.endianness, &context.icons, key); + let p = render_key(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(); }