feat: Enhance brush settings and add new layer paste functionality
- Added brush color variant and variant amount settings to the brush panel. - Introduced a checkbox for color variant and a slider for variant amount in the brush settings. - Implemented "Paste as New Layer" functionality, allowing users to paste clipboard images directly onto a new layer. - Updated menus to include the new paste option with a shortcut. - Improved layer panel iconography by replacing text buttons with SVG icons for visibility and lock toggles. - Created new SVG assets for closed eye and watercolor icons. - Enhanced history panel to provide richer descriptions for brush strokes and vector shape modifications. - Fixed the issue where the first brush stroke was missing from the history panel.
This commit is contained in:
+200
-20
@@ -1222,21 +1222,120 @@ pub fn draw_watercolor_brush(
|
||||
is_eraser: bool,
|
||||
mask: Option<&[u8]>,
|
||||
) {
|
||||
let mut rng = rand::thread_rng();
|
||||
let effective_size = size * pressure;
|
||||
let particle_opacity = opacity * 0.2 * pressure;
|
||||
draw_dab(
|
||||
pixels,
|
||||
width,
|
||||
height,
|
||||
cx,
|
||||
cy,
|
||||
effective_size,
|
||||
hardness,
|
||||
color,
|
||||
particle_opacity,
|
||||
is_eraser,
|
||||
mask,
|
||||
);
|
||||
if effective_size < 0.5 {
|
||||
return;
|
||||
}
|
||||
let base_opacity = opacity * 0.2 * pressure;
|
||||
let center_color = color;
|
||||
|
||||
// ── Central irregular core ──────────────────────────────────────────────
|
||||
// Build the main mark from several overlapping soft dabs so the boundary
|
||||
// is lumpy instead of a perfect circle.
|
||||
let core_dabs = rng.gen_range(2..=4);
|
||||
for _ in 0..core_dabs {
|
||||
let angle = rng.gen_range(0.0..std::f32::consts::TAU);
|
||||
let offset = effective_size * rng.gen_range(0.0..0.12);
|
||||
let dx = angle.cos() * offset;
|
||||
let dy = angle.sin() * offset;
|
||||
let s = effective_size * rng.gen_range(0.85..1.15);
|
||||
let a = base_opacity * rng.gen_range(0.7..1.3);
|
||||
draw_dab(
|
||||
pixels,
|
||||
width,
|
||||
height,
|
||||
cx + dx,
|
||||
cy + dy,
|
||||
s,
|
||||
hardness,
|
||||
center_color,
|
||||
a,
|
||||
is_eraser,
|
||||
mask,
|
||||
);
|
||||
}
|
||||
|
||||
// ── Satellite puddles ───────────────────────────────────────────────────
|
||||
// Secondary dabs pulled away from the stroke path by water tension,
|
||||
// giving the edge its characteristic ragged bloom.
|
||||
let satellite_count = (effective_size / 6.0).clamp(2.0, 8.0) as i32;
|
||||
for _ in 0..satellite_count {
|
||||
let angle = rng.gen_range(0.0..std::f32::consts::TAU);
|
||||
let dist = effective_size * rng.gen_range(0.25..0.65);
|
||||
let dx = angle.cos() * dist;
|
||||
let dy = angle.sin() * dist;
|
||||
let s = effective_size * rng.gen_range(0.2..0.55);
|
||||
let a = base_opacity * rng.gen_range(0.25..0.75);
|
||||
draw_dab(
|
||||
pixels,
|
||||
width,
|
||||
height,
|
||||
cx + dx,
|
||||
cy + dy,
|
||||
s,
|
||||
hardness * rng.gen_range(0.5..1.0),
|
||||
center_color,
|
||||
a,
|
||||
is_eraser,
|
||||
mask,
|
||||
);
|
||||
}
|
||||
|
||||
// ── Tiny splatter particles ───────────────────────────────────────────
|
||||
// Small droplets flung outward, visible on high-resolution strokes.
|
||||
let splatter_count = (effective_size * 1.2).clamp(3.0, 22.0) as i32;
|
||||
for _ in 0..splatter_count {
|
||||
let angle = rng.gen_range(0.0..std::f32::consts::TAU);
|
||||
let dist = effective_size * rng.gen_range(0.45..1.15);
|
||||
let dx = angle.cos() * dist;
|
||||
let dy = angle.sin() * dist;
|
||||
let s = effective_size * rng.gen_range(0.03..0.14);
|
||||
let a = base_opacity * rng.gen_range(0.4..1.3);
|
||||
draw_dab(
|
||||
pixels,
|
||||
width,
|
||||
height,
|
||||
cx + dx,
|
||||
cy + dy,
|
||||
s,
|
||||
rng.gen_range(0.0..0.4),
|
||||
center_color,
|
||||
a,
|
||||
is_eraser,
|
||||
mask,
|
||||
);
|
||||
}
|
||||
|
||||
// ── Back-run / blossom bursts ───────────────────────────────────────────
|
||||
// A few very large, faint "blooms" that extend beyond the main stroke,
|
||||
// simulating water pushing pigment to the edges.
|
||||
let bloom_count = if effective_size > 25.0 {
|
||||
rng.gen_range(1..=3)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
for _ in 0..bloom_count {
|
||||
let angle = rng.gen_range(0.0..std::f32::consts::TAU);
|
||||
let dist = effective_size * rng.gen_range(0.5..0.9);
|
||||
let dx = angle.cos() * dist;
|
||||
let dy = angle.sin() * dist;
|
||||
let s = effective_size * rng.gen_range(0.6..1.0);
|
||||
let a = base_opacity * rng.gen_range(0.15..0.35);
|
||||
draw_dab(
|
||||
pixels,
|
||||
width,
|
||||
height,
|
||||
cx + dx,
|
||||
cy + dy,
|
||||
s,
|
||||
0.0,
|
||||
center_color,
|
||||
a,
|
||||
is_eraser,
|
||||
mask,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply calligraphy brush - multiple parallel dabs
|
||||
@@ -2432,20 +2531,95 @@ fn quadratic_bezier(p0: f32, p1: f32, p2: f32, t: f32) -> f32 {
|
||||
/// `amount` is expected in [0, 1]. Hue is shifted by ±(amount*60°),
|
||||
/// lightness by ±(amount*0.4). Saturation is preserved to keep the
|
||||
/// color within the same family (green stays green, purple stays purple).
|
||||
/// Per-thread remembered HSL walk state for one base color.
|
||||
///
|
||||
/// Keeps the last hue/lightness/saturation offsets so successive dabs move smoothly
|
||||
/// instead of jumping to independent random values. The state is keyed by the
|
||||
/// base color so unrelated colors do not interfere.
|
||||
thread_local! {
|
||||
static LAST_HSL_WALK: std::cell::RefCell<std::collections::HashMap<u64, (f32, f32, f32)>> =
|
||||
std::cell::RefCell::new(std::collections::HashMap::new());
|
||||
}
|
||||
|
||||
/// Hash helper for the base color used as thread-local state key.
|
||||
fn color_hash(color: [u8; 4]) -> u64 {
|
||||
((color[0] as u64) << 24)
|
||||
| ((color[1] as u64) << 16)
|
||||
| ((color[2] as u64) << 8)
|
||||
| (color[3] as u64)
|
||||
}
|
||||
|
||||
/// Vary the base color smoothly in HSL space and avoid harsh sequential contrasts.
|
||||
///
|
||||
/// **Logic & Workflow:**
|
||||
/// 1. Loads the previous HSL offset for this base color from thread-local state.
|
||||
/// 2. Takes a small random step so the transition from the previous dab is gradual.
|
||||
/// 3. Rejects offsets that land in the complementary hue zone (150°..210° from base),
|
||||
/// which is where high-contrast color combinations occur.
|
||||
/// 4. Stores the new offset back into thread-local state.
|
||||
///
|
||||
/// **Arguments:**
|
||||
/// * `color` — base RGBA color.
|
||||
/// * `amount` — 0.0..1.0 overall variation strength.
|
||||
/// * `rng` — random source.
|
||||
pub fn vary_color_hsl<R: Rng>(color: [u8; 4], amount: f32, rng: &mut R) -> [u8; 4] {
|
||||
let clamped = amount.clamp(0.0, 1.0);
|
||||
if clamped <= 0.0 {
|
||||
return color;
|
||||
}
|
||||
let (h, s, l) = rgb_to_hsl(color[0], color[1], color[2]);
|
||||
let hue_shift = rng.gen_range(-1.0..=1.0) * clamped * 60.0;
|
||||
let light_shift = rng.gen_range(-1.0..=1.0) * clamped * 0.4;
|
||||
let new_h = (h + hue_shift).rem_euclid(360.0);
|
||||
let new_l = (l + light_shift).clamp(0.0, 1.0);
|
||||
let (r, g, b) = hsl_to_rgb(new_h, s, new_l);
|
||||
|
||||
let (base_h, base_s, base_l) = rgb_to_hsl(color[0], color[1], color[2]);
|
||||
let key = color_hash(color);
|
||||
|
||||
let (mut hue_off, mut light_off, mut sat_off) = LAST_HSL_WALK
|
||||
.with_borrow(|m| m.get(&key).copied().unwrap_or((0.0, 0.0, 0.0)));
|
||||
|
||||
// Maximum per-dab step: keeps transitions smooth.
|
||||
let max_hue_step = clamped * 18.0; // ±18° per dab at amount=1
|
||||
let max_light_step = clamped * 0.12;
|
||||
let max_sat_step = clamped * 0.10;
|
||||
|
||||
// Total range stays within a harmonious neighbourhood of the base color.
|
||||
let max_hue_off = clamped * 60.0;
|
||||
let max_light_off = clamped * 0.35;
|
||||
let max_sat_off = clamped * 0.25;
|
||||
|
||||
for _ in 0..8 {
|
||||
let step_h = rng.gen_range(-1.0..=1.0) * max_hue_step;
|
||||
hue_off = (hue_off + step_h).clamp(-max_hue_off, max_hue_off);
|
||||
|
||||
let step_l = rng.gen_range(-1.0..=1.0) * max_light_step;
|
||||
light_off = (light_off + step_l).clamp(-max_light_off, max_light_off);
|
||||
|
||||
let step_s = rng.gen_range(-1.0..=1.0) * max_sat_step;
|
||||
sat_off = (sat_off + step_s).clamp(-max_sat_off, max_sat_off);
|
||||
|
||||
// Reject complementary (high-contrast) hue offsets.
|
||||
// The complementary zone is 150°..210° away from the base hue.
|
||||
let abs_hue = hue_off.abs();
|
||||
if abs_hue < 150.0 || abs_hue > 210.0 {
|
||||
break;
|
||||
}
|
||||
// If we land in the complementary zone, nudge back toward base and retry.
|
||||
hue_off *= 0.7;
|
||||
}
|
||||
|
||||
LAST_HSL_WALK.with_borrow_mut(|m| {
|
||||
m.insert(key, (hue_off, light_off, sat_off));
|
||||
});
|
||||
|
||||
let new_h = (base_h + hue_off).rem_euclid(360.0);
|
||||
let new_s = (base_s + sat_off).clamp(0.0, 1.0);
|
||||
let new_l = (base_l + light_off).clamp(0.0, 1.0);
|
||||
let (r, g, b) = hsl_to_rgb(new_h, new_s, new_l);
|
||||
[r, g, b, color[3]]
|
||||
}
|
||||
|
||||
/// Reset the smooth color-walk state. Useful when starting a fresh stroke or test.
|
||||
pub fn reset_color_variant_walk() {
|
||||
LAST_HSL_WALK.with_borrow_mut(|m| m.clear());
|
||||
}
|
||||
|
||||
fn rgb_to_hsl(r: u8, g: u8, b: u8) -> (f32, f32, f32) {
|
||||
let rf = r as f32 / 255.0;
|
||||
let gf = g as f32 / 255.0;
|
||||
@@ -2975,6 +3149,12 @@ pub fn draw_specialized_stroke(
|
||||
return;
|
||||
}
|
||||
|
||||
// Start each stroke from the base color so successive strokes do not inherit
|
||||
// a strong leftover offset from the previous stroke.
|
||||
if color_variant && variant_amount > 0.0 {
|
||||
reset_color_variant_walk();
|
||||
}
|
||||
|
||||
let spacing = brush_spacing_pixels(size, spacing_ratio);
|
||||
|
||||
for i in 0..points.len() {
|
||||
|
||||
Reference in New Issue
Block a user