|
|
|
@@ -0,0 +1,688 @@
|
|
|
|
|
//! Filter parameter panels — per-filter parameter editing UI.
|
|
|
|
|
//!
|
|
|
|
|
//! **Purpose:** Provides slider/input controls for each of the 25+ filter types,
|
|
|
|
|
//! allowing users to tune filter parameters in real-time with live preview.
|
|
|
|
|
//!
|
|
|
|
|
//! **Pattern:** Each filter gets a dedicated view function that reads current
|
|
|
|
|
//! parameter values from a `serde_json::Value` and emits
|
|
|
|
|
//! `Message::FilterParamChanged(key, value)` on user interaction.
|
|
|
|
|
//! Default values match the engine's `unwrap_or` fallbacks for consistency.
|
|
|
|
|
|
|
|
|
|
use crate::app::Message;
|
|
|
|
|
use crate::theme::ThemeColors;
|
|
|
|
|
use hcie_engine_api::FilterType;
|
|
|
|
|
use iced::widget::{column, container, row, slider, text};
|
|
|
|
|
use iced::{Element, Length};
|
|
|
|
|
use serde_json::json;
|
|
|
|
|
|
|
|
|
|
/// Read a float parameter from the JSON params, falling back to a default.
|
|
|
|
|
fn param_f64(params: &serde_json::Value, key: &str, default: f64) -> f64 {
|
|
|
|
|
params
|
|
|
|
|
.get(key)
|
|
|
|
|
.and_then(|v| v.as_f64())
|
|
|
|
|
.unwrap_or(default)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Read an integer parameter from the JSON params, falling back to a default.
|
|
|
|
|
fn param_u64(params: &serde_json::Value, key: &str, default: u64) -> u64 {
|
|
|
|
|
params
|
|
|
|
|
.get(key)
|
|
|
|
|
.and_then(|v| v.as_u64())
|
|
|
|
|
.unwrap_or(default)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Read a boolean parameter from the JSON params, falling back to a default.
|
|
|
|
|
fn param_bool(params: &serde_json::Value, key: &str, default: bool) -> bool {
|
|
|
|
|
params
|
|
|
|
|
.get(key)
|
|
|
|
|
.and_then(|v| v.as_bool())
|
|
|
|
|
.unwrap_or(default)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Read a string parameter from the JSON params, falling back to a default.
|
|
|
|
|
fn param_str<'a>(params: &'a serde_json::Value, key: &str, default: &'a str) -> &'a str {
|
|
|
|
|
params
|
|
|
|
|
.get(key)
|
|
|
|
|
.and_then(|v| v.as_str())
|
|
|
|
|
.unwrap_or(default)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Build a labeled float slider row.
|
|
|
|
|
///
|
|
|
|
|
/// Returns a row with label, slider, and numeric display. The slider
|
|
|
|
|
/// emits `Message::FilterParamChanged(filter_id, json!({key: new_value}))`
|
|
|
|
|
/// when the user drags it.
|
|
|
|
|
fn float_slider(
|
|
|
|
|
label: &str,
|
|
|
|
|
key: &str,
|
|
|
|
|
filter_id: &str,
|
|
|
|
|
value: f64,
|
|
|
|
|
min: f64,
|
|
|
|
|
max: f64,
|
|
|
|
|
step: f64,
|
|
|
|
|
_colors: ThemeColors,
|
|
|
|
|
) -> Element<'static, Message> {
|
|
|
|
|
let val = value as f32;
|
|
|
|
|
let min_f = min as f32;
|
|
|
|
|
let max_f = max as f32;
|
|
|
|
|
let step_f = step as f32;
|
|
|
|
|
let key_owned = key.to_string();
|
|
|
|
|
let filter_owned = filter_id.to_string();
|
|
|
|
|
let label_owned = label.to_string();
|
|
|
|
|
|
|
|
|
|
let lbl = text(label_owned).size(11).width(Length::Fixed(80.0));
|
|
|
|
|
let display = text(format!("{:.1}", value)).size(11).width(Length::Fixed(40.0));
|
|
|
|
|
|
|
|
|
|
let s = slider(min_f..=max_f, val, move |v| {
|
|
|
|
|
let rounded = (v as f64 / step).round() * step;
|
|
|
|
|
Message::FilterParamChanged(
|
|
|
|
|
filter_owned.clone(),
|
|
|
|
|
json!({ key_owned.clone(): rounded }),
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
.step(step_f)
|
|
|
|
|
.width(Length::Fill);
|
|
|
|
|
|
|
|
|
|
row![lbl, s, display].spacing(4).into()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Build a labeled integer slider row.
|
|
|
|
|
fn int_slider(
|
|
|
|
|
label: &str,
|
|
|
|
|
key: &str,
|
|
|
|
|
filter_id: &str,
|
|
|
|
|
value: u64,
|
|
|
|
|
min: u64,
|
|
|
|
|
max: u64,
|
|
|
|
|
_colors: ThemeColors,
|
|
|
|
|
) -> Element<'static, Message> {
|
|
|
|
|
let val = value as f32;
|
|
|
|
|
let min_f = min as f32;
|
|
|
|
|
let max_f = max as f32;
|
|
|
|
|
let key_owned = key.to_string();
|
|
|
|
|
let filter_owned = filter_id.to_string();
|
|
|
|
|
let label_owned = label.to_string();
|
|
|
|
|
|
|
|
|
|
let lbl = text(label_owned).size(11).width(Length::Fixed(80.0));
|
|
|
|
|
let display = text(format!("{}", value)).size(11).width(Length::Fixed(40.0));
|
|
|
|
|
|
|
|
|
|
let s = slider(min_f..=max_f, val, move |v| {
|
|
|
|
|
Message::FilterParamChanged(
|
|
|
|
|
filter_owned.clone(),
|
|
|
|
|
json!({ key_owned.clone(): v.round() as u64 }),
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
.step(1.0)
|
|
|
|
|
.width(Length::Fill);
|
|
|
|
|
|
|
|
|
|
row![lbl, s, display].spacing(4).into()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Build a section header for a group of parameters.
|
|
|
|
|
fn section_header(label: &str, colors: ThemeColors) -> Element<'static, Message> {
|
|
|
|
|
let label_owned = label.to_string();
|
|
|
|
|
container(text(label_owned).size(11).style(move |_theme: &iced::Theme| iced::widget::text::Style {
|
|
|
|
|
color: Some(colors.text_secondary),
|
|
|
|
|
}))
|
|
|
|
|
.padding(iced::Padding::default().top(4.0).bottom(2.0))
|
|
|
|
|
.into()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Build the parameter panel for a specific filter type.
|
|
|
|
|
///
|
|
|
|
|
/// Returns a column of slider/input widgets tailored to the filter's parameters.
|
|
|
|
|
/// The panel reads current values from `params` and emits
|
|
|
|
|
/// `Message::FilterParamChanged` on user interaction.
|
|
|
|
|
pub fn view(
|
|
|
|
|
filter: FilterType,
|
|
|
|
|
params: &serde_json::Value,
|
|
|
|
|
colors: ThemeColors,
|
|
|
|
|
) -> Element<'static, Message> {
|
|
|
|
|
let content: Element<'static, Message> = match filter {
|
|
|
|
|
// ── Blur ─────────────────────────────────────────────────────────
|
|
|
|
|
FilterType::BoxBlur | FilterType::BoxBlurGamma => {
|
|
|
|
|
let filter_id = if filter == FilterType::BoxBlur {
|
|
|
|
|
"box_blur"
|
|
|
|
|
} else {
|
|
|
|
|
"box_blur_gamma"
|
|
|
|
|
};
|
|
|
|
|
let radius = param_u64(params, "radius", 3);
|
|
|
|
|
column![
|
|
|
|
|
section_header("Box Blur", colors),
|
|
|
|
|
int_slider("Radius", "radius", filter_id, radius, 1, 100, colors),
|
|
|
|
|
]
|
|
|
|
|
.spacing(2)
|
|
|
|
|
.into()
|
|
|
|
|
}
|
|
|
|
|
FilterType::GaussianBlur | FilterType::GaussianBlurGamma => {
|
|
|
|
|
let filter_id = if filter == FilterType::GaussianBlur {
|
|
|
|
|
"gaussian_blur"
|
|
|
|
|
} else {
|
|
|
|
|
"gaussian_blur_gamma"
|
|
|
|
|
};
|
|
|
|
|
let radius = param_f64(params, "radius", 2.0);
|
|
|
|
|
column![
|
|
|
|
|
section_header("Gaussian Blur", colors),
|
|
|
|
|
float_slider("Radius", "radius", filter_id, radius, 0.1, 50.0, 0.1, colors),
|
|
|
|
|
]
|
|
|
|
|
.spacing(2)
|
|
|
|
|
.into()
|
|
|
|
|
}
|
|
|
|
|
FilterType::MotionBlur => {
|
|
|
|
|
let angle = param_f64(params, "angle", 0.0);
|
|
|
|
|
let distance = param_u64(params, "distance", 10);
|
|
|
|
|
column![
|
|
|
|
|
section_header("Motion Blur", colors),
|
|
|
|
|
float_slider("Angle", "angle", "motion_blur", angle, 0.0, 360.0, 1.0, colors),
|
|
|
|
|
int_slider("Distance", "distance", "motion_blur", distance, 1, 500, colors),
|
|
|
|
|
]
|
|
|
|
|
.spacing(2)
|
|
|
|
|
.into()
|
|
|
|
|
}
|
|
|
|
|
FilterType::MedianFilter => {
|
|
|
|
|
let radius = param_u64(params, "radius", 1);
|
|
|
|
|
column![
|
|
|
|
|
section_header("Median Filter", colors),
|
|
|
|
|
int_slider("Radius", "radius", "median_filter", radius, 1, 20, colors),
|
|
|
|
|
]
|
|
|
|
|
.spacing(2)
|
|
|
|
|
.into()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Sharpen ──────────────────────────────────────────────────────
|
|
|
|
|
FilterType::UnsharpMask => {
|
|
|
|
|
let amount = param_f64(params, "amount", 1.0);
|
|
|
|
|
let radius = param_f64(params, "radius", 1.0);
|
|
|
|
|
column![
|
|
|
|
|
section_header("Unsharp Mask", colors),
|
|
|
|
|
float_slider("Amount", "amount", "sharpen", amount, 0.1, 5.0, 0.1, colors),
|
|
|
|
|
float_slider("Radius", "radius", "sharpen", radius, 0.1, 20.0, 0.1, colors),
|
|
|
|
|
]
|
|
|
|
|
.spacing(2)
|
|
|
|
|
.into()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Pixelate ─────────────────────────────────────────────────────
|
|
|
|
|
FilterType::Mosaic => {
|
|
|
|
|
let size = param_u64(params, "size", 8);
|
|
|
|
|
column![
|
|
|
|
|
section_header("Mosaic", colors),
|
|
|
|
|
int_slider("Size", "size", "mosaic", size, 2, 128, colors),
|
|
|
|
|
]
|
|
|
|
|
.spacing(2)
|
|
|
|
|
.into()
|
|
|
|
|
}
|
|
|
|
|
FilterType::Crystallize => {
|
|
|
|
|
let size = param_u64(params, "size", 16);
|
|
|
|
|
column![
|
|
|
|
|
section_header("Crystallize", colors),
|
|
|
|
|
int_slider("Cell Size", "size", "crystallize", size, 4, 128, colors),
|
|
|
|
|
]
|
|
|
|
|
.spacing(2)
|
|
|
|
|
.into()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Distort ──────────────────────────────────────────────────────
|
|
|
|
|
FilterType::Pinch => {
|
|
|
|
|
let amount = param_f64(params, "amount", 0.5);
|
|
|
|
|
column![
|
|
|
|
|
section_header("Pinch", colors),
|
|
|
|
|
float_slider("Amount", "amount", "pinch", amount, -1.0, 1.0, 0.01, colors),
|
|
|
|
|
]
|
|
|
|
|
.spacing(2)
|
|
|
|
|
.into()
|
|
|
|
|
}
|
|
|
|
|
FilterType::Twirl => {
|
|
|
|
|
let angle = param_f64(params, "angle", 45.0);
|
|
|
|
|
column![
|
|
|
|
|
section_header("Twirl", colors),
|
|
|
|
|
float_slider("Angle", "angle", "twirl", angle, -360.0, 360.0, 1.0, colors),
|
|
|
|
|
]
|
|
|
|
|
.spacing(2)
|
|
|
|
|
.into()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Stylize ──────────────────────────────────────────────────────
|
|
|
|
|
FilterType::OilPaint => {
|
|
|
|
|
let radius = param_u64(params, "radius", 3);
|
|
|
|
|
column![
|
|
|
|
|
section_header("Oil Paint", colors),
|
|
|
|
|
int_slider("Radius", "radius", "oil_paint", radius, 1, 20, colors),
|
|
|
|
|
]
|
|
|
|
|
.spacing(2)
|
|
|
|
|
.into()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Color & Light ────────────────────────────────────────────────
|
|
|
|
|
FilterType::Levels => {
|
|
|
|
|
let shadows = param_f64(params, "shadows", 0.0);
|
|
|
|
|
let midtones = param_f64(params, "midtones", 50.0);
|
|
|
|
|
let highlights = param_f64(params, "highlights", 100.0);
|
|
|
|
|
let out_min = param_f64(params, "output_min", 0.0);
|
|
|
|
|
let out_max = param_f64(params, "output_max", 100.0);
|
|
|
|
|
column![
|
|
|
|
|
section_header("Levels", colors),
|
|
|
|
|
float_slider("Shadows", "shadows", "levels", shadows, 0.0, 255.0, 1.0, colors),
|
|
|
|
|
float_slider("Midtones", "midtones", "levels", midtones, 0.0, 255.0, 1.0, colors),
|
|
|
|
|
float_slider("Highlights", "highlights", "levels", highlights, 0.0, 255.0, 1.0, colors),
|
|
|
|
|
float_slider("Out Min", "output_min", "levels", out_min, 0.0, 255.0, 1.0, colors),
|
|
|
|
|
float_slider("Out Max", "output_max", "levels", out_max, 0.0, 255.0, 1.0, colors),
|
|
|
|
|
]
|
|
|
|
|
.spacing(2)
|
|
|
|
|
.into()
|
|
|
|
|
}
|
|
|
|
|
FilterType::Vibrance => {
|
|
|
|
|
let vibrance = param_f64(params, "vibrance", 0.0);
|
|
|
|
|
column![
|
|
|
|
|
section_header("Vibrance", colors),
|
|
|
|
|
float_slider("Vibrance", "vibrance", "vibrance", vibrance, -100.0, 100.0, 1.0, colors),
|
|
|
|
|
]
|
|
|
|
|
.spacing(2)
|
|
|
|
|
.into()
|
|
|
|
|
}
|
|
|
|
|
FilterType::Exposure => {
|
|
|
|
|
let exposure = param_f64(params, "exposure", 0.0);
|
|
|
|
|
let offset = param_f64(params, "offset", 0.0);
|
|
|
|
|
let gamma = param_f64(params, "gamma_correction", 1.0);
|
|
|
|
|
column![
|
|
|
|
|
section_header("Exposure", colors),
|
|
|
|
|
float_slider("Exposure", "exposure", "exposure", exposure, -5.0, 5.0, 0.01, colors),
|
|
|
|
|
float_slider("Offset", "offset", "exposure", offset, -1.0, 1.0, 0.01, colors),
|
|
|
|
|
float_slider("Gamma", "gamma_correction", "exposure", gamma, 0.1, 5.0, 0.01, colors),
|
|
|
|
|
]
|
|
|
|
|
.spacing(2)
|
|
|
|
|
.into()
|
|
|
|
|
}
|
|
|
|
|
FilterType::Posterize => {
|
|
|
|
|
let levels = param_u64(params, "levels", 4);
|
|
|
|
|
column![
|
|
|
|
|
section_header("Posterize", colors),
|
|
|
|
|
int_slider("Levels", "levels", "posterize", levels, 2, 32, colors),
|
|
|
|
|
]
|
|
|
|
|
.spacing(2)
|
|
|
|
|
.into()
|
|
|
|
|
}
|
|
|
|
|
FilterType::Threshold => {
|
|
|
|
|
let min_lum = param_f64(params, "min_luminance", 50.0);
|
|
|
|
|
let max_lum = param_f64(params, "max_luminance", 100.0);
|
|
|
|
|
column![
|
|
|
|
|
section_header("Threshold", colors),
|
|
|
|
|
float_slider("Min Luminance", "min_luminance", "threshold", min_lum, 0.0, 100.0, 1.0, colors),
|
|
|
|
|
float_slider("Max Luminance", "max_luminance", "threshold", max_lum, 0.0, 100.0, 1.0, colors),
|
|
|
|
|
]
|
|
|
|
|
.spacing(2)
|
|
|
|
|
.into()
|
|
|
|
|
}
|
|
|
|
|
FilterType::ChannelMixer => {
|
|
|
|
|
let mono = param_bool(params, "monochrome", false);
|
|
|
|
|
let mono_r = param_f64(params, "monochrome_r", 40.0);
|
|
|
|
|
let mono_g = param_f64(params, "monochrome_g", 40.0);
|
|
|
|
|
let mono_b = param_f64(params, "monochrome_b", 20.0);
|
|
|
|
|
let mono_c = param_f64(params, "monochrome_constant", 0.0);
|
|
|
|
|
let mut col = column![
|
|
|
|
|
section_header("Channel Mixer", colors),
|
|
|
|
|
]
|
|
|
|
|
.spacing(2);
|
|
|
|
|
|
|
|
|
|
// Monochrome toggle
|
|
|
|
|
let filter_id = "channel_mixer";
|
|
|
|
|
{
|
|
|
|
|
let mono_val = mono;
|
|
|
|
|
let lbl = text("Monochrome").size(11).width(Length::Fixed(80.0));
|
|
|
|
|
let toggle_text = if mono_val { "ON" } else { "OFF" };
|
|
|
|
|
let toggle_btn = iced::widget::button(text(toggle_text).size(11))
|
|
|
|
|
.on_press(Message::FilterParamChanged(
|
|
|
|
|
filter_id.to_string(),
|
|
|
|
|
json!({ "monochrome": !mono_val }),
|
|
|
|
|
))
|
|
|
|
|
.padding([2, 8]);
|
|
|
|
|
col = col.push(row![lbl, toggle_btn].spacing(4));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if mono {
|
|
|
|
|
col = col.push(float_slider("Monochrome R", "monochrome_r", filter_id, mono_r, -200.0, 200.0, 1.0, colors));
|
|
|
|
|
col = col.push(float_slider("Monochrome G", "monochrome_g", filter_id, mono_g, -200.0, 200.0, 1.0, colors));
|
|
|
|
|
col = col.push(float_slider("Monochrome B", "monochrome_b", filter_id, mono_b, -200.0, 200.0, 1.0, colors));
|
|
|
|
|
col = col.push(float_slider("Constant", "monochrome_constant", filter_id, mono_c, -200.0, 200.0, 1.0, colors));
|
|
|
|
|
} else {
|
|
|
|
|
// RGB channels
|
|
|
|
|
let red_r = param_f64(params, "red_r", 100.0);
|
|
|
|
|
let red_g = param_f64(params, "red_g", 0.0);
|
|
|
|
|
let red_b = param_f64(params, "red_b", 0.0);
|
|
|
|
|
let red_c = param_f64(params, "red_constant", 0.0);
|
|
|
|
|
let green_r = param_f64(params, "green_r", 0.0);
|
|
|
|
|
let green_g = param_f64(params, "green_g", 100.0);
|
|
|
|
|
let green_b = param_f64(params, "green_b", 0.0);
|
|
|
|
|
let green_c = param_f64(params, "green_constant", 0.0);
|
|
|
|
|
let blue_r = param_f64(params, "blue_r", 0.0);
|
|
|
|
|
let blue_g = param_f64(params, "blue_g", 0.0);
|
|
|
|
|
let blue_b = param_f64(params, "blue_b", 100.0);
|
|
|
|
|
let blue_c = param_f64(params, "blue_constant", 0.0);
|
|
|
|
|
|
|
|
|
|
col = col.push(section_header("Red Channel", colors));
|
|
|
|
|
col = col.push(float_slider("Red→R", "red_r", filter_id, red_r, -200.0, 200.0, 1.0, colors));
|
|
|
|
|
col = col.push(float_slider("Red→G", "red_g", filter_id, red_g, -200.0, 200.0, 1.0, colors));
|
|
|
|
|
col = col.push(float_slider("Red→B", "red_b", filter_id, red_b, -200.0, 200.0, 1.0, colors));
|
|
|
|
|
col = col.push(float_slider("Red Const", "red_constant", filter_id, red_c, -200.0, 200.0, 1.0, colors));
|
|
|
|
|
|
|
|
|
|
col = col.push(section_header("Green Channel", colors));
|
|
|
|
|
col = col.push(float_slider("Green→R", "green_r", filter_id, green_r, -200.0, 200.0, 1.0, colors));
|
|
|
|
|
col = col.push(float_slider("Green→G", "green_g", filter_id, green_g, -200.0, 200.0, 1.0, colors));
|
|
|
|
|
col = col.push(float_slider("Green→B", "green_b", filter_id, green_b, -200.0, 200.0, 1.0, colors));
|
|
|
|
|
col = col.push(float_slider("Green Const", "green_constant", filter_id, green_c, -200.0, 200.0, 1.0, colors));
|
|
|
|
|
|
|
|
|
|
col = col.push(section_header("Blue Channel", colors));
|
|
|
|
|
col = col.push(float_slider("Blue→R", "blue_r", filter_id, blue_r, -200.0, 200.0, 1.0, colors));
|
|
|
|
|
col = col.push(float_slider("Blue→G", "blue_g", filter_id, blue_g, -200.0, 200.0, 1.0, colors));
|
|
|
|
|
col = col.push(float_slider("Blue→B", "blue_b", filter_id, blue_b, -200.0, 200.0, 1.0, colors));
|
|
|
|
|
col = col.push(float_slider("Blue Const", "blue_constant", filter_id, blue_c, -200.0, 200.0, 1.0, colors));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
col.into()
|
|
|
|
|
}
|
|
|
|
|
FilterType::BlackAndWhite => {
|
|
|
|
|
let reds = param_f64(params, "reds", 40.0);
|
|
|
|
|
let yellows = param_f64(params, "yellows", 60.0);
|
|
|
|
|
let greens = param_f64(params, "greens", 40.0);
|
|
|
|
|
let cyans = param_f64(params, "cyans", 60.0);
|
|
|
|
|
let blues = param_f64(params, "blues", 20.0);
|
|
|
|
|
let magentas = param_f64(params, "magentas", 80.0);
|
|
|
|
|
let tint_r = param_f64(params, "tint_r", 255.0);
|
|
|
|
|
let tint_g = param_f64(params, "tint_g", 255.0);
|
|
|
|
|
let tint_b = param_f64(params, "tint_b", 255.0);
|
|
|
|
|
column![
|
|
|
|
|
section_header("Black & White", colors),
|
|
|
|
|
float_slider("Reds", "reds", "black_and_white", reds, 0.0, 100.0, 1.0, colors),
|
|
|
|
|
float_slider("Yellows", "yellows", "black_and_white", yellows, 0.0, 100.0, 1.0, colors),
|
|
|
|
|
float_slider("Greens", "greens", "black_and_white", greens, 0.0, 100.0, 1.0, colors),
|
|
|
|
|
float_slider("Cyans", "cyans", "black_and_white", cyans, 0.0, 100.0, 1.0, colors),
|
|
|
|
|
float_slider("Blues", "blues", "black_and_white", blues, 0.0, 100.0, 1.0, colors),
|
|
|
|
|
float_slider("Magentas", "magentas", "black_and_white", magentas, 0.0, 100.0, 1.0, colors),
|
|
|
|
|
section_header("Tint Color", colors),
|
|
|
|
|
float_slider("Tint R", "tint_r", "black_and_white", tint_r, 0.0, 255.0, 1.0, colors),
|
|
|
|
|
float_slider("Tint G", "tint_g", "black_and_white", tint_g, 0.0, 255.0, 1.0, colors),
|
|
|
|
|
float_slider("Tint B", "tint_b", "black_and_white", tint_b, 0.0, 255.0, 1.0, colors),
|
|
|
|
|
]
|
|
|
|
|
.spacing(2)
|
|
|
|
|
.into()
|
|
|
|
|
}
|
|
|
|
|
FilterType::SelectiveColor => {
|
|
|
|
|
let filter_id = "selective_color";
|
|
|
|
|
// Mode toggle
|
|
|
|
|
let mode = param_str(params, "mode", "relative");
|
|
|
|
|
let is_absolute = mode == "absolute";
|
|
|
|
|
|
|
|
|
|
let mut col = column![
|
|
|
|
|
section_header("Selective Color", colors),
|
|
|
|
|
]
|
|
|
|
|
.spacing(2);
|
|
|
|
|
|
|
|
|
|
// Mode toggle
|
|
|
|
|
{
|
|
|
|
|
let lbl = text("Mode").size(11).width(Length::Fixed(80.0));
|
|
|
|
|
let toggle_btn = iced::widget::button(text(mode.to_string()).size(11))
|
|
|
|
|
.on_press(Message::FilterParamChanged(
|
|
|
|
|
filter_id.to_string(),
|
|
|
|
|
json!({ "mode": if is_absolute { "relative" } else { "absolute" } }),
|
|
|
|
|
))
|
|
|
|
|
.padding([2, 8]);
|
|
|
|
|
col = col.push(row![lbl, toggle_btn].spacing(4));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Per-color-range CMYK sliders
|
|
|
|
|
let ranges = [
|
|
|
|
|
("Reds", "reds"),
|
|
|
|
|
("Yellows", "yellows"),
|
|
|
|
|
("Greens", "greens"),
|
|
|
|
|
("Cyans", "cyans"),
|
|
|
|
|
("Blues", "blues"),
|
|
|
|
|
("Magentas", "magentas"),
|
|
|
|
|
("Whites", "whites"),
|
|
|
|
|
("Neutrals", "neutrals"),
|
|
|
|
|
("Blacks", "blacks"),
|
|
|
|
|
];
|
|
|
|
|
let cmyk = [("Cyan", "cyan"), ("Magenta", "magenta"), ("Yellow", "yellow"), ("Black", "black")];
|
|
|
|
|
|
|
|
|
|
for (range_name, range_key) in &ranges {
|
|
|
|
|
col = col.push(section_header(range_name, colors));
|
|
|
|
|
for (cmyk_name, cmyk_key) in &cmyk {
|
|
|
|
|
let param_key = format!("{}_{}", range_key, cmyk_key);
|
|
|
|
|
let val = param_f64(params, ¶m_key, 0.0);
|
|
|
|
|
col = col.push(float_slider(
|
|
|
|
|
cmyk_name,
|
|
|
|
|
¶m_key,
|
|
|
|
|
filter_id,
|
|
|
|
|
val,
|
|
|
|
|
-100.0,
|
|
|
|
|
100.0,
|
|
|
|
|
1.0,
|
|
|
|
|
colors,
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
col.into()
|
|
|
|
|
}
|
|
|
|
|
FilterType::GammaCorrection => {
|
|
|
|
|
let gamma = param_f64(params, "gamma", 2.2);
|
|
|
|
|
let inverse = param_bool(params, "inverse", false);
|
|
|
|
|
let mut col = column![
|
|
|
|
|
section_header("Gamma Correction", colors),
|
|
|
|
|
float_slider("Gamma", "gamma", "gamma_correction", gamma, 0.1, 5.0, 0.1, colors),
|
|
|
|
|
]
|
|
|
|
|
.spacing(2);
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
let lbl = text("Inverse").size(11).width(Length::Fixed(80.0));
|
|
|
|
|
let toggle_text = if inverse { "ON" } else { "OFF" };
|
|
|
|
|
let toggle_btn = iced::widget::button(text(toggle_text).size(11))
|
|
|
|
|
.on_press(Message::FilterParamChanged(
|
|
|
|
|
"gamma_correction".to_string(),
|
|
|
|
|
json!({ "inverse": !inverse }),
|
|
|
|
|
))
|
|
|
|
|
.padding([2, 8]);
|
|
|
|
|
col = col.push(row![lbl, toggle_btn].spacing(4));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
col.into()
|
|
|
|
|
}
|
|
|
|
|
FilterType::ExtractChannel => {
|
|
|
|
|
let channel = param_str(params, "channel", "red");
|
|
|
|
|
let channels = ["red", "green", "blue", "alpha"];
|
|
|
|
|
let mut col = column![
|
|
|
|
|
section_header("Extract Channel", colors),
|
|
|
|
|
]
|
|
|
|
|
.spacing(2);
|
|
|
|
|
|
|
|
|
|
for ch in &channels {
|
|
|
|
|
let is_active = channel == *ch;
|
|
|
|
|
let filter_id = "extract_channel";
|
|
|
|
|
let ch_label = text(ch.to_string()).size(11).width(Length::Fixed(80.0));
|
|
|
|
|
let btn = iced::widget::button(text(if is_active { format!(">> {}", ch) } else { ch.to_string() }).size(11))
|
|
|
|
|
.on_press(Message::FilterParamChanged(
|
|
|
|
|
filter_id.to_string(),
|
|
|
|
|
json!({ "channel": ch }),
|
|
|
|
|
))
|
|
|
|
|
.padding([2, 8]);
|
|
|
|
|
col = col.push(row![ch_label, btn].spacing(4));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
col.into()
|
|
|
|
|
}
|
|
|
|
|
FilterType::GradientMap => {
|
|
|
|
|
let reverse = param_bool(params, "reverse", false);
|
|
|
|
|
column![
|
|
|
|
|
section_header("Gradient Map", colors),
|
|
|
|
|
{
|
|
|
|
|
let lbl = text("Reverse").size(11).width(Length::Fixed(80.0));
|
|
|
|
|
let toggle_text = if reverse { "ON" } else { "OFF" };
|
|
|
|
|
let toggle_btn = iced::widget::button(text(toggle_text).size(11))
|
|
|
|
|
.on_press(Message::FilterParamChanged(
|
|
|
|
|
"gradient_map".to_string(),
|
|
|
|
|
json!({ "reverse": !reverse }),
|
|
|
|
|
))
|
|
|
|
|
.padding([2, 8]);
|
|
|
|
|
row![lbl, toggle_btn].spacing(4)
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
text("Gradient stops configured via color picker").size(10)
|
|
|
|
|
},
|
|
|
|
|
]
|
|
|
|
|
.spacing(2)
|
|
|
|
|
.into()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Restore ──────────────────────────────────────────────────────
|
|
|
|
|
FilterType::Dehaze => {
|
|
|
|
|
let strength = param_f64(params, "strength", 50.0);
|
|
|
|
|
column![
|
|
|
|
|
section_header("Dehaze", colors),
|
|
|
|
|
float_slider("Strength", "strength", "dehaze", strength, 0.0, 100.0, 1.0, colors),
|
|
|
|
|
]
|
|
|
|
|
.spacing(2)
|
|
|
|
|
.into()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Noise & Pattern ──────────────────────────────────────────────
|
|
|
|
|
FilterType::NoisePattern => {
|
|
|
|
|
let filter_id = "noise_pattern";
|
|
|
|
|
let scale = param_f64(params, "scale", 10.0);
|
|
|
|
|
let reverse = param_bool(params, "reverse", false);
|
|
|
|
|
let noise_type = param_str(params, "noise_type", "perlin");
|
|
|
|
|
let fractal_type = param_str(params, "fractal_type", "none");
|
|
|
|
|
let fractal_octaves = param_u64(params, "fractal_octaves", 3);
|
|
|
|
|
let fractal_lacunarity = param_f64(params, "fractal_lacunarity", 2.0);
|
|
|
|
|
let fractal_gain = param_f64(params, "fractal_gain", 0.5);
|
|
|
|
|
let domain_warp_type = param_str(params, "domain_warp_type", "none");
|
|
|
|
|
let domain_warp_amp = param_f64(params, "domain_warp_amplitude", 100.0);
|
|
|
|
|
let cellular_distance = param_str(params, "cellular_distance_function", "euclidean");
|
|
|
|
|
let cellular_return = param_str(params, "cellular_return_type", "nearest");
|
|
|
|
|
let cellular_jitter = param_f64(params, "cellular_jitter", 1.0);
|
|
|
|
|
|
|
|
|
|
let mut col = column![
|
|
|
|
|
section_header("Noise Pattern", colors),
|
|
|
|
|
float_slider("Scale", "scale", filter_id, scale, 0.1, 100.0, 0.1, colors),
|
|
|
|
|
]
|
|
|
|
|
.spacing(2);
|
|
|
|
|
|
|
|
|
|
// Reverse toggle
|
|
|
|
|
{
|
|
|
|
|
let lbl = text("Reverse").size(11).width(Length::Fixed(80.0));
|
|
|
|
|
let toggle_text = if reverse { "ON" } else { "OFF" };
|
|
|
|
|
let toggle_btn = iced::widget::button(text(toggle_text).size(11))
|
|
|
|
|
.on_press(Message::FilterParamChanged(
|
|
|
|
|
filter_id.to_string(),
|
|
|
|
|
json!({ "reverse": !reverse }),
|
|
|
|
|
))
|
|
|
|
|
.padding([2, 8]);
|
|
|
|
|
col = col.push(row![lbl, toggle_btn].spacing(4));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Noise type selector
|
|
|
|
|
{
|
|
|
|
|
let noise_types = ["value", "perlin", "simplex", "cellular"];
|
|
|
|
|
col = col.push(section_header("Noise Type", colors));
|
|
|
|
|
for nt in &noise_types {
|
|
|
|
|
let is_active = noise_type == *nt;
|
|
|
|
|
let btn = iced::widget::button(
|
|
|
|
|
text(if is_active { format!(">> {}", nt) } else { nt.to_string() }).size(11),
|
|
|
|
|
)
|
|
|
|
|
.on_press(Message::FilterParamChanged(
|
|
|
|
|
filter_id.to_string(),
|
|
|
|
|
json!({ "noise_type": nt }),
|
|
|
|
|
))
|
|
|
|
|
.padding([2, 8]);
|
|
|
|
|
col = col.push(btn);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Fractal type selector
|
|
|
|
|
{
|
|
|
|
|
let fractal_types = ["none", "fbm", "ridged", "ping_pong", "domain_warp"];
|
|
|
|
|
col = col.push(section_header("Fractal Type", colors));
|
|
|
|
|
for ft in &fractal_types {
|
|
|
|
|
let is_active = fractal_type == *ft;
|
|
|
|
|
let btn = iced::widget::button(
|
|
|
|
|
text(if is_active { format!(">> {}", ft) } else { ft.to_string() }).size(11),
|
|
|
|
|
)
|
|
|
|
|
.on_press(Message::FilterParamChanged(
|
|
|
|
|
filter_id.to_string(),
|
|
|
|
|
json!({ "fractal_type": ft }),
|
|
|
|
|
))
|
|
|
|
|
.padding([2, 8]);
|
|
|
|
|
col = col.push(btn);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if fractal_type != "none" {
|
|
|
|
|
col = col.push(int_slider("Octaves", "fractal_octaves", filter_id, fractal_octaves, 1, 10, colors));
|
|
|
|
|
col = col.push(float_slider("Lacunarity", "fractal_lacunarity", filter_id, fractal_lacunarity, 1.0, 4.0, 0.1, colors));
|
|
|
|
|
col = col.push(float_slider("Gain", "fractal_gain", filter_id, fractal_gain, 0.0, 2.0, 0.01, colors));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Domain warp selector
|
|
|
|
|
{
|
|
|
|
|
let warp_types = ["none", "basic", "general"];
|
|
|
|
|
col = col.push(section_header("Domain Warp", colors));
|
|
|
|
|
for wt in &warp_types {
|
|
|
|
|
let is_active = domain_warp_type == *wt;
|
|
|
|
|
let btn = iced::widget::button(
|
|
|
|
|
text(if is_active { format!(">> {}", wt) } else { wt.to_string() }).size(11),
|
|
|
|
|
)
|
|
|
|
|
.on_press(Message::FilterParamChanged(
|
|
|
|
|
filter_id.to_string(),
|
|
|
|
|
json!({ "domain_warp_type": wt }),
|
|
|
|
|
))
|
|
|
|
|
.padding([2, 8]);
|
|
|
|
|
col = col.push(btn);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if domain_warp_type != "none" {
|
|
|
|
|
col = col.push(float_slider("Warp Amp", "domain_warp_amplitude", filter_id, domain_warp_amp, 0.0, 500.0, 1.0, colors));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Cellular settings (shown when noise_type is "cellular")
|
|
|
|
|
if noise_type == "cellular" {
|
|
|
|
|
col = col.push(section_header("Cellular", colors));
|
|
|
|
|
// Distance function
|
|
|
|
|
let dist_funcs = ["euclidean", "manhattan", "chebyshev", "euclidean_sq"];
|
|
|
|
|
for df in &dist_funcs {
|
|
|
|
|
let is_active = cellular_distance == *df;
|
|
|
|
|
let btn = iced::widget::button(
|
|
|
|
|
text(if is_active { format!(">> {}", df) } else { df.to_string() }).size(11),
|
|
|
|
|
)
|
|
|
|
|
.on_press(Message::FilterParamChanged(
|
|
|
|
|
filter_id.to_string(),
|
|
|
|
|
json!({ "cellular_distance_function": df }),
|
|
|
|
|
))
|
|
|
|
|
.padding([2, 8]);
|
|
|
|
|
col = col.push(btn);
|
|
|
|
|
}
|
|
|
|
|
// Return type
|
|
|
|
|
let ret_types = ["nearest", "second", "distance", "difference", "sum"];
|
|
|
|
|
col = col.push(section_header("Return Type", colors));
|
|
|
|
|
for rt in &ret_types {
|
|
|
|
|
let is_active = cellular_return == *rt;
|
|
|
|
|
let btn = iced::widget::button(
|
|
|
|
|
text(if is_active { format!(">> {}", rt) } else { rt.to_string() }).size(11),
|
|
|
|
|
)
|
|
|
|
|
.on_press(Message::FilterParamChanged(
|
|
|
|
|
filter_id.to_string(),
|
|
|
|
|
json!({ "cellular_return_type": rt }),
|
|
|
|
|
))
|
|
|
|
|
.padding([2, 8]);
|
|
|
|
|
col = col.push(btn);
|
|
|
|
|
}
|
|
|
|
|
col = col.push(float_slider("Jitter", "cellular_jitter", filter_id, cellular_jitter, 0.0, 1.0, 0.01, colors));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
col.into()
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
container(content)
|
|
|
|
|
.width(Length::Fill)
|
|
|
|
|
.padding(4)
|
|
|
|
|
.into()
|
|
|
|
|
}
|