From 75156e7e0063e65636d29d4323d4f66821fd8a5b Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Sat, 30 Mar 2024 21:39:35 -0500 Subject: [PATCH 1/7] feat: ability to associate timestamps with movements --- README.md | 24 ++++++++++++++++++++- src/math.ts | 15 +++++++++++++ src/spoof.ts | 59 +++++++++++++++++++++++++++++++++++++++++++++------- 3 files changed, 89 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 1e3ca03..7c82478 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,28 @@ const route = path(from, to) */ ``` +Generating movement data between 2 coordinates with timestamps. +```js +import { path } from "ghost-cursor" + +const from = { x: 100, y: 100 } +const to = { x: 600, y: 700 } + +const routes = path(from, to, { showTimestamps: true }) + +/** + * [ + * { x: 100, y: 100, timestamp: 1711850430643 }, + * { x: 114.78071695023473, y: 97.52340709495319, timestamp: 1711850430697 }, + * { x: 129.1362373468682, y: 96.60141853603243, timestamp: 1711850430749 }, + * { x: 143.09468422606352, y: 97.18676354029148, timestamp: 1711850430799 }, + * { x: 156.68418062398405, y: 99.23217132478408, timestamp: 1711850430848 }, + * ... and so on + * ] + */ +``` + + Usage with puppeteer: ```js @@ -126,7 +148,7 @@ Generates a set of points for mouse movement between two coordinates. - **optionsOrSpread (optional):** Additional options for generating the path. - `spreadOverride (number):` Override the spread of the generated path. - `moveSpeed (number):` Speed of mouse movement. - + - `showTimestamps (boolean):` Generate timestamps for each point based on the trapezoidal rule. ## How does it work diff --git a/src/math.ts b/src/math.ts index 605c84c..676d7c2 100644 --- a/src/math.ts +++ b/src/math.ts @@ -4,6 +4,9 @@ export interface Vector { x: number y: number } +export interface TimedVector extends Vector { + timestamp: number +} export const origin: Vector = { x: 0, y: 0 } // maybe i should've just imported a vector library lol @@ -81,3 +84,15 @@ export const bezierCurve = ( ) return new Bezier(start, ...anchors, finish) } + +export const bezierCurveSpeed = ( + t: number, + P0: Vector, + P1: Vector, + P2: Vector, + P3: Vector +): number => { + const B1 = 3 * (1 - t) ** 2 * (P1.x - P0.x) + 6 * (1 - t) * t * (P2.x - P1.x) + 3 * t ** 2 * (P3.x - P2.x) + const B2 = 3 * (1 - t) ** 2 * (P1.y - P0.y) + 6 * (1 - t) * t * (P2.y - P1.y) + 3 * t ** 2 * (P3.y - P2.y) + return Math.sqrt(B1 ** 2 + B2 ** 2) +} diff --git a/src/spoof.ts b/src/spoof.ts index 83efc6f..c837378 100644 --- a/src/spoof.ts +++ b/src/spoof.ts @@ -2,7 +2,9 @@ import { ElementHandle, Page, BoundingBox, CDPSession } from 'puppeteer' import debug from 'debug' import { Vector, + TimedVector, bezierCurve, + bezierCurveSpeed, direction, magnitude, origin, @@ -31,6 +33,7 @@ export interface ClickOptions extends MoveOptions { export interface PathOptions { readonly spreadOverride?: number readonly moveSpeed?: number + readonly showTimestamps?: boolean } export interface GhostCursor { @@ -152,11 +155,11 @@ const getElementBox = async ( } } -export function path (point: Vector, target: Vector, optionsOrSpread?: number | PathOptions) -export function path (point: Vector, target: BoundingBox, optionsOrSpread?: number | PathOptions) -export function path (start: Vector, end: BoundingBox | Vector, optionsOrSpread?: number | PathOptions): Vector[] { - const spreadOverride = typeof optionsOrSpread === 'number' ? optionsOrSpread : optionsOrSpread?.spreadOverride - const moveSpeed = typeof optionsOrSpread === 'object' && optionsOrSpread.moveSpeed +export function path (point: Vector, target: Vector, options?: number | PathOptions) +export function path (point: Vector, target: BoundingBox, options?: number | PathOptions) +export function path (start: Vector, end: BoundingBox | Vector, options?: number | PathOptions): Vector[] | TimedVector[] { + const spreadOverride = typeof options === 'number' ? options : options?.spreadOverride + const moveSpeed = typeof options === 'object' && options.moveSpeed const defaultWidth = 100 const minSteps = 25 @@ -168,17 +171,57 @@ export function path (start: Vector, end: BoundingBox | Vector, optionsOrSpread? const baseTime = speed * minSteps const steps = Math.ceil((Math.log2(fitts(length, width) + 1) + baseTime) * 3) const re = curve.getLUT(steps) - return clampPositive(re) + return clampPositive(re, options) } -const clampPositive = (vectors: Vector[]): Vector[] => { +const clampPositive = (vectors: Vector[], options?: number | PathOptions): Vector[] | TimedVector[] => { const clamp0 = (elem: number): number => Math.max(0, elem) - return vectors.map((vector) => { + const clampedVectors = vectors.map((vector) => { return { x: clamp0(vector.x), y: clamp0(vector.y) } }) + + return (typeof options === 'number' || options?.showTimestamps === false) ? clampedVectors : generateTimestamps(clampedVectors, options) +} + +const generateTimestamps = (vectors: Vector[], options?: PathOptions): TimedVector[] => { + const speed = options?.moveSpeed ?? (Math.random() * 0.5 + 0.5) + const timeToMove = (P0: Vector, P1: Vector, P2: Vector, P3: Vector, samples: number): number => { + let total = 0 + const dt = 1 / samples + + for (let t = 0; t < 1; t += dt) { + const v1 = bezierCurveSpeed(t * dt, P0, P1, P2, P3) + const v2 = bezierCurveSpeed(t, P0, P1, P2, P3) + total += (v1 + v2) * dt / 2 + } + + return Math.round(total / speed) + } + + const timedVectors = vectors.map((vector) => { + return { + ...vector, + timestamp: 0 + } + }) + + for (let i = 0; i < timedVectors.length; i++) { + const P0 = i === 0 ? timedVectors[i] : timedVectors[i - 1] + const P1 = timedVectors[i] + const P2 = i === timedVectors.length - 1 ? timedVectors[i] : timedVectors[i + 1] + const P3 = i === timedVectors.length - 1 ? timedVectors[i] : timedVectors[i + 1] + const time = timeToMove(P0, P1, P2, P3, timedVectors.length) + + timedVectors[i] = { + ...timedVectors[i], + timestamp: i === 0 ? Date.now() : (timedVectors[i - 1] as TimedVector).timestamp + time + } + } + + return timedVectors } const overshootThreshold = 500 From 8e2e580159c82842b592147a9e07b517798e2e25 Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Sat, 30 Mar 2024 21:51:40 -0500 Subject: [PATCH 2/7] chore: changed plural routes to route in docs --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7c82478..7f428d9 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ import { path } from "ghost-cursor" const from = { x: 100, y: 100 } const to = { x: 600, y: 700 } -const routes = path(from, to, { showTimestamps: true }) +const route = path(from, to, { showTimestamps: true }) /** * [ From 2053cf5926abbddf898696f571071b6af555db4d Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Sun, 12 May 2024 20:28:21 -0500 Subject: [PATCH 3/7] chore: utilize CDP's Input.dispatchMouseEvent directyl --- src/spoof.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/spoof.ts b/src/spoof.ts index 83fc0e4..bb1bd38 100644 --- a/src/spoof.ts +++ b/src/spoof.ts @@ -358,16 +358,28 @@ export const createCursor = ( // Move the mouse over a number of vectors const tracePath = async ( - vectors: Iterable, + vectors: Iterable, abortOnMove: boolean = false ): Promise => { + const cdpClient = getCDPClient(page) + for (const v of vectors) { try { // In case this is called from random mouse movements and the users wants to move the mouse, abort if (abortOnMove && moving) { return } - await page.mouse.move(v.x, v.y) + + const dispatchParams: Protocol.Input.DispatchMouseEventRequest = { + type: 'mouseMoved', + x: v.x, + y: v.y + } + + if ('timestamp' in v) dispatchParams.timestamp = v.timestamp + + await cdpClient.send('Input.dispatchMouseEvent', dispatchParams) + previous = v } catch (error) { // Exit function if the browser is no longer connected From 4d4e115eafaeee2281cb8f484c20fad09ea262e2 Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Sun, 12 May 2024 20:30:20 -0500 Subject: [PATCH 4/7] chore: add missing puppeteer type --- src/spoof.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spoof.ts b/src/spoof.ts index bb1bd38..b882eb1 100644 --- a/src/spoof.ts +++ b/src/spoof.ts @@ -1,4 +1,4 @@ -import { ElementHandle, Page, BoundingBox, CDPSession } from 'puppeteer' +import { ElementHandle, Page, BoundingBox, CDPSession, Protocol } from 'puppeteer' import debug from 'debug' import { Vector, From 12cc327977486b11e16671aab5b3dc0b88e12c37 Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Tue, 14 May 2024 23:07:00 -0500 Subject: [PATCH 5/7] chore(conflicts): missing changes from merge conflict resolution --- src/spoof.ts | 163 ++++++++++++++++++++++++++------------------------- 1 file changed, 83 insertions(+), 80 deletions(-) diff --git a/src/spoof.ts b/src/spoof.ts index ae3ae8f..41323f4 100644 --- a/src/spoof.ts +++ b/src/spoof.ts @@ -16,82 +16,85 @@ const log = debug('ghost-cursor') export interface BoxOptions { /** - * Percentage of padding to be added around the element. - * @default 0 - */ + * Percentage of padding to be added inside the element. + * Example: + * - `0` = may be anywhere within the element. + * - `100` = will always be center of element. + * @default 0 + */ readonly paddingPercentage?: number } export interface MoveOptions extends BoxOptions, Pick { /** - * Time to wait for the selector to appear in milliseconds. - * Default is to not wait for selector. - */ + * Time to wait for the selector to appear in milliseconds. + * Default is to not wait for selector. + */ readonly waitForSelector?: number /** - * Delay after moving the mouse in milliseconds. If `randomizeMoveDelay=true`, delay is randomized from 0 to `moveDelay`. - * @default 0 - */ + * Delay after moving the mouse in milliseconds. If `randomizeMoveDelay=true`, delay is randomized from 0 to `moveDelay`. + * @default 0 + */ readonly moveDelay?: number /** - * Randomize delay between actions from `0` to `moveDelay`. See `moveDelay` docs. - * @default true - */ + * Randomize delay between actions from `0` to `moveDelay`. See `moveDelay` docs. + * @default true + */ readonly randomizeMoveDelay?: boolean /** - * Maximum number of attempts to mouse-over the element. - * @default 10 - */ + * Maximum number of attempts to mouse-over the element. + * @default 10 + */ readonly maxTries?: number /** - * Distance from current location to destination that triggers overshoot to - * occur. (Below this distance, no overshoot will occur). - * @default 500 - */ + * Distance from current location to destination that triggers overshoot to + * occur. (Below this distance, no overshoot will occur). + * @default 500 + */ readonly overshootThreshold?: number } export interface ClickOptions extends MoveOptions { /** - * Delay before initiating the click action in milliseconds. - * @default 0 - */ + * Delay before initiating the click action in milliseconds. + * @default 0 + */ readonly hesitate?: number /** - * Delay between mousedown and mouseup in milliseconds. - * @default 0 - */ + * Delay between mousedown and mouseup in milliseconds. + * @default 0 + */ readonly waitForClick?: number /** - * @default 2000 - */ + * @default 2000 + */ readonly moveDelay?: number } export interface PathOptions { /** - * Override the spread of the generated path. - */ + * Override the spread of the generated path. + */ readonly spreadOverride?: number /** - * Speed of mouse movement. - * Default is random. - */ + * Speed of mouse movement. + * Default is random. + */ readonly moveSpeed?: number readonly showTimestamps?: boolean } export interface RandomMoveOptions extends Pick { /** - * @default 2000 - */ + * @default 2000 + */ readonly moveDelay?: number } export interface MoveToOptions extends PathOptions, Pick { /** - * @default 0 - */ + * @default 0 + */ readonly moveDelay?: number } @@ -109,7 +112,7 @@ export interface GhostCursor { getLocation: () => Vector } -// Helper function to wait a specified number of milliseconds +/** Helper function to wait a specified number of milliseconds */ const delay = async (ms: number): Promise => { if (ms < 1) return return await new Promise((resolve) => setTimeout(resolve, ms)) @@ -127,7 +130,7 @@ const fitts = (distance: number, width: number): number => { return a + b * id } -// Get a random point on a box +/** Get a random point on a box */ const getRandomBoxPoint = ( { x, y, width, height }: BoundingBox, options?: BoxOptions @@ -137,8 +140,8 @@ const getRandomBoxPoint = ( if ( options?.paddingPercentage !== undefined && - options?.paddingPercentage > 0 && - options?.paddingPercentage < 100 + options?.paddingPercentage > 0 && + options?.paddingPercentage <= 100 ) { paddingWidth = (width * options.paddingPercentage) / 100 paddingHeight = (height * options.paddingPercentage) / 100 @@ -150,10 +153,10 @@ const getRandomBoxPoint = ( } } -// The function signature to access the internal CDP client changed in puppeteer 14.4.1 +/** The function signature to access the internal CDP client changed in puppeteer 14.4.1 */ const getCDPClient = (page: any): CDPSession => typeof page._client === 'function' ? page._client() : page._client -// Get a random point on a browser window +/** Get a random point on a browser window */ export const getRandomPagePoint = async (page: Page): Promise => { const targetId: string = (page.target() as any)._targetId const window = await getCDPClient(page).send( @@ -168,7 +171,7 @@ export const getRandomPagePoint = async (page: Page): Promise => { }) } -// Using this method to get correct position of Inline elements (elements like ) +/** Using this method to get correct position of Inline elements (elements like ``) */ const getElementBox = async ( page: Page, element: ElementHandle, @@ -192,9 +195,9 @@ const getElementBox = async ( if (!relativeToMainFrame) { const elementFrame = await element.contentFrame() const iframes = - elementFrame != null - ? await elementFrame.parentFrame()?.$$('xpath/.//iframe') - : null + elementFrame != null + ? await elementFrame.parentFrame()?.$$('xpath/.//iframe') + : null let frame: ElementHandle | undefined if (iframes != null) { for (const iframe of iframes) { @@ -204,9 +207,9 @@ const getElementBox = async ( if (frame != null) { const boundingBox = await frame.boundingBox() elementBox.x = - boundingBox !== null ? elementBox.x - boundingBox.x : elementBox.x + boundingBox !== null ? elementBox.x - boundingBox.x : elementBox.x elementBox.y = - boundingBox !== null ? elementBox.y - boundingBox.y : elementBox.y + boundingBox !== null ? elementBox.y - boundingBox.y : elementBox.y } } @@ -292,9 +295,9 @@ const shouldOvershoot = (a: Vector, b: Vector, threshold: number): boolean => const intersectsElement = (vec: Vector, box: BoundingBox): boolean => { return ( vec.x > box.x && - vec.x <= box.x + box.width && - vec.y > box.y && - vec.y <= box.y + box.height + vec.x <= box.x + box.width && + vec.y > box.y && + vec.y <= box.y + box.height ) } @@ -315,42 +318,42 @@ const boundingBoxWithFallback = async ( export const createCursor = ( page: Page, /** - * Cursor start position. - * @default { x: 0, y: 0 } - */ + * Cursor start position. + * @default { x: 0, y: 0 } + */ start: Vector = origin, /** - * Initially perform random movements. - * If `move`,`click`, etc. is performed, these random movements end. - * @default false - */ + * Initially perform random movements. + * If `move`,`click`, etc. is performed, these random movements end. + * @default false + */ performRandomMoves: boolean = false, defaultOptions: { /** - * Default options for the `randomMove` function that occurs when `performRandomMoves=true` - * @default RandomMoveOptions - */ + * Default options for the `randomMove` function that occurs when `performRandomMoves=true` + * @default RandomMoveOptions + */ randomMove?: RandomMoveOptions /** - * Default options for the `move` function - * @default MoveOptions - */ + * Default options for the `move` function + * @default MoveOptions + */ move?: MoveOptions /** - * Default options for the `moveTo` function - * @default MoveToOptions - */ + * Default options for the `moveTo` function + * @default MoveToOptions + */ moveTo?: MoveToOptions /** - * Default options for the `click` function - * @default ClickOptions - */ + * Default options for the `click` function + * @default ClickOptions + */ click?: ClickOptions } = {} ): GhostCursor => { // this is kind of arbitrary, not a big fan but it seems to work - const overshootSpread = 10 - const overshootRadius = 120 + const OVERSHOOT_SPREAD = 10 + const OVERSHOOT_RADIUS = 120 let previous: Vector = start // Initial state: mouse is not moving @@ -358,7 +361,7 @@ export const createCursor = ( // Move the mouse over a number of vectors const tracePath = async ( - vectors: Iterable, + vectors: Iterable, abortOnMove: boolean = false ): Promise => { const cdpClient = getCDPClient(page) @@ -406,8 +409,8 @@ export const createCursor = ( } await delay(optionsResolved.moveDelay * (optionsResolved.randomizeMoveDelay ? Math.random() : 1)) randomMove(options).then( - (_) => {}, - (_) => {} + (_) => { }, + (_) => { } ) // fire and forget, recursive function } catch (_) { log('Warning: stopping random mouse movements') @@ -503,7 +506,7 @@ export const createCursor = ( } if (elem === null) { throw new Error( - `Could not find element with selector "${selector}", make sure you're waiting for the elements by specifying "waitForSelector"` + `Could not find element with selector "${selector}", make sure you're waiting for the elements by specifying "waitForSelector"` ) } } else { @@ -535,7 +538,7 @@ export const createCursor = ( optionsResolved.overshootThreshold ) const to = overshooting - ? overshoot(destination, overshootRadius) + ? overshoot(destination, OVERSHOOT_RADIUS) : destination await tracePath(path(previous, to, optionsResolved)) @@ -543,7 +546,7 @@ export const createCursor = ( if (overshooting) { const correction = path(to, { ...dimensions, ...destination }, { ...optionsResolved, - spreadOverride: overshootSpread + spreadOverride: OVERSHOOT_SPREAD }) await tracePath(correction) @@ -589,8 +592,8 @@ export const createCursor = ( // Start random mouse movements. Do not await the promise but return immediately if (performRandomMoves) { randomMove().then( - (_) => {}, - (_) => {} + (_) => { }, + (_) => { } ) } From 9889073fb35ee9878ccbd1ff94c2edb75e2f7387 Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Wed, 15 May 2024 20:41:55 -0500 Subject: [PATCH 6/7] chore: clean up code and wording --- README.md | 8 ++++---- src/spoof.ts | 13 ++++--------- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index eb91604..687b45f 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ import { path } from "ghost-cursor" const from = { x: 100, y: 100 } const to = { x: 600, y: 700 } -const route = path(from, to, { showTimestamps: true }) +const route = path(from, to, { useTimestamps: true }) /** * [ @@ -159,16 +159,16 @@ Installs a mouse helper on the page. Makes pointer visible. Use for debugging on Gets a random point on the browser window. -#### `path(point: Vector, target: Vector, optionsOrSpread?: number | PathOptions): Vector[]` +#### `path(point: Vector, target: Vector, options?: number | PathOptions): Vector[] | TimedVector[]` Generates a set of points for mouse movement between two coordinates. - **point:** Starting point of the movement. - **target:** Ending point of the movement. -- **optionsOrSpread (optional):** Additional options for generating the path. +- **options (optional):** Additional options for generating the path. Can also be a number which will set `spreadOverride`. - `spreadOverride (number):` Override the spread of the generated path. - `moveSpeed (number):` Speed of mouse movement. Default is random. - - `showTimestamps (boolean):` Generate timestamps for each point based on the trapezoidal rule. + - `useTimestamps (boolean):` Generate timestamps for each point based on the trapezoidal rule. ## How does it work diff --git a/src/spoof.ts b/src/spoof.ts index 41323f4..e7c521f 100644 --- a/src/spoof.ts +++ b/src/spoof.ts @@ -81,7 +81,7 @@ export interface PathOptions { * Default is random. */ readonly moveSpeed?: number - readonly showTimestamps?: boolean + readonly useTimestamps?: boolean } export interface RandomMoveOptions extends Pick { @@ -248,7 +248,7 @@ const clampPositive = (vectors: Vector[], options?: PathOptions): Vector[] | Tim y: Math.max(0, vector.y) })) - return options?.showTimestamps === false ? clampedVectors : generateTimestamps(clampedVectors, options) + return options?.useTimestamps === true ? generateTimestamps(clampedVectors, options) : clampedVectors } const generateTimestamps = (vectors: Vector[], options?: PathOptions): TimedVector[] => { @@ -266,12 +266,7 @@ const generateTimestamps = (vectors: Vector[], options?: PathOptions): TimedVect return Math.round(total / speed) } - const timedVectors = vectors.map((vector) => { - return { - ...vector, - timestamp: 0 - } - }) + const timedVectors: TimedVector[] = vectors.map((vector) => ({ ...vector, timestamp: 0 })) for (let i = 0; i < timedVectors.length; i++) { const P0 = i === 0 ? timedVectors[i] : timedVectors[i - 1] @@ -282,7 +277,7 @@ const generateTimestamps = (vectors: Vector[], options?: PathOptions): TimedVect timedVectors[i] = { ...timedVectors[i], - timestamp: i === 0 ? Date.now() : (timedVectors[i - 1] as TimedVector).timestamp + time + timestamp: i === 0 ? Date.now() : timedVectors[i - 1].timestamp + time } } From f774708185cd7142658d33f968f74ec75d4d9eb0 Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Wed, 15 May 2024 21:08:44 -0500 Subject: [PATCH 7/7] style: fix broken whitespace --- src/spoof.ts | 150 ++++++++++++++++++++++++++------------------------- 1 file changed, 77 insertions(+), 73 deletions(-) diff --git a/src/spoof.ts b/src/spoof.ts index e7c521f..9cc2b3d 100644 --- a/src/spoof.ts +++ b/src/spoof.ts @@ -16,85 +16,89 @@ const log = debug('ghost-cursor') export interface BoxOptions { /** - * Percentage of padding to be added inside the element. - * Example: - * - `0` = may be anywhere within the element. - * - `100` = will always be center of element. - * @default 0 - */ + * Percentage of padding to be added inside the element. + * Example: + * - `0` = may be anywhere within the element. + * - `100` = will always be center of element. + * @default 0 + */ readonly paddingPercentage?: number } export interface MoveOptions extends BoxOptions, Pick { /** - * Time to wait for the selector to appear in milliseconds. - * Default is to not wait for selector. - */ + * Time to wait for the selector to appear in milliseconds. + * Default is to not wait for selector. + */ readonly waitForSelector?: number /** - * Delay after moving the mouse in milliseconds. If `randomizeMoveDelay=true`, delay is randomized from 0 to `moveDelay`. - * @default 0 - */ + * Delay after moving the mouse in milliseconds. If `randomizeMoveDelay=true`, delay is randomized from 0 to `moveDelay`. + * @default 0 + */ readonly moveDelay?: number /** - * Randomize delay between actions from `0` to `moveDelay`. See `moveDelay` docs. - * @default true - */ + * Randomize delay between actions from `0` to `moveDelay`. See `moveDelay` docs. + * @default true + */ readonly randomizeMoveDelay?: boolean /** - * Maximum number of attempts to mouse-over the element. - * @default 10 - */ + * Maximum number of attempts to mouse-over the element. + * @default 10 + */ readonly maxTries?: number /** - * Distance from current location to destination that triggers overshoot to - * occur. (Below this distance, no overshoot will occur). - * @default 500 - */ + * Distance from current location to destination that triggers overshoot to + * occur. (Below this distance, no overshoot will occur). + * @default 500 + */ readonly overshootThreshold?: number } export interface ClickOptions extends MoveOptions { /** - * Delay before initiating the click action in milliseconds. - * @default 0 - */ + * Delay before initiating the click action in milliseconds. + * @default 0 + */ readonly hesitate?: number /** - * Delay between mousedown and mouseup in milliseconds. - * @default 0 - */ + * Delay between mousedown and mouseup in milliseconds. + * @default 0 + */ readonly waitForClick?: number /** - * @default 2000 - */ + * @default 2000 + */ readonly moveDelay?: number } export interface PathOptions { /** - * Override the spread of the generated path. - */ + * Override the spread of the generated path. + */ readonly spreadOverride?: number /** - * Speed of mouse movement. - * Default is random. - */ + * Speed of mouse movement. + * Default is random. + */ readonly moveSpeed?: number + + /** + * Generate timestamps for each point in the path. + */ readonly useTimestamps?: boolean } export interface RandomMoveOptions extends Pick { /** - * @default 2000 - */ + * @default 2000 + */ readonly moveDelay?: number } export interface MoveToOptions extends PathOptions, Pick { /** - * @default 0 - */ + * @default 0 + */ readonly moveDelay?: number } @@ -140,8 +144,8 @@ const getRandomBoxPoint = ( if ( options?.paddingPercentage !== undefined && - options?.paddingPercentage > 0 && - options?.paddingPercentage <= 100 + options?.paddingPercentage > 0 && + options?.paddingPercentage <= 100 ) { paddingWidth = (width * options.paddingPercentage) / 100 paddingHeight = (height * options.paddingPercentage) / 100 @@ -195,9 +199,9 @@ const getElementBox = async ( if (!relativeToMainFrame) { const elementFrame = await element.contentFrame() const iframes = - elementFrame != null - ? await elementFrame.parentFrame()?.$$('xpath/.//iframe') - : null + elementFrame != null + ? await elementFrame.parentFrame()?.$$('xpath/.//iframe') + : null let frame: ElementHandle | undefined if (iframes != null) { for (const iframe of iframes) { @@ -207,9 +211,9 @@ const getElementBox = async ( if (frame != null) { const boundingBox = await frame.boundingBox() elementBox.x = - boundingBox !== null ? elementBox.x - boundingBox.x : elementBox.x + boundingBox !== null ? elementBox.x - boundingBox.x : elementBox.x elementBox.y = - boundingBox !== null ? elementBox.y - boundingBox.y : elementBox.y + boundingBox !== null ? elementBox.y - boundingBox.y : elementBox.y } } @@ -290,9 +294,9 @@ const shouldOvershoot = (a: Vector, b: Vector, threshold: number): boolean => const intersectsElement = (vec: Vector, box: BoundingBox): boolean => { return ( vec.x > box.x && - vec.x <= box.x + box.width && - vec.y > box.y && - vec.y <= box.y + box.height + vec.x <= box.x + box.width && + vec.y > box.y && + vec.y <= box.y + box.height ) } @@ -313,36 +317,36 @@ const boundingBoxWithFallback = async ( export const createCursor = ( page: Page, /** - * Cursor start position. - * @default { x: 0, y: 0 } - */ + * Cursor start position. + * @default { x: 0, y: 0 } + */ start: Vector = origin, /** - * Initially perform random movements. - * If `move`,`click`, etc. is performed, these random movements end. - * @default false - */ + * Initially perform random movements. + * If `move`,`click`, etc. is performed, these random movements end. + * @default false + */ performRandomMoves: boolean = false, defaultOptions: { /** - * Default options for the `randomMove` function that occurs when `performRandomMoves=true` - * @default RandomMoveOptions - */ + * Default options for the `randomMove` function that occurs when `performRandomMoves=true` + * @default RandomMoveOptions + */ randomMove?: RandomMoveOptions /** - * Default options for the `move` function - * @default MoveOptions - */ + * Default options for the `move` function + * @default MoveOptions + */ move?: MoveOptions /** - * Default options for the `moveTo` function - * @default MoveToOptions - */ + * Default options for the `moveTo` function + * @default MoveToOptions + */ moveTo?: MoveToOptions /** - * Default options for the `click` function - * @default ClickOptions - */ + * Default options for the `click` function + * @default ClickOptions + */ click?: ClickOptions } = {} ): GhostCursor => { @@ -404,8 +408,8 @@ export const createCursor = ( } await delay(optionsResolved.moveDelay * (optionsResolved.randomizeMoveDelay ? Math.random() : 1)) randomMove(options).then( - (_) => { }, - (_) => { } + (_) => {}, + (_) => {} ) // fire and forget, recursive function } catch (_) { log('Warning: stopping random mouse movements') @@ -501,7 +505,7 @@ export const createCursor = ( } if (elem === null) { throw new Error( - `Could not find element with selector "${selector}", make sure you're waiting for the elements by specifying "waitForSelector"` + `Could not find element with selector "${selector}", make sure you're waiting for the elements by specifying "waitForSelector"` ) } } else { @@ -587,8 +591,8 @@ export const createCursor = ( // Start random mouse movements. Do not await the promise but return immediately if (performRandomMoves) { randomMove().then( - (_) => { }, - (_) => { } + (_) => {}, + (_) => {} ) }