arn-browser 0.0.4 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -1
- package/src/human-cursor/HumanCursor.js +448 -0
- package/src/human-cursor/bezier.js +248 -0
- package/src/human-cursor/index.d.ts +154 -0
- package/src/human-cursor/index.js +9 -0
- package/src/human-cursor/randomizer.js +149 -0
- package/src/human-cursor/tweening.js +260 -0
- package/src/utility/launchBrowser.d.ts +28 -1
- package/src/utility/launchBrowser.js +53 -22
- package/src/utility/multilogin_token_manager.js +26 -48
- package/src/utility/proxy-utility/custom-proxy.js +1 -45
- package/src/utility/proxy-utility/proxy-helper.d.ts +1 -1
- package/src/utility/proxy-utility/proxy-helper.js +21 -44
- package/rowser_automation_env.js +0 -32
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bezier curve calculator and human-like trajectory generator
|
|
3
|
+
* Ported from Python HumanCursor library
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { easeOutQuad } from './tweening.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Calculates the binomial coefficient "n choose k"
|
|
10
|
+
* @param {number} n
|
|
11
|
+
* @param {number} k
|
|
12
|
+
* @returns {number}
|
|
13
|
+
*/
|
|
14
|
+
function binomial(n, k) {
|
|
15
|
+
let result = 1;
|
|
16
|
+
for (let i = 1; i <= k; i++) {
|
|
17
|
+
result = result * (n - i + 1) / i;
|
|
18
|
+
}
|
|
19
|
+
return result;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Calculate the i-th component of a Bernstein polynomial of degree n
|
|
24
|
+
* @param {number} x - Point on curve (0 to 1)
|
|
25
|
+
* @param {number} i - Index
|
|
26
|
+
* @param {number} n - Degree
|
|
27
|
+
* @returns {number}
|
|
28
|
+
*/
|
|
29
|
+
function bernsteinPolynomialPoint(x, i, n) {
|
|
30
|
+
return binomial(n, i) * Math.pow(x, i) * Math.pow(1 - x, n - i);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Returns a function that calculates a point on the Bezier curve
|
|
35
|
+
* @param {Array<[number, number]>} points - Control points
|
|
36
|
+
* @returns {function(number): [number, number]}
|
|
37
|
+
*/
|
|
38
|
+
function bernsteinPolynomial(points) {
|
|
39
|
+
return function (t) {
|
|
40
|
+
const n = points.length - 1;
|
|
41
|
+
let x = 0;
|
|
42
|
+
let y = 0;
|
|
43
|
+
for (let i = 0; i <= n; i++) {
|
|
44
|
+
const bern = bernsteinPolynomialPoint(t, i, n);
|
|
45
|
+
x += points[i][0] * bern;
|
|
46
|
+
y += points[i][1] * bern;
|
|
47
|
+
}
|
|
48
|
+
return [x, y];
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Calculate n points on a Bezier curve
|
|
54
|
+
* @param {number} n - Number of points to generate
|
|
55
|
+
* @param {Array<[number, number]>} points - Control points
|
|
56
|
+
* @returns {Array<[number, number]>}
|
|
57
|
+
*/
|
|
58
|
+
function calculatePointsInCurve(n, points) {
|
|
59
|
+
const curvePoints = [];
|
|
60
|
+
const getBezierPoint = bernsteinPolynomial(points);
|
|
61
|
+
for (let i = 0; i < n; i++) {
|
|
62
|
+
const t = i / (n - 1);
|
|
63
|
+
curvePoints.push(getBezierPoint(t));
|
|
64
|
+
}
|
|
65
|
+
return curvePoints;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Check if a value is numeric
|
|
70
|
+
* @param {*} val
|
|
71
|
+
* @returns {boolean}
|
|
72
|
+
*/
|
|
73
|
+
function isNumeric(val) {
|
|
74
|
+
return typeof val === 'number' && !isNaN(val);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Check if list contains valid points
|
|
79
|
+
* @param {Array} list
|
|
80
|
+
* @returns {boolean}
|
|
81
|
+
*/
|
|
82
|
+
function isListOfPoints(list) {
|
|
83
|
+
if (!Array.isArray(list)) return false;
|
|
84
|
+
return list.every(p =>
|
|
85
|
+
Array.isArray(p) &&
|
|
86
|
+
p.length === 2 &&
|
|
87
|
+
isNumeric(p[0]) &&
|
|
88
|
+
isNumeric(p[1])
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Random integer between min and max (inclusive)
|
|
94
|
+
* @param {number} min
|
|
95
|
+
* @param {number} max
|
|
96
|
+
* @returns {number}
|
|
97
|
+
*/
|
|
98
|
+
function randInt(min, max) {
|
|
99
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Random float with normal distribution
|
|
104
|
+
* @param {number} mean
|
|
105
|
+
* @param {number} stdDev
|
|
106
|
+
* @returns {number}
|
|
107
|
+
*/
|
|
108
|
+
function randomNormal(mean, stdDev) {
|
|
109
|
+
// Box-Muller transform
|
|
110
|
+
const u1 = Math.random();
|
|
111
|
+
const u2 = Math.random();
|
|
112
|
+
const z0 = Math.sqrt(-2.0 * Math.log(u1)) * Math.cos(2.0 * Math.PI * u2);
|
|
113
|
+
return z0 * stdDev + mean;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Generates human-like mouse trajectory using Bezier curves
|
|
118
|
+
*/
|
|
119
|
+
export class HumanizeMouseTrajectory {
|
|
120
|
+
/**
|
|
121
|
+
* @param {[number, number]} fromPoint - Starting point [x, y]
|
|
122
|
+
* @param {[number, number]} toPoint - Ending point [x, y]
|
|
123
|
+
* @param {Object} options - Curve generation options
|
|
124
|
+
*/
|
|
125
|
+
constructor(fromPoint, toPoint, options = {}) {
|
|
126
|
+
this.fromPoint = fromPoint;
|
|
127
|
+
this.toPoint = toPoint;
|
|
128
|
+
this.points = this.generateCurve(options);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Generates the curve based on options
|
|
133
|
+
* @param {Object} options
|
|
134
|
+
* @returns {Array<[number, number]>}
|
|
135
|
+
*/
|
|
136
|
+
generateCurve(options) {
|
|
137
|
+
const offsetBoundaryX = options.offsetBoundaryX ?? 80;
|
|
138
|
+
const offsetBoundaryY = options.offsetBoundaryY ?? 80;
|
|
139
|
+
const leftBoundary = (options.leftBoundary ?? Math.min(this.fromPoint[0], this.toPoint[0])) - offsetBoundaryX;
|
|
140
|
+
const rightBoundary = (options.rightBoundary ?? Math.max(this.fromPoint[0], this.toPoint[0])) + offsetBoundaryX;
|
|
141
|
+
const downBoundary = (options.downBoundary ?? Math.min(this.fromPoint[1], this.toPoint[1])) - offsetBoundaryY;
|
|
142
|
+
const upBoundary = (options.upBoundary ?? Math.max(this.fromPoint[1], this.toPoint[1])) + offsetBoundaryY;
|
|
143
|
+
const knotsCount = options.knotsCount ?? 2;
|
|
144
|
+
const distortionMean = options.distortionMean ?? 1;
|
|
145
|
+
const distortionStdDev = options.distortionStdDev ?? 1;
|
|
146
|
+
const distortionFrequency = options.distortionFrequency ?? 0.5;
|
|
147
|
+
const tween = options.tweening ?? easeOutQuad;
|
|
148
|
+
const targetPoints = options.targetPoints ?? 100;
|
|
149
|
+
|
|
150
|
+
const internalKnots = this.generateInternalKnots(
|
|
151
|
+
leftBoundary, rightBoundary, downBoundary, upBoundary, knotsCount
|
|
152
|
+
);
|
|
153
|
+
let points = this.generatePoints(internalKnots);
|
|
154
|
+
points = this.distortPoints(points, distortionMean, distortionStdDev, distortionFrequency);
|
|
155
|
+
points = this.tweenPoints(points, tween, targetPoints);
|
|
156
|
+
return points;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Generates random internal knots for the curve
|
|
161
|
+
* @param {number} lBoundary
|
|
162
|
+
* @param {number} rBoundary
|
|
163
|
+
* @param {number} dBoundary
|
|
164
|
+
* @param {number} uBoundary
|
|
165
|
+
* @param {number} knotsCount
|
|
166
|
+
* @returns {Array<[number, number]>}
|
|
167
|
+
*/
|
|
168
|
+
generateInternalKnots(lBoundary, rBoundary, dBoundary, uBoundary, knotsCount) {
|
|
169
|
+
if (knotsCount < 0) knotsCount = 0;
|
|
170
|
+
if (lBoundary > rBoundary) {
|
|
171
|
+
throw new Error('leftBoundary must be less than or equal to rightBoundary');
|
|
172
|
+
}
|
|
173
|
+
if (dBoundary > uBoundary) {
|
|
174
|
+
throw new Error('downBoundary must be less than or equal to upperBoundary');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const knots = [];
|
|
178
|
+
for (let i = 0; i < knotsCount; i++) {
|
|
179
|
+
const x = randInt(Math.floor(lBoundary), Math.floor(rBoundary));
|
|
180
|
+
const y = randInt(Math.floor(dBoundary), Math.floor(uBoundary));
|
|
181
|
+
knots.push([x, y]);
|
|
182
|
+
}
|
|
183
|
+
return knots;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Generates points from Bezier curve
|
|
188
|
+
* @param {Array<[number, number]>} knots
|
|
189
|
+
* @returns {Array<[number, number]>}
|
|
190
|
+
*/
|
|
191
|
+
generatePoints(knots) {
|
|
192
|
+
const midPtsCnt = Math.max(
|
|
193
|
+
Math.abs(this.fromPoint[0] - this.toPoint[0]),
|
|
194
|
+
Math.abs(this.fromPoint[1] - this.toPoint[1]),
|
|
195
|
+
2
|
|
196
|
+
);
|
|
197
|
+
const allKnots = [this.fromPoint, ...knots, this.toPoint];
|
|
198
|
+
return calculatePointsInCurve(Math.floor(midPtsCnt), allKnots);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Distorts points to add human-like imperfections
|
|
203
|
+
* @param {Array<[number, number]>} points
|
|
204
|
+
* @param {number} distortionMean
|
|
205
|
+
* @param {number} distortionStdDev
|
|
206
|
+
* @param {number} distortionFrequency
|
|
207
|
+
* @returns {Array<[number, number]>}
|
|
208
|
+
*/
|
|
209
|
+
distortPoints(points, distortionMean, distortionStdDev, distortionFrequency) {
|
|
210
|
+
if (distortionFrequency < 0 || distortionFrequency > 1) {
|
|
211
|
+
throw new Error('distortionFrequency must be in range [0, 1]');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const distorted = [points[0]];
|
|
215
|
+
for (let i = 1; i < points.length - 1; i++) {
|
|
216
|
+
const [x, y] = points[i];
|
|
217
|
+
const delta = Math.random() < distortionFrequency
|
|
218
|
+
? randomNormal(distortionMean, distortionStdDev)
|
|
219
|
+
: 0;
|
|
220
|
+
distorted.push([x, y + delta]);
|
|
221
|
+
}
|
|
222
|
+
distorted.push(points[points.length - 1]);
|
|
223
|
+
return distorted;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Applies tweening to points
|
|
228
|
+
* @param {Array<[number, number]>} points
|
|
229
|
+
* @param {function(number): number} tween
|
|
230
|
+
* @param {number} targetPoints
|
|
231
|
+
* @returns {Array<[number, number]>}
|
|
232
|
+
*/
|
|
233
|
+
tweenPoints(points, tween, targetPoints) {
|
|
234
|
+
if (targetPoints < 2) {
|
|
235
|
+
throw new Error('targetPoints must be >= 2');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const result = [];
|
|
239
|
+
for (let i = 0; i < targetPoints; i++) {
|
|
240
|
+
const t = i / (targetPoints - 1);
|
|
241
|
+
const index = Math.floor(tween(t) * (points.length - 1));
|
|
242
|
+
result.push(points[index]);
|
|
243
|
+
}
|
|
244
|
+
return result;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export { calculatePointsInCurve, bernsteinPolynomial };
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript declarations for human-cursor-playwright
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Page, Locator, Response, PageScreenshotOptions, WaitForURLOptions } from 'playwright';
|
|
6
|
+
|
|
7
|
+
// ============= Tweening Functions =============
|
|
8
|
+
|
|
9
|
+
export type EasingFunction = (n: number) => number;
|
|
10
|
+
|
|
11
|
+
export function linear(n: number): number;
|
|
12
|
+
export function easeOutQuad(n: number): number;
|
|
13
|
+
export function easeInQuad(n: number): number;
|
|
14
|
+
export function easeInOutQuad(n: number): number;
|
|
15
|
+
export function easeOutCubic(n: number): number;
|
|
16
|
+
export function easeInCubic(n: number): number;
|
|
17
|
+
export function easeInOutCubic(n: number): number;
|
|
18
|
+
export function easeOutQuart(n: number): number;
|
|
19
|
+
export function easeInQuart(n: number): number;
|
|
20
|
+
export function easeInOutQuart(n: number): number;
|
|
21
|
+
export function easeOutQuint(n: number): number;
|
|
22
|
+
export function easeInQuint(n: number): number;
|
|
23
|
+
export function easeInOutQuint(n: number): number;
|
|
24
|
+
export function easeOutSine(n: number): number;
|
|
25
|
+
export function easeInSine(n: number): number;
|
|
26
|
+
export function easeInOutSine(n: number): number;
|
|
27
|
+
export function easeOutExpo(n: number): number;
|
|
28
|
+
export function easeInExpo(n: number): number;
|
|
29
|
+
export function easeInOutExpo(n: number): number;
|
|
30
|
+
export function easeOutCirc(n: number): number;
|
|
31
|
+
export function easeInCirc(n: number): number;
|
|
32
|
+
export function easeInOutCirc(n: number): number;
|
|
33
|
+
|
|
34
|
+
export const easingFunctions: Record<string, EasingFunction>;
|
|
35
|
+
|
|
36
|
+
// ============= Bezier Curve =============
|
|
37
|
+
|
|
38
|
+
export interface CurveOptions {
|
|
39
|
+
offsetBoundaryX?: number;
|
|
40
|
+
offsetBoundaryY?: number;
|
|
41
|
+
knotsCount?: number;
|
|
42
|
+
distortionMean?: number;
|
|
43
|
+
distortionStdDev?: number;
|
|
44
|
+
distortionFrequency?: number;
|
|
45
|
+
tweening?: EasingFunction;
|
|
46
|
+
targetPoints?: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export class HumanizeMouseTrajectory {
|
|
50
|
+
fromPoint: [number, number];
|
|
51
|
+
toPoint: [number, number];
|
|
52
|
+
points: Array<[number, number]>;
|
|
53
|
+
|
|
54
|
+
constructor(fromPoint: [number, number], toPoint: [number, number], options?: CurveOptions);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ============= HumanLocator =============
|
|
58
|
+
|
|
59
|
+
export interface HumanClickOptions {
|
|
60
|
+
clickCount?: number;
|
|
61
|
+
button?: 'left' | 'right' | 'middle';
|
|
62
|
+
delay?: number;
|
|
63
|
+
position?: { x: number; y: number };
|
|
64
|
+
steady?: boolean;
|
|
65
|
+
moveSpeed?: number;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface HumanFillOptions extends HumanClickOptions {
|
|
69
|
+
minDelay?: number;
|
|
70
|
+
maxDelay?: number;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Locator with human-like cursor movement
|
|
75
|
+
* All Playwright Locator methods work, with click/fill/type using human cursor
|
|
76
|
+
*/
|
|
77
|
+
export interface HumanLocator extends Locator {
|
|
78
|
+
click(options?: HumanClickOptions): Promise<void>;
|
|
79
|
+
dblclick(options?: HumanClickOptions): Promise<void>;
|
|
80
|
+
fill(value: string, options?: HumanFillOptions): Promise<void>;
|
|
81
|
+
type(text: string, options?: HumanFillOptions): Promise<void>;
|
|
82
|
+
hover(options?: HumanClickOptions): Promise<void>;
|
|
83
|
+
press(key: string, options?: HumanClickOptions): Promise<void>;
|
|
84
|
+
|
|
85
|
+
// Chainable methods return HumanLocator
|
|
86
|
+
filter(options: { has?: Locator; hasNot?: Locator; hasText?: string | RegExp }): HumanLocator;
|
|
87
|
+
first(): HumanLocator;
|
|
88
|
+
last(): HumanLocator;
|
|
89
|
+
nth(index: number): HumanLocator;
|
|
90
|
+
locator(selector: string): HumanLocator;
|
|
91
|
+
getByText(text: string | RegExp, options?: { exact?: boolean }): HumanLocator;
|
|
92
|
+
getByRole(role: string, options?: { name?: string | RegExp; exact?: boolean }): HumanLocator;
|
|
93
|
+
getByLabel(text: string | RegExp, options?: { exact?: boolean }): HumanLocator;
|
|
94
|
+
getByPlaceholder(text: string | RegExp, options?: { exact?: boolean }): HumanLocator;
|
|
95
|
+
getByTestId(testId: string | RegExp): HumanLocator;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ============= HumanCursor Options =============
|
|
99
|
+
|
|
100
|
+
export interface CreateCursorOptions {
|
|
101
|
+
/** Enable/disable human-like cursor movement (default: true) */
|
|
102
|
+
humanize?: boolean;
|
|
103
|
+
/** Maximum movement time in seconds (default: 1.5) */
|
|
104
|
+
maxTime?: number;
|
|
105
|
+
/** Minimum movement time in seconds (default: 0.5) */
|
|
106
|
+
minTime?: number;
|
|
107
|
+
/** Auto-show cursor indicator after goto (default: true) */
|
|
108
|
+
showCursor?: boolean;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ============= HumanPage =============
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Page with human-like cursor movement
|
|
115
|
+
* All Playwright Page methods work, with locator methods returning HumanLocators
|
|
116
|
+
*/
|
|
117
|
+
export interface HumanPage extends Page {
|
|
118
|
+
// Override locator methods to return HumanLocator
|
|
119
|
+
locator(selector: string): HumanLocator;
|
|
120
|
+
getByText(text: string | RegExp, options?: { exact?: boolean }): HumanLocator;
|
|
121
|
+
getByRole(role: string, options?: { name?: string | RegExp; exact?: boolean }): HumanLocator;
|
|
122
|
+
getByLabel(text: string | RegExp, options?: { exact?: boolean }): HumanLocator;
|
|
123
|
+
getByPlaceholder(text: string | RegExp, options?: { exact?: boolean }): HumanLocator;
|
|
124
|
+
getByAltText(text: string | RegExp, options?: { exact?: boolean }): HumanLocator;
|
|
125
|
+
getByTitle(text: string | RegExp, options?: { exact?: boolean }): HumanLocator;
|
|
126
|
+
getByTestId(testId: string | RegExp): HumanLocator;
|
|
127
|
+
|
|
128
|
+
// Additional cursor utilities
|
|
129
|
+
showCursor(): Promise<void>;
|
|
130
|
+
originCoordinates: [number, number];
|
|
131
|
+
setPosition(x: number, y: number): void;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ============= Main Export =============
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Create a cursor that acts as a drop-in replacement for Page
|
|
138
|
+
* All Page methods work, but locator actions (click, fill, type, hover) use human cursor
|
|
139
|
+
*
|
|
140
|
+
* @example
|
|
141
|
+
* ```ts
|
|
142
|
+
* const cursor = createCursor(page, { showCursor: true });
|
|
143
|
+
* await cursor.goto('https://example.com');
|
|
144
|
+
* await cursor.locator('button').click(); // Uses human cursor!
|
|
145
|
+
* ```
|
|
146
|
+
*/
|
|
147
|
+
export function createCursor(page: Page, options?: CreateCursorOptions): HumanPage;
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* @deprecated Use createCursor() instead
|
|
151
|
+
*/
|
|
152
|
+
export class HumanCursor {
|
|
153
|
+
constructor(page: Page, options?: CreateCursorOptions);
|
|
154
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* human-cursor-playwright
|
|
3
|
+
* Human-like cursor movements for Playwright
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export { HumanCursor, createCursor } from './HumanCursor.js';
|
|
7
|
+
export { HumanizeMouseTrajectory, calculatePointsInCurve, bernsteinPolynomial } from './bezier.js';
|
|
8
|
+
export * from './tweening.js';
|
|
9
|
+
export { generateRandomCurveParameters, calculateAbsoluteOffset } from './randomizer.js';
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Random curve parameter generation
|
|
3
|
+
* Ported from Python HumanCursor library
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as tweening from './tweening.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Random integer between min and max (inclusive)
|
|
10
|
+
* @param {number} min
|
|
11
|
+
* @param {number} max
|
|
12
|
+
* @returns {number}
|
|
13
|
+
*/
|
|
14
|
+
function randInt(min, max) {
|
|
15
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Randomly select from array
|
|
20
|
+
* @template T
|
|
21
|
+
* @param {T[]} arr
|
|
22
|
+
* @returns {T}
|
|
23
|
+
*/
|
|
24
|
+
function randomChoice(arr) {
|
|
25
|
+
return arr[Math.floor(Math.random() * arr.length)];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Weighted random selection
|
|
30
|
+
* @template T
|
|
31
|
+
* @param {T[]} choices
|
|
32
|
+
* @param {number[]} weights
|
|
33
|
+
* @returns {T}
|
|
34
|
+
*/
|
|
35
|
+
function weightedChoice(choices, weights) {
|
|
36
|
+
const totalWeight = weights.reduce((a, b) => a + b, 0);
|
|
37
|
+
let random = Math.random() * totalWeight;
|
|
38
|
+
for (let i = 0; i < choices.length; i++) {
|
|
39
|
+
random -= weights[i];
|
|
40
|
+
if (random <= 0) return choices[i];
|
|
41
|
+
}
|
|
42
|
+
return choices[choices.length - 1];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Calculate exact pixel offsets from relative position values
|
|
47
|
+
* @param {{ width: number, height: number }} size - Element dimensions
|
|
48
|
+
* @param {[number, number]} relativePosition - [x, y] percentages (0 to 1)
|
|
49
|
+
* @returns {[number, number]} Absolute pixel offsets
|
|
50
|
+
*/
|
|
51
|
+
export function calculateAbsoluteOffset(size, relativePosition) {
|
|
52
|
+
const xFinal = size.width * relativePosition[0];
|
|
53
|
+
const yFinal = size.height * relativePosition[1];
|
|
54
|
+
return [Math.floor(xFinal), Math.floor(yFinal)];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* List of easing functions for random selection
|
|
59
|
+
*/
|
|
60
|
+
const tweenOptions = [
|
|
61
|
+
tweening.easeOutExpo,
|
|
62
|
+
tweening.easeInOutQuint,
|
|
63
|
+
tweening.easeInOutSine,
|
|
64
|
+
tweening.easeInOutQuart,
|
|
65
|
+
tweening.easeInOutExpo,
|
|
66
|
+
tweening.easeInOutCubic,
|
|
67
|
+
tweening.easeInOutCirc,
|
|
68
|
+
tweening.linear,
|
|
69
|
+
tweening.easeOutSine,
|
|
70
|
+
tweening.easeOutQuart,
|
|
71
|
+
tweening.easeOutQuint,
|
|
72
|
+
tweening.easeOutCubic,
|
|
73
|
+
tweening.easeOutCirc,
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Generate random parameters for human-like curve
|
|
78
|
+
* @param {{ width: number, height: number }} viewportSize - Browser viewport size
|
|
79
|
+
* @param {[number, number]} preOrigin - Starting point
|
|
80
|
+
* @param {[number, number]} postDestination - Ending point
|
|
81
|
+
* @returns {Object} Curve parameters
|
|
82
|
+
*/
|
|
83
|
+
export function generateRandomCurveParameters(viewportSize, preOrigin, postDestination) {
|
|
84
|
+
const viewportWidth = viewportSize.width;
|
|
85
|
+
const viewportHeight = viewportSize.height;
|
|
86
|
+
|
|
87
|
+
const minWidth = viewportWidth * 0.15;
|
|
88
|
+
const maxWidth = viewportWidth * 0.85;
|
|
89
|
+
const minHeight = viewportHeight * 0.15;
|
|
90
|
+
const maxHeight = viewportHeight * 0.85;
|
|
91
|
+
|
|
92
|
+
const tween = randomChoice(tweenOptions);
|
|
93
|
+
|
|
94
|
+
let offsetBoundaryX = randomChoice(
|
|
95
|
+
weightedChoice(
|
|
96
|
+
[[20, 45], [45, 75], [75, 100]],
|
|
97
|
+
[0.2, 0.65, 0.15]
|
|
98
|
+
).map(v => randInt(v, v))
|
|
99
|
+
) || randInt(45, 75);
|
|
100
|
+
|
|
101
|
+
let offsetBoundaryY = randomChoice(
|
|
102
|
+
weightedChoice(
|
|
103
|
+
[[20, 45], [45, 75], [75, 100]],
|
|
104
|
+
[0.2, 0.65, 0.15]
|
|
105
|
+
).map(v => randInt(v, v))
|
|
106
|
+
) || randInt(45, 75);
|
|
107
|
+
|
|
108
|
+
let knotsCount = weightedChoice(
|
|
109
|
+
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
|
|
110
|
+
[0.15, 0.36, 0.17, 0.12, 0.08, 0.04, 0.03, 0.02, 0.015, 0.005]
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const distortionMean = randInt(80, 110) / 100;
|
|
114
|
+
const distortionStdDev = randInt(85, 110) / 100;
|
|
115
|
+
const distortionFrequency = randInt(25, 70) / 100;
|
|
116
|
+
|
|
117
|
+
const targetPoints = randomChoice(
|
|
118
|
+
weightedChoice(
|
|
119
|
+
[[35, 45], [45, 60], [60, 80]],
|
|
120
|
+
[0.53, 0.32, 0.15]
|
|
121
|
+
).map(range => randInt(range[0], range[1]))
|
|
122
|
+
) || randInt(45, 60);
|
|
123
|
+
|
|
124
|
+
// Reduce curve complexity if near viewport edges
|
|
125
|
+
const isOriginNearEdge =
|
|
126
|
+
preOrigin[0] < minWidth || preOrigin[0] > maxWidth ||
|
|
127
|
+
preOrigin[1] < minHeight || preOrigin[1] > maxHeight;
|
|
128
|
+
|
|
129
|
+
const isDestNearEdge =
|
|
130
|
+
postDestination[0] < minWidth || postDestination[0] > maxWidth ||
|
|
131
|
+
postDestination[1] < minHeight || postDestination[1] > maxHeight;
|
|
132
|
+
|
|
133
|
+
if (isOriginNearEdge || isDestNearEdge) {
|
|
134
|
+
offsetBoundaryX = 1;
|
|
135
|
+
offsetBoundaryY = 1;
|
|
136
|
+
knotsCount = 1;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
offsetBoundaryX,
|
|
141
|
+
offsetBoundaryY,
|
|
142
|
+
knotsCount,
|
|
143
|
+
distortionMean,
|
|
144
|
+
distortionStdDev,
|
|
145
|
+
distortionFrequency,
|
|
146
|
+
tween,
|
|
147
|
+
targetPoints
|
|
148
|
+
};
|
|
149
|
+
}
|