/** * Motion Generator - Drawing the Soul * * hCaptcha uses mouse trajectory analysis to detect bots. * Straight lines = robot = death. * * We generate human-like mouse movements using: * - Bezier curves for smooth paths * - Perlin noise for natural jitter * - Realistic velocity profiles (slow start, fast middle, slow end) */ export class MotionGenerator { constructor(options = {}) { this.screenWidth = options.screenWidth || 1920; this.screenHeight = options.screenHeight || 1080; this.checkboxPos = options.checkboxPos || { x: 200, y: 300 }; } /** * Generate complete motion data matching hCaptcha's expected format */ generate() { const startTime = Date.now(); const duration = this._randomBetween(800, 2000); // Human reaction time // Starting point (off-screen or edge) const start = { x: this._randomBetween(-50, 50), y: this._randomBetween(this.screenHeight / 2, this.screenHeight), }; // Target: the checkbox const end = { x: this.checkboxPos.x + this._randomBetween(-5, 5), y: this.checkboxPos.y + this._randomBetween(-5, 5), }; // Generate movement points const mm = this._generateMouseMoves(start, end, startTime, duration); // Mouse down/up at the end const clickTime = startTime + duration + this._randomBetween(50, 150); const md = [[end.x, end.y, clickTime]]; const mu = [[end.x, end.y, clickTime + this._randomBetween(80, 150)]]; return { st: startTime, // Start timestamp dct: startTime, // Document creation time mm, // Mouse moves: [[x, y, timestamp], ...] md, // Mouse down mu, // Mouse up topLevel: { st: startTime - this._randomBetween(1000, 3000), sc: { availWidth: this.screenWidth, availHeight: this.screenHeight - 40, width: this.screenWidth, height: this.screenHeight, colorDepth: 24, pixelDepth: 24, }, nv: { vendorSub: '', productSub: '20030107', vendor: 'Google Inc.', maxTouchPoints: 0, hardwareConcurrency: 8, cookieEnabled: true, appCodeName: 'Mozilla', appName: 'Netscape', appVersion: '5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', platform: 'Win32', product: 'Gecko', language: 'en-US', onLine: true, deviceMemory: 8, }, dr: '', inv: false, exec: false, }, v: 1, }; } /** * Generate mouse movement points using Bezier curves */ _generateMouseMoves(start, end, startTime, duration) { const points = []; const numPoints = this._randomBetween(30, 60); // Control points for cubic Bezier const cp1 = { x: start.x + (end.x - start.x) * 0.3 + this._randomBetween(-100, 100), y: start.y + (end.y - start.y) * 0.1 + this._randomBetween(-50, 50), }; const cp2 = { x: start.x + (end.x - start.x) * 0.7 + this._randomBetween(-50, 50), y: start.y + (end.y - start.y) * 0.9 + this._randomBetween(-30, 30), }; for (let i = 0; i < numPoints; i++) { // Non-linear time distribution (ease-in-out) const rawT = i / (numPoints - 1); const t = this._easeInOutCubic(rawT); // Bezier interpolation const pos = this._cubicBezier(start, cp1, cp2, end, t); // Add micro-jitter (human hands shake) pos.x += this._randomBetween(-2, 2); pos.y += this._randomBetween(-2, 2); // Timestamp with slight randomness const timestamp = startTime + Math.floor(duration * rawT) + this._randomBetween(-5, 5); points.push([Math.round(pos.x), Math.round(pos.y), timestamp]); } // Sort by timestamp points.sort((a, b) => a[2] - b[2]); return points; } /** * Cubic Bezier interpolation */ _cubicBezier(p0, p1, p2, p3, t) { const t2 = t * t; const t3 = t2 * t; const mt = 1 - t; const mt2 = mt * mt; const mt3 = mt2 * mt; return { x: mt3 * p0.x + 3 * mt2 * t * p1.x + 3 * mt * t2 * p2.x + t3 * p3.x, y: mt3 * p0.y + 3 * mt2 * t * p1.y + 3 * mt * t2 * p2.y + t3 * p3.y, }; } /** * Easing function for natural movement */ _easeInOutCubic(t) { return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; } _randomBetween(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; } }