104 lines
3.6 KiB
Rust
104 lines
3.6 KiB
Rust
//! Brush dynamics — pressure, velocity, tilt mapping for brush strokes.
|
||
|
||
use hcie_protocol::BrushTip;
|
||
|
||
/// Per-stroke dynamic parameters computed from input state.
|
||
#[derive(Debug, Clone, Copy, Default)]
|
||
pub struct StrokeDynamics {
|
||
pub pressure: f32, // 0.0–1.0
|
||
pub velocity: f32, // Pixels per frame
|
||
pub tilt: (f32, f32), // (x_tilt, y_tilt) in degrees
|
||
}
|
||
|
||
/// Compute effective brush size from base size, dynamics, and pressure.
|
||
pub fn effective_size(base_size: f32, dynamics: &StrokeDynamics, pressure_sensitive: bool) -> f32 {
|
||
if !pressure_sensitive {
|
||
return base_size;
|
||
}
|
||
let p = dynamics.pressure.clamp(0.0, 1.0);
|
||
// Quadratic falloff for natural feel
|
||
let factor = p * p * (3.0 - 2.0 * p); // smoothstep
|
||
base_size * factor.max(0.1)
|
||
}
|
||
|
||
/// Compute effective opacity from base opacity, dynamics, and pressure.
|
||
pub fn effective_opacity(base_opacity: f32, dynamics: &StrokeDynamics, pressure_sensitive: bool) -> f32 {
|
||
if !pressure_sensitive {
|
||
return base_opacity;
|
||
}
|
||
let p = dynamics.pressure.clamp(0.0, 1.0);
|
||
base_opacity * p
|
||
}
|
||
|
||
/// Compute effective flow from base flow, dynamics, and pressure.
|
||
pub fn effective_flow(base_flow: f32, dynamics: &StrokeDynamics, pressure_sensitive: bool) -> f32 {
|
||
if !pressure_sensitive {
|
||
return base_flow;
|
||
}
|
||
let p = dynamics.pressure.clamp(0.0, 1.0);
|
||
base_flow * p
|
||
}
|
||
|
||
/// Compute velocity-based size modifier (faster = thinner).
|
||
pub fn velocity_size_modifier(velocity: f32) -> f32 {
|
||
// At 0 velocity = 1.0, at 100 px/frame = 0.5
|
||
let factor = 1.0 - (velocity / 200.0).clamp(0.0, 0.5);
|
||
factor.clamp(0.5, 1.0)
|
||
}
|
||
|
||
/// Build a `BrushTip` with dynamics applied.
|
||
pub fn build_dynamic_tip(base: &BrushTip, dynamics: &StrokeDynamics, pressure_size: bool, pressure_opacity: bool) -> BrushTip {
|
||
let mut tip = base.clone();
|
||
tip.size = effective_size(base.size, dynamics, pressure_size);
|
||
tip.opacity = effective_opacity(base.opacity, dynamics, pressure_opacity);
|
||
tip
|
||
}
|
||
|
||
/// Simulate tablet pressure from mouse velocity (fallback for non-tablet input).
|
||
/// Slower movement = higher pressure.
|
||
pub fn simulate_pressure_from_velocity(velocity: f32, max_velocity: f32) -> f32 {
|
||
let t = (velocity / max_velocity).clamp(0.0, 1.0);
|
||
// Inverse: slow = high pressure
|
||
(1.0 - t * t).clamp(0.2, 1.0)
|
||
}
|
||
|
||
/// Track velocity from consecutive points.
|
||
#[derive(Debug, Clone, Default)]
|
||
pub struct VelocityTracker {
|
||
last_pos: Option<(f32, f32)>,
|
||
last_time: Option<f32>,
|
||
}
|
||
|
||
impl VelocityTracker {
|
||
pub fn new() -> Self { Self::default() }
|
||
|
||
pub fn update(&mut self, x: f32, y: f32, time: f32) -> f32 {
|
||
let velocity = match (self.last_pos, self.last_time) {
|
||
(Some((lx, ly)), Some(lt)) => {
|
||
let dt = (time - lt).max(0.001);
|
||
let dist = ((x - lx).powi(2) + (y - ly).powi(2)).sqrt();
|
||
dist / dt
|
||
}
|
||
_ => 0.0,
|
||
};
|
||
self.last_pos = Some((x, y));
|
||
self.last_time = Some(time);
|
||
velocity
|
||
}
|
||
|
||
pub fn reset(&mut self) {
|
||
self.last_pos = None;
|
||
self.last_time = None;
|
||
}
|
||
}
|
||
|
||
/// Add jitter to a color for textured brushes.
|
||
pub fn jitter_color(color: [u8; 4], variation: u8, rng: &mut rand::rngs::ThreadRng) -> [u8; 4] {
|
||
use rand::Rng;
|
||
let jitter = variation as f32 / 255.0;
|
||
let r = (color[0] as f32 * (1.0 - jitter) + rng.gen::<f32>() * jitter * 255.0).clamp(0.0, 255.0) as u8;
|
||
let g = (color[1] as f32 * (1.0 - jitter) + rng.gen::<f32>() * jitter * 255.0).clamp(0.0, 255.0) as u8;
|
||
let b = (color[2] as f32 * (1.0 - jitter) + rng.gen::<f32>() * jitter * 255.0).clamp(0.0, 255.0) as u8;
|
||
[r, g, b, color[3]]
|
||
}
|