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:
+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