Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
ulixee
GitHub Repository: ulixee/secret-agent
Path: blob/main/plugins/default-human-emulator/generateVector.ts
1031 views
1
import IPoint from '@secret-agent/interfaces/IPoint';
2
import Bezier from './Bezier';
3
4
export default function generateVector(
5
startPoint: IPoint,
6
destinationPoint: IPoint,
7
targetWidth: number,
8
overshoot: { threshold: number; radius: number; spread: number },
9
) {
10
const shouldOvershoot = magnitude(direction(startPoint, destinationPoint)) > overshoot.threshold;
11
12
const firstTargetPoint = shouldOvershoot
13
? getOvershootPoint(destinationPoint, overshoot.radius)
14
: destinationPoint;
15
const points = path(startPoint, firstTargetPoint);
16
17
if (shouldOvershoot) {
18
const correction = path(firstTargetPoint, destinationPoint, targetWidth, overshoot.spread);
19
points.push(...correction);
20
}
21
return points.map(point => {
22
return { x: Math.round(point.x * 10) / 10, y: Math.round(point.y * 10) / 10 };
23
});
24
}
25
26
function path(start: IPoint, finish: IPoint, targetWidth = 100, spreadOverride?: number): IPoint[] {
27
const minSteps = 25;
28
29
if (!targetWidth || Number.isNaN(targetWidth)) targetWidth = 1;
30
31
let spread = spreadOverride;
32
if (!spread) {
33
const vec = direction(start, finish);
34
spread = Math.min(magnitude(vec), 200);
35
}
36
const anchors = generateBezierAnchors(start, finish, spread);
37
38
const curve = new Bezier(start, ...anchors, finish);
39
const length = curve.length() * 0.8;
40
const baseTime = Math.random() * minSteps;
41
let steps = Math.ceil((Math.log2(fitts(length, targetWidth) + 1) + baseTime) * 3);
42
if (Number.isNaN(steps)) steps = 25;
43
44
return curve
45
.getLookupTable(steps)
46
.map(vector => ({
47
x: vector.x,
48
y: vector.y,
49
}))
50
.filter(({ x, y }) => !Number.isNaN(x) && !Number.isNaN(y));
51
}
52
53
const sub = (a: IPoint, b: IPoint): IPoint => ({ x: a.x - b.x, y: a.y - b.y });
54
const div = (a: IPoint, b: number): IPoint => ({ x: a.x / b, y: a.y / b });
55
const mult = (a: IPoint, b: number): IPoint => ({ x: a.x * b, y: a.y * b });
56
const add = (a: IPoint, b: IPoint): IPoint => ({ x: a.x + b.x, y: a.y + b.y });
57
58
function randomVectorOnLine(a: IPoint, b: IPoint) {
59
const vec = direction(a, b);
60
const multiplier = Math.random();
61
return add(a, mult(vec, multiplier));
62
}
63
64
function randomNormalLine(a: IPoint, b: IPoint, range: number): IPoint[] {
65
const randMid = randomVectorOnLine(a, b);
66
const normalV = setMagnitude(perpendicular(direction(a, randMid)), range);
67
return [randMid, normalV];
68
}
69
70
function generateBezierAnchors(a: IPoint, b: IPoint, spread: number): IPoint[] {
71
const side = Math.round(Math.random()) === 1 ? 1 : -1;
72
const calc = (): IPoint => {
73
const [randMid, normalV] = randomNormalLine(a, b, spread);
74
const choice = mult(normalV, side);
75
return randomVectorOnLine(randMid, add(randMid, choice));
76
};
77
return [calc(), calc()].sort((sortA, sortB) => sortA.x - sortB.x);
78
}
79
80
function getOvershootPoint(coordinate: IPoint, radius: number) {
81
const a = Math.random() * 2 * Math.PI;
82
const rad = radius * Math.sqrt(Math.random());
83
const vector = { x: rad * Math.cos(a), y: rad * Math.sin(a) };
84
return add(coordinate, vector);
85
}
86
87
/**
88
* Calculate the amount of time needed to move from (x1, y1) to (x2, y2)
89
* given the width of the element being clicked on
90
* https://en.wikipedia.org/wiki/Fitts%27s_law
91
*/
92
function fitts(distance: number, width: number) {
93
return 2 * Math.log2(distance / width + 1);
94
}
95
96
function direction(a: IPoint, b: IPoint) {
97
return sub(b, a);
98
}
99
100
function perpendicular(a: IPoint) {
101
return { x: a.y, y: -1 * a.x };
102
}
103
104
function magnitude(a: IPoint) {
105
return Math.sqrt(a.x ** 2 + a.y ** 2);
106
}
107
108
function unit(a: IPoint) {
109
return div(a, magnitude(a));
110
}
111
112
function setMagnitude(a: IPoint, amount: number) {
113
return mult(unit(a), amount);
114
}
115
116