windmouse_rs/
lib.rs

1//! WindMouse: A Rust implementation of the WindMouse algorithm
2//!
3//! This module provides a WindMouse struct and associated methods to generate
4//! realistic mouse movement paths. The algorithm simulates mouse movements
5//! with consideration for gravity, wind, and randomness to create more
6//! human-like cursor trajectories.
7//!
8//! # Dependencies
9//!
10//! - rand = "0.9.0-alpha.2"
11//!   We're using the updated syntax for future compatibility with Rust 2024.
12//!   Unfortunately this requires the 0.9.0 Alpha.
13//!
14//! - thiserror = "1.0.63"
15//!   For custom error handling.
16//!
17//! # Example
18//!
19//! ```
20//! use windmouse::{WindMouse, Coordinate};
21//!
22//! let wind_mouse = WindMouse::new_default();
23//! let start = Coordinate::new(0.0, 0.0);
24//! let end = Coordinate::new(100.0, 100.0);
25//! let points = wind_mouse.generate_points(start, end);
26//! ```
27
28use rand::prelude::*;
29use thiserror::Error;
30
31#[derive(Debug, Error)]
32pub enum WindMouseError {
33    #[error("Invalid wait time: min_wait ({min_wait}) must be less than or equal to max_wait ({max_wait})")]
34    InvalidWaitTime { min_wait: f32, max_wait: f32 },
35    #[error("Invalid parameter: {0} must be non-negative")]
36    NegativeParameter(&'static str),
37}
38
39/// Represents a 2D coordinate with floating-point precision
40#[derive(Clone, Copy, Debug)]
41pub struct Coordinate {
42    pub x: f32,
43    pub y: f32,
44}
45
46impl Coordinate {
47    pub fn new(x: f32, y: f32) -> Self {
48        Self { x, y }
49    }
50
51    /// Converts the floating-point coordinate to integer values
52    pub fn as_i32(&self) -> [i32; 2] {
53        [self.x.round() as i32, self.y.round() as i32]
54    }
55}
56
57/// WindMouse struct containing parameters for the mouse movement algorithm
58///
59/// This struct holds all the parameters that influence the behavior of the
60/// WindMouse algorithm, allowing for fine-tuning of the generated mouse movements.
61///
62/// # Parameters
63///
64/// * `gravity`: Influences how strongly the mouse is pulled towards the target.
65///   Higher values make the movement more direct.
66///
67/// * `wind`: Determines the amount of randomness in the mouse's path.
68///   Higher values create more curved and unpredictable movements.
69///
70/// * `min_wait`: The minimum time (in milliseconds) to wait between mouse movements.
71///   This helps simulate human-like pauses in movement.
72///
73/// * `max_wait`: The maximum time (in milliseconds) to wait between mouse movements.
74///   Along with `min_wait`, this creates variability in movement speed.
75///
76/// * `max_step`: The maximum distance the mouse can move in a single step.
77///   This prevents unrealistically fast movements.
78///
79/// * `target_area`: The distance from the end point at which the algorithm
80///   starts to slow down and become more precise.
81///
82/// * `mouse_speed`: A general speed factor for the mouse movement.
83///   Higher values result in faster overall movement.
84///
85/// * `random_speed`: An additional randomness factor for mouse speed.
86///   This helps create more natural, variable-speed movements.
87#[derive(Debug)]
88pub struct WindMouse {
89    pub gravity: f32,
90    pub wind: f32,
91    pub min_wait: f32,
92    pub max_wait: f32,
93    pub max_step: f32,
94    pub target_area: f32,
95    pub mouse_speed: f32,
96    pub random_speed: f32,
97}
98
99impl WindMouse {
100    /// Creates a new WindMouse instance with the specified parameters
101    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> {
102        if min_wait > max_wait {
103            return Err(WindMouseError::InvalidWaitTime { min_wait, max_wait });
104        }
105
106        for &(value, name) in &[(mouse_speed, "mouse_speed"), (gravity, "gravity"), (wind, "wind"),
107            (min_wait, "min_wait"), (max_wait, "max_wait"), (max_step, "max_step"), (target_area, "target_area")] {
108            if value < 0.0 {
109                return Err(WindMouseError::NegativeParameter(name));
110            }
111        }
112
113        let random_seed = random::<f32>() * 10.0;
114        let random_speed = (random_seed / 2.0 + mouse_speed / 10.0).max(0.1);
115
116        Ok(WindMouse {
117            gravity,
118            wind,
119            min_wait,
120            max_wait,
121            max_step,
122            target_area,
123            mouse_speed,
124            random_speed,
125        })
126    }
127
128    /// Creates a new WindMouse instance with default values for all parameters except target_area
129    ///
130    /// This function provides a convenient way to create a WindMouse instance with
131    /// sensible defaults for most parameters.
132    ///
133    /// # Arguments
134    ///
135    /// * `target_area` - The distance from the end point at which the algorithm starts to slow down
136    ///
137    /// # Default values
138    ///
139    /// * `mouse_speed`: 10.0
140    /// * `gravity`: 9.0
141    /// * `wind`: 3.0
142    /// * `min_wait`: 2.0
143    /// * `max_wait`: 10.0
144    /// * `max_step`: 10.0
145    /// * `target_area`: 100.0
146    pub fn new_default() -> Self {
147        Self::new(10.0, 9.0, 3.0, 2.0, 10.0, 10.0, 100.0).expect("Default values should always be valid")
148    }
149
150    /// Generates a series of points representing the mouse movement path
151    /// from the start coordinate to the end coordinate
152    pub fn generate_points(&self, start: Coordinate, end: Coordinate) -> Vec<[i32; 3]> {
153        let mut rng = thread_rng();
154        let mut current = start;
155        let mut wind_x = rng.random::<f32>() * 10.0;
156        let mut wind_y = rng.random::<f32>() * 10.0;
157        let mut velocity_x = 0.0;
158        let mut velocity_y = 0.0;
159        let wait_diff = self.max_wait - self.min_wait;
160        let sqrt2 = 2.0_f32.sqrt();
161        let sqrt3 = 3.0_f32.sqrt();
162        let sqrt5 = 5.0_f32.sqrt();
163
164        let mut points = Vec::new();
165        let mut current_wait = 0;
166
167        loop {
168            // Calculate distance to the end point
169            let dist = Self::hypot(end.x - current.x, end.y - current.y);
170            if dist <= 1.0 {
171                break;
172            }
173
174            // Adjust wind based on distance to target
175            let wind = self.wind.min(dist);
176
177            if dist >= self.target_area {
178                // Apply wind if we're far from the target
179                let w = rng.random::<f32>() * wind * 2.0 + 1.0;
180                wind_x = wind_x / sqrt3 + (w - wind) / sqrt5;
181                wind_y = wind_y / sqrt3 + (w - wind) / sqrt5;
182            } else {
183                // Reduce wind as we get closer to the target
184                wind_x /= sqrt2;
185                wind_y /= sqrt2;
186            }
187
188            // Update velocity based on wind and gravity
189            velocity_x += wind_x;
190            velocity_y += wind_y;
191            velocity_x += self.gravity * (end.x - current.x) / dist;
192            velocity_y += self.gravity * (end.y - current.y) / dist;
193
194            // Normalize velocity if it exceeds max_step
195            let velocity_mag = Self::hypot(velocity_x, velocity_y);
196            if velocity_mag > self.max_step {
197                let random_dist = self.max_step / 2.0 + rng.random::<f32>() * self.max_step / 2.0;
198                velocity_x = (velocity_x / velocity_mag) * random_dist;
199                velocity_y = (velocity_y / velocity_mag) * random_dist;
200            }
201
202            // Move the cursor
203            let old = current;
204            current.x += velocity_x;
205            current.y += velocity_y;
206
207            // Calculate wait time based on step size
208            let step = Self::hypot(current.x - old.x, current.y - old.y);
209            let wait = (wait_diff * (step / self.max_step) + self.min_wait).round() as i32;
210            current_wait += wait;
211
212            // Add point to the list if it's different from the previous one
213            let new = Coordinate { x: current.x.round(), y: current.y.round() };
214            if new.as_i32() != old.as_i32() {
215                points.push([new.as_i32()[0], new.as_i32()[1], current_wait]);
216            }
217        }
218
219        // Ensure the end point is included
220        let end_point = end.as_i32();
221        if points.last().map(|&p| [p[0], p[1]]) != Some([end_point[0], end_point[1]]) {
222            points.push([end_point[0], end_point[1], current_wait]);
223        }
224
225        points
226    }
227
228    /// Calculates the hypotenuse (Euclidean distance) between two points
229    fn hypot(dx: f32, dy: f32) -> f32 {
230        (dx * dx + dy * dy).sqrt()
231    }
232}
233
234#[cfg(test)]
235mod tests {
236    use super::*;
237
238    #[test]
239    fn test_new_valid_parameters() {
240        assert!(WindMouse::new(10.0, 9.0, 3.0, 2.0, 10.0, 10.0, 100.0).is_ok());
241    }
242
243    #[test]
244    fn test_new_invalid_wait_times() {
245        assert!(matches!(
246            WindMouse::new(10.0, 9.0, 3.0, 10.0, 2.0, 10.0, 100.0),
247            Err(WindMouseError::InvalidWaitTime { .. })
248        ));
249    }
250
251    #[test]
252    fn test_new_negative_parameter() {
253        assert!(matches!(
254            WindMouse::new(-1.0, 9.0, 3.0, 2.0, 10.0, 10.0, 100.0),
255            Err(WindMouseError::NegativeParameter("mouse_speed"))
256        ));
257    }
258
259    #[test]
260    fn test_generate_points() {
261        let wind_mouse = WindMouse::new_default();
262        let start = Coordinate::new(0.0, 0.0);
263        let end = Coordinate::new(100.0, 100.0);
264        let points = wind_mouse.generate_points(start, end);
265        assert!(!points.is_empty());
266        assert_eq!(points.last().unwrap()[0..2], [100, 100]);
267    }
268}