#include #include #include #include #include #include #include "MaskConfig.hpp" /** * Human-like mouse movement generator. Ported from: * https://github.com/riflosnake/HumanCursor/blob/main/humancursor/utilities/human_curve_generator.py * Modified to use a more human-like easing function. */ class BezierCalculator { public: static long long factorial(int n) { if (n < 0) return -1; // Indicate error long long result = 1; for (int i = 2; i <= n; i++) result *= i; return result; } static double binomial(int n, int k) { return static_cast(factorial(n)) / (factorial(k) * factorial(n - k)); } static double bernsteinPolynomialPoint(double x, int i, int n) { return binomial(n, i) * std::pow(x, i) * std::pow(1 - x, n - i); } static std::vector bernsteinPolynomial( const std::vector>& points, double t) { int n = static_cast(points.size()) - 1; double x = 0.0; double y = 0.0; for (int i = 0; i <= n; i++) { double bern = bernsteinPolynomialPoint(t, i, n); x += points[i].first * bern; y += points[i].second * bern; } return {x, y}; } static std::vector> calculatePointsInCurve( int nPoints, const std::vector>& points) { std::vector> curvePoints; for (int i = 0; i < nPoints; i++) { double t = static_cast(i) / (nPoints - 1); curvePoints.push_back(bernsteinPolynomial(points, t)); } return curvePoints; } }; class HumanizeMouseTrajectory { public: HumanizeMouseTrajectory(const std::pair& fromPoint, const std::pair& toPoint) : fromPoint(fromPoint), toPoint(toPoint) { generateCurve(); } std::vector getPoints() const { std::vector flatPoints; flatPoints.reserve(points.size() * 2); for (const auto& point : points) { flatPoints.push_back(static_cast(std::round(point[0]))); flatPoints.push_back(static_cast(std::round(point[1]))); } return flatPoints; } private: std::pair fromPoint; std::pair toPoint; std::vector> points; void generateCurve() { double leftBoundary = std::min(fromPoint.first, toPoint.first) - 80.0; double rightBoundary = std::max(fromPoint.first, toPoint.first) + 80.0; double downBoundary = std::min(fromPoint.second, toPoint.second) - 80.0; double upBoundary = std::max(fromPoint.second, toPoint.second) + 80.0; std::vector> internalKnots = generateInternalKnots(leftBoundary, rightBoundary, downBoundary, upBoundary, 2); std::vector> curvePoints = generatePoints(internalKnots); curvePoints = distortPoints(curvePoints, 1.0, 1.0, 0.5); points = tweenPoints(curvePoints); } double easeOutQuad(double n) const { assert(n >= 0.0 && n <= 1.0 && "Argument must be between 0.0 and 1.0."); return -n * (n - 2); } std::vector> generateInternalKnots( double lBoundary, double rBoundary, double dBoundary, double uBoundary, int knotsCount) const { assert(isNumeric(lBoundary) && isNumeric(rBoundary) && isNumeric(dBoundary) && isNumeric(uBoundary) && "Boundaries must be numeric values"); assert(knotsCount >= 0 && "knotsCount must be non-negative"); assert(lBoundary <= rBoundary && "Left boundary must be less than or equal to right boundary"); assert(dBoundary <= uBoundary && "Down boundary must be less than or equal to upper boundary"); std::vector knotsX = randomChoiceDoubles(lBoundary, rBoundary, knotsCount); std::vector knotsY = randomChoiceDoubles(dBoundary, uBoundary, knotsCount); std::vector> knots; for (int i = 0; i < knotsCount; i++) { knots.emplace_back(knotsX[i], knotsY[i]); } return knots; } std::vector randomChoiceDoubles(double min, double max, int size) const { std::vector choices; std::uniform_real_distribution dist(min, max); for (int i = 0; i < size; i++) { choices.push_back(dist(randomEngine)); } return choices; } std::vector> generatePoints( const std::vector>& knots) const { assert(isListOfPoints(knots) && "Knots must be a valid list of points"); int midPtsCnt = static_cast( std::max({std::abs(fromPoint.first - toPoint.first), std::abs(fromPoint.second - toPoint.second), 2.0})); std::vector> controlPoints = knots; controlPoints.insert(controlPoints.begin(), fromPoint); controlPoints.push_back(toPoint); return BezierCalculator::calculatePointsInCurve(midPtsCnt, controlPoints); } std::vector> distortPoints( const std::vector>& points, double distortionMean, double distortionStDev, double distortionFrequency) const { assert(isNumeric(distortionMean) && isNumeric(distortionStDev) && isNumeric(distortionFrequency) && "Distortions must be numeric"); assert(isListOfPoints(points) && "Points must be a valid list of points"); assert(0.0 <= distortionFrequency && distortionFrequency <= 1.0 && "distortion_frequency must be in range [0,1]"); std::vector> distorted; distorted.push_back(points.front()); std::normal_distribution normalDist(distortionMean, distortionStDev); std::uniform_real_distribution uniformDist(0.0, 1.0); for (size_t i = 1; i < points.size() - 1; i++) { double x = points[i][0]; double y = points[i][1]; double delta = 0.0; if (uniformDist(randomEngine) < distortionFrequency) { delta = std::round(normalDist(randomEngine)); } distorted.push_back({x, y + delta}); } distorted.push_back(points.back()); return distorted; } int32_t getMaxTime() const { if (auto maxTime = MaskConfig::GetDouble("humanize:maxTime")) { return static_cast(maxTime.value() * 100); } return 150; } std::vector> tweenPoints( const std::vector>& points) const { assert(isListOfPoints(points) && "List of points not valid"); double totalLength = 0.0; for (size_t i = 1; i < points.size(); ++i) { double dx = points[i][0] - points[i - 1][0]; double dy = points[i][1] - points[i - 1][1]; totalLength += std::sqrt(dx * dx + dy * dy); } // Uses a power scale to keep the speed consistent int targetPoints = std::min( getMaxTime(), std::max(2, static_cast(std::pow(totalLength, 0.25) * 20))); std::vector> res; for (int i = 0; i < targetPoints; i++) { double t = static_cast(i) / (targetPoints - 1); double easedT = easeOutQuad(t); int index = static_cast(easedT * (points.size() - 1)); res.push_back(points[index]); } return res; } bool isNumeric(double val) const { return !std::isnan(val); } bool isListOfPoints( const std::vector>& points) const { for (const auto& p : points) { if (!isNumeric(p.first) || !isNumeric(p.second)) return false; } return true; } bool isListOfPoints(const std::vector>& points) const { for (const auto& p : points) { if (p.size() != 2 || !isNumeric(p[0]) || !isNumeric(p[1])) return false; } return true; } mutable std::default_random_engine randomEngine{ static_cast(std::time(nullptr))}; };