feat: GUI 控制的,LED 灯条颜色预览。

This commit is contained in:
Ivan Li 2023-03-26 10:48:50 +08:00
parent 3ede04c31b
commit 1a3102257e
23 changed files with 1067 additions and 192 deletions

83
src-tauri/Cargo.lock generated
View File

@ -181,7 +181,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "497049e9477329f8f6a559972ee42e117487d01d1e8c2cc9f836ea6fa23a9e1a" checksum = "497049e9477329f8f6a559972ee42e117487d01d1e8c2cc9f836ea6fa23a9e1a"
dependencies = [ dependencies = [
"serde", "serde",
"toml", "toml 0.5.11",
] ]
[[package]] [[package]]
@ -189,9 +189,6 @@ name = "cc"
version = "1.0.79" version = "1.0.79"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f"
dependencies = [
"jobserver",
]
[[package]] [[package]]
name = "cesu8" name = "cesu8"
@ -270,6 +267,12 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
[[package]]
name = "color_space"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3776b2bcc4e914db501bb9be9572dd706e344b9eb8f882894f3daa651d281381"
[[package]] [[package]]
name = "combine" name = "combine"
version = "4.6.6" version = "4.6.6"
@ -1046,6 +1049,12 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286"
[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]] [[package]]
name = "html5ever" name = "html5ever"
version = "0.25.2" version = "0.25.2"
@ -1245,15 +1254,6 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
[[package]]
name = "jobserver"
version = "0.1.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "json-patch" name = "json-patch"
version = "0.2.7" version = "0.2.7"
@ -1289,15 +1289,6 @@ version = "0.2.140"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99227334921fae1a979cf0bfdfcc6b3e5ce376ef57e16fb6fb3ea2ed6095f80c" checksum = "99227334921fae1a979cf0bfdfcc6b3e5ce376ef57e16fb6fb3ea2ed6095f80c"
[[package]]
name = "libwebp-sys"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439fd1885aa28937e7edcd68d2e793cb4a22f8733460d2519fbafd2b215672bf"
dependencies = [
"cc",
]
[[package]] [[package]]
name = "line-wrap" name = "line-wrap"
version = "0.1.1" version = "0.1.1"
@ -2155,6 +2146,15 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "serde_spanned"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0efd8caf556a6cebd3b285caf480045fcc1ac04f6bd786b09a6f11af30c4fcf4"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "serde_with" name = "serde_with"
version = "1.14.0" version = "1.14.0"
@ -2364,7 +2364,7 @@ dependencies = [
"cfg-expr 0.9.1", "cfg-expr 0.9.1",
"heck 0.3.3", "heck 0.3.3",
"pkg-config", "pkg-config",
"toml", "toml 0.5.11",
"version-compare 0.0.11", "version-compare 0.0.11",
] ]
@ -2377,7 +2377,7 @@ dependencies = [
"cfg-expr 0.11.0", "cfg-expr 0.11.0",
"heck 0.4.1", "heck 0.4.1",
"pkg-config", "pkg-config",
"toml", "toml 0.5.11",
"version-compare 0.1.1", "version-compare 0.1.1",
] ]
@ -2647,21 +2647,21 @@ name = "test-demo"
version = "0.0.0" version = "0.0.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"base64 0.21.0", "color_space",
"core-graphics", "core-graphics",
"display-info", "display-info",
"env_logger", "env_logger",
"hex",
"log", "log",
"paris", "paris",
"percent-encoding", "percent-encoding",
"png",
"serde", "serde",
"serde_json", "serde_json",
"tauri", "tauri",
"tauri-build", "tauri-build",
"tokio", "tokio",
"toml 0.7.3",
"url-build-parse", "url-build-parse",
"webp",
] ]
[[package]] [[package]]
@ -2782,11 +2782,26 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "toml"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b403acf6f2bb0859c93c7f0d967cb4a75a7ac552100f9322faf64dc047669b21"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit",
]
[[package]] [[package]]
name = "toml_datetime" name = "toml_datetime"
version = "0.6.1" version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622" checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "toml_edit" name = "toml_edit"
@ -2795,6 +2810,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc18466501acd8ac6a3f615dd29a3438f8ca6bb3b19537138b3106e575621274" checksum = "dc18466501acd8ac6a3f615dd29a3438f8ca6bb3b19537138b3106e575621274"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"serde",
"serde_spanned",
"toml_datetime", "toml_datetime",
"winnow", "winnow",
] ]
@ -3044,16 +3061,6 @@ dependencies = [
"system-deps 6.0.3", "system-deps 6.0.3",
] ]
[[package]]
name = "webp"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf022f821f166079a407d000ab57e84de020e66ffbbf4edde999bc7d6e371cae"
dependencies = [
"image",
"libwebp-sys",
]
[[package]] [[package]]
name = "webview2-com" name = "webview2-com"
version = "0.19.1" version = "0.19.1"
@ -3310,7 +3317,7 @@ version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b68db261ef59e9e52806f688020631e987592bd83619edccda9c47d42cde4f6c" checksum = "b68db261ef59e9e52806f688020631e987592bd83619edccda9c47d42cde4f6c"
dependencies = [ dependencies = [
"toml", "toml 0.5.11",
] ]
[[package]] [[package]]

View File

@ -16,11 +16,8 @@ tauri-build = { version = "1.2", features = [] }
tauri = { version = "1.2", features = ["shell-open"] } tauri = { version = "1.2", features = ["shell-open"] }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
webp = "0.2.2"
base64 = "0.21.0"
core-graphics = "0.22.3" core-graphics = "0.22.3"
display-info = "0.4.1" display-info = "0.4.1"
png = "0.17.7"
anyhow = "1.0.69" anyhow = "1.0.69"
tokio = {version = "1.26.0", features = ["full"] } tokio = {version = "1.26.0", features = ["full"] }
paris = { version = "1.5", features = ["timestamps", "macros"] } paris = { version = "1.5", features = ["timestamps", "macros"] }
@ -28,6 +25,9 @@ log = "0.4.17"
env_logger = "0.10.0" env_logger = "0.10.0"
percent-encoding = "2.2.0" percent-encoding = "2.2.0"
url-build-parse = "9.0.0" url-build-parse = "9.0.0"
color_space = "0.5.3"
hex = "0.4.3"
toml = "0.7.3"
[features] [features]
# this feature is used for production builds or when `devPath` points to the filesystem # this feature is used for production builds or when `devPath` points to the filesystem

View File

@ -0,0 +1,139 @@
use std::env::current_dir;
use paris::{error, info};
use serde::{Deserialize, Serialize};
use tauri::api::path::config_dir;
#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
pub enum Border {
Top,
Bottom,
Left,
Right,
}
#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
pub struct LedStripConfigOfBorders {
pub top: Option<LedStripConfig>,
pub bottom: Option<LedStripConfig>,
pub left: Option<LedStripConfig>,
pub right: Option<LedStripConfig>,
}
#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
pub struct LedStripConfig {
pub index: usize,
pub border: Border,
pub display_id: u32,
pub start_pos: usize,
pub len: usize,
}
impl LedStripConfig {
pub async fn read_config() -> anyhow::Result<Vec<Self>> {
// config path
let path = config_dir()
.unwrap_or(current_dir().unwrap())
.join("led_strip_config.toml");
let exists = tokio::fs::try_exists(path.clone())
.await
.map_err(|e| anyhow::anyhow!("Failed to check config file exists: {}", e))?;
if exists {
let config = tokio::fs::read_to_string(path).await?;
let config: Vec<Self> = toml::from_str(&config)
.map_err(|e| anyhow::anyhow!("Failed to parse config file: {}", e))?;
Ok(config)
} else {
info!("config file not exist, fallback to default config");
Ok(Self::get_default_config().await?)
}
}
pub async fn write_config(configs: &Vec<Self>) -> anyhow::Result<()> {
let path = config_dir()
.unwrap_or(current_dir().unwrap())
.join("led_strip_config.toml");
let config = toml::to_string(configs)
.map_err(|e| anyhow::anyhow!("Failed to parse config file: {}", e))?;
tokio::fs::write(path, config)
.await
.map_err(|e| anyhow::anyhow!("Failed to write config file: {}", e))?;
Ok(())
}
pub async fn get_default_config() -> anyhow::Result<Vec<Self>> {
let displays = display_info::DisplayInfo::all().map_err(|e| {
error!("can not list display info: {}", e);
anyhow::anyhow!("can not list display info: {}", e)
})?;
let mut configs = Vec::new();
for (i, display) in displays.iter().enumerate() {
for j in 0..4 {
let config = Self {
index: j + i * 4 * 30,
display_id: display.id,
border: match j {
0 => Border::Top,
1 => Border::Bottom,
2 => Border::Left,
3 => Border::Right,
_ => unreachable!(),
},
start_pos: 0,
len: 30,
};
configs.push(config);
}
}
Ok(configs)
}
}
#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
pub struct DisplayConfig {
pub id: u32,
pub index_of_display: usize,
pub display_width: usize,
pub display_height: usize,
pub led_strip_of_borders: LedStripConfigOfBorders,
pub scale_factor: f32,
}
impl LedStripConfigOfBorders {
pub fn default() -> Self {
Self {
top: None,
bottom: None,
left: None,
right: None,
}
}
}
impl DisplayConfig {
pub fn default(
id: u32,
index_of_display: usize,
display_width: usize,
display_height: usize,
scale_factor: f32,
) -> Self {
Self {
id,
index_of_display,
display_width,
display_height,
led_strip_of_borders: LedStripConfigOfBorders::default(),
scale_factor,
}
}
}

View File

@ -0,0 +1,3 @@
mod config;
pub use config::*;

View File

@ -0,0 +1,13 @@
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
pub enum Brightness {
Relative(i16),
Absolute(u16),
}
#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
pub struct DisplayBrightness {
pub brightness: Brightness,
pub display_index: usize,
}

View File

@ -0,0 +1,36 @@
use std::time::SystemTime;
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
pub struct DisplayConfig {
pub id: usize,
pub brightness: u16,
pub max_brightness: u16,
pub min_brightness: u16,
pub contrast: u16,
pub max_contrast: u16,
pub min_contrast: u16,
pub mode: u16,
pub max_mode: u16,
pub min_mode: u16,
pub last_modified_at: SystemTime,
}
impl DisplayConfig {
pub fn default(index: usize) -> Self {
Self {
id: index,
brightness: 30,
contrast: 50,
mode: 0,
last_modified_at: SystemTime::now(),
max_brightness: 100,
min_brightness: 0,
max_contrast: 100,
min_contrast: 0,
max_mode: 15,
min_mode: 0,
}
}
}

View File

@ -0,0 +1,186 @@
use std::{
borrow::Borrow,
collections::HashMap,
ops::Sub,
sync::Arc,
time::{Duration, SystemTime},
};
use base64::Config;
use ddc_hi::Display;
use paris::{error, info, warn};
use tauri::async_runtime::Mutex;
use tokio::sync::{broadcast, OwnedMutexGuard};
use tracing::warn;
use crate::{display::Brightness, models, rpc};
use super::{display_config::DisplayConfig, DisplayBrightness};
use ddc_hi::Ddc;
pub struct Manager {
displays: Arc<Mutex<HashMap<usize, Arc<Mutex<DisplayConfig>>>>>,
}
impl Manager {
pub fn global() -> &'static Self {
static DISPLAY_MANAGER: once_cell::sync::OnceCell<Manager> =
once_cell::sync::OnceCell::new();
DISPLAY_MANAGER.get_or_init(|| Self::create())
}
pub fn create() -> Self {
let instance = Self {
displays: Arc::new(Mutex::new(HashMap::new())),
};
instance
}
pub async fn subscribe_display_brightness(&self) {
let rpc = rpc::Manager::global().await;
let mut rx = rpc.client().subscribe_change_display_brightness_rx();
loop {
if let Ok(display_brightness) = rx.recv().await {
if let Err(err) = self.set_display_brightness(display_brightness).await {
error!("set_display_brightness failed. {:?}", err);
}
}
}
}
fn read_display_config_by_ddc(index: usize) -> anyhow::Result<DisplayConfig> {
let mut displays = Display::enumerate();
match displays.get_mut(index) {
Some(display) => {
let mut config = DisplayConfig::default(index);
match display.handle.get_vcp_feature(0x10) {
Ok(value) => {
config.max_brightness = value.maximum();
config.min_brightness = 0;
config.brightness = value.value();
}
Err(_) => {}
};
match display.handle.get_vcp_feature(0x12) {
Ok(value) => {
config.max_contrast = value.maximum();
config.min_contrast = 0;
config.contrast = value.value();
}
Err(_) => {}
};
match display.handle.get_vcp_feature(0xdc) {
Ok(value) => {
config.max_mode = value.maximum();
config.min_mode = 0;
config.mode = value.value();
}
Err(_) => {}
};
Ok(config)
}
None => anyhow::bail!("display#{} is missed.", index),
}
}
async fn get_display(&self, index: usize) -> anyhow::Result<OwnedMutexGuard<DisplayConfig>> {
let mut displays = self.displays.lock().await;
match displays.get_mut(&index) {
Some(config) => {
let mut config = config.to_owned().lock_owned().await;
if config.last_modified_at > SystemTime::now().sub(Duration::from_secs(10)) {
info!("cached");
return Ok(config);
}
return match Self::read_display_config_by_ddc(index) {
Ok(config) => {
let id = config.id;
let value = Arc::new(Mutex::new(config));
let valueGuard = value.clone().lock_owned().await;
displays.insert(id, value);
info!("read form ddc");
Ok(valueGuard)
}
Err(err) => {
warn!(
"can not read config from display by ddc, use CACHED value. {:?}",
err
);
config.last_modified_at = SystemTime::now();
Ok(config)
}
};
}
None => {
let config = Self::read_display_config_by_ddc(index).map_err(|err| {
anyhow::anyhow!(
"can not read config from display by ddc,use DEFAULT value. {:?}",
err
)
})?;
let id = config.id;
let value = Arc::new(Mutex::new(config));
let valueGuard = value.clone().lock_owned().await;
displays.insert(id, value);
Ok(valueGuard)
}
}
}
pub async fn set_display_brightness(
&self,
display_brightness: DisplayBrightness,
) -> anyhow::Result<()> {
match Display::enumerate().get_mut(display_brightness.display_index) {
Some(display) => {
match self.get_display(display_brightness.display_index).await {
Ok(mut config) => {
let curr = config.brightness;
info!("curr_brightness: {:?}", curr);
let mut target = match display_brightness.brightness {
Brightness::Relative(v) => curr.wrapping_add_signed(v),
Brightness::Absolute(v) => v,
};
if target.gt(&config.max_brightness) {
target = config.max_brightness;
} else if target.lt(&config.min_brightness) {
target = config.min_brightness;
}
config.brightness = target;
display
.handle
.set_vcp_feature(0x10, target as u16)
.map_err(|err| anyhow::anyhow!("can not set brightness. {:?}", err))?;
let rpc = rpc::Manager::global().await;
rpc.publish_desktop_cmd(
format!("display{}/brightness", display_brightness.display_index).as_str(),
target.to_be_bytes().to_vec(),
)
.await;
}
Err(err) => {
info!(
"can not get display#{} brightness. {:?}",
display_brightness.display_index, err
);
if let Brightness::Absolute(v) = display_brightness.brightness {
display.handle.set_vcp_feature(0x10, v).map_err(|err| {
anyhow::anyhow!("can not set brightness. {:?}", err)
})?;
};
}
};
}
None => {
warn!("display#{} is not found.", display_brightness.display_index);
}
}
Ok(())
}
}

View File

@ -0,0 +1,11 @@
// mod brightness;
// mod manager;
mod display_config;
pub use display_config::*;
// pub use brightness::*;
// pub use manager::*;

View File

@ -0,0 +1,54 @@
use color_space::{Hsv, Rgb};
use serde::Serialize;
#[derive(Clone, Copy, Debug)]
pub struct LedColor {
bits: [u8; 3],
}
impl LedColor {
pub fn default() -> Self {
Self { bits: [0, 0, 0] }
}
pub fn new(r: u8, g: u8, b: u8) -> Self {
Self { bits: [r, g, b] }
}
pub fn from_hsv(h: f64, s: f64, v: f64) -> Self {
let rgb = Rgb::from(Hsv::new(h, s, v));
Self { bits: [rgb.r as u8, rgb.g as u8, rgb.b as u8] }
}
pub fn get_rgb(&self) -> [u8; 3] {
self.bits
}
pub fn is_empty(&self) -> bool {
self.bits.iter().any(|bit| *bit == 0)
}
pub fn set_rgb(&mut self, r: u8, g: u8, b: u8) -> &Self {
self.bits = [r, g, b];
self
}
pub fn merge(&mut self, r: u8, g: u8, b: u8) -> &Self {
self.bits = [
(self.bits[0] / 2 + r / 2),
(self.bits[1] / 2 + g / 2),
(self.bits[2] / 2 + b / 2),
];
self
}
}
impl Serialize for LedColor {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let hex = format!("#{}", hex::encode(self.bits));
serializer.serialize_str(hex.as_str())
}
}

View File

@ -1,15 +1,19 @@
// Prevents additional console window on WiOk(ndows in release, DO NOT REMOVE!! // Prevents additional console window on WiOk(ndows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
mod ambient_light;
mod display;
mod led_color;
pub mod screenshot; pub mod screenshot;
mod screenshot_manager; mod screenshot_manager;
use base64::Engine; use ambient_light::LedStripConfig;
use core_graphics::display::{ use core_graphics::display::{
kCGNullWindowID, kCGWindowImageDefault, kCGWindowListOptionOnScreenOnly, CGDisplay, kCGNullWindowID, kCGWindowImageDefault, kCGWindowListOptionOnScreenOnly, CGDisplay,
}; };
use display_info::DisplayInfo; use display_info::DisplayInfo;
use paris::{error, info}; use paris::{error, info, warn};
use screenshot::Screenshot;
use screenshot_manager::ScreenshotManager; use screenshot_manager::ScreenshotManager;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::to_string; use serde_json::to_string;
@ -52,92 +56,6 @@ fn list_display_info() -> Result<String, String> {
Ok(json_str) Ok(json_str)
} }
#[tauri::command]
fn take_screenshot(display_id: u32, scale_factor: f32) -> Result<String, String> {
let exec = || {
println!("take_screenshot");
let start_at = std::time::Instant::now();
let cg_display = CGDisplay::new(display_id);
let cg_image = CGDisplay::screenshot(
cg_display.bounds(),
kCGWindowListOptionOnScreenOnly,
kCGNullWindowID,
kCGWindowImageDefault,
)
.ok_or_else(|| anyhow::anyhow!("Display#{}: take screenshot failed", display_id))?;
// println!("take screenshot took {}ms", start_at.elapsed().as_millis());
let buffer = cg_image.data();
let bytes_per_row = cg_image.bytes_per_row() as f32;
let height = cg_image.height();
let width = cg_image.width();
let image_height = (height as f32 / scale_factor) as u32;
let image_width = (width as f32 / scale_factor) as u32;
// println!(
// "raw image: {}x{}, output image: {}x{}",
// width, height, image_width, image_height
// );
// // from bitmap vec
let mut image_buffer = vec![0u8; (image_width * image_height * 3) as usize];
for y in 0..image_height {
for x in 0..image_width {
let offset =
(((y as f32) * bytes_per_row + (x as f32) * 4.0) * scale_factor) as usize;
let b = buffer[offset];
let g = buffer[offset + 1];
let r = buffer[offset + 2];
let offset = (y * image_width + x) as usize;
image_buffer[offset * 3] = r;
image_buffer[offset * 3 + 1] = g;
image_buffer[offset * 3 + 2] = b;
}
}
println!(
"convert to image buffer took {}ms",
start_at.elapsed().as_millis()
);
// to png image
// let mut image_png = Vec::new();
// let mut encoder = png::Encoder::new(&mut image_png, image_width, image_height);
// encoder.set_color(png::ColorType::Rgb);
// encoder.set_depth(png::BitDepth::Eight);
// let mut writer = encoder
// .write_header()
// .map_err(|e| anyhow::anyhow!("png: {}", anyhow::anyhow!(e.to_string())))?;
// writer
// .write_image_data(&image_buffer)
// .map_err(|e| anyhow::anyhow!("png: {}", anyhow::anyhow!(e.to_string())))?;
// writer
// .finish()
// .map_err(|e| anyhow::anyhow!("png: {}", anyhow::anyhow!(e.to_string())))?;
// println!("encode to png took {}ms", start_at.elapsed().as_millis());
let image_webp =
webp::Encoder::from_rgb(&image_buffer, image_width, image_height).encode(90f32);
// // base64 image
let mut image_base64 = String::new();
image_base64.push_str("data:image/webp;base64,");
let encoded = base64::engine::general_purpose::STANDARD_NO_PAD.encode(&*image_webp);
image_base64.push_str(encoded.as_str());
println!("took {}ms", start_at.elapsed().as_millis());
println!("image_base64: {}", image_base64.len());
Ok(image_base64)
};
exec().map_err(|e: anyhow::Error| {
println!("error: {}", e);
e.to_string()
})
}
#[tauri::command] #[tauri::command]
async fn subscribe_encoded_screenshot_updated( async fn subscribe_encoded_screenshot_updated(
window: tauri::Window, window: tauri::Window,
@ -153,6 +71,72 @@ async fn subscribe_encoded_screenshot_updated(
}) })
} }
#[tauri::command]
async fn read_led_strip_configs() -> Result<Vec<ambient_light::LedStripConfig>, String> {
let configs = ambient_light::LedStripConfig::read_config()
.await
.map_err(|e| {
error!("can not read led strip configs: {}", e);
e.to_string()
})?;
Ok(configs)
}
#[tauri::command]
async fn write_led_strip_configs(
configs: Vec<ambient_light::LedStripConfig>,
) -> Result<(), String> {
ambient_light::LedStripConfig::write_config(&configs)
.await
.map_err(|e| {
error!("can not write led strip configs: {}", e);
e.to_string()
})
}
#[tauri::command]
async fn get_led_strips_sample_points(
config: LedStripConfig,
) -> Result<Vec<screenshot::LedSamplePoints>, String> {
let displays = DisplayInfo::all().map_err(|e| {
error!("can not read led strip config: {}", e);
e.to_string()
});
let display = displays?.into_iter().find(|d| d.id == config.display_id);
if let None = display {
return Err(format!("display not found: {}", config.display_id));
}
let display = display.unwrap();
let config = screenshot::Screenshot::get_sample_point(
config,
display.width as usize,
display.height as usize,
);
Ok(config)
}
#[tauri::command]
async fn get_one_edge_colors(
display_id: u32,
sample_points: Vec<screenshot::LedSamplePoints>,
) -> Result<Vec<led_color::LedColor>, String> {
let screenshot_manager = ScreenshotManager::global().await;
let channels = screenshot_manager.channels.read().await;
if let Some(rx) = channels.get(&display_id) {
let rx = rx.clone();
let screenshot = rx.borrow().clone();
let bytes = screenshot.bytes.read().await;
let colors =
Screenshot::get_one_edge_colors(&sample_points, &bytes, screenshot.bytes_per_row);
Ok(colors)
} else {
Err(format!("display not found: {}", display_id))
}
}
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
env_logger::init(); env_logger::init();
@ -162,9 +146,12 @@ async fn main() {
tauri::Builder::default() tauri::Builder::default()
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
greet, greet,
take_screenshot,
list_display_info, list_display_info,
subscribe_encoded_screenshot_updated subscribe_encoded_screenshot_updated,
read_led_strip_configs,
write_led_strip_configs,
get_led_strips_sample_points,
get_one_edge_colors
]) ])
.register_uri_scheme_protocol("ambient-light", move |_app, request| { .register_uri_scheme_protocol("ambient-light", move |_app, request| {
let response = ResponseBuilder::new().header("Access-Control-Allow-Origin", "*"); let response = ResponseBuilder::new().header("Access-Control-Allow-Origin", "*");
@ -212,20 +199,33 @@ async fn main() {
let screenshot = rx.borrow().clone(); let screenshot = rx.borrow().clone();
let bytes = screenshot.bytes.read().await; let bytes = screenshot.bytes.read().await;
let (scale_factor, width, height) = if url.query.is_some() let (scale_factor_x, scale_factor_y, width, height) = if url.query.is_some()
&& url.query.as_ref().unwrap().contains_key("height") && url.query.as_ref().unwrap().contains_key("height")
&& url.query.as_ref().unwrap().contains_key("width") && url.query.as_ref().unwrap().contains_key("width")
{ {
let width = let width = url.query.as_ref().unwrap()["width"]
url.query.as_ref().unwrap()["width"].parse::<u32>().unwrap(); .parse::<u32>()
.map_err(|err| {
warn!("width parse error: {}", err);
err
})?;
let height = url.query.as_ref().unwrap()["height"] let height = url.query.as_ref().unwrap()["height"]
.parse::<u32>() .parse::<u32>()
.unwrap(); .map_err(|err| {
(screenshot.width as f32 / width as f32, width, height) warn!("height parse error: {}", err);
err
})?;
(
screenshot.width as f32 / width as f32,
screenshot.height as f32 / height as f32,
width,
height,
)
} else { } else {
log::debug!("scale by scale_factor"); log::debug!("scale by scale_factor");
let scale_factor = screenshot.scale_factor; let scale_factor = screenshot.scale_factor;
( (
scale_factor,
scale_factor, scale_factor,
(screenshot.width as f32 / scale_factor) as u32, (screenshot.width as f32 / scale_factor) as u32,
(screenshot.height as f32 / scale_factor) as u32, (screenshot.height as f32 / scale_factor) as u32,
@ -245,8 +245,9 @@ async fn main() {
for y in 0..height { for y in 0..height {
for x in 0..width { for x in 0..width {
let offset = ((y as f32) * scale_factor) as usize * bytes_per_row as usize let offset = ((y as f32) * scale_factor_y).floor() as usize
+ ((x as f32) * scale_factor) as usize * 4; * bytes_per_row as usize
+ ((x as f32) * scale_factor_x).floor() as usize * 4;
let b = bytes[offset]; let b = bytes[offset];
let g = bytes[offset + 1]; let g = bytes[offset + 1];
let r = bytes[offset + 2]; let r = bytes[offset + 2];

View File

@ -1,8 +1,14 @@
use std::iter;
use std::sync::Arc; use std::sync::Arc;
use serde::Serialize; use serde::{Deserialize, Serialize};
use tauri::async_runtime::RwLock; use tauri::async_runtime::RwLock;
use crate::{
ambient_light::{DisplayConfig, LedStripConfig},
led_color::LedColor,
};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Screenshot { pub struct Screenshot {
pub display_id: u32, pub display_id: u32,
@ -11,6 +17,7 @@ pub struct Screenshot {
pub bytes_per_row: usize, pub bytes_per_row: usize,
pub bytes: Arc<RwLock<Vec<u8>>>, pub bytes: Arc<RwLock<Vec<u8>>>,
pub scale_factor: f32, pub scale_factor: f32,
pub sample_points: ScreenSamplePoints,
} }
impl Screenshot { impl Screenshot {
@ -21,6 +28,7 @@ impl Screenshot {
bytes_per_row: usize, bytes_per_row: usize,
bytes: Vec<u8>, bytes: Vec<u8>,
scale_factor: f32, scale_factor: f32,
sample_points: ScreenSamplePoints,
) -> Self { ) -> Self {
Self { Self {
display_id, display_id,
@ -29,14 +37,251 @@ impl Screenshot {
bytes_per_row, bytes_per_row,
bytes: Arc::new(RwLock::new(bytes)), bytes: Arc::new(RwLock::new(bytes)),
scale_factor, scale_factor,
sample_points,
}
}
pub fn get_sample_point(
config: LedStripConfig,
width: usize,
height: usize,
) -> Vec<LedSamplePoints> {
match config.border {
crate::ambient_light::Border::Top => {
Self::get_one_edge_sample_points(height / 8, width, config.len, 5)
}
crate::ambient_light::Border::Bottom => {
let points = Self::get_one_edge_sample_points(height / 9, width, config.len, 1);
points
.into_iter()
.map(|groups| -> Vec<Point> {
groups.into_iter().map(|(x, y)| (x, height - y)).collect()
})
.collect()
}
crate::ambient_light::Border::Left => {
let points = Self::get_one_edge_sample_points(width / 16, height, config.len, 1);
points
.into_iter()
.map(|groups| -> Vec<Point> {
groups.into_iter().map(|(x, y)| (y, x)).collect()
})
.collect()
}
crate::ambient_light::Border::Right => {
let points = Self::get_one_edge_sample_points(width / 16, height, config.len, 1);
points
.into_iter()
.map(|groups| -> Vec<Point> {
groups.into_iter().map(|(x, y)| (width - y, x)).collect()
})
.collect()
} }
} }
} }
fn get_sample_points(config: DisplayConfig) -> ScreenSamplePoints {
let top = match config.led_strip_of_borders.top {
Some(led_strip_config) => Self::get_one_edge_sample_points(
config.display_height / 8,
config.display_width,
led_strip_config.len,
1,
),
None => {
vec![]
}
};
let bottom: Vec<LedSamplePoints> = match config.led_strip_of_borders.bottom {
Some(led_strip_config) => {
let points = Self::get_one_edge_sample_points(
config.display_height / 9,
config.display_width,
led_strip_config.len,
5,
);
points
.into_iter()
.map(|groups| -> Vec<Point> {
groups
.into_iter()
.map(|(x, y)| (x, config.display_height - y))
.collect()
})
.collect()
}
None => {
vec![]
}
};
let left: Vec<LedSamplePoints> = match config.led_strip_of_borders.left {
Some(led_strip_config) => {
let points = Self::get_one_edge_sample_points(
config.display_width / 16,
config.display_height,
led_strip_config.len,
5,
);
points
.into_iter()
.map(|groups| -> Vec<Point> {
groups.into_iter().map(|(x, y)| (y, x)).collect()
})
.collect()
}
None => {
vec![]
}
};
let right: Vec<LedSamplePoints> = match config.led_strip_of_borders.right {
Some(led_strip_config) => {
let points = Self::get_one_edge_sample_points(
config.display_width / 16,
config.display_height,
led_strip_config.len,
5,
);
points
.into_iter()
.map(|groups| -> Vec<Point> {
groups
.into_iter()
.map(|(x, y)| (config.display_width - y, x))
.collect()
})
.collect()
}
None => {
vec![]
}
};
ScreenSamplePoints {
top,
bottom,
left,
right,
}
}
fn get_one_edge_sample_points(
width: usize,
length: usize,
leds: usize,
single_axis_points: usize,
) -> Vec<LedSamplePoints> {
let cell_size_x = length as f64 / single_axis_points as f64 / leds as f64;
let cell_size_y = width / single_axis_points;
let point_start_y = cell_size_y / 2;
let point_start_x = cell_size_x / 2.0;
let point_y_list: Vec<usize> = (point_start_y..width).step_by(cell_size_y).collect();
let point_x_list: Vec<usize> = iter::successors(Some(point_start_x), |i| {
let next = i + cell_size_x;
(next < (length as f64)).then_some(next)
})
.map(|i| i as usize)
.collect();
let points: Vec<Point> = point_x_list
.iter()
.map(|&x| point_y_list.iter().map(move |&y| (x, y)))
.flatten()
.collect();
points
.chunks(single_axis_points * single_axis_points)
.into_iter()
.map(|points| Vec::from(points))
.collect()
}
pub async fn get_colors(&self) -> DisplayColorsOfLedStrips {
let bitmap = self.bytes.read().await;
let top =
Self::get_one_edge_colors(&self.sample_points.top, bitmap.as_ref(), self.bytes_per_row)
.into_iter()
.flat_map(|color| color.get_rgb())
.collect();
let bottom = Self::get_one_edge_colors(
&self.sample_points.bottom,
bitmap.as_ref(),
self.bytes_per_row,
)
.into_iter()
.flat_map(|color| color.get_rgb())
.collect();
let left = Self::get_one_edge_colors(
&self.sample_points.left,
bitmap.as_ref(),
self.bytes_per_row,
)
.into_iter()
.flat_map(|color| color.get_rgb())
.collect();
let right = Self::get_one_edge_colors(
&self.sample_points.right,
bitmap.as_ref(),
self.bytes_per_row,
)
.into_iter()
.flat_map(|color| color.get_rgb())
.collect();
DisplayColorsOfLedStrips {
top,
bottom,
left,
right,
}
}
pub fn get_one_edge_colors(
sample_points_of_leds: &Vec<LedSamplePoints>,
bitmap: &Vec<u8>,
bytes_per_row: usize,
) -> Vec<LedColor> {
let mut colors = vec![];
for led_points in sample_points_of_leds {
let mut r = 0.0;
let mut g = 0.0;
let mut b = 0.0;
let len = led_points.len() as f64;
for (x, y) in led_points {
let position = x * 4 + y * bytes_per_row;
b += bitmap[position] as f64;
g += bitmap[position + 1] as f64;
r += bitmap[position + 2] as f64;
}
let color = LedColor::new((r / len) as u8, (g / len) as u8, (b / len) as u8);
// paris::info!("color: {:?}", color.get_rgb());
colors.push(color);
}
colors
}
}
type Point = (usize, usize);
pub type LedSamplePoints = Vec<Point>;
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct ScreenSamplePoints {
pub top: Vec<LedSamplePoints>,
pub bottom: Vec<LedSamplePoints>,
pub left: Vec<LedSamplePoints>,
pub right: Vec<LedSamplePoints>,
}
pub struct DisplayColorsOfLedStrips {
pub top: Vec<u8>,
pub bottom: Vec<u8>,
pub left: Vec<u8>,
pub right: Vec<u8>,
}
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]
pub struct ScreenshotPayload { pub struct ScreenshotPayload {
pub display_id: u32, pub display_id: u32,
pub height: u32, pub height: u32,
pub width: u32, pub width: u32,
// pub base64_image: String,
} }

View File

@ -1,6 +1,5 @@
use std::{collections::HashMap, sync::Arc}; use std::{collections::HashMap, sync::Arc};
use base64::Engine;
use core_graphics::display::{ use core_graphics::display::{
kCGNullWindowID, kCGWindowImageDefault, kCGWindowListOptionOnScreenOnly, CGDisplay, kCGNullWindowID, kCGWindowImageDefault, kCGWindowListOptionOnScreenOnly, CGDisplay,
}; };
@ -8,7 +7,7 @@ use paris::{error, info, warn};
use tauri::{async_runtime::RwLock, Window}; use tauri::{async_runtime::RwLock, Window};
use tokio::sync::{watch, OnceCell}; use tokio::sync::{watch, OnceCell};
use crate::screenshot::{Screenshot, ScreenshotPayload}; use crate::screenshot::{Screenshot, ScreenshotPayload, ScreenSamplePoints};
pub fn take_screenshot(display_id: u32, scale_factor: f32) -> anyhow::Result<Screenshot> { pub fn take_screenshot(display_id: u32, scale_factor: f32) -> anyhow::Result<Screenshot> {
log::debug!("take_screenshot"); log::debug!("take_screenshot");
@ -40,6 +39,7 @@ pub fn take_screenshot(display_id: u32, scale_factor: f32) -> anyhow::Result<Scr
bytes_per_row, bytes_per_row,
bytes, bytes,
scale_factor, scale_factor,
ScreenSamplePoints { top: vec![], bottom: vec![], left: vec![], right: vec![] }
)) ))
} }
@ -205,48 +205,4 @@ impl ScreenshotManager {
} }
} }
async fn encode_screenshot_to_base64(screenshot: &Screenshot) -> anyhow::Result<String> {
let bytes = screenshot.bytes.read().await;
let scale_factor = screenshot.scale_factor;
let image_height = (screenshot.height as f32 / scale_factor) as u32;
let image_width = (screenshot.width as f32 / scale_factor) as u32;
let mut image_buffer = vec![0u8; (image_width * image_height * 3) as usize];
for y in 0..image_height {
for x in 0..image_width {
let offset = (((y as f32) * screenshot.bytes_per_row as f32 + (x as f32) * 4.0)
* scale_factor) as usize;
let b = bytes[offset];
let g = bytes[offset + 1];
let r = bytes[offset + 2];
let offset = (y * image_width + x) as usize;
image_buffer[offset * 3] = r;
image_buffer[offset * 3 + 1] = g;
image_buffer[offset * 3 + 2] = b;
}
}
let mut image_png = Vec::new();
let mut encoder = png::Encoder::new(&mut image_png, image_width, image_height);
encoder.set_color(png::ColorType::Rgb);
encoder.set_depth(png::BitDepth::Eight);
let mut writer = encoder
.write_header()
.map_err(|e| anyhow::anyhow!("png: {}", anyhow::anyhow!(e.to_string())))?;
writer
.write_image_data(&image_buffer)
.map_err(|e| anyhow::anyhow!("png: {}", anyhow::anyhow!(e.to_string())))?;
writer
.finish()
.map_err(|e| anyhow::anyhow!("png: {}", anyhow::anyhow!(e.to_string())))?;
let mut base64_image = String::new();
base64_image.push_str("data:image/webp;base64,");
let encoded = base64::engine::general_purpose::STANDARD_NO_PAD.encode(&*image_png);
base64_image.push_str(encoded.as_str());
Ok(base64_image)
}
} }

View File

@ -4,6 +4,8 @@ import { DisplayView } from './components/display-view';
import { DisplayListContainer } from './components/display-list-container'; import { DisplayListContainer } from './components/display-list-container';
import { displayStore, setDisplayStore } from './stores/display.store'; import { displayStore, setDisplayStore } from './stores/display.store';
import { path } from '@tauri-apps/api'; import { path } from '@tauri-apps/api';
import { LedStripConfig } from './models/led-strip-config';
import { setLedStripStore } from './stores/led-strip.store';
function App() { function App() {
createEffect(() => { createEffect(() => {
@ -12,6 +14,13 @@ function App() {
displays: JSON.parse(displays), displays: JSON.parse(displays),
}); });
}); });
invoke<LedStripConfig[]>('read_led_strip_configs').then((strips) => {
console.log(strips);
setLedStripStore({
strips,
});
});
}); });
return ( return (

View File

@ -1,7 +1,10 @@
import { Component, createMemo } from 'solid-js'; import { Component, createMemo } from 'solid-js';
import { LedStripConfigOfBorders } from '../models/display-config';
import { DisplayInfo } from '../models/display-info.model'; import { DisplayInfo } from '../models/display-info.model';
import { displayStore } from '../stores/display.store'; import { displayStore } from '../stores/display.store';
import { ledStripStore } from '../stores/led-strip.store';
import { DisplayInfoPanel } from './display-info-panel'; import { DisplayInfoPanel } from './display-info-panel';
import { LedStripPart } from './led-strip-part';
import { ScreenView } from './screen-view'; import { ScreenView } from './screen-view';
type DisplayViewProps = { type DisplayViewProps = {
@ -19,6 +22,12 @@ export const DisplayView: Component<DisplayViewProps> = (props) => {
height: `${size().height}px`, height: `${size().height}px`,
width: `${size().width}px`, width: `${size().width}px`,
})); }));
const ledStripConfigs = createMemo(() => {
console.log('ledStripConfigs', ledStripStore.strips);
return ledStripStore.strips.filter((c) => c.display_id === props.display.id);
});
return ( return (
<section <section
class="absolute bg-gray-300 grid grid-cols-[16px,auto,16px] grid-rows-[16px,auto,16px] overflow-hidden" class="absolute bg-gray-300 grid grid-cols-[16px,auto,16px] grid-rows-[16px,auto,16px] overflow-hidden"
@ -35,10 +44,22 @@ export const DisplayView: Component<DisplayViewProps> = (props) => {
display={props.display} display={props.display}
class="absolute bg-slate-50/10 top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 rounded backdrop-blur w-1/3 min-w-[300px] text-black" class="absolute bg-slate-50/10 top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 rounded backdrop-blur w-1/3 min-w-[300px] text-black"
/> />
<div class="row-start-1 col-start-2">Test</div> <LedStripPart
<div class="row-start-2 col-start-1">Test</div> class="row-start-1 col-start-2 flex-row"
<div class="row-start-2 col-start-3">Test</div> config={ledStripConfigs().find((c) => c.border === 'Top')}
<div class="row-start-3 col-start-2">Test</div> />
<LedStripPart
class="row-start-2 col-start-1 flex-col"
config={ledStripConfigs().find((c) => c.border === 'Left')}
/>
<LedStripPart
class="row-start-2 col-start-3 flex-col"
config={ledStripConfigs().find((c) => c.border === 'Right')}
/>
<LedStripPart
class="row-start-3 col-start-2 flex-row"
config={ledStripConfigs().find((c) => c.border === 'Bottom')}
/>
</section> </section>
); );
}; };

View File

@ -0,0 +1,130 @@
import { invoke } from '@tauri-apps/api';
import { listen } from '@tauri-apps/api/event';
import {
Component,
createEffect,
createMemo,
createSignal,
For,
JSX,
on,
onCleanup,
splitProps,
} from 'solid-js';
import { borders } from '../constants/border';
import { LedStripConfig } from '../models/led-strip-config';
type LedStripPartProps = {
config?: LedStripConfig | null;
} & JSX.HTMLAttributes<HTMLElement>;
type PixelProps = {
color: string;
};
async function subscribeScreenshotUpdate(displayId: number) {
await invoke('subscribe_encoded_screenshot_updated', {
displayId,
});
}
export const Pixel: Component<PixelProps> = (props) => {
const style = createMemo(() => ({
background: props.color,
}));
return (
<div
class="inline-block flex-shrink w-2 h-2 aspect-square rounded-full border border-black"
style={style()}
title={props.color}
/>
);
};
export const LedStripPart: Component<LedStripPartProps> = (props) => {
const [localProps, rootProps] = splitProps(props, ['config']);
const [ledSamplePoints, setLedSamplePoints] = createSignal();
const [colors, setColors] = createSignal<string[]>([]);
createEffect(() => {
const samplePoints = ledSamplePoints();
if (!localProps.config || !samplePoints) {
return;
}
let pendingCount = 0;
const unlisten = listen<{
base64_image: string;
display_id: number;
height: number;
width: number;
}>('encoded-screenshot-updated', (event) => {
if (event.payload.display_id !== localProps.config!.display_id) {
return;
}
if (pendingCount >= 1) {
return;
}
pendingCount++;
console.log({
samplePoints,
displayId: event.payload.display_id,
border: localProps.config!.border,
});
invoke<string[]>('get_one_edge_colors', {
samplePoints,
displayId: event.payload.display_id,
})
.then((colors) => {
setColors(colors);
})
.finally(() => {
pendingCount--;
});
});
subscribeScreenshotUpdate(localProps.config.display_id);
onCleanup(() => {
unlisten.then((unlisten) => unlisten());
});
});
createEffect(() => {
if (localProps.config) {
invoke('get_led_strips_sample_points', {
config: localProps.config,
}).then((points) => {
console.log({ points });
setLedSamplePoints(points);
});
}
});
const pixels = createMemo(() => {
const _colors = colors();
if (_colors) {
return <For each={_colors}>{(item) => <Pixel color={item} />}</For>;
} else if (localProps.config) {
return null;
return (
<For each={new Array(localProps.config.len).fill(undefined)}>
{() => <Pixel color="transparent" />}
</For>
);
}
});
return (
<section
{...rootProps}
class={
'bg-yellow-50 flex flex-nowrap justify-around items-center overflow-hidden' +
rootProps.class
}
>
{pixels()}
</section>
);
};

View File

@ -65,7 +65,7 @@ export const ScreenView: Component<ScreenViewProps> = (props) => {
}; };
const draw = (cached: boolean = false) => { const draw = (cached: boolean = false) => {
const { drawX, drawY, drawWidth, drawHeight } = drawInfo(); const { drawX, drawY } = drawInfo();
let _ctx = ctx(); let _ctx = ctx();
let raw = imageData(); let raw = imageData();
@ -82,6 +82,7 @@ export const ScreenView: Component<ScreenViewProps> = (props) => {
}; };
createEffect(() => { createEffect(() => {
let pendingCount = 0;
const unlisten = listen<{ const unlisten = listen<{
base64_image: string; base64_image: string;
display_id: number; display_id: number;
@ -94,12 +95,15 @@ export const ScreenView: Component<ScreenViewProps> = (props) => {
`displays/${localProps.displayId}?width=${drawWidth}&height=${drawHeight}`, `displays/${localProps.displayId}?width=${drawWidth}&height=${drawHeight}`,
'ambient-light', 'ambient-light',
); );
if (pendingCount >= 1) {
return;
}
pendingCount++;
fetch(url, { fetch(url, {
mode: 'cors', mode: 'cors',
}) })
.then((res) => res.body?.getReader().read()) .then((res) => res.body?.getReader().read())
.then((buffer) => { .then((buffer) => {
// console.log(buffer?.value?.length);
if (buffer?.value) { if (buffer?.value) {
setImageData({ setImageData({
buffer: new Uint8ClampedArray(buffer?.value), buffer: new Uint8ClampedArray(buffer?.value),
@ -110,6 +114,9 @@ export const ScreenView: Component<ScreenViewProps> = (props) => {
setImageData(null); setImageData(null);
} }
draw(); draw();
})
.finally(() => {
pendingCount--;
}); });
} }

2
src/constants/border.ts Normal file
View File

@ -0,0 +1,2 @@
export const borders = ['Top', 'Right', 'Bottom', 'Left'] as const;
export type Borders = typeof borders[number];

View File

@ -0,0 +1,16 @@
import { Borders } from '../constants/border';
import { LedStripConfig } from './led-strip-config';
export class LedStripConfigOfBorders implements Record<Borders, LedStripConfig | null> {
constructor(
public top: LedStripConfig | null = null,
public bottom: LedStripConfig | null = null,
public left: LedStripConfig | null = null,
public right: LedStripConfig | null = null,
) {}
}
export class DisplayConfig {
led_strip_of_borders = new LedStripConfigOfBorders();
constructor(public id: number) {}
}

View File

@ -0,0 +1,10 @@
import { Borders } from '../constants/border';
export class LedStripConfig {
constructor(
public readonly display_id: number,
public readonly border: Borders,
public start_pos: number,
public len: number,
) {}
}

View File

@ -0,0 +1,8 @@
import { DisplayConfig } from './display-config';
export class PickerConfiguration {
constructor(
public display_configs: DisplayConfig[] = [],
public config_version: number = 1,
) {}
}

1
src/models/pixel-rgb.ts Normal file
View File

@ -0,0 +1 @@
export type PixelRgb = [number, number, number];

View File

@ -0,0 +1,12 @@
import { DisplayConfig } from './display-config';
export class ScreenshotDto {
encode_image!: string;
config!: DisplayConfig;
colors!: {
top: Uint8Array;
bottom: Uint8Array;
left: Uint8Array;
right: Uint8Array;
};
}

View File

@ -0,0 +1,8 @@
import { createStore } from 'solid-js/store';
import { DisplayConfig } from '../models/display-config';
import { LedStripConfig } from '../models/led-strip-config';
export const [ledStripStore, setLedStripStore] = createStore({
displays: new Array<DisplayConfig>(),
strips: new Array<LedStripConfig>(),
});