Files
hcie-rust-v3.05/hcie-brush-engine/src/dynamics.rs
T
2026-07-09 02:59:53 +03:00

104 lines
3.6 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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.01.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]]
}