Path: blob/main/plugins/default-human-emulator/generateVector.ts
1031 views
import IPoint from '@secret-agent/interfaces/IPoint';1import Bezier from './Bezier';23export default function generateVector(4startPoint: IPoint,5destinationPoint: IPoint,6targetWidth: number,7overshoot: { threshold: number; radius: number; spread: number },8) {9const shouldOvershoot = magnitude(direction(startPoint, destinationPoint)) > overshoot.threshold;1011const firstTargetPoint = shouldOvershoot12? getOvershootPoint(destinationPoint, overshoot.radius)13: destinationPoint;14const points = path(startPoint, firstTargetPoint);1516if (shouldOvershoot) {17const correction = path(firstTargetPoint, destinationPoint, targetWidth, overshoot.spread);18points.push(...correction);19}20return points.map(point => {21return { x: Math.round(point.x * 10) / 10, y: Math.round(point.y * 10) / 10 };22});23}2425function path(start: IPoint, finish: IPoint, targetWidth = 100, spreadOverride?: number): IPoint[] {26const minSteps = 25;2728if (!targetWidth || Number.isNaN(targetWidth)) targetWidth = 1;2930let spread = spreadOverride;31if (!spread) {32const vec = direction(start, finish);33spread = Math.min(magnitude(vec), 200);34}35const anchors = generateBezierAnchors(start, finish, spread);3637const curve = new Bezier(start, ...anchors, finish);38const length = curve.length() * 0.8;39const baseTime = Math.random() * minSteps;40let steps = Math.ceil((Math.log2(fitts(length, targetWidth) + 1) + baseTime) * 3);41if (Number.isNaN(steps)) steps = 25;4243return curve44.getLookupTable(steps)45.map(vector => ({46x: vector.x,47y: vector.y,48}))49.filter(({ x, y }) => !Number.isNaN(x) && !Number.isNaN(y));50}5152const sub = (a: IPoint, b: IPoint): IPoint => ({ x: a.x - b.x, y: a.y - b.y });53const div = (a: IPoint, b: number): IPoint => ({ x: a.x / b, y: a.y / b });54const mult = (a: IPoint, b: number): IPoint => ({ x: a.x * b, y: a.y * b });55const add = (a: IPoint, b: IPoint): IPoint => ({ x: a.x + b.x, y: a.y + b.y });5657function randomVectorOnLine(a: IPoint, b: IPoint) {58const vec = direction(a, b);59const multiplier = Math.random();60return add(a, mult(vec, multiplier));61}6263function randomNormalLine(a: IPoint, b: IPoint, range: number): IPoint[] {64const randMid = randomVectorOnLine(a, b);65const normalV = setMagnitude(perpendicular(direction(a, randMid)), range);66return [randMid, normalV];67}6869function generateBezierAnchors(a: IPoint, b: IPoint, spread: number): IPoint[] {70const side = Math.round(Math.random()) === 1 ? 1 : -1;71const calc = (): IPoint => {72const [randMid, normalV] = randomNormalLine(a, b, spread);73const choice = mult(normalV, side);74return randomVectorOnLine(randMid, add(randMid, choice));75};76return [calc(), calc()].sort((sortA, sortB) => sortA.x - sortB.x);77}7879function getOvershootPoint(coordinate: IPoint, radius: number) {80const a = Math.random() * 2 * Math.PI;81const rad = radius * Math.sqrt(Math.random());82const vector = { x: rad * Math.cos(a), y: rad * Math.sin(a) };83return add(coordinate, vector);84}8586/**87* Calculate the amount of time needed to move from (x1, y1) to (x2, y2)88* given the width of the element being clicked on89* https://en.wikipedia.org/wiki/Fitts%27s_law90*/91function fitts(distance: number, width: number) {92return 2 * Math.log2(distance / width + 1);93}9495function direction(a: IPoint, b: IPoint) {96return sub(b, a);97}9899function perpendicular(a: IPoint) {100return { x: a.y, y: -1 * a.x };101}102103function magnitude(a: IPoint) {104return Math.sqrt(a.x ** 2 + a.y ** 2);105}106107function unit(a: IPoint) {108return div(a, magnitude(a));109}110111function setMagnitude(a: IPoint, amount: number) {112return mult(unit(a), amount);113}114115116