use std::time::Duration;
use serde_json::Value;
use crate::{Error, Result};
#[derive(Debug, Clone, Default)]
pub struct ImageView {
pub x: f64,
pub y: f64,
pub w: f64,
pub h: f64,
pub natural_w: f64,
pub natural_h: f64,
pub src: String,
}
impl ImageView {
pub fn scale_x(&self) -> f64 {
if self.natural_w > 1.0 {
self.w / self.natural_w
} else {
1.0
}
}
pub fn scale_y(&self) -> f64 {
if self.natural_h > 1.0 {
self.h / self.natural_h
} else {
self.scale_x()
}
}
pub fn map(&self, px: f64, py: f64) -> (f64, f64) {
(self.x + px * self.scale_x(), self.y + py * self.scale_y())
}
pub fn map_u32(&self, p: (u32, u32)) -> (f64, f64) {
self.map(p.0 as f64, p.1 as f64)
}
pub fn is_valid(&self) -> bool {
self.w > 1.0
}
pub fn clamp_point(&self, p: (f64, f64)) -> (f64, f64) {
(
p.0.clamp(self.x, self.x + self.w.max(0.0)),
p.1.clamp(self.y, self.y + self.h.max(0.0)),
)
}
}
#[derive(Debug, Clone, Copy)]
pub struct HumanClickOpts {
pub px_per_point: f64,
pub min_points: usize,
pub max_points: usize,
pub bow: f64,
pub jitter: f64,
}
impl Default for HumanClickOpts {
fn default() -> Self {
Self {
px_per_point: 7.0,
min_points: 22,
max_points: 64,
bow: 0.2,
jitter: 1.3,
}
}
}
struct Rng(u64);
impl Rng {
fn new(seed: u64) -> Self {
Self(seed | 1)
}
fn f(&mut self) -> f64 {
self.0 ^= self.0 << 13;
self.0 ^= self.0 >> 7;
self.0 ^= self.0 << 17;
(self.0 >> 11) as f64 / (1u64 << 53) as f64
}
}
fn seed_now() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos() as u64)
.unwrap_or(0x9E37_79B9_7F4A_7C15)
| 1
}
fn track_between(
from: (f64, f64),
to: (f64, f64),
rng: &mut Rng,
o: &HumanClickOpts,
) -> Vec<(f64, f64, u64)> {
let dist = ((to.0 - from.0).powi(2) + (to.1 - from.1).powi(2)).sqrt();
let n = ((dist / o.px_per_point.max(1.0)) as usize).clamp(o.min_points, o.max_points);
let (mx, my) = ((from.0 + to.0) / 2.0, (from.1 + to.1) / 2.0);
let (perp_x, perp_y) = (-(to.1 - from.1), to.0 - from.0);
let plen = (perp_x * perp_x + perp_y * perp_y).sqrt().max(1.0);
let bow = (rng.f() - 0.5) * dist * o.bow;
let (cx, cy) = (mx + perp_x / plen * bow, my + perp_y / plen * bow);
let mut out = Vec::with_capacity(n);
for i in 1..=n {
let t = i as f64 / n as f64;
let s = 10.0 * t.powi(3) - 15.0 * t.powi(4) + 6.0 * t.powi(5); let u = 1.0 - s;
let x = u * u * from.0 + 2.0 * u * s * cx + s * s * to.0 + (rng.f() - 0.5) * o.jitter;
let y = u * u * from.1 + 2.0 * u * s * cy + s * s * to.1 + (rng.f() - 0.5) * o.jitter;
out.push((x, y, 5 + (rng.f() * 11.0) as u64));
}
out
}
const IMAGE_VIEW_JS: &str = r#"((sel)=>{const e=document.querySelector(sel);if(!e)return '';const r=e.getBoundingClientRect();
return JSON.stringify({x:r.x,y:r.y,w:r.width,h:r.height,nw:e.naturalWidth||0,nh:e.naturalHeight||0,src:e.currentSrc||e.src||''});})"#;
#[async_trait::async_trait]
pub trait Humanize {
async fn hm_move(&self, x: f64, y: f64) -> Result<()>;
async fn hm_move_fast(&self, x: f64, y: f64) -> Result<()> {
self.hm_move(x, y).await
}
async fn hm_down(&self, x: f64, y: f64) -> Result<()>;
async fn hm_up(&self, x: f64, y: f64) -> Result<()>;
async fn hm_eval(&self, js: &str) -> Result<Value>;
async fn image_view(&self, selector: &str) -> Result<ImageView> {
let call = format!(
"({IMAGE_VIEW_JS})({})",
serde_json::to_string(selector).unwrap_or_default()
);
let v = self.hm_eval(&call).await?;
let s = v.as_str().unwrap_or_default();
if s.is_empty() {
return Err(Error::msg(format!("image_view: 未找到元素 {selector}")));
}
let j: Value =
serde_json::from_str(s).map_err(|e| Error::msg(format!("image_view: {e}")))?;
Ok(ImageView {
x: j["x"].as_f64().unwrap_or(0.0),
y: j["y"].as_f64().unwrap_or(0.0),
w: j["w"].as_f64().unwrap_or(0.0),
h: j["h"].as_f64().unwrap_or(0.0),
natural_w: j["nw"].as_f64().unwrap_or(0.0),
natural_h: j["nh"].as_f64().unwrap_or(0.0),
src: j["src"].as_str().unwrap_or("").to_string(),
})
}
async fn human_click(&self, points: &[(f64, f64)]) -> Result<()> {
self.human_click_with(points, &HumanClickOpts::default())
.await
}
async fn human_click_with(&self, points: &[(f64, f64)], opts: &HumanClickOpts) -> Result<()> {
if points.is_empty() {
return Ok(());
}
let mut rng = Rng::new(seed_now());
let first = points[0];
let mut cur = (
first.0 + (rng.f() - 0.5) * 60.0,
first.1 - 24.0 - rng.f() * 40.0,
);
for &p in points {
for (x, y, d) in track_between(cur, p, &mut rng, opts) {
self.hm_move_fast(x, y).await?;
tokio::time::sleep(Duration::from_millis(d)).await;
}
tokio::time::sleep(Duration::from_millis(40 + (rng.f() * 90.0) as u64)).await;
self.hm_move(p.0 + (rng.f() - 0.5) * 1.2, p.1 + (rng.f() - 0.5) * 1.2)
.await?;
tokio::time::sleep(Duration::from_millis(30 + (rng.f() * 40.0) as u64)).await;
self.hm_down(p.0, p.1).await?;
tokio::time::sleep(Duration::from_millis(45 + (rng.f() * 55.0) as u64)).await;
self.hm_up(p.0, p.1).await?;
cur = p;
tokio::time::sleep(Duration::from_millis(130 + (rng.f() * 220.0) as u64)).await;
}
Ok(())
}
}
pub async fn fetch_image(url: &str) -> Result<Vec<u8>> {
let bytes = reqwest::get(url)
.await
.map_err(|e| Error::msg(format!("fetch_image: {e}")))?
.bytes()
.await
.map_err(|e| Error::msg(format!("fetch_image: {e}")))?;
Ok(bytes.to_vec())
}
#[cfg(feature = "camoufox")]
#[async_trait::async_trait]
impl Humanize for crate::browser::Tab {
async fn hm_move(&self, x: f64, y: f64) -> Result<()> {
self.mouse_move(x, y).await
}
async fn hm_move_fast(&self, x: f64, y: f64) -> Result<()> {
self.mouse_move_fast(x, y)
}
async fn hm_down(&self, x: f64, y: f64) -> Result<()> {
self.mouse_down(x, y).await
}
async fn hm_up(&self, x: f64, y: f64) -> Result<()> {
self.mouse_up(x, y).await
}
async fn hm_eval(&self, js: &str) -> Result<Value> {
self.run_js(js).await
}
}
#[cfg(feature = "cdp")]
#[async_trait::async_trait]
impl Humanize for crate::cdp::ChromiumTab {
async fn hm_move(&self, x: f64, y: f64) -> Result<()> {
self.mouse_move(x, y).await
}
async fn hm_move_fast(&self, x: f64, y: f64) -> Result<()> {
self.mouse_move_fast(x, y)
}
async fn hm_down(&self, x: f64, y: f64) -> Result<()> {
self.mouse_down(x, y).await
}
async fn hm_up(&self, x: f64, y: f64) -> Result<()> {
self.mouse_up(x, y).await
}
async fn hm_eval(&self, js: &str) -> Result<Value> {
self.run_js(js).await
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn image_view_maps_with_scale() {
let v = ImageView {
x: 100.0,
y: 200.0,
w: 320.0,
h: 160.0,
natural_w: 640.0,
natural_h: 320.0,
src: String::new(),
};
assert_eq!(v.scale_x(), 0.5);
assert_eq!(v.scale_y(), 0.5);
assert_eq!(v.map_u32((200, 100)), (200.0, 250.0));
}
#[test]
fn clamp_point_keeps_inside_rect() {
let v = ImageView {
x: 100.0,
y: 200.0,
w: 320.0,
h: 160.0,
natural_w: 640.0,
natural_h: 320.0,
src: String::new(),
};
assert_eq!(v.clamp_point((90.0, 190.0)), (100.0, 200.0)); assert_eq!(v.clamp_point((999.0, 999.0)), (420.0, 360.0)); assert_eq!(v.clamp_point((250.0, 280.0)), (250.0, 280.0)); }
#[test]
fn track_dense_and_endpoints() {
let mut rng = Rng::new(42);
let o = HumanClickOpts::default();
let pts = track_between((0.0, 0.0), (300.0, 0.0), &mut rng, &o);
assert!(pts.len() >= o.min_points && pts.len() <= o.max_points);
let last = pts.last().unwrap();
assert!((last.0 - 300.0).abs() < 3.0 && last.1.abs() < 3.0);
}
}