rustenium 1.1.9

A modern, robust, high-performance WebDriver BiDi automation library for Rust
Documentation
use rand::Rng;
use super::mouse::Point;

// ── Easing functions ─────────────────────────────────────────────────────────

fn ease_out_cubic(t: f64) -> f64 { let t = t - 1.0; t * t * t + 1.0 }
fn ease_out_quart(t: f64) -> f64 { let t = t - 1.0; -(t * t * t * t - 1.0) }
fn ease_out_quint(t: f64) -> f64 { let t = t - 1.0; t * t * t * t * t + 1.0 }
fn ease_out_expo(t: f64) -> f64 { if t == 1.0 { 1.0 } else { -f64::powf(2.0, -10.0 * t) + 1.0 } }
fn ease_out_sine(t: f64) -> f64 { (t * std::f64::consts::FRAC_PI_2).sin() }
fn ease_out_circ(t: f64) -> f64 { let t = t - 1.0; (1.0 - t * t).sqrt() }
fn ease_in_out_cubic(t: f64) -> f64 { if t < 0.5 { 4.0 * t * t * t } else { let t = 2.0 * t - 2.0; 0.5 * t * t * t + 1.0 } }
fn ease_in_out_quart(t: f64) -> f64 { if t < 0.5 { 8.0 * t * t * t * t } else { let t = t - 1.0; -8.0 * t * t * t * t + 1.0 } }
fn ease_in_out_quint(t: f64) -> f64 { if t < 0.5 { 16.0 * t * t * t * t * t } else { let t = 2.0 * t - 2.0; 0.5 * t * t * t * t * t + 1.0 } }
fn ease_in_out_sine(t: f64) -> f64 { -0.5 * ((std::f64::consts::PI * t).cos() - 1.0) }
fn ease_in_out_expo(t: f64) -> f64 {
    if t == 0.0 { return 0.0; }
    if t == 1.0 { return 1.0; }
    if t < 0.5 { 0.5 * f64::powf(2.0, 20.0 * t - 10.0) } else { 1.0 - 0.5 * f64::powf(2.0, -20.0 * t + 10.0) }
}
fn ease_in_out_circ(t: f64) -> f64 {
    if t < 0.5 { 0.5 * (1.0 - (1.0 - 4.0 * t * t).sqrt()) } else { 0.5 * ((1.0 - (2.0 * t - 2.0).powi(2)).sqrt() + 1.0) }
}
pub(crate) fn linear(t: f64) -> f64 { t }

pub(crate) type EasingFn = fn(f64) -> f64;

pub(crate) const EASING_FUNCTIONS: &[EasingFn] = &[
    ease_out_expo, ease_in_out_quint, ease_in_out_sine, ease_in_out_quart,
    ease_in_out_expo, ease_in_out_cubic, ease_in_out_circ, linear,
    ease_out_sine, ease_out_quart, ease_out_quint, ease_out_cubic, ease_out_circ,
];

// ── Bezier (N-degree) ────────────────────────────────────────────────────────

fn binomial(n: u64, k: u64) -> f64 {
    if k > n { return 0.0; }
    let mut result = 1.0;
    for i in 0..k {
        result *= (n - i) as f64 / (i + 1) as f64;
    }
    result
}

fn bernstein(t: f64, i: u64, n: u64) -> f64 {
    binomial(n, i) * t.powi(i as i32) * (1.0 - t).powi((n - i) as i32)
}

fn bezier_point(t: f64, control_points: &[Point]) -> Point {
    let n = (control_points.len() - 1) as u64;
    let (mut x, mut y) = (0.0, 0.0);
    for (i, cp) in control_points.iter().enumerate() {
        let b = bernstein(t, i as u64, n);
        x += cp.x * b;
        y += cp.y * b;
    }
    Point { x, y }
}

fn bezier_curve(num_points: usize, control_points: &[Point]) -> Vec<Point> {
    (0..num_points)
        .map(|i| bezier_point(i as f64 / (num_points - 1).max(1) as f64, control_points))
        .collect()
}

// ── Curve parameters & generation ────────────────────────────────────────────

pub struct CurveParams {
    pub offset_boundary_x: f64,
    pub offset_boundary_y: f64,
    pub knots_count: usize,
    pub distortion_mean: f64,
    pub distortion_stdev: f64,
    pub distortion_frequency: f64,
    pub tween: EasingFn,
    pub target_points: usize,
}

pub fn random_curve_params(_from: Point, _to: Point) -> CurveParams {
    let mut rng = rand::rng();

    let tween = EASING_FUNCTIONS[rng.random_range(0..EASING_FUNCTIONS.len())];
    let offset_boundary_x = weighted_range_pick(&mut rng, &[(20, 45, 0.2), (45, 75, 0.65), (75, 100, 0.15)]);
    let offset_boundary_y = weighted_range_pick(&mut rng, &[(20, 45, 0.2), (45, 75, 0.65), (75, 100, 0.15)]);

    let knots_count = weighted_pick(&mut rng, &[
        (1, 0.15), (2, 0.36), (3, 0.17), (4, 0.12), (5, 0.08),
        (6, 0.04), (7, 0.03), (8, 0.02), (9, 0.015), (10, 0.005),
    ]);

    let distortion_mean = rng.random_range(80..=110) as f64 / 100.0;
    let distortion_stdev = rng.random_range(85..=110) as f64 / 100.0;
    let distortion_frequency = rng.random_range(25..=70) as f64 / 100.0;
    let target_points = weighted_range_pick(&mut rng, &[(35, 45, 0.53), (45, 60, 0.32), (60, 80, 0.15)]) as usize;

    CurveParams {
        offset_boundary_x, offset_boundary_y, knots_count,
        distortion_mean, distortion_stdev, distortion_frequency,
        tween, target_points,
    }
}

pub struct Trajectory {
    pub points: Vec<Point>,
    /// Delay in milliseconds before dispatching each point; len == points.len() - 1.
    pub step_delays_ms: Vec<u64>,
}

pub fn generate_trajectory(from: Point, to: Point, params: &CurveParams) -> Trajectory {
    let mut rng = rand::rng();

    let left = from.x.min(to.x) - params.offset_boundary_x;
    let right = from.x.max(to.x) + params.offset_boundary_x;
    let bottom = from.y.min(to.y) - params.offset_boundary_y;
    let top = from.y.max(to.y) + params.offset_boundary_y;

    let mut control_points = vec![from];
    for _ in 0..params.knots_count {
        control_points.push(Point {
            x: rng.random_range(left..=right),
            y: rng.random_range(bottom..=top),
        });
    }
    control_points.push(to);

    let mid_points = ((from.x - to.x).abs().max((from.y - to.y).abs()) as usize).max(2);
    let mut points = bezier_curve(mid_points, &control_points);

    for i in 1..points.len().saturating_sub(1) {
        if rng.random_range(0.0..1.0) < params.distortion_frequency {
            let delta = gauss(&mut rng, params.distortion_mean, params.distortion_stdev);
            points[i].y += delta;
        }
    }

    let tween = params.tween;
    let n = params.target_points.max(2);
    let sampled: Vec<Point> = (0..n)
        .map(|i| {
            let t = i as f64 / (n - 1) as f64;
            let index = (tween(t) * (points.len() - 1) as f64) as usize;
            points[index.min(points.len() - 1)]
        })
        .collect();

    // Total move duration based on Euclidean distance.
    // The spatial easing already encodes slow/fast regions, so uniform-ish step delays
    // naturally reproduce the acceleration curve — jitter adds per-step hand tremor.
    let dist = ((to.x - from.x).powi(2) + (to.y - from.y).powi(2)).sqrt();
    let total_ms = {
        let base = (80.0 + dist * 1.1).clamp(120.0, 1400.0);
        (base * rng.random_range(0.88_f64..1.12_f64)) as u64
    };

    let step_delays_ms = if n < 2 {
        vec![]
    } else {
        let per = (total_ms as f64 / (n - 1) as f64).max(1.0);
        (0..n - 1)
            .map(|_| (per * rng.random_range(0.78_f64..1.22_f64)).round().max(1.0) as u64)
            .collect()
    };

    Trajectory { points: sampled, step_delays_ms }
}

// ── Utilities ────────────────────────────────────────────────────────────────

fn weighted_range_pick(rng: &mut impl Rng, ranges: &[(i32, i32, f64)]) -> f64 {
    let total: f64 = ranges.iter().map(|r| r.2).sum();
    let mut roll = rng.random_range(0.0..total);
    for &(lo, hi, weight) in ranges {
        roll -= weight;
        if roll <= 0.0 {
            return rng.random_range(lo..hi) as f64;
        }
    }
    ranges.last().map(|r| rng.random_range(r.0..r.1) as f64).unwrap_or(50.0)
}

pub(crate) fn weighted_pick(rng: &mut impl Rng, items: &[(usize, f64)]) -> usize {
    let total: f64 = items.iter().map(|i| i.1).sum();
    let mut roll = rng.random_range(0.0..total);
    for &(val, weight) in items {
        roll -= weight;
        if roll <= 0.0 { return val; }
    }
    items.last().map(|i| i.0).unwrap_or(2)
}

pub(crate) fn gauss(rng: &mut impl Rng, mean: f64, stdev: f64) -> f64 {
    let u1: f64 = rng.random_range(0.0001..1.0);
    let u2: f64 = rng.random_range(0.0..std::f64::consts::TAU);
    mean + stdev * (-2.0 * u1.ln()).sqrt() * u2.cos()
}

/// Generate `count` random durations in `latency_range` that sum to `total_secs`.
pub fn generate_durations(count: usize, total_secs: f64, latency_range: (f64, f64)) -> Vec<f64> {
    if count == 0 { return vec![]; }
    let mut rng = rand::rng();
    let mut durations: Vec<f64> = (0..count)
        .map(|_| rng.random_range(latency_range.0..=latency_range.1))
        .collect();
    let sum: f64 = durations.iter().sum();
    if sum > 0.0 {
        let scale = total_secs / sum;
        for d in &mut durations { *d *= scale; }
    }
    durations
}

#[cfg(test)]
mod tests {
    use super::*;
    use super::super::mouse::Point;

    #[test]
    fn bezier_point_at_t0_returns_start() {
        let cps = vec![
            Point { x: 0.0, y: 0.0 },
            Point { x: 50.0, y: 100.0 },
            Point { x: 100.0, y: 0.0 },
        ];
        let p = bezier_point(0.0, &cps);
        assert!((p.x).abs() < f64::EPSILON);
        assert!((p.y).abs() < f64::EPSILON);
    }

    #[test]
    fn bezier_point_at_t1_returns_end() {
        let cps = vec![
            Point { x: 0.0, y: 0.0 },
            Point { x: 50.0, y: 100.0 },
            Point { x: 200.0, y: 50.0 },
        ];
        let p = bezier_point(1.0, &cps);
        assert!((p.x - 200.0).abs() < 1e-9);
        assert!((p.y - 50.0).abs() < 1e-9);
    }

    #[test]
    fn bezier_curve_produces_correct_count() {
        let cps = vec![
            Point { x: 0.0, y: 0.0 },
            Point { x: 50.0, y: 100.0 },
            Point { x: 100.0, y: 0.0 },
        ];
        let pts = bezier_curve(50, &cps);
        assert_eq!(pts.len(), 50);
    }

    #[test]
    fn bezier_curve_first_and_last_match_endpoints() {
        let cps = vec![
            Point { x: 10.0, y: 20.0 },
            Point { x: 50.0, y: 80.0 },
            Point { x: 90.0, y: 20.0 },
        ];
        let pts = bezier_curve(100, &cps);
        assert!((pts[0].x - 10.0).abs() < 1e-9);
        assert!((pts[0].y - 20.0).abs() < 1e-9);
        assert!((pts[99].x - 90.0).abs() < 1e-9);
        assert!((pts[99].y - 20.0).abs() < 1e-9);
    }

    #[test]
    fn n_degree_bezier_with_many_control_points() {
        let cps = vec![
            Point { x: 0.0, y: 0.0 },
            Point { x: 20.0, y: 80.0 },
            Point { x: 40.0, y: 10.0 },
            Point { x: 60.0, y: 90.0 },
            Point { x: 80.0, y: 20.0 },
            Point { x: 100.0, y: 100.0 },
        ];
        let start = bezier_point(0.0, &cps);
        let end = bezier_point(1.0, &cps);
        assert!((start.x).abs() < 1e-9);
        assert!((end.x - 100.0).abs() < 1e-9);
        assert!((end.y - 100.0).abs() < 1e-9);
    }

    #[test]
    fn binomial_coefficients() {
        assert!((binomial(4, 2) - 6.0).abs() < 1e-9);
        assert!((binomial(5, 0) - 1.0).abs() < 1e-9);
        assert!((binomial(5, 5) - 1.0).abs() < 1e-9);
        assert!((binomial(10, 3) - 120.0).abs() < 1e-9);
    }

    #[test]
    fn binomial_edge_cases() {
        assert!((binomial(0, 0) - 1.0).abs() < 1e-9);
        assert!((binomial(1, 0) - 1.0).abs() < 1e-9);
        assert!((binomial(1, 1) - 1.0).abs() < 1e-9);
        assert!((binomial(3, 4)).abs() < 1e-9);
    }

    #[test]
    fn easing_functions_boundaries() {
        for f in EASING_FUNCTIONS {
            let at_0 = f(0.0);
            let at_1 = f(1.0);
            assert!((at_0).abs() < 1e-9, "easing(0) should be ~0, got {}", at_0);
            assert!((at_1 - 1.0).abs() < 1e-9, "easing(1) should be ~1, got {}", at_1);
        }
    }

    #[test]
    fn linear_easing_is_identity() {
        for i in 0..=100 {
            let t = i as f64 / 100.0;
            assert!((linear(t) - t).abs() < 1e-9);
        }
    }

    #[test]
    fn gauss_distribution_reasonable_mean() {
        let mut rng = rand::rng();
        let mut sum = 0.0;
        let n = 1000;
        for _ in 0..n {
            sum += gauss(&mut rng, 1.0, 1.0);
        }
        let mean = sum / n as f64;
        assert!((mean - 1.0).abs() < 0.2, "gauss mean should be ~1.0, got {}", mean);
    }

    #[test]
    fn gauss_with_zero_stdev_returns_mean() {
        let mut rng = rand::rng();
        for _ in 0..50 {
            let v = gauss(&mut rng, 5.0, 0.0);
            assert!((v - 5.0).abs() < 1e-9);
        }
    }

    #[test]
    fn weighted_pick_returns_valid_values() {
        let mut rng = rand::rng();
        let items: &[(usize, f64)] = &[(1, 0.5), (2, 0.3), (3, 0.2)];
        for _ in 0..100 {
            let v = weighted_pick(&mut rng, items);
            assert!(v >= 1 && v <= 3);
        }
    }

    #[test]
    fn weighted_pick_respects_weights() {
        let mut rng = rand::rng();
        let items: &[(usize, f64)] = &[(1, 0.99), (2, 0.01)];
        let mut count_1 = 0;
        for _ in 0..1000 {
            if weighted_pick(&mut rng, items) == 1 { count_1 += 1; }
        }
        assert!(count_1 > 900, "heavily weighted item should appear >90%, got {}", count_1);
    }
}