149 lines
5.2 KiB
Rust
149 lines
5.2 KiB
Rust
//! Brush settings for the raster paint engine
|
||
//!
|
||
//! Settings that describe the appearance and behavior of a paint brush.
|
||
//! Compatible with MyPaint .myb brush file format (subset).
|
||
|
||
use serde::{Deserialize, Serialize};
|
||
|
||
/// Settings for a paint brush
|
||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||
pub struct BrushSettings {
|
||
/// log(radius) base value; actual radius = exp(radius_log)
|
||
pub radius_log: f32,
|
||
/// Edge hardness 0.0 (fully soft/gaussian) to 1.0 (hard edge)
|
||
pub hardness: f32,
|
||
/// Base opacity 0.0–1.0
|
||
pub opaque: f32,
|
||
/// Dab spacing as fraction of radius (smaller = denser strokes)
|
||
pub dabs_per_radius: f32,
|
||
/// HSV hue (0.0–1.0); usually overridden by stroke color
|
||
pub color_h: f32,
|
||
/// HSV saturation (0.0–1.0)
|
||
pub color_s: f32,
|
||
/// HSV value (0.0–1.0)
|
||
pub color_v: f32,
|
||
/// How much pressure increases/decreases radius
|
||
/// Final radius = exp(radius_log + pressure_radius_gain * pressure)
|
||
pub pressure_radius_gain: f32,
|
||
/// How much pressure increases/decreases opacity
|
||
/// Final opacity = opaque * (1 + pressure_opacity_gain * (pressure - 0.5))
|
||
pub pressure_opacity_gain: f32,
|
||
}
|
||
|
||
impl BrushSettings {
|
||
/// Default soft round brush (smooth Gaussian falloff)
|
||
pub fn default_round_soft() -> Self {
|
||
Self {
|
||
radius_log: 2.0, // radius ≈ 7.4 px
|
||
hardness: 0.1,
|
||
opaque: 0.8,
|
||
dabs_per_radius: 0.25,
|
||
color_h: 0.0,
|
||
color_s: 0.0,
|
||
color_v: 0.0,
|
||
pressure_radius_gain: 0.5,
|
||
pressure_opacity_gain: 1.0,
|
||
}
|
||
}
|
||
|
||
/// Default hard round brush (sharp edge)
|
||
pub fn default_round_hard() -> Self {
|
||
Self {
|
||
radius_log: 2.0,
|
||
hardness: 0.9,
|
||
opaque: 1.0,
|
||
dabs_per_radius: 0.2,
|
||
color_h: 0.0,
|
||
color_s: 0.0,
|
||
color_v: 0.0,
|
||
pressure_radius_gain: 0.3,
|
||
pressure_opacity_gain: 0.8,
|
||
}
|
||
}
|
||
|
||
/// Compute actual radius at a given pressure level
|
||
pub fn radius_at_pressure(&self, pressure: f32) -> f32 {
|
||
let r = self.radius_log + self.pressure_radius_gain * (pressure - 0.5);
|
||
r.exp().clamp(0.5, 500.0)
|
||
}
|
||
|
||
/// Compute actual opacity at a given pressure level
|
||
pub fn opacity_at_pressure(&self, pressure: f32) -> f32 {
|
||
let o = self.opaque * (1.0 + self.pressure_opacity_gain * (pressure - 0.5));
|
||
o.clamp(0.0, 1.0)
|
||
}
|
||
|
||
/// Parse a MyPaint .myb JSON brush file (subset).
|
||
///
|
||
/// Reads `radius_logarithmic`, `hardness`, `opaque`, `dabs_per_basic_radius`,
|
||
/// `color_h`, `color_s`, `color_v` from the `settings` key's `base_value` fields.
|
||
pub fn from_myb(json: &str) -> Result<Self, String> {
|
||
let v: serde_json::Value =
|
||
serde_json::from_str(json).map_err(|e| format!("JSON parse error: {e}"))?;
|
||
|
||
let settings = v.get("settings").ok_or("Missing 'settings' key")?;
|
||
|
||
let read_base = |name: &str, default: f32| -> f32 {
|
||
settings
|
||
.get(name)
|
||
.and_then(|s| s.get("base_value"))
|
||
.and_then(|bv| bv.as_f64())
|
||
.map(|f| f as f32)
|
||
.unwrap_or(default)
|
||
};
|
||
|
||
// Pressure dynamics: read from the "inputs" mapping of radius/opacity
|
||
// For simplicity, look for the pressure input point in radius_logarithmic
|
||
let pressure_radius_gain = settings
|
||
.get("radius_logarithmic")
|
||
.and_then(|s| s.get("inputs"))
|
||
.and_then(|inp| inp.get("pressure"))
|
||
.and_then(|pts| pts.as_array())
|
||
.and_then(|arr| {
|
||
// arr = [[x0,y0],[x1,y1],...] – approximate as linear gain at x=1.0
|
||
if arr.len() >= 2 {
|
||
let y0 = arr[0].get(1)?.as_f64()? as f32;
|
||
let y1 = arr[arr.len() - 1].get(1)?.as_f64()? as f32;
|
||
Some((y1 - y0) * 0.5)
|
||
} else {
|
||
None
|
||
}
|
||
})
|
||
.unwrap_or(0.5);
|
||
|
||
let pressure_opacity_gain = settings
|
||
.get("opaque")
|
||
.and_then(|s| s.get("inputs"))
|
||
.and_then(|inp| inp.get("pressure"))
|
||
.and_then(|pts| pts.as_array())
|
||
.and_then(|arr| {
|
||
if arr.len() >= 2 {
|
||
let y0 = arr[0].get(1)?.as_f64()? as f32;
|
||
let y1 = arr[arr.len() - 1].get(1)?.as_f64()? as f32;
|
||
Some(y1 - y0)
|
||
} else {
|
||
None
|
||
}
|
||
})
|
||
.unwrap_or(1.0);
|
||
|
||
Ok(Self {
|
||
radius_log: read_base("radius_logarithmic", 2.0),
|
||
hardness: read_base("hardness", 0.5).clamp(0.0, 1.0),
|
||
opaque: read_base("opaque", 1.0).clamp(0.0, 1.0),
|
||
dabs_per_radius: read_base("dabs_per_basic_radius", 0.25).clamp(0.01, 10.0),
|
||
color_h: read_base("color_h", 0.0),
|
||
color_s: read_base("color_s", 0.0),
|
||
color_v: read_base("color_v", 0.0),
|
||
pressure_radius_gain,
|
||
pressure_opacity_gain,
|
||
})
|
||
}
|
||
}
|
||
|
||
impl Default for BrushSettings {
|
||
fn default() -> Self {
|
||
Self::default_round_soft()
|
||
}
|
||
}
|