feat: add cyclic color and speed options for brush settings
This commit is contained in:
@@ -587,6 +587,10 @@ pub struct ToolState {
|
||||
pub brush_color_variant: bool,
|
||||
/// Magnitude of the per-dab color variation (0.0..1.0).
|
||||
pub brush_variant_amount: f32,
|
||||
/// True when the brush color should cycle through the hue over stroke distance.
|
||||
pub brush_cyclic_color: bool,
|
||||
/// Speed of the hue cycle along the stroke (0.01..5.0).
|
||||
pub brush_cyclic_speed: f32,
|
||||
/// Timestamp of the last update() call for frame timing.
|
||||
pub last_update_instant: std::time::Instant,
|
||||
/// Imported brush presets from ABR/KPP files.
|
||||
@@ -621,6 +625,8 @@ impl Default for ToolState {
|
||||
brush_spacing: 1.0,
|
||||
brush_color_variant: false,
|
||||
brush_variant_amount: 0.0,
|
||||
brush_cyclic_color: false,
|
||||
brush_cyclic_speed: 0.5,
|
||||
last_update_instant: std::time::Instant::now(),
|
||||
imported_brushes: Vec::new(),
|
||||
active_brush_preset: None,
|
||||
@@ -778,6 +784,8 @@ pub enum Message {
|
||||
BrushSpacingChanged(f32),
|
||||
BrushColorVariantToggled(bool),
|
||||
BrushVariantAmountChanged(f32),
|
||||
BrushCyclicColorToggled(bool),
|
||||
BrushCyclicSpeedChanged(f32),
|
||||
BrushImportAbr,
|
||||
BrushImportAbrFile(Result<Vec<hcie_engine_api::BrushPreset>, String>),
|
||||
ResetToolDefaults,
|
||||
@@ -1319,6 +1327,7 @@ fn build_brush_tip(state: &ToolState) -> BrushTip {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Converts an imported/default brush-engine preset tip into the protocol tip used by `Engine`.
|
||||
///
|
||||
/// **Arguments:** `source` is the preset-owned brush-engine tip. **Returns:** A protocol tip with
|
||||
@@ -2564,7 +2573,7 @@ impl HcieIcedApp {
|
||||
/// Apply brush parameters from tool state to the engine.
|
||||
///
|
||||
/// Called before starting a stroke to ensure the engine uses
|
||||
/// the current brush size, opacity, hardness, and style.
|
||||
/// the current brush size, opacity, hardness, style, and cyclic color.
|
||||
fn apply_brush_params(&mut self) {
|
||||
let mut tip = self
|
||||
.tool_state
|
||||
@@ -2577,6 +2586,15 @@ impl HcieIcedApp {
|
||||
tip.opacity = self.tool_state.brush_opacity;
|
||||
tip.hardness = self.tool_state.brush_hardness;
|
||||
tip.spacing = self.tool_state.brush_spacing;
|
||||
// Cyclic color is handled by engine state, not the tip, but preview rendering
|
||||
// also reads the tip's time dynamics. Synchronize both here.
|
||||
tip.time_enabled = self.tool_state.brush_cyclic_color;
|
||||
tip.time_size_start = 1.0;
|
||||
tip.time_size_end = 1.0;
|
||||
tip.time_opacity_start = 1.0;
|
||||
tip.time_opacity_end = 1.0;
|
||||
tip.time_angle_start = 0.0;
|
||||
tip.time_angle_end = 0.0;
|
||||
match self.tool_state.active_tool {
|
||||
Tool::Pen => {
|
||||
tip.style = BrushStyle::Round;
|
||||
@@ -2592,7 +2610,10 @@ impl HcieIcedApp {
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
self.documents[self.active_doc].engine.set_brush_tip(tip);
|
||||
let engine = &mut self.documents[self.active_doc].engine;
|
||||
engine.set_brush_tip(tip);
|
||||
engine.set_cyclic_color(self.tool_state.brush_cyclic_color);
|
||||
engine.set_cyclic_speed(self.tool_state.brush_cyclic_speed);
|
||||
}
|
||||
|
||||
/// Push the current SVG editor model into the active vector shape so the
|
||||
@@ -4955,6 +4976,22 @@ impl HcieIcedApp {
|
||||
self.settings.update_from_tool_state(&self.tool_state);
|
||||
let _ = self.settings.save();
|
||||
}
|
||||
Message::BrushCyclicColorToggled(enabled) => {
|
||||
self.tool_state.brush_cyclic_color = enabled;
|
||||
self.documents[self.active_doc]
|
||||
.engine
|
||||
.set_cyclic_color(enabled);
|
||||
self.settings.update_from_tool_state(&self.tool_state);
|
||||
let _ = self.settings.save();
|
||||
}
|
||||
Message::BrushCyclicSpeedChanged(speed) => {
|
||||
self.tool_state.brush_cyclic_speed = speed;
|
||||
self.documents[self.active_doc]
|
||||
.engine
|
||||
.set_cyclic_speed(speed);
|
||||
self.settings.update_from_tool_state(&self.tool_state);
|
||||
let _ = self.settings.save();
|
||||
}
|
||||
Message::SelectionToleranceChanged(tol) => {
|
||||
self.settings.tool_settings.magic_wand_tolerance = tol as f32;
|
||||
self.settings.tool_settings.flood_fill_tolerance = tol as f32;
|
||||
|
||||
@@ -184,6 +184,8 @@ pub fn panel_body<'a>(
|
||||
app.tool_state.brush_hardness,
|
||||
app.tool_state.brush_color_variant,
|
||||
app.tool_state.brush_variant_amount,
|
||||
app.tool_state.brush_cyclic_color,
|
||||
app.tool_state.brush_cyclic_speed,
|
||||
app.brush_category,
|
||||
&app.tool_state.imported_brushes,
|
||||
app.tool_state.active_brush_preset.as_ref(),
|
||||
@@ -236,6 +238,10 @@ pub fn panel_body<'a>(
|
||||
app.tool_state.brush_size,
|
||||
app.tool_state.brush_opacity,
|
||||
app.tool_state.brush_hardness,
|
||||
app.tool_state.brush_color_variant,
|
||||
app.tool_state.brush_variant_amount,
|
||||
app.tool_state.brush_cyclic_color,
|
||||
app.tool_state.brush_cyclic_speed,
|
||||
doc.selection_rect,
|
||||
doc.vector_draw,
|
||||
colors,
|
||||
|
||||
@@ -290,6 +290,7 @@ struct PreviewKey {
|
||||
size: u8,
|
||||
hardness: u8,
|
||||
opacity: u8,
|
||||
cyclic: bool,
|
||||
r: u8,
|
||||
g: u8,
|
||||
b: u8,
|
||||
@@ -508,12 +509,20 @@ fn build_tip_from_state(
|
||||
size: f32,
|
||||
opacity: f32,
|
||||
hardness: f32,
|
||||
cyclic_color: bool,
|
||||
) -> hcie_engine_api::BrushTip {
|
||||
hcie_engine_api::BrushTip {
|
||||
style,
|
||||
size,
|
||||
opacity,
|
||||
hardness,
|
||||
time_enabled: cyclic_color,
|
||||
time_size_start: 1.0,
|
||||
time_size_end: 1.0,
|
||||
time_opacity_start: 1.0,
|
||||
time_opacity_end: 1.0,
|
||||
time_angle_start: 0.0,
|
||||
time_angle_end: 0.0,
|
||||
..hcie_engine_api::BrushTip::default()
|
||||
}
|
||||
}
|
||||
@@ -527,6 +536,7 @@ fn preview_key(
|
||||
size: f32,
|
||||
opacity: f32,
|
||||
hardness: f32,
|
||||
cyclic: bool,
|
||||
fg_color: [u8; 4],
|
||||
) -> PreviewKey {
|
||||
PreviewKey {
|
||||
@@ -534,6 +544,7 @@ fn preview_key(
|
||||
size: size.clamp(1.0, 255.0) as u8,
|
||||
hardness: (hardness * 100.0).clamp(0.0, 100.0) as u8,
|
||||
opacity: (opacity * 100.0).clamp(0.0, 100.0) as u8,
|
||||
cyclic,
|
||||
r: fg_color[0],
|
||||
g: fg_color[1],
|
||||
b: fg_color[2],
|
||||
@@ -562,13 +573,14 @@ fn get_live_stroke_preview(
|
||||
size: f32,
|
||||
opacity: f32,
|
||||
hardness: f32,
|
||||
cyclic_color: bool,
|
||||
fg_color: [u8; 4],
|
||||
) -> iced::widget::image::Handle {
|
||||
// Clamp the rendered brush size so a 110 px soft brush does not fill the
|
||||
// entire 40 px preview well with a solid blob. The preview shows texture and
|
||||
// stroke character; the slider still reports the real painting size.
|
||||
let preview_size = size.clamp(4.0, LIVE_PREVIEW_BRUSH_SIZE);
|
||||
let key = preview_key(style, preview_size, opacity, hardness, fg_color);
|
||||
let key = preview_key(style, preview_size, opacity, hardness, cyclic_color, fg_color);
|
||||
let cache = LIVE_PREVIEW_CACHE.get_or_init(|| Mutex::new(HashMap::new()));
|
||||
let mut cache = cache
|
||||
.lock()
|
||||
@@ -576,7 +588,7 @@ fn get_live_stroke_preview(
|
||||
if let Some(handle) = cache.get(&key) {
|
||||
return handle.clone();
|
||||
}
|
||||
let tip = build_tip_from_state(style, preview_size, opacity, hardness);
|
||||
let tip = build_tip_from_state(style, preview_size, opacity, hardness, cyclic_color);
|
||||
let pixels =
|
||||
render_stroke_preview(LIVE_PREVIEW_IMG_SIZE, LIVE_PREVIEW_IMG_SIZE, &tip, fg_color);
|
||||
let handle = iced::widget::image::Handle::from_rgba(
|
||||
@@ -588,6 +600,11 @@ fn get_live_stroke_preview(
|
||||
handle
|
||||
}
|
||||
|
||||
/// Clamp a cyclic speed value to the engine's valid range.
|
||||
fn clamp_cyclic_speed(speed: f32) -> f32 {
|
||||
speed.clamp(0.01, 5.0)
|
||||
}
|
||||
|
||||
/// Get or generate a realistic stroke preview for a built-in brush style.
|
||||
///
|
||||
/// **Purpose:** Provides the thumbnails shown in the built-in style grid. The
|
||||
@@ -637,6 +654,8 @@ pub fn view(
|
||||
current_hardness: f32,
|
||||
brush_color_variant: bool,
|
||||
brush_variant_amount: f32,
|
||||
brush_cyclic_color: bool,
|
||||
brush_cyclic_speed: f32,
|
||||
active_category: BrushCategory,
|
||||
imported_presets: &[hcie_engine_api::BrushPreset],
|
||||
active_brush_preset: Option<&hcie_engine_api::BrushPreset>,
|
||||
@@ -1021,6 +1040,16 @@ pub fn view(
|
||||
colors,
|
||||
Message::BrushVariantAmountChanged,
|
||||
);
|
||||
let cyclic_speed_slider = plain_slider(
|
||||
"Cyclic Speed",
|
||||
clamp_cyclic_speed(brush_cyclic_speed),
|
||||
0.01..=5.0,
|
||||
0.01,
|
||||
"x",
|
||||
2,
|
||||
colors,
|
||||
Message::BrushCyclicSpeedChanged,
|
||||
);
|
||||
|
||||
// Compact color-variant toggle: an empty 12 px checkbox plus an 8 px label so
|
||||
// the control matches the rest of the compact panel typography.
|
||||
@@ -1033,6 +1062,15 @@ pub fn view(
|
||||
.spacing(4)
|
||||
.align_y(iced::Alignment::Center);
|
||||
|
||||
let cyclic_color_check = row![
|
||||
checkbox("", brush_cyclic_color)
|
||||
.on_toggle(Message::BrushCyclicColorToggled)
|
||||
.size(12),
|
||||
text("Cyclic color").size(LABEL_SIZE),
|
||||
]
|
||||
.spacing(4)
|
||||
.align_y(iced::Alignment::Center);
|
||||
|
||||
// Live preview — show a realistic engine-rendered stroke using the active
|
||||
// size, opacity, hardness, and foreground color. The preview is
|
||||
// parameter-keyed and cached so it updates in real time without
|
||||
@@ -1042,6 +1080,7 @@ pub fn view(
|
||||
current_size,
|
||||
current_opacity,
|
||||
current_hardness,
|
||||
brush_cyclic_color,
|
||||
fg_color,
|
||||
);
|
||||
let live_preview = container(
|
||||
@@ -1091,6 +1130,8 @@ pub fn view(
|
||||
hardness_slider,
|
||||
color_variant_check,
|
||||
variant_slider,
|
||||
cyclic_color_check,
|
||||
cyclic_speed_slider,
|
||||
]
|
||||
.spacing(ITEM_SPACING)
|
||||
.padding(3);
|
||||
|
||||
@@ -93,6 +93,10 @@ pub fn view<'a>(
|
||||
brush_size: f32,
|
||||
brush_opacity: f32,
|
||||
brush_hardness: f32,
|
||||
brush_color_variant: bool,
|
||||
brush_variant_amount: f32,
|
||||
brush_cyclic_color: bool,
|
||||
brush_cyclic_speed: f32,
|
||||
selection_rect: Option<(f32, f32, f32, f32)>,
|
||||
vector_draw: Option<((f32, f32), (f32, f32))>,
|
||||
colors: ThemeColors,
|
||||
@@ -134,12 +138,25 @@ pub fn view<'a>(
|
||||
// Build tool/shape-specific section
|
||||
let tool_section: Element<'a, Message> = match active_tool {
|
||||
Tool::Pen | Tool::Brush | Tool::Eraser => {
|
||||
brush_properties(brush_size, brush_opacity, brush_hardness, colors)
|
||||
brush_properties(
|
||||
brush_size,
|
||||
brush_opacity,
|
||||
brush_hardness,
|
||||
brush_color_variant,
|
||||
brush_variant_amount,
|
||||
brush_cyclic_color,
|
||||
brush_cyclic_speed,
|
||||
colors,
|
||||
)
|
||||
}
|
||||
Tool::Spray => spray_properties(
|
||||
brush_size,
|
||||
brush_opacity,
|
||||
brush_hardness,
|
||||
brush_color_variant,
|
||||
brush_variant_amount,
|
||||
brush_cyclic_color,
|
||||
brush_cyclic_speed,
|
||||
settings_spray_particle_size,
|
||||
settings_spray_density,
|
||||
colors,
|
||||
@@ -511,11 +528,20 @@ fn vector_tool_properties<'a>(
|
||||
col.into()
|
||||
}
|
||||
|
||||
/// Shared cyclic speed clamp used by brush and spray property sections.
|
||||
fn clamp_cyclic_speed(speed: f32) -> f32 {
|
||||
speed.clamp(0.01, 5.0)
|
||||
}
|
||||
|
||||
/// Brush tool properties (Pen, Brush, Eraser).
|
||||
fn brush_properties<'a>(
|
||||
size: f32,
|
||||
opacity: f32,
|
||||
hardness: f32,
|
||||
color_variant: bool,
|
||||
variant_amount: f32,
|
||||
cyclic_color: bool,
|
||||
cyclic_speed: f32,
|
||||
colors: ThemeColors,
|
||||
) -> Element<'a, Message> {
|
||||
column![
|
||||
@@ -529,6 +555,40 @@ fn brush_properties<'a>(
|
||||
plain_slider("Hardness", hardness, 0.0..=1.0, 0.01, "%", 0, colors, |v| {
|
||||
Message::BrushHardnessChanged(v)
|
||||
},),
|
||||
row![
|
||||
checkbox("Color Variant", color_variant)
|
||||
.on_toggle(Message::BrushColorVariantToggled)
|
||||
.size(11),
|
||||
]
|
||||
.spacing(4)
|
||||
.align_y(iced::Alignment::Center),
|
||||
plain_slider(
|
||||
"Variant Amount",
|
||||
variant_amount,
|
||||
0.0..=1.0,
|
||||
0.01,
|
||||
"%",
|
||||
0,
|
||||
colors,
|
||||
Message::BrushVariantAmountChanged,
|
||||
),
|
||||
row![
|
||||
checkbox("Cyclic Color", cyclic_color)
|
||||
.on_toggle(Message::BrushCyclicColorToggled)
|
||||
.size(11),
|
||||
]
|
||||
.spacing(4)
|
||||
.align_y(iced::Alignment::Center),
|
||||
plain_slider(
|
||||
"Cyclic Speed",
|
||||
clamp_cyclic_speed(cyclic_speed),
|
||||
0.01..=5.0,
|
||||
0.01,
|
||||
"x",
|
||||
2,
|
||||
colors,
|
||||
Message::BrushCyclicSpeedChanged,
|
||||
),
|
||||
]
|
||||
.spacing(4)
|
||||
.into()
|
||||
@@ -539,6 +599,10 @@ fn spray_properties<'a>(
|
||||
size: f32,
|
||||
opacity: f32,
|
||||
hardness: f32,
|
||||
color_variant: bool,
|
||||
variant_amount: f32,
|
||||
cyclic_color: bool,
|
||||
cyclic_speed: f32,
|
||||
particle_size: f32,
|
||||
density: u32,
|
||||
colors: ThemeColors,
|
||||
@@ -574,6 +638,40 @@ fn spray_properties<'a>(
|
||||
plain_slider("Hardness", hardness, 0.0..=1.0, 0.01, "%", 0, colors, |v| {
|
||||
Message::BrushHardnessChanged(v)
|
||||
},),
|
||||
row![
|
||||
checkbox("Color Variant", color_variant)
|
||||
.on_toggle(Message::BrushColorVariantToggled)
|
||||
.size(11),
|
||||
]
|
||||
.spacing(4)
|
||||
.align_y(iced::Alignment::Center),
|
||||
plain_slider(
|
||||
"Variant Amount",
|
||||
variant_amount,
|
||||
0.0..=1.0,
|
||||
0.01,
|
||||
"%",
|
||||
0,
|
||||
colors,
|
||||
Message::BrushVariantAmountChanged,
|
||||
),
|
||||
row![
|
||||
checkbox("Cyclic Color", cyclic_color)
|
||||
.on_toggle(Message::BrushCyclicColorToggled)
|
||||
.size(11),
|
||||
]
|
||||
.spacing(4)
|
||||
.align_y(iced::Alignment::Center),
|
||||
plain_slider(
|
||||
"Cyclic Speed",
|
||||
clamp_cyclic_speed(cyclic_speed),
|
||||
0.01..=5.0,
|
||||
0.01,
|
||||
"x",
|
||||
2,
|
||||
colors,
|
||||
Message::BrushCyclicSpeedChanged,
|
||||
),
|
||||
]
|
||||
.spacing(4)
|
||||
.into()
|
||||
|
||||
@@ -134,6 +134,17 @@ fn brush_settings(
|
||||
|v| Message::BrushVariantAmountChanged(v / 100.0),
|
||||
c
|
||||
),
|
||||
checkbox("Cyclic Color", ts.brush_cyclic_color)
|
||||
.on_toggle(Message::BrushCyclicColorToggled)
|
||||
.size(11),
|
||||
prop_slider(
|
||||
"Cyclic Speed",
|
||||
ts.brush_cyclic_speed.clamp(0.01, 5.0),
|
||||
0.01,
|
||||
5.0,
|
||||
Message::BrushCyclicSpeedChanged,
|
||||
c
|
||||
),
|
||||
]
|
||||
.spacing(4)
|
||||
}
|
||||
@@ -186,6 +197,28 @@ fn spray_settings(
|
||||
|v| Message::BrushHardnessChanged(v / 100.0),
|
||||
c
|
||||
),
|
||||
checkbox("Color Variant", ts.brush_color_variant)
|
||||
.on_toggle(Message::BrushColorVariantToggled)
|
||||
.size(11),
|
||||
prop_slider(
|
||||
"Variant Amount",
|
||||
ts.brush_variant_amount * 100.0,
|
||||
0.0,
|
||||
100.0,
|
||||
|v| Message::BrushVariantAmountChanged(v / 100.0),
|
||||
c
|
||||
),
|
||||
checkbox("Cyclic Color", ts.brush_cyclic_color)
|
||||
.on_toggle(Message::BrushCyclicColorToggled)
|
||||
.size(11),
|
||||
prop_slider(
|
||||
"Cyclic Speed",
|
||||
ts.brush_cyclic_speed.clamp(0.01, 5.0),
|
||||
0.01,
|
||||
5.0,
|
||||
Message::BrushCyclicSpeedChanged,
|
||||
c
|
||||
),
|
||||
]
|
||||
.spacing(4)
|
||||
}
|
||||
|
||||
@@ -59,6 +59,12 @@ pub struct ToolSettings {
|
||||
/// Magnitude of the per-dab color variation (0.0..1.0).
|
||||
#[serde(default)]
|
||||
pub brush_variant_amount: f32,
|
||||
/// True when the brush color should cycle through the hue over stroke distance.
|
||||
#[serde(default)]
|
||||
pub brush_cyclic_color: bool,
|
||||
/// Speed of the hue cycle along the stroke (0.01..5.0).
|
||||
#[serde(default)]
|
||||
pub brush_cyclic_speed: f32,
|
||||
pub eraser_size: f32,
|
||||
pub eraser_opacity: f32,
|
||||
pub pen_size: f32,
|
||||
@@ -192,6 +198,8 @@ impl Default for ToolSettings {
|
||||
brush_spacing: 1.0,
|
||||
brush_color_variant: false,
|
||||
brush_variant_amount: 0.0,
|
||||
brush_cyclic_color: false,
|
||||
brush_cyclic_speed: 0.5,
|
||||
eraser_size: 20.0,
|
||||
eraser_opacity: 1.0,
|
||||
pen_size: 2.0,
|
||||
@@ -344,6 +352,8 @@ impl AppSettings {
|
||||
self.tool_settings.brush_hardness = tool_state.brush_hardness;
|
||||
self.tool_settings.brush_color_variant = tool_state.brush_color_variant;
|
||||
self.tool_settings.brush_variant_amount = tool_state.brush_variant_amount;
|
||||
self.tool_settings.brush_cyclic_color = tool_state.brush_cyclic_color;
|
||||
self.tool_settings.brush_cyclic_speed = tool_state.brush_cyclic_speed;
|
||||
self.tool_settings.last_active_tool = format!("{:?}", tool_state.active_tool);
|
||||
}
|
||||
|
||||
@@ -354,6 +364,8 @@ impl AppSettings {
|
||||
tool_state.brush_hardness = self.tool_settings.brush_hardness;
|
||||
tool_state.brush_color_variant = self.tool_settings.brush_color_variant;
|
||||
tool_state.brush_variant_amount = self.tool_settings.brush_variant_amount;
|
||||
tool_state.brush_cyclic_color = self.tool_settings.brush_cyclic_color;
|
||||
tool_state.brush_cyclic_speed = self.tool_settings.brush_cyclic_speed;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user