1use std::sync::atomic::{AtomicU64, Ordering};
8use std::time::{Duration, SystemTime, UNIX_EPOCH};
9
10#[derive(Debug, Clone, Copy, PartialEq)]
12pub struct Point {
13 pub x: f64,
14 pub y: f64,
15}
16
17pub struct MouseEngine {
23 pub min_speed: f64,
24 pub max_speed: f64,
25 pub steps_per_second: u32,
26 seed: AtomicU64,
27}
28
29impl Default for MouseEngine {
30 fn default() -> Self {
31 let seed = SystemTime::now()
32 .duration_since(UNIX_EPOCH)
33 .map(|duration| duration.as_nanos() as u64)
34 .unwrap_or(0x9e37_79b9_7f4a_7c15)
35 | 1;
36 Self {
37 min_speed: 400.0,
38 max_speed: 800.0,
39 steps_per_second: 60,
40 seed: AtomicU64::new(seed),
41 }
42 }
43}
44
45impl MouseEngine {
46 pub fn new() -> Self {
47 Self::default()
48 }
49
50 #[cfg(test)]
51 fn with_seed(seed: u64) -> Self {
52 Self {
53 min_speed: 400.0,
54 max_speed: 800.0,
55 steps_per_second: 60,
56 seed: AtomicU64::new(seed | 1),
57 }
58 }
59
60 fn next_unit(&self) -> f64 {
61 let mut current = self.seed.load(Ordering::Relaxed);
62 loop {
63 let next = current
64 .wrapping_mul(6_364_136_223_846_793_005)
65 .wrapping_add(1_442_695_040_888_963_407);
66 match self.seed.compare_exchange_weak(
67 current,
68 next,
69 Ordering::Relaxed,
70 Ordering::Relaxed,
71 ) {
72 Ok(_) => return next as f64 / u64::MAX as f64,
73 Err(actual) => current = actual,
74 }
75 }
76 }
77
78 fn bezier_curve(&self, start: Point, end: Point, cp1: Point, cp2: Point, t: f64) -> Point {
80 let t2 = t * t;
81 let t3 = t2 * t;
82 let mt = 1.0 - t;
83 let mt2 = mt * mt;
84 let mt3 = mt2 * mt;
85
86 Point {
87 x: mt3 * start.x + 3.0 * mt2 * t * cp1.x + 3.0 * mt * t2 * cp2.x + t3 * end.x,
88 y: mt3 * start.y + 3.0 * mt2 * t * cp1.y + 3.0 * mt * t2 * cp2.y + t3 * end.y,
89 }
90 }
91
92 fn generate_control_points(&self, start: Point, end: Point) -> (Point, Point) {
94 let dx = end.x - start.x;
95 let dy = end.y - start.y;
96 let distance = (dx * dx + dy * dy).sqrt();
97 if distance < f64::EPSILON {
98 return (start, end);
99 }
100
101 let direction = (dx / distance, dy / distance);
102 let normal = (-direction.1, direction.0);
103 let bend = (self.next_unit() * 2.0 - 1.0) * distance * 0.12;
104 let along = (self.next_unit() * 2.0 - 1.0) * distance * 0.04;
105
106 let cp1 = Point {
107 x: start.x + dx * 0.28 + normal.0 * bend + direction.0 * along,
108 y: start.y + dy * 0.28 + normal.1 * bend + direction.1 * along,
109 };
110 let cp2 = Point {
111 x: start.x + dx * 0.72 - normal.0 * bend * 0.7 + direction.0 * along * 0.4,
112 y: start.y + dy * 0.72 - normal.1 * bend * 0.7 + direction.1 * along * 0.4,
113 };
114 (cp1, cp2)
115 }
116
117 pub fn generate_path(&self, start: Point, end: Point) -> Vec<Point> {
119 let dx = end.x - start.x;
120 let dy = end.y - start.y;
121 let distance = (dx * dx + dy * dy).sqrt();
122 if distance < f64::EPSILON {
123 return vec![start];
124 }
125
126 let speed = self.min_speed + (self.max_speed - self.min_speed) * self.next_unit();
127 let duration = distance / speed;
128 let steps = (duration * self.steps_per_second as f64)
129 .round()
130 .clamp(8.0, 120.0) as usize;
131 let (cp1, cp2) = self.generate_control_points(start, end);
132 let mut points = Vec::with_capacity(steps + 1);
133
134 for index in 0..=steps {
135 let t = index as f64 / steps as f64;
136 let eased = if t < 0.5 {
137 2.0 * t * t
138 } else {
139 1.0 - (-2.0 * t + 2.0).powi(2) / 2.0
140 };
141 points.push(self.bezier_curve(start, end, cp1, cp2, eased));
142 }
143 points
144 }
145
146 pub fn move_delay(&self, _start: Point, _end: Point) -> Duration {
152 let cadence = 1.0 / self.steps_per_second.max(1) as f64;
153 let jitter = 0.9 + self.next_unit() * 0.2;
154 Duration::from_secs_f64((cadence * jitter).max(0.001))
155 }
156
157 pub fn click_delay(&self) -> Duration {
159 Duration::from_secs_f64(0.04 + self.next_unit() * 0.05)
160 }
161
162 pub fn generate_click_events(&self, point: Point) -> Vec<MouseEvent> {
164 vec![
165 MouseEvent {
166 event_type: "mousePressed".to_string(),
167 x: point.x,
168 y: point.y,
169 button: "left".to_string(),
170 click_count: 1,
171 },
172 MouseEvent {
173 event_type: "mouseReleased".to_string(),
174 x: point.x,
175 y: point.y,
176 button: "left".to_string(),
177 click_count: 1,
178 },
179 ]
180 }
181
182 pub fn generate_double_click_events(&self, point: Point) -> Vec<MouseEvent> {
183 vec![
184 MouseEvent {
185 event_type: "mousePressed".to_string(),
186 x: point.x,
187 y: point.y,
188 button: "left".to_string(),
189 click_count: 1,
190 },
191 MouseEvent {
192 event_type: "mouseReleased".to_string(),
193 x: point.x,
194 y: point.y,
195 button: "left".to_string(),
196 click_count: 1,
197 },
198 MouseEvent {
199 event_type: "mousePressed".to_string(),
200 x: point.x,
201 y: point.y,
202 button: "left".to_string(),
203 click_count: 2,
204 },
205 MouseEvent {
206 event_type: "mouseReleased".to_string(),
207 x: point.x,
208 y: point.y,
209 button: "left".to_string(),
210 click_count: 2,
211 },
212 ]
213 }
214
215 pub fn generate_drag_events(&self, start: Point, end: Point) -> Vec<MouseEvent> {
216 let path = self.generate_path(start, end);
217 let mut events = vec![MouseEvent {
218 event_type: "mousePressed".to_string(),
219 x: start.x,
220 y: start.y,
221 button: "left".to_string(),
222 click_count: 1,
223 }];
224 for point in path.iter().skip(1).take(path.len().saturating_sub(2)) {
225 events.push(MouseEvent {
226 event_type: "mouseMoved".to_string(),
227 x: point.x,
228 y: point.y,
229 button: "left".to_string(),
230 click_count: 1,
231 });
232 }
233 events.push(MouseEvent {
234 event_type: "mouseReleased".to_string(),
235 x: end.x,
236 y: end.y,
237 button: "left".to_string(),
238 click_count: 1,
239 });
240 events
241 }
242}
243
244#[derive(Debug, Clone)]
245pub struct MouseEvent {
246 pub event_type: String,
247 pub x: f64,
248 pub y: f64,
249 pub button: String,
250 pub click_count: u32,
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256
257 #[test]
258 fn path_preserves_endpoints_and_has_bounded_samples() {
259 let engine = MouseEngine::with_seed(7);
260 let start = Point { x: 0.0, y: 0.0 };
261 let end = Point {
262 x: 1_000.0,
263 y: 600.0,
264 };
265 let path = engine.generate_path(start, end);
266 assert_eq!(path.first(), Some(&start));
267 assert_eq!(path.last(), Some(&end));
268 assert!((8..=121).contains(&path.len()));
269 }
270
271 #[test]
272 fn path_is_not_always_a_teleport() {
273 let engine = MouseEngine::with_seed(11);
274 let path = engine.generate_path(Point { x: 0.0, y: 0.0 }, Point { x: 400.0, y: 0.0 });
275 assert!(path.len() > 2);
276 assert!(path.iter().any(|point| point.y.abs() > f64::EPSILON));
277 }
278
279 #[test]
280 fn click_events_only_press_and_release_after_motion() {
281 let events = MouseEngine::with_seed(13).generate_click_events(Point { x: 2.0, y: 3.0 });
282 assert_eq!(events.len(), 2);
283 assert_eq!(events[0].event_type, "mousePressed");
284 assert_eq!(events[1].event_type, "mouseReleased");
285 }
286
287 #[test]
288 fn movement_and_click_delays_stay_in_human_ranges() {
289 let engine = MouseEngine::with_seed(17);
290 let move_delay = engine.move_delay(Point { x: 0.0, y: 0.0 }, Point { x: 5.0, y: 5.0 });
291 assert!((Duration::from_millis(15)..=Duration::from_millis(19)).contains(&move_delay));
292 assert!(
293 (Duration::from_millis(40)..=Duration::from_millis(90)).contains(&engine.click_delay())
294 );
295 }
296}