feat: enhance star brush with sparkle effect and update UI label
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-25 22:56:14 +03:00
parent 19a7e51813
commit cea60143b1
2 changed files with 82 additions and 27 deletions
+81 -26
View File
@@ -201,26 +201,32 @@ fn generate_square_stamp(d: usize, center: f32, r: f32, hardness: f32) -> Vec<u8
} }
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) -> Vec<u8> {
let points = 5u32; let arm_count = 4u32;
let inner_r = r * 0.4; let arm_width = r * 0.22;
let inner_r = r * 0.08;
(0..d * d) (0..d * d)
.map(|i| { .map(|i| {
let x = (i % d) as f32 - center; let x = (i % d) as f32 - center;
let y = (i / d) as f32 - center; let y = (i / d) as f32 - center;
let angle = y.atan2(x); let angle = y.atan2(x);
let dist = (x * x + y * y).sqrt(); let dist = (x * x + y * y).sqrt();
let point_angle = std::f32::consts::PI * 2.0 / points 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 edge = if a < point_angle / 2.0 { let arm_center = point_angle / 2.0;
inner_r + (r - inner_r) * (a / (point_angle / 2.0)) let arm_dist = (a - arm_center).abs();
} else { let max_arm_dist = arm_width / r;
r - (r - inner_r) * ((a - point_angle / 2.0) / (point_angle / 2.0)) if dist > r {
};
if dist >= edge {
0 0
} else { } else if arm_dist < max_arm_dist && dist > inner_r {
let alpha = 1.0 - (dist / edge).powf(1.0 - hardness.clamp(0.0, 0.99)); let arm_falloff = 1.0 - (arm_dist / max_arm_dist);
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 (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 {
0
} }
}) })
.collect() .collect()
@@ -1374,6 +1380,11 @@ fn draw_grain_brush(
let x1 = (cx + radius).ceil().min(width.saturating_sub(1) as f32) as u32; let x1 = (cx + radius).ceil().min(width.saturating_sub(1) as f32) as u32;
let y0 = (cy - radius).floor().max(0.0) as u32; let y0 = (cy - radius).floor().max(0.0) as u32;
let y1 = (cy + radius).ceil().min(height.saturating_sub(1) as f32) as u32; let y1 = (cy + radius).ceil().min(height.saturating_sub(1) as f32) as u32;
let seed_x = (cx.to_bits().wrapping_mul(73_856_093)
^ cy.to_bits().wrapping_mul(19_349_663)) as u32;
let seed_y = seed_x.wrapping_mul(668_265_263);
for y in y0..=y1 { for y in y0..=y1 {
for x in x0..=x1 { for x in x0..=x1 {
let dx = x as f32 - cx; let dx = x as f32 - cx;
@@ -1382,16 +1393,22 @@ fn draw_grain_brush(
if distance > 1.0 { if distance > 1.0 {
continue; continue;
} }
let ox = x.wrapping_add(seed_x);
let oy = y.wrapping_add(seed_y);
let hash = let hash =
((x.wrapping_mul(73_856_093) ^ y.wrapping_mul(19_349_663)) & 1023) as f32 / 1023.0; ((ox.wrapping_mul(73_856_093) ^ oy.wrapping_mul(19_349_663)) & 1023) as f32 / 1023.0;
let texture = if woven { let texture = if woven {
0.55 + 0.45 * ((x as f32 * 0.42).sin() * (y as f32 * 0.31).cos()).abs() 0.55 + 0.45 * ((x as f32 * 0.42 + seed_x as f32 * 0.001).sin()
* (y as f32 * 0.31 + seed_y as f32 * 0.001).cos())
.abs()
} else { } else {
hash hash
}; };
if texture < threshold { let grain_alpha = if texture < threshold {
continue; (texture / threshold).clamp(0.0, 1.0)
} } else {
0.55 + 0.45 * texture
};
let falloff = if distance <= hardness { let falloff = if distance <= hardness {
1.0 1.0
} else { } else {
@@ -1407,7 +1424,7 @@ fn draw_grain_brush(
* pressure * pressure
* falloff * falloff
* mask_alpha * mask_alpha
* (0.55 + 0.45 * texture) * grain_alpha
* (color[3] as f32 / 255.0); * (color[3] as f32 / 255.0);
let offset = index * 4; let offset = index * 4;
if is_eraser { if is_eraser {
@@ -1627,6 +1644,38 @@ fn draw_star_brush(
mask, mask,
Some(&stamp), Some(&stamp),
); );
let sparkle_count = ((effective_size * 0.15) as u32).clamp(2, 8);
let sparkle_r = effective_size * 0.12;
let tip_sm = BrushTip {
style: BrushStyle::Star,
size: sparkle_r,
hardness,
..BrushTip::default()
};
let stamp_sm = generate_brush_stamp(&tip_sm);
let seed = cx.to_bits().wrapping_mul(73_856_093)
^ cy.to_bits().wrapping_mul(19_349_663);
for i in 0..sparkle_count {
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 dist = ((hash_i >> 16) & 0xFFFF) as f32 / 65536.0 * effective_size * 0.6;
let sx = cx + angle.cos() * dist;
let sy = cy + angle.sin() * dist;
draw_dab_with_stamp(
pixels,
width,
height,
sx,
sy,
sparkle_r * 2.0,
hardness,
color,
opacity * pressure * 0.5,
is_eraser,
mask,
Some(&stamp_sm),
);
}
} }
/// Apply oil paint brush - coherent, directional bristle tracks with pigment pickup. /// Apply oil paint brush - coherent, directional bristle tracks with pigment pickup.
@@ -2964,6 +3013,10 @@ pub fn draw_crayon_brush(
let [fr, fg, fb, _] = color; let [fr, fg, fb, _] = color;
let seed_x = (cx.to_bits().wrapping_mul(73_856_093)
^ cy.to_bits().wrapping_mul(19_349_663)) as u32;
let seed_y = seed_x.wrapping_mul(668_265_263);
for py in y_min..=y_max { for py in y_min..=y_max {
for px in x_min..=x_max { for px in x_min..=x_max {
let dx = px as f32 - cx; let dx = px as f32 - cx;
@@ -2990,20 +3043,22 @@ pub fn draw_crayon_brush(
(1.0 - (d_sq - hard_sq) * soft_range_inv).clamp(0.0, 1.0) (1.0 - (d_sq - hard_sq) * soft_range_inv).clamp(0.0, 1.0)
}; };
// Stable canvas-space paper tooth keeps pores aligned across consecutive dabs. let ox = (px as u32).wrapping_add(seed_x);
let noise = ((px as u32).wrapping_mul(73_856_093) let oy = (py as u32).wrapping_add(seed_y);
^ (py as u32).wrapping_mul(19_349_663)) let noise = ox.wrapping_mul(73_856_093) ^ oy.wrapping_mul(19_349_663);
& 1023; let grain = (noise & 1023) as f32 / 1023.0;
let grain = noise as f32 / 1023.0; let threshold = 0.18 * (1.0 - pressure) + 0.08;
if grain < 0.18 * (1.0 - pressure) + 0.08 { let grain_alpha = if grain < threshold {
continue; (grain / threshold).clamp(0.0, 1.0)
} } else {
0.35 + 0.65 * grain
};
let brush_alpha = (alpha_factor let brush_alpha = (alpha_factor
* opacity * opacity
* pressure * pressure
* (color[3] as f32 / 255.0) * (color[3] as f32 / 255.0)
* (0.35 + 0.65 * grain) * grain_alpha
* (mask_val as f32 / 255.0) * (mask_val as f32 / 255.0)
* 255.0) * 255.0)
.round() as u8; .round() as u8;
@@ -1718,7 +1718,7 @@ pub fn show_brushes_ui(app: &mut HcieApp, ui: &mut egui::Ui) {
(hcie_engine_api::tools::BrushStyle::Marker, "Marker"), (hcie_engine_api::tools::BrushStyle::Marker, "Marker"),
(hcie_engine_api::tools::BrushStyle::Sketch, "Sketch"), (hcie_engine_api::tools::BrushStyle::Sketch, "Sketch"),
(hcie_engine_api::tools::BrushStyle::Hatch, "Hatch"), (hcie_engine_api::tools::BrushStyle::Hatch, "Hatch"),
(hcie_engine_api::tools::BrushStyle::Star, "Stipple Dots"), (hcie_engine_api::tools::BrushStyle::Star, "Star Sparkle"),
(hcie_engine_api::tools::BrushStyle::Glow, "Glow"), (hcie_engine_api::tools::BrushStyle::Glow, "Glow"),
(hcie_engine_api::tools::BrushStyle::Airbrush, "Airbrush"), (hcie_engine_api::tools::BrushStyle::Airbrush, "Airbrush"),
(hcie_engine_api::tools::BrushStyle::Pencil, "Pencil"), (hcie_engine_api::tools::BrushStyle::Pencil, "Pencil"),