feat: add star sparkle brush preset and enhance star brush parameters
mandatory-regression-gate / deterministic-tests (push) Waiting to run
mandatory-regression-gate / protected-performance-path (push) Waiting to run

This commit is contained in:
Your Name
2026-07-26 03:50:02 +03:00
parent acd7de5dd3
commit a64f1184cd
6 changed files with 318 additions and 74 deletions
+151 -50
View File
@@ -111,7 +111,22 @@ pub fn generate_brush_stamp(tip: &BrushTip) -> Vec<u8> {
match tip.style { match tip.style {
BrushStyle::Round => generate_round_stamp(d, center, r, tip.hardness), BrushStyle::Round => generate_round_stamp(d, center, r, tip.hardness),
BrushStyle::Square => generate_square_stamp(d, center, r, tip.hardness), BrushStyle::Square => generate_square_stamp(d, center, r, tip.hardness),
BrushStyle::Star => generate_star_stamp(d, center, r, tip.hardness), BrushStyle::Star => generate_star_stamp(
d,
center,
r,
tip.hardness,
if tip.roundness > 0.0 {
tip.roundness
} else {
0.22
},
if tip.density > 0.0 {
tip.density
} else {
0.08
},
),
BrushStyle::SoftRound => generate_round_stamp(d, center, r, 0.0), BrushStyle::SoftRound => generate_round_stamp(d, center, r, 0.0),
BrushStyle::HardRound => generate_round_stamp(d, center, r, 1.0), BrushStyle::HardRound => generate_round_stamp(d, center, r, 1.0),
BrushStyle::Bitmap => { BrushStyle::Bitmap => {
@@ -200,10 +215,19 @@ fn generate_square_stamp(d: usize, center: f32, r: f32, hardness: f32) -> Vec<u8
.collect() .collect()
} }
fn generate_star_stamp(d: usize, center: f32, r: f32, hardness: f32) -> Vec<u8> { fn generate_star_stamp(
d: usize,
center: f32,
r: f32,
hardness: f32,
arm_width_ratio: f32,
inner_r_ratio: f32,
) -> Vec<u8> {
let arm_count = 4u32; let arm_count = 4u32;
let arm_width = r * 0.22; let arm_width = arm_width_ratio.clamp(0.08, 0.35);
let inner_r = r * 0.08; let inner_r = r * inner_r_ratio.clamp(0.04, 0.12);
let hardness = hardness.clamp(0.0, 1.0);
let point_sharpness = 1.2 + (0.35 - arm_width) / 0.27 * 2.8;
(0..d * d) (0..d * d)
.map(|i| { .map(|i| {
let x = (i % d) as f32 - center; let x = (i % d) as f32 - center;
@@ -212,21 +236,19 @@ fn generate_star_stamp(d: usize, center: f32, r: f32, hardness: f32) -> Vec<u8>
let dist = (x * x + y * y).sqrt(); let dist = (x * x + y * y).sqrt();
let point_angle = std::f32::consts::PI * 2.0 / arm_count as f32; let point_angle = std::f32::consts::PI * 2.0 / arm_count as f32;
let a = angle.rem_euclid(point_angle); let a = angle.rem_euclid(point_angle);
let arm_center = point_angle / 2.0; let arm_dist = a.min(point_angle - a);
let arm_dist = (a - arm_center).abs(); let angular_distance = (arm_dist / (point_angle * 0.5)).clamp(0.0, 1.0);
let max_arm_dist = arm_width / r; let point_profile = (1.0 - angular_distance).powf(point_sharpness);
if dist > r { let edge = inner_r + (r - inner_r) * point_profile;
let soft_width = (r * (1.0 - hardness) * 0.35).max(0.75);
let hard_edge = (edge - soft_width).max(0.0);
if dist >= edge {
0 0
} else if arm_dist < max_arm_dist && dist > inner_r { } else if dist <= hard_edge {
let arm_falloff = 1.0 - (arm_dist / max_arm_dist); 255
let radial = ((dist - inner_r) / (r - inner_r)).clamp(0.0, 1.0);
let alpha = arm_falloff * (1.0 - radial.powf(1.0 - hardness.clamp(0.0, 0.99)));
(alpha * 255.0).round() as u8
} else if dist <= inner_r {
let alpha = (1.0 - dist / inner_r).max(0.5);
(alpha * 255.0).round() as u8
} else { } else {
0 (((edge - dist) / soft_width).clamp(0.0, 1.0) * 255.0).round() as u8
} }
}) })
.collect() .collect()
@@ -650,6 +672,10 @@ fn draw_brush_style_dab(
brush_hardness, brush_hardness,
is_eraser, is_eraser,
mask, mask,
angle,
rotation_random,
roundness,
density,
), ),
BrushStyle::Rock => draw_rock_brush( BrushStyle::Rock => draw_rock_brush(
pixels, pixels,
@@ -1621,59 +1647,134 @@ fn draw_star_brush(
hardness: f32, hardness: f32,
is_eraser: bool, is_eraser: bool,
mask: Option<&[u8]>, mask: Option<&[u8]>,
angle: f32,
rotation_random: f32,
roundness: f32,
density: f32,
) { ) {
let effective_size = size * pressure; let seed = cx.to_bits().wrapping_mul(73_856_093)
^ cy.to_bits().wrapping_mul(19_349_663);
let size_variance = roundness;
let size_scale = if size_variance > 0.0 {
let r = ((seed & 0xFFFF) as f32 / 65536.0 - 0.5) * 2.0 * size_variance;
(1.0 + r).clamp(0.3, 2.0)
} else {
1.0
};
let effective_size = size * pressure * size_scale;
let angle_variance = rotation_random;
let dab_angle = if angle_variance > 0.0 {
let r = ((seed >> 16) as f32 / 65536.0 - 0.5) * 2.0 * angle_variance;
angle + r * std::f32::consts::PI
} else {
angle
};
let arm_width_ratio = if roundness > 0.0 { roundness } else { 0.22 };
let inner_r_ratio = if density > 0.0 { density } else { 0.08 };
let tip = BrushTip { let tip = BrushTip {
style: BrushStyle::Star, style: BrushStyle::Star,
size: effective_size * 0.5, size: effective_size * 0.5,
hardness, hardness,
roundness: arm_width_ratio,
density: inner_r_ratio,
..BrushTip::default() ..BrushTip::default()
}; };
let stamp = generate_brush_stamp(&tip); let stamp = generate_brush_stamp(&tip);
draw_dab_with_stamp(
pixels, let cos_a = dab_angle.cos();
width, let sin_a = dab_angle.sin();
height, let stamp_rotated = if dab_angle.abs() > f32::EPSILON {
cx, let stamp_d = (effective_size * 0.5 * 2.0).ceil() as usize;
cy, let stamp_center = stamp_d as f32 * 0.5;
effective_size, let d = stamp_d;
hardness, let center = d as f32 * 0.5;
color, let mut rotated = vec![0u8; d * d];
opacity * pressure, for y in 0..d {
is_eraser, for x in 0..d {
mask, let px = x as f32 - center;
Some(&stamp), let py = y as f32 - center;
); let rx = px * cos_a + py * sin_a;
let sparkle_count = ((effective_size * 0.15) as u32).clamp(2, 8); let ry = -px * sin_a + py * cos_a;
let sparkle_r = effective_size * 0.12; let sx = rx + stamp_center;
let tip_sm = BrushTip { let sy = ry + stamp_center;
style: BrushStyle::Star, let ix = sx.floor() as i32;
size: sparkle_r, let iy = sy.floor() as i32;
hardness, if ix >= 0 && iy >= 0 && ix < stamp_d as i32 - 1 && iy < stamp_d as i32 - 1 {
..BrushTip::default() let fx = sx - ix as f32;
let fy = sy - iy as f32;
let d00 = stamp[iy as usize * stamp_d + ix as usize] as f32;
let d10 = stamp[iy as usize * stamp_d + ix as usize + 1] as f32;
let d01 = stamp[(iy as usize + 1) * stamp_d + ix as usize] as f32;
let d11 = stamp[(iy as usize + 1) * stamp_d + ix as usize + 1] as f32;
let v = d00 * (1.0 - fx) * (1.0 - fy)
+ d10 * fx * (1.0 - fy)
+ d01 * (1.0 - fx) * fy
+ d11 * fx * fy;
rotated[y * d + x] = v.round() as u8;
}
}
}
rotated
} else {
stamp
}; };
let stamp_sm = generate_brush_stamp(&tip_sm);
let seed = cx.to_bits().wrapping_mul(73_856_093) let sparkle_count = if density > 0.0 {
^ cy.to_bits().wrapping_mul(19_349_663); (effective_size * 0.08 * density).clamp(2.0, 6.0) as u32
} else {
((effective_size * 0.05) as u32).clamp(2, 4)
};
let base_star_size = effective_size * 0.24;
let footprint_radius = effective_size * 0.5;
let phase = (seed & 0xFFFF) as f32 / 65536.0 * std::f32::consts::TAU;
const GOLDEN_ANGLE: f32 = 2.399_963_1;
for i in 0..sparkle_count { for i in 0..sparkle_count {
let hash_i = seed.wrapping_add(i.wrapping_mul(668_265_263)); let hash_i = seed.wrapping_add(i.wrapping_mul(668_265_263));
let angle = (hash_i & 0xFFFF) as f32 / 65536.0 * std::f32::consts::TAU; let spark_size_var = if size_variance > 0.0 {
let dist = ((hash_i >> 16) & 0xFFFF) as f32 / 65536.0 * effective_size * 0.6; let rv = ((hash_i.wrapping_add(31337) & 0xFFFF) as f32 / 65536.0 - 0.5)
let sx = cx + angle.cos() * dist; * 2.0
let sy = cy + angle.sin() * dist; * size_variance;
(1.0 + rv).clamp(0.3, 2.0)
} else {
1.0
};
let star_size = base_star_size * spark_size_var;
let max_center_radius = (footprint_radius - star_size * 0.5).max(0.0);
let radial_fraction = ((i as f32 + 0.5) / sparkle_count as f32).sqrt();
let spark_angle = phase + i as f32 * GOLDEN_ANGLE;
let dist = radial_fraction * max_center_radius;
let sx = cx + spark_angle.cos() * dist;
let sy = cy + spark_angle.sin() * dist;
let spark_color = if color[3] > 0 {
let hue_shift = ((hash_i.wrapping_add(7919) & 0xFF) as f32 / 255.0 - 0.5) * 30.0;
let [r, g, b, a] = color;
let (h, s, l) = rgb_to_hsl(r, g, b);
let h = (h + hue_shift).rem_euclid(360.0);
let (rr, gg, bb) = hsl_to_rgb(h, s, l);
[rr, gg, bb, a]
} else {
color
};
draw_dab_with_stamp( draw_dab_with_stamp(
pixels, pixels,
width, width,
height, height,
sx, sx,
sy, sy,
sparkle_r * 2.0, star_size,
hardness, hardness,
color, spark_color,
opacity * pressure * 0.5, opacity * pressure * 0.9,
is_eraser, is_eraser,
mask, mask,
Some(&stamp_sm), Some(&stamp_rotated),
); );
} }
} }
@@ -233,6 +233,14 @@ pub fn dry_media_presets() -> Vec<BrushPreset> {
false, false,
true, true,
), ),
preset(
"star_sparkle",
"Star Sparkle",
"Effects",
tip(BrushStyle::Star, 14.0, 0.95, 1.0, 0.35),
true,
false,
),
] ]
} }
@@ -245,14 +253,14 @@ mod tests {
#[test] #[test]
fn dry_media_catalog_is_complete_and_unique() { fn dry_media_catalog_is_complete_and_unique() {
let presets = dry_media_presets(); let presets = dry_media_presets();
assert_eq!(presets.len(), 19); assert_eq!(presets.len(), 20);
assert_eq!( assert_eq!(
presets presets
.iter() .iter()
.map(|preset| &preset.id) .map(|preset| &preset.id)
.collect::<HashSet<_>>() .collect::<HashSet<_>>()
.len(), .len(),
19 20
); );
assert!(presets assert!(presets
.iter() .iter()
@@ -587,6 +587,12 @@ pub struct ToolState {
pub brush_color_variant: bool, pub brush_color_variant: bool,
/// Magnitude of the per-dab color variation (0.0..1.0). /// Magnitude of the per-dab color variation (0.0..1.0).
pub brush_variant_amount: f32, pub brush_variant_amount: f32,
/// Star brush base rotation angle in radians.
pub brush_angle: f32,
/// Star brush per-dab size variance (0.0..1.0).
pub brush_size_variance: f32,
/// Star brush per-dab angle variance (0.0..1.0).
pub brush_angle_variance: f32,
/// True when the brush color should cycle through the hue over stroke distance. /// True when the brush color should cycle through the hue over stroke distance.
pub brush_cyclic_color: bool, pub brush_cyclic_color: bool,
/// Speed of the hue cycle along the stroke (0.01..5.0). /// Speed of the hue cycle along the stroke (0.01..5.0).
@@ -625,6 +631,9 @@ impl Default for ToolState {
brush_spacing: 1.0, brush_spacing: 1.0,
brush_color_variant: false, brush_color_variant: false,
brush_variant_amount: 0.0, brush_variant_amount: 0.0,
brush_angle: 0.0,
brush_size_variance: 0.0,
brush_angle_variance: 0.0,
brush_cyclic_color: false, brush_cyclic_color: false,
brush_cyclic_speed: 0.5, brush_cyclic_speed: 0.5,
last_update_instant: std::time::Instant::now(), last_update_instant: std::time::Instant::now(),
@@ -784,6 +793,9 @@ pub enum Message {
BrushSpacingChanged(f32), BrushSpacingChanged(f32),
BrushColorVariantToggled(bool), BrushColorVariantToggled(bool),
BrushVariantAmountChanged(f32), BrushVariantAmountChanged(f32),
BrushAngleChanged(f32),
BrushSizeVarianceChanged(f32),
BrushAngleVarianceChanged(f32),
BrushCyclicColorToggled(bool), BrushCyclicColorToggled(bool),
BrushCyclicSpeedChanged(f32), BrushCyclicSpeedChanged(f32),
BrushImportAbr, BrushImportAbr,
@@ -1323,6 +1335,9 @@ fn build_brush_tip(state: &ToolState) -> BrushTip {
spacing: state.brush_spacing, spacing: state.brush_spacing,
color_variant: state.brush_color_variant, color_variant: state.brush_color_variant,
variant_amount: state.brush_variant_amount, variant_amount: state.brush_variant_amount,
angle: state.brush_angle,
roundness: state.brush_size_variance,
rotation_random: state.brush_angle_variance,
..BrushTip::default() ..BrushTip::default()
} }
} }
@@ -4976,6 +4991,21 @@ impl HcieIcedApp {
self.settings.update_from_tool_state(&self.tool_state); self.settings.update_from_tool_state(&self.tool_state);
let _ = self.settings.save(); let _ = self.settings.save();
} }
Message::BrushAngleChanged(angle) => {
self.tool_state.brush_angle = angle;
self.settings.update_from_tool_state(&self.tool_state);
let _ = self.settings.save();
}
Message::BrushSizeVarianceChanged(variance) => {
self.tool_state.brush_size_variance = variance;
self.settings.update_from_tool_state(&self.tool_state);
let _ = self.settings.save();
}
Message::BrushAngleVarianceChanged(variance) => {
self.tool_state.brush_angle_variance = variance;
self.settings.update_from_tool_state(&self.tool_state);
let _ = self.settings.save();
}
Message::BrushCyclicColorToggled(enabled) => { Message::BrushCyclicColorToggled(enabled) => {
self.tool_state.brush_cyclic_color = enabled; self.tool_state.brush_cyclic_color = enabled;
self.documents[self.active_doc] self.documents[self.active_doc]
@@ -186,6 +186,9 @@ pub fn panel_body<'a>(
app.tool_state.brush_variant_amount, app.tool_state.brush_variant_amount,
app.tool_state.brush_cyclic_color, app.tool_state.brush_cyclic_color,
app.tool_state.brush_cyclic_speed, app.tool_state.brush_cyclic_speed,
app.tool_state.brush_angle,
app.tool_state.brush_size_variance,
app.tool_state.brush_angle_variance,
app.brush_category, app.brush_category,
&app.tool_state.imported_brushes, &app.tool_state.imported_brushes,
app.tool_state.active_brush_preset.as_ref(), app.tool_state.active_brush_preset.as_ref(),
@@ -113,9 +113,9 @@ const BRUSH_STYLES: &[BrushStyleEntry] = &[
category: BrushCategory::Drawing, category: BrushCategory::Drawing,
}, },
BrushStyleEntry { BrushStyleEntry {
name: "Star", name: "Star Sparkle",
style: BrushStyle::Star, style: BrushStyle::Star,
category: BrushCategory::Drawing, category: BrushCategory::Effects,
}, },
BrushStyleEntry { BrushStyleEntry {
name: "Oil", name: "Oil",
@@ -510,6 +510,9 @@ fn build_tip_from_state(
opacity: f32, opacity: f32,
hardness: f32, hardness: f32,
cyclic_color: bool, cyclic_color: bool,
angle: f32,
size_variance: f32,
angle_variance: f32,
) -> hcie_engine_api::BrushTip { ) -> hcie_engine_api::BrushTip {
hcie_engine_api::BrushTip { hcie_engine_api::BrushTip {
style, style,
@@ -523,6 +526,9 @@ fn build_tip_from_state(
time_opacity_end: 1.0, time_opacity_end: 1.0,
time_angle_start: 0.0, time_angle_start: 0.0,
time_angle_end: 0.0, time_angle_end: 0.0,
angle,
roundness: size_variance,
rotation_random: angle_variance,
..hcie_engine_api::BrushTip::default() ..hcie_engine_api::BrushTip::default()
} }
} }
@@ -575,6 +581,9 @@ fn get_live_stroke_preview(
hardness: f32, hardness: f32,
cyclic_color: bool, cyclic_color: bool,
fg_color: [u8; 4], fg_color: [u8; 4],
angle: f32,
size_variance: f32,
angle_variance: f32,
) -> iced::widget::image::Handle { ) -> iced::widget::image::Handle {
// Clamp the rendered brush size so a 110 px soft brush does not fill the // 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 // entire 40 px preview well with a solid blob. The preview shows texture and
@@ -588,7 +597,16 @@ fn get_live_stroke_preview(
if let Some(handle) = cache.get(&key) { if let Some(handle) = cache.get(&key) {
return handle.clone(); return handle.clone();
} }
let tip = build_tip_from_state(style, preview_size, opacity, hardness, cyclic_color); let tip = build_tip_from_state(
style,
preview_size,
opacity,
hardness,
cyclic_color,
angle,
size_variance,
angle_variance,
);
let pixels = let pixels =
render_stroke_preview(LIVE_PREVIEW_IMG_SIZE, LIVE_PREVIEW_IMG_SIZE, &tip, fg_color); render_stroke_preview(LIVE_PREVIEW_IMG_SIZE, LIVE_PREVIEW_IMG_SIZE, &tip, fg_color);
let handle = iced::widget::image::Handle::from_rgba( let handle = iced::widget::image::Handle::from_rgba(
@@ -656,6 +674,9 @@ pub fn view(
brush_variant_amount: f32, brush_variant_amount: f32,
brush_cyclic_color: bool, brush_cyclic_color: bool,
brush_cyclic_speed: f32, brush_cyclic_speed: f32,
brush_angle: f32,
brush_size_variance: f32,
brush_angle_variance: f32,
active_category: BrushCategory, active_category: BrushCategory,
imported_presets: &[hcie_engine_api::BrushPreset], imported_presets: &[hcie_engine_api::BrushPreset],
active_brush_preset: Option<&hcie_engine_api::BrushPreset>, active_brush_preset: Option<&hcie_engine_api::BrushPreset>,
@@ -1051,6 +1072,50 @@ pub fn view(
Message::BrushCyclicSpeedChanged, Message::BrushCyclicSpeedChanged,
); );
let is_star = current_style == BrushStyle::Star;
let star_angle_slider = if is_star {
Some(plain_slider(
"Star Angle",
brush_angle,
0.0..=std::f32::consts::TAU,
0.01,
"rad",
2,
colors,
Message::BrushAngleChanged,
))
} else {
None
};
let star_size_var_slider = if is_star {
Some(plain_slider(
"Size Variance",
brush_size_variance,
0.0..=1.0,
0.01,
"%",
0,
colors,
Message::BrushSizeVarianceChanged,
))
} else {
None
};
let star_angle_var_slider = if is_star {
Some(plain_slider(
"Angle Variance",
brush_angle_variance,
0.0..=1.0,
0.01,
"%",
0,
colors,
Message::BrushAngleVarianceChanged,
))
} else {
None
};
// Compact color-variant toggle: an empty 12 px checkbox plus an 8 px label so // 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. // the control matches the rest of the compact panel typography.
let color_variant_check = row![ let color_variant_check = row![
@@ -1082,6 +1147,9 @@ pub fn view(
current_hardness, current_hardness,
brush_cyclic_color, brush_cyclic_color,
fg_color, fg_color,
brush_angle,
brush_size_variance,
brush_angle_variance,
); );
let live_preview = container( let live_preview = container(
iced::widget::image(preview_handle) iced::widget::image(preview_handle)
@@ -1095,12 +1163,16 @@ pub fn view(
.style(move |_theme| styles::recessed_control(colors)); .style(move |_theme| styles::recessed_control(colors));
// Panel title is in the dock tab — no duplicate inside the panel. // Panel title is in the dock tab — no duplicate inside the panel.
let panel = column![ let mut panel_children: Vec<Element<'static, Message>> = vec![
tabs, tabs.into(),
horizontal_rule(1), horizontal_rule(1).into(),
scrollable(column![grid_rows, wc_rows, media_rows, imported_rows].spacing(GRID_SPACING)) scrollable(column![grid_rows, wc_rows, media_rows, imported_rows].spacing(GRID_SPACING))
.height(Length::Fill), .height(Length::Fill)
horizontal_rule(1), .into(),
horizontal_rule(1).into(),
];
panel_children.push(
row![button(text("Import ABR").size(LABEL_SIZE)) row![button(text("Import ABR").size(LABEL_SIZE))
.on_press(Message::BrushImportAbr) .on_press(Message::BrushImportAbr)
.padding([3, 7]) .padding([3, 7])
@@ -1122,19 +1194,31 @@ pub fn view(
} }
} }
)] )]
.spacing(GRID_SPACING), .spacing(GRID_SPACING)
text("Preview").size(LABEL_SIZE + 1), .into(),
live_preview, );
size_slider, panel_children.push(text("Preview").size(LABEL_SIZE + 1).into());
opacity_slider, panel_children.push(live_preview.into());
hardness_slider, panel_children.push(size_slider.into());
color_variant_check, panel_children.push(opacity_slider.into());
variant_slider, panel_children.push(hardness_slider.into());
cyclic_color_check, panel_children.push(color_variant_check.into());
cyclic_speed_slider, panel_children.push(variant_slider.into());
] panel_children.push(cyclic_color_check.into());
.spacing(ITEM_SPACING) panel_children.push(cyclic_speed_slider.into());
.padding(3); if let Some(s) = star_angle_slider {
panel_children.push(s.into());
}
if let Some(s) = star_size_var_slider {
panel_children.push(s.into());
}
if let Some(s) = star_angle_var_slider {
panel_children.push(s.into());
}
let panel = iced::widget::Column::with_children(panel_children)
.spacing(ITEM_SPACING)
.padding(3);
container(panel) container(panel)
.width(Length::Fill) .width(Length::Fill)
@@ -1181,7 +1265,7 @@ mod tests {
presets.extend(hcie_paint_brushes::paint_presets()); presets.extend(hcie_paint_brushes::paint_presets());
presets.extend(hcie_digital_brushes::digital_presets()); presets.extend(hcie_digital_brushes::digital_presets());
presets.extend(hcie_watercolor_brushes::watercolor_presets()); presets.extend(hcie_watercolor_brushes::watercolor_presets());
assert_eq!(presets.len(), 47); assert_eq!(presets.len(), 48);
assert_eq!( assert_eq!(
presets presets
.iter() .iter()
@@ -59,6 +59,15 @@ pub struct ToolSettings {
/// Magnitude of the per-dab color variation (0.0..1.0). /// Magnitude of the per-dab color variation (0.0..1.0).
#[serde(default)] #[serde(default)]
pub brush_variant_amount: f32, pub brush_variant_amount: f32,
/// Star brush base rotation angle in radians.
#[serde(default)]
pub brush_angle: f32,
/// Star brush per-dab size variance (0.0..1.0).
#[serde(default)]
pub brush_size_variance: f32,
/// Star brush per-dab angle variance (0.0..1.0).
#[serde(default)]
pub brush_angle_variance: f32,
/// True when the brush color should cycle through the hue over stroke distance. /// True when the brush color should cycle through the hue over stroke distance.
#[serde(default)] #[serde(default)]
pub brush_cyclic_color: bool, pub brush_cyclic_color: bool,
@@ -198,6 +207,9 @@ impl Default for ToolSettings {
brush_spacing: 1.0, brush_spacing: 1.0,
brush_color_variant: false, brush_color_variant: false,
brush_variant_amount: 0.0, brush_variant_amount: 0.0,
brush_angle: 0.0,
brush_size_variance: 0.0,
brush_angle_variance: 0.0,
brush_cyclic_color: false, brush_cyclic_color: false,
brush_cyclic_speed: 0.5, brush_cyclic_speed: 0.5,
eraser_size: 20.0, eraser_size: 20.0,
@@ -352,6 +364,9 @@ impl AppSettings {
self.tool_settings.brush_hardness = tool_state.brush_hardness; self.tool_settings.brush_hardness = tool_state.brush_hardness;
self.tool_settings.brush_color_variant = tool_state.brush_color_variant; 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_variant_amount = tool_state.brush_variant_amount;
self.tool_settings.brush_angle = tool_state.brush_angle;
self.tool_settings.brush_size_variance = tool_state.brush_size_variance;
self.tool_settings.brush_angle_variance = tool_state.brush_angle_variance;
self.tool_settings.brush_cyclic_color = tool_state.brush_cyclic_color; 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.brush_cyclic_speed = tool_state.brush_cyclic_speed;
self.tool_settings.last_active_tool = format!("{:?}", tool_state.active_tool); self.tool_settings.last_active_tool = format!("{:?}", tool_state.active_tool);
@@ -364,6 +379,9 @@ impl AppSettings {
tool_state.brush_hardness = self.tool_settings.brush_hardness; tool_state.brush_hardness = self.tool_settings.brush_hardness;
tool_state.brush_color_variant = self.tool_settings.brush_color_variant; 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_variant_amount = self.tool_settings.brush_variant_amount;
tool_state.brush_angle = self.tool_settings.brush_angle;
tool_state.brush_size_variance = self.tool_settings.brush_size_variance;
tool_state.brush_angle_variance = self.tool_settings.brush_angle_variance;
tool_state.brush_cyclic_color = self.tool_settings.brush_cyclic_color; tool_state.brush_cyclic_color = self.tool_settings.brush_cyclic_color;
tool_state.brush_cyclic_speed = self.tool_settings.brush_cyclic_speed; tool_state.brush_cyclic_speed = self.tool_settings.brush_cyclic_speed;
} }