use rand::prelude::*;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum WindMouseError {
#[error("Invalid wait time: min_wait ({min_wait}) must be less than or equal to max_wait ({max_wait})")]
InvalidWaitTime { min_wait: f32, max_wait: f32 },
#[error("Invalid parameter: {0} must be non-negative")]
NegativeParameter(&'static str),
}
#[derive(Clone, Copy, Debug)]
pub struct Coordinate {
pub x: f32,
pub y: f32,
}
impl Coordinate {
pub fn new(x: f32, y: f32) -> Self {
Self { x, y }
}
pub fn as_i32(&self) -> [i32; 2] {
[self.x.round() as i32, self.y.round() as i32]
}
}
#[derive(Debug)]
pub struct WindMouse {
pub gravity: f32,
pub wind: f32,
pub min_wait: f32,
pub max_wait: f32,
pub max_step: f32,
pub target_area: f32,
pub mouse_speed: f32,
pub random_speed: f32,
}
impl WindMouse {
pub fn new(mouse_speed: f32, gravity: f32, wind: f32, min_wait: f32, max_wait: f32, max_step: f32, target_area: f32) -> Result<Self, WindMouseError> {
if min_wait > max_wait {
return Err(WindMouseError::InvalidWaitTime { min_wait, max_wait });
}
for &(value, name) in &[(mouse_speed, "mouse_speed"), (gravity, "gravity"), (wind, "wind"),
(min_wait, "min_wait"), (max_wait, "max_wait"), (max_step, "max_step"), (target_area, "target_area")] {
if value < 0.0 {
return Err(WindMouseError::NegativeParameter(name));
}
}
let random_seed = random::<f32>() * 10.0;
let random_speed = (random_seed / 2.0 + mouse_speed / 10.0).max(0.1);
Ok(WindMouse {
gravity,
wind,
min_wait,
max_wait,
max_step,
target_area,
mouse_speed,
random_speed,
})
}
pub fn new_default() -> Self {
Self::new(10.0, 9.0, 3.0, 2.0, 10.0, 10.0, 100.0).expect("Default values should always be valid")
}
pub fn generate_points(&self, start: Coordinate, end: Coordinate) -> Vec<[i32; 3]> {
let mut rng = thread_rng();
let mut current = start;
let mut wind_x = rng.random::<f32>() * 10.0;
let mut wind_y = rng.random::<f32>() * 10.0;
let mut velocity_x = 0.0;
let mut velocity_y = 0.0;
let wait_diff = self.max_wait - self.min_wait;
let sqrt2 = 2.0_f32.sqrt();
let sqrt3 = 3.0_f32.sqrt();
let sqrt5 = 5.0_f32.sqrt();
let mut points = Vec::new();
let mut current_wait = 0;
loop {
let dist = Self::hypot(end.x - current.x, end.y - current.y);
if dist <= 1.0 {
break;
}
let wind = self.wind.min(dist);
if dist >= self.target_area {
let w = rng.random::<f32>() * wind * 2.0 + 1.0;
wind_x = wind_x / sqrt3 + (w - wind) / sqrt5;
wind_y = wind_y / sqrt3 + (w - wind) / sqrt5;
} else {
wind_x /= sqrt2;
wind_y /= sqrt2;
}
velocity_x += wind_x;
velocity_y += wind_y;
velocity_x += self.gravity * (end.x - current.x) / dist;
velocity_y += self.gravity * (end.y - current.y) / dist;
let velocity_mag = Self::hypot(velocity_x, velocity_y);
if velocity_mag > self.max_step {
let random_dist = self.max_step / 2.0 + rng.random::<f32>() * self.max_step / 2.0;
velocity_x = (velocity_x / velocity_mag) * random_dist;
velocity_y = (velocity_y / velocity_mag) * random_dist;
}
let old = current;
current.x += velocity_x;
current.y += velocity_y;
let step = Self::hypot(current.x - old.x, current.y - old.y);
let wait = (wait_diff * (step / self.max_step) + self.min_wait).round() as i32;
current_wait += wait;
let new = Coordinate { x: current.x.round(), y: current.y.round() };
if new.as_i32() != old.as_i32() {
points.push([new.as_i32()[0], new.as_i32()[1], current_wait]);
}
}
let end_point = end.as_i32();
if points.last().map(|&p| [p[0], p[1]]) != Some([end_point[0], end_point[1]]) {
points.push([end_point[0], end_point[1], current_wait]);
}
points
}
fn hypot(dx: f32, dy: f32) -> f32 {
(dx * dx + dy * dy).sqrt()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_valid_parameters() {
assert!(WindMouse::new(10.0, 9.0, 3.0, 2.0, 10.0, 10.0, 100.0).is_ok());
}
#[test]
fn test_new_invalid_wait_times() {
assert!(matches!(
WindMouse::new(10.0, 9.0, 3.0, 10.0, 2.0, 10.0, 100.0),
Err(WindMouseError::InvalidWaitTime { .. })
));
}
#[test]
fn test_new_negative_parameter() {
assert!(matches!(
WindMouse::new(-1.0, 9.0, 3.0, 2.0, 10.0, 10.0, 100.0),
Err(WindMouseError::NegativeParameter("mouse_speed"))
));
}
#[test]
fn test_generate_points() {
let wind_mouse = WindMouse::new_default();
let start = Coordinate::new(0.0, 0.0);
let end = Coordinate::new(100.0, 100.0);
let points = wind_mouse.generate_points(start, end);
assert!(!points.is_empty());
assert_eq!(points.last().unwrap()[0..2], [100, 100]);
}
}