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}