Skip to content

Commit 1cfb38d

Browse files
committed
feat: Split core and puppeteer features
1 parent a626a73 commit 1cfb38d

File tree

4 files changed

+131
-110
lines changed

4 files changed

+131
-110
lines changed

package.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,17 @@
33
"version": "1.4.1",
44
"description": "Move your mouse like a human in puppeteer or generate realistic movements on any 2D plane",
55
"repository": "https://github.com/Xetera/ghost-cursor",
6+
"exports": {
7+
".": {
8+
"types": "./lib/spoof.d.ts",
9+
"default": "./lib/spoof.js"
10+
},
11+
"./core": {
12+
"types": "./lib/core.d.ts",
13+
"default": "./lib/core.js"
14+
},
15+
"./package.json": "./package.json"
16+
},
617
"main": "lib/spoof.js",
718
"types": "lib/spoof.d.ts",
819
"scripts": {

src/core.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import {
2+
type Vector,
3+
type TimedVector,
4+
type Rectangle,
5+
bezierCurve,
6+
bezierCurveSpeed,
7+
extrapolate
8+
} from './math'
9+
10+
export interface PathOptions {
11+
/**
12+
* Override the spread of the generated path.
13+
*/
14+
readonly spreadOverride?: number
15+
/**
16+
* Speed of mouse movement.
17+
* Default is random.
18+
*/
19+
readonly moveSpeed?: number
20+
21+
/**
22+
* Generate timestamps for each point in the path.
23+
*/
24+
readonly useTimestamps?: boolean
25+
}
26+
27+
/**
28+
* Calculate the amount of time needed to move from (x1, y1) to (x2, y2)
29+
* given the width of the element being clicked on
30+
* https://en.wikipedia.org/wiki/Fitts%27s_law
31+
*/
32+
const fitts = (distance: number, width: number): number => {
33+
const a = 0
34+
const b = 2
35+
const id = Math.log2(distance / width + 1)
36+
return a + b * id
37+
}
38+
39+
/** Generates a set of points for mouse movement between two coordinates. */
40+
export function path (
41+
start: Vector,
42+
end: Vector | Rectangle,
43+
/**
44+
* Additional options for generating the path.
45+
* Can also be a number which will set `spreadOverride`.
46+
*/
47+
// TODO: remove number arg in next major version change, fine to just allow `spreadOverride` in object.
48+
options?: number | PathOptions): Vector[] | TimedVector[] {
49+
const optionsResolved: PathOptions = typeof options === 'number'
50+
? { spreadOverride: options }
51+
: { ...options }
52+
53+
const DEFAULT_WIDTH = 100
54+
const MIN_STEPS = 25
55+
const width = 'width' in end && end.width !== 0 ? end.width : DEFAULT_WIDTH
56+
const curve = bezierCurve(start, end, optionsResolved.spreadOverride)
57+
const length = curve.length() * 0.8
58+
59+
const speed = optionsResolved.moveSpeed !== undefined && optionsResolved.moveSpeed > 0
60+
? (25 / optionsResolved.moveSpeed)
61+
: Math.random()
62+
const baseTime = speed * MIN_STEPS
63+
const steps = Math.ceil((Math.log2(fitts(length, width) + 1) + baseTime) * 3)
64+
const re = curve.getLUT(steps)
65+
return clampPositive(re, optionsResolved)
66+
}
67+
68+
const clampPositive = (vectors: Vector[], options?: PathOptions): Vector[] | TimedVector[] => {
69+
const clampedVectors = vectors.map((vector) => ({
70+
x: Math.max(0, vector.x),
71+
y: Math.max(0, vector.y)
72+
}))
73+
74+
return options?.useTimestamps === true ? generateTimestamps(clampedVectors, options) : clampedVectors
75+
}
76+
77+
const generateTimestamps = (vectors: Vector[], options?: PathOptions): TimedVector[] => {
78+
const speed = options?.moveSpeed ?? (Math.random() * 0.5 + 0.5)
79+
const timeToMove = (P0: Vector, P1: Vector, P2: Vector, P3: Vector, samples: number): number => {
80+
let total = 0
81+
const dt = 1 / samples
82+
83+
for (let t = 0; t < 1; t += dt) {
84+
const v1 = bezierCurveSpeed(t * dt, P0, P1, P2, P3)
85+
const v2 = bezierCurveSpeed(t, P0, P1, P2, P3)
86+
total += (v1 + v2) * dt / 2
87+
}
88+
89+
return Math.round(total / speed)
90+
}
91+
92+
const timedVectors: TimedVector[] = []
93+
94+
for (let i = 0; i < vectors.length; i++) {
95+
if (i === 0) {
96+
timedVectors.push({ ...vectors[i], timestamp: Date.now() })
97+
} else {
98+
const P0 = vectors[i - 1]
99+
const P1 = vectors[i]
100+
const P2 = i + 1 < vectors.length ? vectors[i + 1] : extrapolate(P0, P1)
101+
const P3 = i + 2 < vectors.length ? vectors[i + 2] : extrapolate(P1, P2)
102+
const time = timeToMove(P0, P1, P2, P3, vectors.length)
103+
104+
timedVectors.push({
105+
...vectors[i],
106+
timestamp: timedVectors[i - 1].timestamp + time
107+
})
108+
}
109+
}
110+
111+
return timedVectors
112+
}

src/math.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ export interface Vector {
77
export interface TimedVector extends Vector {
88
timestamp: number
99
}
10+
export interface Rectangle extends Vector {
11+
width: number
12+
height: number
13+
}
14+
1015
export const origin: Vector = { x: 0, y: 0 }
1116

1217
// maybe i should've just imported a vector library lol

src/spoof.ts

Lines changed: 3 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,20 @@
11
import type { ElementHandle, Page, BoundingBox, CDPSession, Protocol } from 'puppeteer'
22
import debug from 'debug'
3+
import { type PathOptions, path } from './core'
34
import {
45
type Vector,
5-
type TimedVector,
6-
bezierCurve,
7-
bezierCurveSpeed,
86
direction,
97
magnitude,
108
origin,
119
overshoot,
1210
add,
1311
clamp,
14-
scale,
15-
extrapolate
12+
scale
1613
} from './math'
1714
import { installMouseHelper } from './mouse-helper'
1815

1916
// TODO: remove in next major version, is now wrapped in the GhostCursor class.
20-
export { installMouseHelper }
17+
export { type PathOptions, path, installMouseHelper }
2118

2219
const log = debug('ghost-cursor')
2320

@@ -128,23 +125,6 @@ export interface ClickOptions extends MoveOptions {
128125
readonly clickCount?: number
129126
}
130127

131-
export interface PathOptions {
132-
/**
133-
* Override the spread of the generated path.
134-
*/
135-
readonly spreadOverride?: number
136-
/**
137-
* Speed of mouse movement.
138-
* Default is random.
139-
*/
140-
readonly moveSpeed?: number
141-
142-
/**
143-
* Generate timestamps for each point in the path.
144-
*/
145-
readonly useTimestamps?: boolean
146-
}
147-
148128
export interface RandomMoveOptions extends Pick<MoveOptions, 'moveDelay' | 'randomizeMoveDelay' | 'moveSpeed'> {
149129
/**
150130
* @default 2000
@@ -205,18 +185,6 @@ const delay = async (ms: number): Promise<void> => {
205185
return await new Promise((resolve) => setTimeout(resolve, ms))
206186
}
207187

208-
/**
209-
* Calculate the amount of time needed to move from (x1, y1) to (x2, y2)
210-
* given the width of the element being clicked on
211-
* https://en.wikipedia.org/wiki/Fitts%27s_law
212-
*/
213-
const fitts = (distance: number, width: number): number => {
214-
const a = 0
215-
const b = 2
216-
const id = Math.log2(distance / width + 1)
217-
return a + b * id
218-
}
219-
220188
/** Get a random point on a box */
221189
const getRandomBoxPoint = (
222190
{ x, y, width, height }: BoundingBox,
@@ -310,81 +278,6 @@ export const getElementBox = async (
310278
}
311279
}
312280

313-
/** Generates a set of points for mouse movement between two coordinates. */
314-
export function path (
315-
start: Vector,
316-
end: Vector | BoundingBox,
317-
/**
318-
* Additional options for generating the path.
319-
* Can also be a number which will set `spreadOverride`.
320-
*/
321-
// TODO: remove number arg in next major version change, fine to just allow `spreadOverride` in object.
322-
options?: number | PathOptions): Vector[] | TimedVector[] {
323-
const optionsResolved: PathOptions = typeof options === 'number'
324-
? { spreadOverride: options }
325-
: { ...options }
326-
327-
const DEFAULT_WIDTH = 100
328-
const MIN_STEPS = 25
329-
const width = 'width' in end && end.width !== 0 ? end.width : DEFAULT_WIDTH
330-
const curve = bezierCurve(start, end, optionsResolved.spreadOverride)
331-
const length = curve.length() * 0.8
332-
333-
const speed = optionsResolved.moveSpeed !== undefined && optionsResolved.moveSpeed > 0
334-
? (25 / optionsResolved.moveSpeed)
335-
: Math.random()
336-
const baseTime = speed * MIN_STEPS
337-
const steps = Math.ceil((Math.log2(fitts(length, width) + 1) + baseTime) * 3)
338-
const re = curve.getLUT(steps)
339-
return clampPositive(re, optionsResolved)
340-
}
341-
342-
const clampPositive = (vectors: Vector[], options?: PathOptions): Vector[] | TimedVector[] => {
343-
const clampedVectors = vectors.map((vector) => ({
344-
x: Math.max(0, vector.x),
345-
y: Math.max(0, vector.y)
346-
}))
347-
348-
return options?.useTimestamps === true ? generateTimestamps(clampedVectors, options) : clampedVectors
349-
}
350-
351-
const generateTimestamps = (vectors: Vector[], options?: PathOptions): TimedVector[] => {
352-
const speed = options?.moveSpeed ?? (Math.random() * 0.5 + 0.5)
353-
const timeToMove = (P0: Vector, P1: Vector, P2: Vector, P3: Vector, samples: number): number => {
354-
let total = 0
355-
const dt = 1 / samples
356-
357-
for (let t = 0; t < 1; t += dt) {
358-
const v1 = bezierCurveSpeed(t * dt, P0, P1, P2, P3)
359-
const v2 = bezierCurveSpeed(t, P0, P1, P2, P3)
360-
total += (v1 + v2) * dt / 2
361-
}
362-
363-
return Math.round(total / speed)
364-
}
365-
366-
const timedVectors: TimedVector[] = []
367-
368-
for (let i = 0; i < vectors.length; i++) {
369-
if (i === 0) {
370-
timedVectors.push({ ...vectors[i], timestamp: Date.now() })
371-
} else {
372-
const P0 = vectors[i - 1]
373-
const P1 = vectors[i]
374-
const P2 = i + 1 < vectors.length ? vectors[i + 1] : extrapolate(P0, P1)
375-
const P3 = i + 2 < vectors.length ? vectors[i + 2] : extrapolate(P1, P2)
376-
const time = timeToMove(P0, P1, P2, P3, vectors.length)
377-
378-
timedVectors.push({
379-
...vectors[i],
380-
timestamp: timedVectors[i - 1].timestamp + time
381-
})
382-
}
383-
}
384-
385-
return timedVectors
386-
}
387-
388281
const shouldOvershoot = (a: Vector, b: Vector, threshold: number): boolean =>
389282
magnitude(direction(a, b)) > threshold
390283

0 commit comments

Comments
 (0)