Source: BezierMouse.js

/**
 * @module BezierMouse
 */

const { Bezier } = require("bezier-js");
const {
  Point,
  mouse,
  Button,
  Region,
  randomPointIn,
} = require("@nut-tree/nut-js");
const Utils = require("./Utils.js");

const { abs, ceil } = Math;

/**
 * BezierMouse class generates natural mouse movement using bezier curves
 */
class BezierMouse {
  /**
   * constructor
   * @param {number} mouseSpeed - mouse movement speed in pixels per second
   */
  constructor(mouseSpeed) {
    mouse.config.mouseSpeed = mouseSpeed || 100;
  }

  /**
   * moveAndClick: moves mouse on a bezier curve and clicks
   * @param {Point} initPos
   * @param {Point} finPos
   * @param {String} clickType - enum: LEFT, MIDDLE, RIGHT
   * @param {Options} opts
   */
  async moveAndClick(initPos, finPos, clickType = "LEFT", opts = {}) {
    await this.move(initPos, finPos, opts);
    await mouse.click(Button[clickType]);
  }

  /**
   * moveAndDoubleClick: moves mouse on a bezier curve and double clicks
   * @param {Point} initPos
   * @param {Point} finPos
   * @param {String} clickType - enum: LEFT, MIDDLE, RIGHT
   * @param {Options} opts
   */
  async moveAndDoubleClick(initPos, finPos, clickType = "LEFT", opts = {}) {
    await this.move(initPos, finPos, opts);
    await mouse.doubleClick(Button[clickType]);
  }

  /**
   * move: moves mouse on a bezier curve
   * @param {Point} initPos
   * @param {Point} finPos
   * @param {Options} opts
   */
  async move(initPos, finPos, opts = {}) {
    let finishPosition = finPos;
    if (!opts.preciseClick) {
      const deviation = 5;
      const randDeviation = () => Utils.randint(deviation / 2, deviation);
      const region = new Region(
        finPos.x,
        finPos.y,
        randDeviation(),
        randDeviation()
      );
      finishPosition = await randomPointIn(region);
    }
    await mouse.move(this.bezierCurveTo(initPos, finishPosition, opts));
  }

  // async click(finPos, clickType, natural = true) {
  //   // TODO:
  //   // - factor in a random (not very likely) total misclick, where the region is large
  //   //    - if this misclick=true, then do a recovery click after to ensure click is made
  //
  //   await mouse.click(Button[clickType]);
  // }

  async bezierCurveTo(initPos, finPos, opts = {}) {
    const curve = this.cubicBezierCurve(initPos, finPos, opts);
    const LUT = curve.getLUT(opts.steps || 100);

    return LUT.map((point) => new Point(point.x, point.y));
  }

  cubicBezierCurve(initPos, finPos, opts = {}) {
    const cntlPt1 = this.getBezierControlPoint(initPos, finPos, opts);
    const cntlPt2 = this.getBezierControlPoint(initPos, finPos, opts);
    const cubicPoints = [initPos, cntlPt1, cntlPt2, finPos];
    return new Bezier(cubicPoints);
  }

  getBezierControlPoint(initPos, finPos, opts = {}) {
    const { deviation = 20, flip = false } = opts;

    const deltaX = abs(ceil(finPos.x) - ceil(initPos.x));
    const deltaY = abs(ceil(finPos.y) - ceil(initPos.y));
    const randDeviation = () => Utils.randint(deviation / 2, deviation);

    const refPointX = flip ? initPos.x : finPos.x;
    const refPointY = flip ? initPos.y : finPos.y;
    return {
      x: refPointX + Utils.choice([-1, 1]) * deltaX * 0.01 * randDeviation(),
      y: refPointY + Utils.choice([-1, 1]) * deltaY * 0.01 * randDeviation(),
    };
  }
}

/* typedefs */
/**
 * @typedef {Object} Point
 * @property {number} x - The x-coordinate of the point.
 * @property {number} y - The y-coordinate of the point.
 */
/**
 * @typedef {Object} Options
 * @property {number} deviation - deviation scale (larger = more curve). default=20.
 * @property {number} flip - controls where the control points are anchored from, false: curves closer to finish position, true: curves closer to init position. default=false.
 * @property {number} steps - number of steps (points) when moving on the bezier curve (t=0 to t=1 at interval 1/steps). default=100.
 * @property {boolean} preciseClick - controls if click action should be on exact point. default=false.
 */

module.exports = BezierMouse;