musealpha / src-tauri /src /color_tools.rs
asdf98's picture
Upload 112 files
3d7d9b5 verified
use serde::{Deserialize, Serialize};
use tauri::{AppHandle, Manager};
/// Export palette colors to various artist application formats
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ColorExportResult {
pub format: String,
pub content: String,
pub filename: String,
}
#[tauri::command]
pub fn color_export(colors: Vec<String>, format: String) -> Result<ColorExportResult, String> {
let content = match format.as_str() {
"hex" => colors.join("\n"),
"css" => colors.iter().enumerate()
.map(|(i, c)| format!(" --color-{}: {};", i + 1, c))
.collect::<Vec<_>>()
.join("\n")
.pipe(|body| format!(":root {{\n{body}\n}}")),
"gpl" => {
let mut lines = vec!["GIMP Palette".to_string(), "Name: Muse Export".to_string(), "#".to_string()];
for color in &colors {
if let Some((r, g, b)) = parse_hex(color) {
lines.push(format!("{r:3} {g:3} {b:3}\t{color}"));
}
}
lines.join("\n")
}
"ase" => {
let mut lines = vec!["// Adobe Swatch Exchange (text preview)".to_string()];
for color in &colors {
if let Some((r, g, b)) = parse_hex(color) {
lines.push(format!("RGB {:.4} {:.4} {:.4} // {color}", r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0));
}
}
lines.join("\n")
}
"procreate" => {
let swatches: Vec<serde_json::Value> = colors.iter().filter_map(|c| {
parse_hex(c).map(|(r, g, b)| serde_json::json!({
"hue": 0, "saturation": 0, "brightness": 0,
"alpha": 1, "colorSpace": 0,
"components": [r as f64 / 255.0, g as f64 / 255.0, b as f64 / 255.0, 1.0]
}))
}).collect();
serde_json::to_string_pretty(&serde_json::json!({
"name": "Muse Export",
"swatches": swatches
})).unwrap_or_default()
}
_ => colors.join("\n"),
};
let ext = match format.as_str() {
"css" => "css", "gpl" => "gpl", "ase" => "ase", "procreate" => "swatches", _ => "txt"
};
Ok(ColorExportResult {
format: format.clone(),
content,
filename: format!("muse-palette.{ext}"),
})
}
#[tauri::command]
pub fn color_search_library(app: AppHandle, hue_min: f32, hue_max: f32) -> Result<Vec<crate::library::LibraryItem>, String> {
let state = app.state::<crate::library::LibraryState>();
let items = state.items.lock().map_err(|_| "library lock poisoned")?;
Ok(items.iter().filter(|item| {
item.colors.iter().any(|c| {
if let Some((r, g, b)) = parse_hex(c) {
let hue = rgb_to_hue(r, g, b);
if hue_min <= hue_max {
hue >= hue_min && hue <= hue_max
} else {
hue >= hue_min || hue <= hue_max
}
} else { false }
})
}).cloned().collect())
}
fn parse_hex(hex: &str) -> Option<(u8, u8, u8)> {
let h = hex.trim_start_matches('#');
if h.len() != 6 { return None; }
let r = u8::from_str_radix(&h[0..2], 16).ok()?;
let g = u8::from_str_radix(&h[2..4], 16).ok()?;
let b = u8::from_str_radix(&h[4..6], 16).ok()?;
Some((r, g, b))
}
fn rgb_to_hue(r: u8, g: u8, b: u8) -> f32 {
let rf = r as f32 / 255.0;
let gf = g as f32 / 255.0;
let bf = b as f32 / 255.0;
let max = rf.max(gf).max(bf);
let min = rf.min(gf).min(bf);
if max == min { return 0.0; }
let d = max - min;
let h = if max == rf { ((gf - bf) / d) % 6.0 }
else if max == gf { (bf - rf) / d + 2.0 }
else { (rf - gf) / d + 4.0 };
((h * 60.0) + 360.0) % 360.0
}
trait PipeExt { fn pipe(self, f: impl FnOnce(String) -> String) -> String; }
impl PipeExt for String { fn pipe(self, f: impl FnOnce(String) -> String) -> String { f(self) } }