Remove stepCount/stepSize; patterns own their geometry
The stepCount and stepSize knobs were two controls for one quantity users actually care about (reach), and the number of steps is an implementation detail nobody meaningfully tunes. Each pattern has a natural size and resolution — a jitter is inherently small, an arc a broad curve — so those now live as constants in each strategy rather than as global config. - strategies.ts: each pattern defines its own step count and size; MoveContext drops `config` down to pure geometry (start/width/height/rng), and the module no longer imports Config at all (dissolving the type-only-import cycle workaround). line stays byte-for-byte: 250 one-pixel steps. - executor.ts: executePath takes `config` for pacing (stepDelay); the path itself needs nothing from it. - config.ts / cli.ts / move.ts / config.default.json: drop stepCount and stepSize from the type, seed, validation, resolver, CLI flags (-n, -s), and help. stepDelay stays as the one pacing lever. - configFile.ts: tolerate the removed keys instead of rejecting them — every pre-1.3.0 install seeded stepCount, so a hard "unknown key" failure on upgrade is avoided. They're ignored with a one-line stderr notice; genuine unknown keys still error. The -n/--step-count CLI flag (shipped since 1.0.0) is now an unknown option; config files degrade gracefully, command lines don't. Stays in the unpushed 1.3.0 release. 64 tests pass.
This commit is contained in:
+2
-11
@@ -16,8 +16,6 @@
|
||||
* -m, --move-interval Idle time (seconds) before a sweep fires.
|
||||
* -c, --check-interval Cursor poll cadence (seconds).
|
||||
* -d, --step-delay Pause between synthetic steps (ms).
|
||||
* -n, --step-count Steps per sweep (count).
|
||||
* -s, --step-size Pixels moved per step.
|
||||
* -p, --pattern Movement strategy name (see strategies.ts).
|
||||
* -V, --verbose Enable per-sweep / interrupt / bounds logging.
|
||||
* (`-V` capital because `-v` is `--version`.)
|
||||
@@ -48,8 +46,6 @@ export interface ParsedCliArgs {
|
||||
moveInterval: number | undefined; // seconds
|
||||
checkInterval: number | undefined; // seconds
|
||||
stepDelay: number | undefined; // milliseconds
|
||||
stepCount: number | undefined; // count
|
||||
stepSize: number | undefined; // pixels
|
||||
/** Movement strategy name, validated against the registry. */
|
||||
pattern: string | undefined;
|
||||
/**
|
||||
@@ -107,8 +103,6 @@ export function parseCliArgs(): ParsedCliArgs {
|
||||
"move-interval": { type: "string", short: "m" },
|
||||
"check-interval": { type: "string", short: "c" },
|
||||
"step-delay": { type: "string", short: "d" },
|
||||
"step-count": { type: "string", short: "n" },
|
||||
"step-size": { type: "string", short: "s" },
|
||||
pattern: { type: "string", short: "p" },
|
||||
verbose: { type: "boolean", short: "V" },
|
||||
},
|
||||
@@ -131,8 +125,6 @@ export function parseCliArgs(): ParsedCliArgs {
|
||||
moveInterval: parsePositiveNumber("move-interval", values["move-interval"] as string | undefined),
|
||||
checkInterval: parsePositiveNumber("check-interval", values["check-interval"] as string | undefined),
|
||||
stepDelay: parsePositiveNumber("step-delay", values["step-delay"] as string | undefined),
|
||||
stepCount: parsePositiveNumber("step-count", values["step-count"] as string | undefined),
|
||||
stepSize: parsePositiveNumber("step-size", values["step-size"] as string | undefined),
|
||||
pattern: parsePatternName(values.pattern as string | undefined),
|
||||
verbose: values.verbose === true ? true : undefined,
|
||||
};
|
||||
@@ -176,10 +168,9 @@ Options:
|
||||
-m, --move-interval <seconds> Idle time before a sweep fires. Default: ${moveDefaultSec}.
|
||||
-c, --check-interval <seconds> Cursor poll cadence. Default: ${checkDefaultSec}.
|
||||
-d, --step-delay <ms> Pause between synthetic steps. Default: ${DEFAULT_CONFIG.stepDelay}.
|
||||
-n, --step-count <count> Steps per sweep. Default: ${DEFAULT_CONFIG.stepCount}.
|
||||
-s, --step-size <pixels> Pixels moved per step. Default: ${DEFAULT_CONFIG.stepSize}.
|
||||
-p, --pattern <name> Movement strategy. Default: ${DEFAULT_CONFIG.pattern}.
|
||||
One of: ${PATTERN_NAMES.join(", ")}.
|
||||
Each pattern defines its own size and speed.
|
||||
-V, --verbose Log every sweep, interrupt, and bounds event
|
||||
(default prints only the startup banner).
|
||||
|
||||
@@ -189,7 +180,7 @@ Examples:
|
||||
move
|
||||
move --move-interval 180 --check-interval 5
|
||||
move -m 300 -V
|
||||
move --pattern arc --step-size 3
|
||||
move --pattern arc
|
||||
move --config ~/myprofile.json
|
||||
`);
|
||||
}
|
||||
|
||||
+5
-20
@@ -27,8 +27,8 @@ import { isPatternName, type PatternName } from "./strategies.ts";
|
||||
// Single source of truth for default values. The same file ships in the
|
||||
// install tree and is copied to $XDG_CONFIG_HOME/move/config.json on a
|
||||
// fresh install (only if no config exists there yet). Values use the CLI
|
||||
// units (seconds for time fields, ms for stepDelay, pixels for stepCount);
|
||||
// the seconds->ms conversion happens below where DEFAULT_CONFIG is built.
|
||||
// units (seconds for time fields, ms for stepDelay); the seconds->ms
|
||||
// conversion happens below where DEFAULT_CONFIG is built.
|
||||
import seedRaw from "../scripts/config.default.json" with { type: "json" };
|
||||
|
||||
/**
|
||||
@@ -42,12 +42,9 @@ import seedRaw from "../scripts/config.default.json" with { type: "json" };
|
||||
* - `stepDelay` — pause between individual synthetic mouse steps inside
|
||||
* a sweep. Also the window in which the user can
|
||||
* "interrupt" by moving the cursor. Milliseconds.
|
||||
* - `stepCount` — number of steps in a single sweep. Count.
|
||||
* - `stepSize` — pixels moved per step. Decouples "how many steps"
|
||||
* from "how far each step travels" so non-linear
|
||||
* patterns can span meaningful distances. Pixels.
|
||||
* - `pattern` — name of the movement strategy to use (see
|
||||
* `strategies.ts`; e.g. `line`, `walk`, `arc`).
|
||||
* `strategies.ts`; e.g. `line`, `walk`, `arc`). Each
|
||||
* pattern owns its own size and step count.
|
||||
* - `verbose` — whether per-sweep / interrupt / bounds events are
|
||||
* logged. The startup banner is always printed.
|
||||
*/
|
||||
@@ -55,8 +52,6 @@ export interface Config {
|
||||
readonly moveInterval: number;
|
||||
readonly checkInterval: number;
|
||||
readonly stepDelay: number;
|
||||
readonly stepCount: number;
|
||||
readonly stepSize: number;
|
||||
readonly pattern: PatternName;
|
||||
readonly verbose: boolean;
|
||||
}
|
||||
@@ -71,8 +66,6 @@ interface SeedShape {
|
||||
moveInterval: number; // seconds
|
||||
checkInterval: number; // seconds
|
||||
stepDelay: number; // milliseconds
|
||||
stepCount: number; // count
|
||||
stepSize: number; // pixels
|
||||
pattern: string; // strategy name
|
||||
verbose: boolean;
|
||||
}
|
||||
@@ -82,7 +75,7 @@ function assertSeedShape(raw: unknown): asserts raw is SeedShape {
|
||||
throw new Error("scripts/config.default.json: root must be an object");
|
||||
}
|
||||
const r = raw as Record<string, unknown>;
|
||||
for (const key of ["moveInterval", "checkInterval", "stepDelay", "stepCount", "stepSize"] as const) {
|
||||
for (const key of ["moveInterval", "checkInterval", "stepDelay"] as const) {
|
||||
const v = r[key];
|
||||
if (typeof v !== "number" || !Number.isFinite(v) || v <= 0) {
|
||||
throw new Error(`scripts/config.default.json: '${key}' must be a positive finite number (got ${JSON.stringify(v)})`);
|
||||
@@ -110,8 +103,6 @@ export const DEFAULT_CONFIG: Config = {
|
||||
moveInterval: seed.moveInterval * 1000,
|
||||
checkInterval: seed.checkInterval * 1000,
|
||||
stepDelay: seed.stepDelay,
|
||||
stepCount: seed.stepCount,
|
||||
stepSize: seed.stepSize,
|
||||
pattern: seed.pattern,
|
||||
verbose: seed.verbose,
|
||||
};
|
||||
@@ -125,8 +116,6 @@ export const DEFAULT_CONFIG: Config = {
|
||||
* Numeric fields are in CLI / config-file units:
|
||||
* moveInterval, checkInterval — seconds
|
||||
* stepDelay — milliseconds
|
||||
* stepCount — count
|
||||
* stepSize — pixels
|
||||
*
|
||||
* `pattern` is a strategy name (`string | undefined`) and `verbose` is
|
||||
* `boolean | undefined`, so every field shares the same "first defined
|
||||
@@ -142,8 +131,6 @@ export interface ConfigOverrides {
|
||||
readonly moveInterval: number | undefined;
|
||||
readonly checkInterval: number | undefined;
|
||||
readonly stepDelay: number | undefined;
|
||||
readonly stepCount: number | undefined;
|
||||
readonly stepSize: number | undefined;
|
||||
readonly pattern: string | undefined;
|
||||
readonly verbose: boolean | undefined;
|
||||
}
|
||||
@@ -210,8 +197,6 @@ export function resolveConfig(file: ConfigOverrides | null, cli: ConfigOverrides
|
||||
moveInterval: pickSeconds(cli.moveInterval, file?.moveInterval, DEFAULT_CONFIG.moveInterval),
|
||||
checkInterval: pickSeconds(cli.checkInterval, file?.checkInterval, DEFAULT_CONFIG.checkInterval),
|
||||
stepDelay: pickRaw(cli.stepDelay, file?.stepDelay, DEFAULT_CONFIG.stepDelay),
|
||||
stepCount: pickRaw(cli.stepCount, file?.stepCount, DEFAULT_CONFIG.stepCount),
|
||||
stepSize: pickRaw(cli.stepSize, file?.stepSize, DEFAULT_CONFIG.stepSize),
|
||||
pattern: pickRaw(cli.pattern, file?.pattern, DEFAULT_CONFIG.pattern),
|
||||
verbose: pickRaw(cli.verbose, file?.verbose, DEFAULT_CONFIG.verbose),
|
||||
};
|
||||
|
||||
+34
-18
@@ -10,14 +10,14 @@
|
||||
* moveInterval number seconds, positive
|
||||
* checkInterval number seconds, positive
|
||||
* stepDelay number milliseconds, positive
|
||||
* stepCount number count, positive
|
||||
* stepSize number pixels, positive
|
||||
* pattern string a registered strategy name
|
||||
* verbose boolean
|
||||
*
|
||||
* Unknown keys, wrong types, and non-positive numerics are rejected with a
|
||||
* `CliError` so the entry point can exit 2 (user error) with a clear
|
||||
* message pointing at the offending file.
|
||||
* message pointing at the offending file. The removed `stepCount` /
|
||||
* `stepSize` keys are the exception: they're tolerated (ignored with a
|
||||
* one-line notice) so an older seeded config keeps working after upgrade.
|
||||
*
|
||||
* Return semantics:
|
||||
* - `null` when no `explicitPath` was passed and the default path does
|
||||
@@ -37,12 +37,23 @@ const ALLOWED_KEYS: ReadonlySet<string> = new Set<string>([
|
||||
"moveInterval",
|
||||
"checkInterval",
|
||||
"stepDelay",
|
||||
"stepCount",
|
||||
"stepSize",
|
||||
"pattern",
|
||||
"verbose",
|
||||
]);
|
||||
|
||||
/**
|
||||
* Keys that used to be valid but have since been removed. They're tolerated
|
||||
* (not rejected like a genuine unknown key) so upgrading doesn't hard-fail a
|
||||
* config that was seeded with them — every pre-1.3.0 install has `stepCount`
|
||||
* in its file. They no longer do anything: sweep size and step count are now
|
||||
* properties of each movement pattern. A one-line notice points the user at
|
||||
* the file so they can remove them at leisure.
|
||||
*/
|
||||
const DEPRECATED_KEYS: ReadonlySet<string> = new Set<string>([
|
||||
"stepCount",
|
||||
"stepSize",
|
||||
]);
|
||||
|
||||
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
@@ -119,13 +130,26 @@ export function loadConfigFile(explicitPath: string | undefined): ConfigOverride
|
||||
throw new CliError(`config file ${path} must contain a JSON object at the root`);
|
||||
}
|
||||
|
||||
// Strict mode: reject any key we don't know about. Catches typos like
|
||||
// 'movInterval' that would otherwise sail through silently.
|
||||
// Strict mode: reject any key we don't know about (catches typos like
|
||||
// 'movInterval'), but tolerate keys we've since removed — collect those
|
||||
// and warn once, rather than hard-failing a config seeded by an older
|
||||
// install.
|
||||
const deprecatedFound: string[] = [];
|
||||
for (const key of Object.keys(parsed)) {
|
||||
if (!ALLOWED_KEYS.has(key)) {
|
||||
const allowed: string = [...ALLOWED_KEYS].join(", ");
|
||||
throw new CliError(`unknown key '${key}' in ${path} (allowed: ${allowed})`);
|
||||
if (ALLOWED_KEYS.has(key)) continue;
|
||||
if (DEPRECATED_KEYS.has(key)) {
|
||||
deprecatedFound.push(key);
|
||||
continue;
|
||||
}
|
||||
const allowed: string = [...ALLOWED_KEYS].join(", ");
|
||||
throw new CliError(`unknown key '${key}' in ${path} (allowed: ${allowed})`);
|
||||
}
|
||||
if (deprecatedFound.length > 0) {
|
||||
const names: string = deprecatedFound.map((k) => `'${k}'`).join(", ");
|
||||
process.stderr.write(
|
||||
`move: ignoring obsolete key(s) ${names} in ${path}\n` +
|
||||
` (sweep size is now defined by each movement pattern)\n`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -141,14 +165,6 @@ export function loadConfigFile(explicitPath: string | undefined): ConfigOverride
|
||||
"stepDelay" in parsed
|
||||
? requirePositiveNumber("stepDelay", parsed.stepDelay, path)
|
||||
: undefined,
|
||||
stepCount:
|
||||
"stepCount" in parsed
|
||||
? requirePositiveNumber("stepCount", parsed.stepCount, path)
|
||||
: undefined,
|
||||
stepSize:
|
||||
"stepSize" in parsed
|
||||
? requirePositiveNumber("stepSize", parsed.stepSize, path)
|
||||
: undefined,
|
||||
pattern:
|
||||
"pattern" in parsed
|
||||
? requirePatternName("pattern", parsed.pattern, path)
|
||||
|
||||
+8
-3
@@ -23,6 +23,7 @@
|
||||
* without every rounded step being misread as "the user moved the mouse".
|
||||
*/
|
||||
|
||||
import type { Config } from "./config.ts";
|
||||
import type { Device, Point } from "./device.ts";
|
||||
import type { BoundsPolicy, MoveContext, MovementStrategy } from "./strategies.ts";
|
||||
|
||||
@@ -138,21 +139,25 @@ function timestamp(): string {
|
||||
* Contract, per step:
|
||||
* 1. Resolve the ideal target to an on-screen integer (bounds policy).
|
||||
* An `abort`-policy out-of-bounds target ends the sweep (`aborted`).
|
||||
* 2. Command the cursor there and sleep `stepDelay` — also the user's
|
||||
* interrupt window.
|
||||
* 2. Command the cursor there and sleep `config.stepDelay` — also the
|
||||
* user's interrupt window.
|
||||
* 3. Re-read the cursor. If it isn't at the point we just commanded, the
|
||||
* user moved it: return `interrupted` without restoring.
|
||||
*
|
||||
* On a clean run the cursor is restored to `ctx.start` so the next
|
||||
* idle-check sees no net movement, and `completed` is returned.
|
||||
*
|
||||
* `config` supplies only the pacing (`stepDelay`); a strategy's geometry is
|
||||
* entirely self-contained, so the path itself needs nothing from it.
|
||||
*/
|
||||
export async function executePath(
|
||||
strategy: MovementStrategy,
|
||||
ctx: MoveContext,
|
||||
device: Device,
|
||||
log: Logger,
|
||||
config: Config,
|
||||
): Promise<SweepOutcome> {
|
||||
const { start, width, height, config } = ctx;
|
||||
const { start, width, height } = ctx;
|
||||
|
||||
log.event(`Simulating activity (${strategy.name}) at ${timestamp()}...`);
|
||||
|
||||
|
||||
+2
-2
@@ -61,9 +61,9 @@ async function simulateActivity(config: Config, log: Logger, device: Device): Pr
|
||||
const height: number = await device.height();
|
||||
|
||||
const strategy = STRATEGIES[config.pattern] ?? STRATEGIES[DEFAULT_PATTERN]!;
|
||||
const ctx: MoveContext = { start, width, height, config, rng: Math.random };
|
||||
const ctx: MoveContext = { start, width, height, rng: Math.random };
|
||||
|
||||
await executePath(strategy, ctx, device, log);
|
||||
await executePath(strategy, ctx, device, log, config);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -115,8 +115,6 @@ const cliOverrides: ConfigOverrides = {
|
||||
moveInterval: cliArgs.moveInterval,
|
||||
checkInterval: cliArgs.checkInterval,
|
||||
stepDelay: cliArgs.stepDelay,
|
||||
stepCount: cliArgs.stepCount,
|
||||
stepSize: cliArgs.stepSize,
|
||||
pattern: cliArgs.pattern,
|
||||
verbose: cliArgs.verbose,
|
||||
};
|
||||
|
||||
+71
-68
@@ -14,13 +14,16 @@
|
||||
* pixels before commanding the cursor and applies the strategy's declared
|
||||
* `BoundsPolicy` to keep everything on-screen.
|
||||
*
|
||||
* `Config` is imported type-only so that `config.ts` can import the value
|
||||
* exports here (the registry, name list, and validator) without creating a
|
||||
* runtime import cycle.
|
||||
* Each pattern owns its own geometry — how many steps it takes, how far it
|
||||
* reaches, how tight its radius is — as module-private constants below. Those
|
||||
* are properties of the pattern, not user preferences: a jitter is inherently
|
||||
* small and twitchy, an arc inherently a broad curve. There is deliberately
|
||||
* no user knob for sweep size or step count; the cadence (`stepDelay`) is the
|
||||
* only tunable, and it lives in the executor, not here. As a result this
|
||||
* module needs nothing from `Config` and imports only `Point`.
|
||||
*/
|
||||
|
||||
import type { Point } from "./device.ts";
|
||||
import type { Config } from "./config.ts";
|
||||
|
||||
/**
|
||||
* How the executor keeps a strategy's targets on-screen:
|
||||
@@ -47,8 +50,6 @@ export interface MoveContext {
|
||||
readonly width: number;
|
||||
/** Primary-screen height in pixels. */
|
||||
readonly height: number;
|
||||
/** Resolved runtime config (supplies `stepCount`, `stepSize`, ...). */
|
||||
readonly config: Config;
|
||||
/** Uniform [0, 1) source. Defaults to `Math.random`; tests inject a fake. */
|
||||
readonly rng: () => number;
|
||||
}
|
||||
@@ -75,33 +76,25 @@ function clamp(v: number, max: number): number {
|
||||
return v;
|
||||
}
|
||||
|
||||
/**
|
||||
* Total pixel reach of a sweep: number of steps times pixels per step.
|
||||
* Strategies use this to size themselves relative to the configured sweep
|
||||
* length regardless of `stepSize`.
|
||||
*/
|
||||
function reachOf(config: Config): number {
|
||||
return config.stepCount * config.stepSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* `line` — the original behavior, preserved exactly.
|
||||
*
|
||||
* Pick a horizontal direction that keeps the sweep on-screen (right if
|
||||
* there's room, else left); walk `stepCount` steps of `stepSize` pixels
|
||||
* with no vertical movement. With the default `stepSize` of 1 this emits
|
||||
* the identical integer 1px-per-step path the keeper used before the
|
||||
* strategy refactor, which is why its bounds policy is `abort` (the
|
||||
* direction choice guarantees it never triggers).
|
||||
* there's room, else left) and walk `LINE_STEPS` single-pixel steps with no
|
||||
* vertical movement. 250 one-pixel steps is byte-for-byte the sweep the
|
||||
* keeper produced before movement patterns existed, which is why its bounds
|
||||
* policy is `abort` (the direction choice guarantees it never triggers).
|
||||
*/
|
||||
const LINE_STEPS = 250;
|
||||
|
||||
export const line: MovementStrategy = {
|
||||
name: "line",
|
||||
bounds: "abort",
|
||||
*path(ctx: MoveContext): Generator<Point> {
|
||||
const { start, width, config } = ctx;
|
||||
const dx: number = start.x + reachOf(config) < width ? 1 : -1;
|
||||
for (let i = 1; i <= config.stepCount; i++) {
|
||||
yield { x: start.x + i * dx * config.stepSize, y: start.y };
|
||||
const { start, width } = ctx;
|
||||
const dx: number = start.x + LINE_STEPS < width ? 1 : -1;
|
||||
for (let i = 1; i <= LINE_STEPS; i++) {
|
||||
yield { x: start.x + i * dx, y: start.y };
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -109,42 +102,42 @@ export const line: MovementStrategy = {
|
||||
/**
|
||||
* `diagonal` — straight line on both axes at once. Each axis's direction is
|
||||
* chosen independently by available room, so the sweep heads toward the
|
||||
* roomiest corner and stays on-screen.
|
||||
* roomiest corner and stays on-screen. 250 single-pixel steps per axis
|
||||
* (≈250px reach), matching `line`'s magnitude.
|
||||
*/
|
||||
const DIAGONAL_STEPS = 250;
|
||||
|
||||
export const diagonal: MovementStrategy = {
|
||||
name: "diagonal",
|
||||
bounds: "clamp",
|
||||
*path(ctx: MoveContext): Generator<Point> {
|
||||
const { start, width, height, config } = ctx;
|
||||
const reach: number = reachOf(config);
|
||||
const dx: number = start.x + reach < width ? 1 : -1;
|
||||
const dy: number = start.y + reach < height ? 1 : -1;
|
||||
for (let i = 1; i <= config.stepCount; i++) {
|
||||
yield {
|
||||
x: start.x + i * dx * config.stepSize,
|
||||
y: start.y + i * dy * config.stepSize,
|
||||
};
|
||||
const { start, width, height } = ctx;
|
||||
const dx: number = start.x + DIAGONAL_STEPS < width ? 1 : -1;
|
||||
const dy: number = start.y + DIAGONAL_STEPS < height ? 1 : -1;
|
||||
for (let i = 1; i <= DIAGONAL_STEPS; i++) {
|
||||
yield { x: start.x + i * dx, y: start.y + i * dy };
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* `jitter` — many small random hops within a local radius of the start.
|
||||
* Subtle "fidget" activity rather than a broad sweep. The radius scales off
|
||||
* the sweep length (like the other patterns) so every hop is a real,
|
||||
* distinct pixel move rather than rounding onto the pixel the cursor is
|
||||
* already on. The executor restores the cursor to `start` after a clean
|
||||
* run, so the net displacement is zero.
|
||||
* `jitter` — many small random hops within a tight radius of the start.
|
||||
* Subtle "fidget" activity rather than a broad sweep. The radius is large
|
||||
* enough that every hop is a real, distinct pixel move rather than rounding
|
||||
* onto the pixel the cursor already occupies. The executor restores the
|
||||
* cursor to `start` after a clean run, so the net displacement is zero.
|
||||
*/
|
||||
const JITTER_STEPS = 80;
|
||||
const JITTER_RADIUS = 30;
|
||||
|
||||
export const jitter: MovementStrategy = {
|
||||
name: "jitter",
|
||||
bounds: "clamp",
|
||||
*path(ctx: MoveContext): Generator<Point> {
|
||||
const { start, config, rng } = ctx;
|
||||
const radius: number = Math.max(4, reachOf(config) / 8);
|
||||
for (let i = 1; i <= config.stepCount; i++) {
|
||||
const { start, rng } = ctx;
|
||||
for (let i = 1; i <= JITTER_STEPS; i++) {
|
||||
const angle: number = rng() * 2 * Math.PI;
|
||||
const r: number = rng() * radius;
|
||||
const r: number = rng() * JITTER_RADIUS;
|
||||
yield { x: start.x + Math.cos(angle) * r, y: start.y + Math.sin(angle) * r };
|
||||
}
|
||||
},
|
||||
@@ -152,20 +145,25 @@ export const jitter: MovementStrategy = {
|
||||
|
||||
/**
|
||||
* `walk` — an unbounded cumulative random walk: each step adds a random
|
||||
* per-axis delta in `[-stepSize, +stepSize]`. The generator itself lets the
|
||||
* per-axis delta in `[-WALK_STEP, +WALK_STEP]`. The per-step magnitude is
|
||||
* deliberately several pixels so the walk actually roams — a ±1px walk over
|
||||
* this many steps would drift only ~√N pixels net. The generator lets the
|
||||
* position drift freely; the executor's `reflect` policy mirrors it back
|
||||
* on-screen, so the cursor bounces off the edges instead of escaping.
|
||||
*/
|
||||
const WALK_STEPS = 200;
|
||||
const WALK_STEP = 4;
|
||||
|
||||
export const walk: MovementStrategy = {
|
||||
name: "walk",
|
||||
bounds: "reflect",
|
||||
*path(ctx: MoveContext): Generator<Point> {
|
||||
const { start, config, rng } = ctx;
|
||||
const { start, rng } = ctx;
|
||||
let x: number = start.x;
|
||||
let y: number = start.y;
|
||||
for (let i = 1; i <= config.stepCount; i++) {
|
||||
x += (rng() * 2 - 1) * config.stepSize;
|
||||
y += (rng() * 2 - 1) * config.stepSize;
|
||||
for (let i = 1; i <= WALK_STEPS; i++) {
|
||||
x += (rng() * 2 - 1) * WALK_STEP;
|
||||
y += (rng() * 2 - 1) * WALK_STEP;
|
||||
yield { x, y };
|
||||
}
|
||||
},
|
||||
@@ -173,21 +171,23 @@ export const walk: MovementStrategy = {
|
||||
|
||||
/**
|
||||
* `arc` — a smooth quadratic Bézier curve from the start to a random
|
||||
* on-screen endpoint roughly `reach` pixels away, bowed out by a control
|
||||
* point offset perpendicular to the straight path. Produces natural,
|
||||
* hand-like curved motion.
|
||||
* on-screen endpoint `ARC_REACH` pixels away, bowed out by a control point
|
||||
* offset perpendicular to the straight path. `ARC_STEPS` samples keep the
|
||||
* curve smooth. Produces natural, hand-like curved motion.
|
||||
*/
|
||||
const ARC_STEPS = 120;
|
||||
const ARC_REACH = 300;
|
||||
|
||||
export const arc: MovementStrategy = {
|
||||
name: "arc",
|
||||
bounds: "clamp",
|
||||
*path(ctx: MoveContext): Generator<Point> {
|
||||
const { start, width, height, config, rng } = ctx;
|
||||
const reach: number = reachOf(config);
|
||||
const { start, width, height, rng } = ctx;
|
||||
|
||||
// Endpoint: a random direction, `reach` away, clamped on-screen.
|
||||
// Endpoint: a random direction, `ARC_REACH` away, clamped on-screen.
|
||||
const angle: number = rng() * 2 * Math.PI;
|
||||
const endX: number = clamp(start.x + Math.cos(angle) * reach, width);
|
||||
const endY: number = clamp(start.y + Math.sin(angle) * reach, height);
|
||||
const endX: number = clamp(start.x + Math.cos(angle) * ARC_REACH, width);
|
||||
const endY: number = clamp(start.y + Math.sin(angle) * ARC_REACH, height);
|
||||
|
||||
// Control point: midpoint pushed along the perpendicular so the path
|
||||
// bows rather than running straight. Direction/magnitude randomized.
|
||||
@@ -196,12 +196,12 @@ export const arc: MovementStrategy = {
|
||||
const perpX: number = -(endY - start.y);
|
||||
const perpY: number = endX - start.x;
|
||||
const perpLen: number = Math.hypot(perpX, perpY) || 1;
|
||||
const bow: number = (rng() * 2 - 1) * reach * 0.5;
|
||||
const bow: number = (rng() * 2 - 1) * ARC_REACH * 0.5;
|
||||
const ctrlX: number = clamp(midX + (perpX / perpLen) * bow, width);
|
||||
const ctrlY: number = clamp(midY + (perpY / perpLen) * bow, height);
|
||||
|
||||
for (let i = 1; i <= config.stepCount; i++) {
|
||||
const t: number = i / config.stepCount;
|
||||
for (let i = 1; i <= ARC_STEPS; i++) {
|
||||
const t: number = i / ARC_STEPS;
|
||||
const u: number = 1 - t;
|
||||
yield {
|
||||
x: u * u * start.x + 2 * u * t * ctrlX + t * t * endX,
|
||||
@@ -213,20 +213,23 @@ export const arc: MovementStrategy = {
|
||||
|
||||
/**
|
||||
* `figureEight` — traces a Gerono lemniscate (a figure-eight) around the
|
||||
* start point over one full period, so it returns to the origin. Amplitude
|
||||
* scales with `reach`.
|
||||
* start point over one full period, so it returns to the origin.
|
||||
* `FIG8_AMP` sets its half-width (≈250px across); `FIG8_STEPS` samples keep
|
||||
* the curve smooth.
|
||||
*/
|
||||
const FIG8_STEPS = 90;
|
||||
const FIG8_AMP = 125;
|
||||
|
||||
export const figureEight: MovementStrategy = {
|
||||
name: "figureEight",
|
||||
bounds: "clamp",
|
||||
*path(ctx: MoveContext): Generator<Point> {
|
||||
const { start, config } = ctx;
|
||||
const amp: number = reachOf(config) / 2;
|
||||
for (let i = 1; i <= config.stepCount; i++) {
|
||||
const t: number = (2 * Math.PI * i) / config.stepCount;
|
||||
const { start } = ctx;
|
||||
for (let i = 1; i <= FIG8_STEPS; i++) {
|
||||
const t: number = (2 * Math.PI * i) / FIG8_STEPS;
|
||||
yield {
|
||||
x: start.x + amp * Math.sin(t),
|
||||
y: start.y + amp * Math.sin(t) * Math.cos(t),
|
||||
x: start.x + FIG8_AMP * Math.sin(t),
|
||||
y: start.y + FIG8_AMP * Math.sin(t) * Math.cos(t),
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user