Collapse bounds policies to reflect-only; drop abort and clamp
The executor kept every commanded point on-screen via a per-strategy
BoundsPolicy of abort / clamp / reflect. Measured against the real
strategies, the other two earned nothing: abort truncated a sweep at the
first edge (line on a narrow screen ran only 90 of 250 steps), and clamp
could park the cursor against an edge (a monotonic ramp stalled 162 steps
in a row) -- both counter to the program's whole purpose of keeping the
cursor moving. reflect bounces off the edge and keeps going, and is
already what line/diagonal need in loop mode. arc's declared clamp was
provably dead code (it clamps its own endpoint, so no sample ever leaves
the screen).
Collapse to reflect-only:
- strategies.ts: remove the BoundsPolicy type and the `bounds` field from
the interface and all six strategies. Keep the local clamp() helper --
it's arc's endpoint geometry, not an on-screen policy; docstring says so.
- executor.ts: resolveTarget loses its policy parameter and its null
return and just reflects both axes; delete clampInt; SweepOutcome drops
"aborted"; ExecuteOptions drops `bounds`; remove the Out of bounds log.
- keeper.ts: loopOpts is now { restore: false, loop: true } -- the
reflect override added with loop mode is redundant.
- tests: drop the abort-outcome, clamp, and bounds-override tests; simplify
fixed() to take no policy; add a regression test that a monotonic ramp
past an edge never yields two identical points in a row (the guarantee
that motivated removing clamp).
Behavior is unchanged for every pattern at normal cursor positions
(verified: line's normal sweep is byte-identical). The only differences
are at a screen edge, where motion now bounces instead of stopping. No
config keys, flags, or pattern names changed.
Docs updated to match, including in-code comments, the README strategies
table (Bounds column removed) and verbose description, the sequence
diagram (resolveTarget signature + getPosition/width ordering + a loop-mode
note), and a CHANGELOG Changed entry.
This commit is contained in:
+2
-2
@@ -17,7 +17,7 @@
|
||||
* -c, --check-interval Cursor poll cadence (seconds).
|
||||
* -d, --step-delay Pause between synthetic steps (ms).
|
||||
* -p, --pattern Movement strategy name (see strategies.ts).
|
||||
* -V, --verbose Enable per-sweep / interrupt / bounds logging.
|
||||
* -V, --verbose Enable per-sweep / interrupt logging.
|
||||
* (`-V` capital because `-v` is `--version`.)
|
||||
* -l, --loop Loop mode: once triggered, keep moving
|
||||
* until the user moves the mouse (or Ctrl+C).
|
||||
@@ -180,7 +180,7 @@ Options:
|
||||
-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
|
||||
-V, --verbose Log every sweep and interrupt
|
||||
(default prints only the startup banner).
|
||||
-l, --loop Loop mode: once a sweep is triggered,
|
||||
keep moving until you move the mouse (or
|
||||
|
||||
+2
-2
@@ -45,8 +45,8 @@ import seedRaw from "../scripts/config.default.json" with { type: "json" };
|
||||
* - `pattern` — name of the movement strategy to use (see
|
||||
* `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.
|
||||
* - `verbose` — whether per-sweep / interrupt events are logged. The
|
||||
* startup banner is always printed.
|
||||
* - `loop` — loop mode: once a sweep is triggered, keep
|
||||
* repeating the movement until the user moves the mouse
|
||||
* (or Ctrl+C), rather than firing a single sweep. See
|
||||
|
||||
+31
-70
@@ -7,13 +7,13 @@
|
||||
* *everything else* about carrying a sweep out against a `Device`:
|
||||
*
|
||||
* - round each ideal target to whole pixels,
|
||||
* - keep it on-screen per the strategy's `BoundsPolicy`,
|
||||
* - keep it on-screen by reflecting coordinates that fall past an edge,
|
||||
* - command the cursor and pace it with `stepDelay`,
|
||||
* - detect real-user interruption after each step,
|
||||
* - restore the cursor to the origin on a clean run.
|
||||
*
|
||||
* Writing this once means new patterns inherit correct real-user-wins,
|
||||
* bounds, and restore semantics for free. It's pure with respect to I/O —
|
||||
* on-screen, and restore semantics for free. It's pure with respect to I/O —
|
||||
* all side effects go through the injected `Device`, so it's unit-testable
|
||||
* with a fake.
|
||||
*
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
import type { Config } from "./config.ts";
|
||||
import type { Device, Point } from "./device.ts";
|
||||
import type { BoundsPolicy, MoveContext, MovementStrategy } from "./strategies.ts";
|
||||
import type { MoveContext, MovementStrategy } from "./strategies.ts";
|
||||
|
||||
/**
|
||||
* Minimal log surface used by the executor and the keeper loop.
|
||||
@@ -41,11 +41,10 @@ export interface Logger {
|
||||
/**
|
||||
* How a sweep ended:
|
||||
* - `completed` — full path ran and the cursor was restored to start.
|
||||
* - `interrupted` — real user activity detected mid-sweep; aborted without
|
||||
* snapping back.
|
||||
* - `aborted` — an `abort`-policy target went out of bounds.
|
||||
* - `interrupted` — real user activity detected mid-sweep; the sweep stopped
|
||||
* without snapping back.
|
||||
*/
|
||||
export type SweepOutcome = "completed" | "interrupted" | "aborted";
|
||||
export type SweepOutcome = "completed" | "interrupted";
|
||||
|
||||
/**
|
||||
* Per-call knobs for `executePath`. All optional; the defaults reproduce the
|
||||
@@ -56,11 +55,6 @@ export type SweepOutcome = "completed" | "interrupted" | "aborted";
|
||||
* Default `true`. Loop (`--loop`) mode passes `false`:
|
||||
* chained cycles must not snap back between iterations, and an
|
||||
* infinite `loopPath` never reaches the restore anyway.
|
||||
* - `bounds` — override the strategy's declared `BoundsPolicy`. Loop mode
|
||||
* forces `"reflect"` for every pattern so edge-seeking paths
|
||||
* bounce off the screen instead of aborting (`line`) or
|
||||
* sticking in a corner (`clamp`). Absent, the strategy's own
|
||||
* `bounds` is used, so single-sweep behavior is unchanged.
|
||||
* - `loop` — prefer the strategy's infinite `loopPath` when it defines
|
||||
* one. Falls back to `path` when the strategy has no
|
||||
* `loopPath`, so a plain chained-repeat caller can pass this
|
||||
@@ -68,7 +62,6 @@ export type SweepOutcome = "completed" | "interrupted" | "aborted";
|
||||
*/
|
||||
export interface ExecuteOptions {
|
||||
readonly restore?: boolean;
|
||||
readonly bounds?: BoundsPolicy;
|
||||
readonly loop?: boolean;
|
||||
}
|
||||
|
||||
@@ -81,20 +74,17 @@ export interface ExecuteOptions {
|
||||
const READBACK_TOLERANCE: number = 2;
|
||||
|
||||
/**
|
||||
* Pixels to inset the `clamp` / `reflect` travel range from each screen edge.
|
||||
* Keeps edge-seeking patterns off the literal first/last pixel, where DPI
|
||||
* scaling and multi-monitor boundaries most often make the OS place the
|
||||
* cursor a hair off what we commanded (which the readback check would then
|
||||
* misread as the user). `abort` (used by `line`) is deliberately left on the
|
||||
* full `[0, max - 1]` range, so its behavior is unchanged.
|
||||
* Pixels to inset the travel range from each screen edge. Keeps edge-seeking
|
||||
* patterns off the literal first/last pixel, where DPI scaling and
|
||||
* multi-monitor boundaries most often make the OS place the cursor a hair off
|
||||
* what we commanded (which the readback check would then misread as the user).
|
||||
*/
|
||||
const EDGE_MARGIN: number = 2;
|
||||
|
||||
/**
|
||||
* The inclusive `[lo, hi]` integer range an axis of length `max` may travel
|
||||
* under the `clamp` / `reflect` policies: `[0, max - 1]` inset by
|
||||
* `EDGE_MARGIN` on each side. Screens too small to inset fall back to the
|
||||
* full range so the math never inverts.
|
||||
* The inclusive `[lo, hi]` integer range an axis of length `max` may travel:
|
||||
* `[0, max - 1]` inset by `EDGE_MARGIN` on each side. Screens too small to
|
||||
* inset fall back to the full range so the math never inverts.
|
||||
*/
|
||||
function travelRange(max: number): { lo: number; hi: number } {
|
||||
const hiEdge: number = max - 1;
|
||||
@@ -102,18 +92,11 @@ function travelRange(max: number): { lo: number; hi: number } {
|
||||
return { lo: EDGE_MARGIN, hi: hiEdge - EDGE_MARGIN };
|
||||
}
|
||||
|
||||
/** Round to whole pixels and clamp into the inset travel range for `max`. */
|
||||
function clampInt(v: number, max: number): number {
|
||||
const { lo, hi } = travelRange(max);
|
||||
const r: number = Math.round(v);
|
||||
if (r < lo) return lo;
|
||||
if (r > hi) return hi;
|
||||
return r;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mirror `v` into the inset travel range for `max` as a triangle wave, so
|
||||
* values past an edge bounce back inside instead of clamping flat against it.
|
||||
* values past an edge bounce back inside instead of running off it. This is
|
||||
* the sole on-screen policy: a coordinate that overshoots an edge reflects
|
||||
* back in, so a pattern keeps moving instead of parking against the boundary.
|
||||
*/
|
||||
function reflectInt(v: number, max: number): number {
|
||||
const { lo, hi } = travelRange(max);
|
||||
@@ -125,27 +108,11 @@ function reflectInt(v: number, max: number): number {
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a strategy's ideal target to an on-screen integer pixel under the
|
||||
* given policy. Returns `null` when policy is `abort` and the (rounded)
|
||||
* target lies outside the screen — the signal to stop the sweep.
|
||||
* Resolve a strategy's ideal (possibly fractional, possibly off-screen) target
|
||||
* to an on-screen integer pixel by reflecting each axis into its travel range.
|
||||
*/
|
||||
function resolveTarget(
|
||||
policy: BoundsPolicy,
|
||||
p: Point,
|
||||
width: number,
|
||||
height: number,
|
||||
): Point | null {
|
||||
if (policy === "reflect") {
|
||||
return { x: reflectInt(p.x, width), y: reflectInt(p.y, height) };
|
||||
}
|
||||
if (policy === "clamp") {
|
||||
return { x: clampInt(p.x, width), y: clampInt(p.y, height) };
|
||||
}
|
||||
// abort: round, then reject anything off-screen.
|
||||
const x: number = Math.round(p.x);
|
||||
const y: number = Math.round(p.y);
|
||||
if (x < 0 || x >= width || y < 0 || y >= height) return null;
|
||||
return { x, y };
|
||||
function resolveTarget(p: Point, width: number, height: number): Point {
|
||||
return { x: reflectInt(p.x, width), y: reflectInt(p.y, height) };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -162,8 +129,8 @@ function timestamp(): string {
|
||||
* against `device`.
|
||||
*
|
||||
* 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`).
|
||||
* 1. Resolve the ideal target to an on-screen integer by reflecting it
|
||||
* into the travel range.
|
||||
* 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
|
||||
@@ -174,9 +141,8 @@ function timestamp(): string {
|
||||
* `options.restore === false` (loop mode), in which case the cursor is
|
||||
* left where the last step put it.
|
||||
*
|
||||
* `options` (all optional, see `ExecuteOptions`) let loop mode reuse
|
||||
* this same driver: `bounds` overrides the strategy's policy (loop mode
|
||||
* forces `reflect`), `loop` selects the strategy's infinite `loopPath`, and
|
||||
* `options` (all optional, see `ExecuteOptions`) let loop mode reuse this
|
||||
* same driver: `loop` selects the strategy's infinite `loopPath`, and
|
||||
* `restore` suppresses the snap-back. Omitting `options` reproduces the
|
||||
* original single-sweep contract exactly.
|
||||
*
|
||||
@@ -192,18 +158,13 @@ export async function executePath(
|
||||
options?: ExecuteOptions,
|
||||
): Promise<SweepOutcome> {
|
||||
const { start, width, height } = ctx;
|
||||
const policy: BoundsPolicy = options?.bounds ?? strategy.bounds;
|
||||
const path: Iterable<Point> =
|
||||
options?.loop && strategy.loopPath ? strategy.loopPath(ctx) : strategy.path(ctx);
|
||||
|
||||
log.event(`Simulating activity (${strategy.name}) at ${timestamp()}...`);
|
||||
|
||||
for (const target of path) {
|
||||
const point: Point | null = resolveTarget(policy, target, width, height);
|
||||
if (point === null) {
|
||||
log.event(`Out of bounds at ${timestamp()}; aborting simulation.`);
|
||||
return "aborted";
|
||||
}
|
||||
const point: Point = resolveTarget(target, width, height);
|
||||
|
||||
await device.setPosition(point);
|
||||
await device.sleep(config.stepDelay);
|
||||
@@ -213,17 +174,17 @@ export async function executePath(
|
||||
Math.abs(current.x - point.x) > READBACK_TOLERANCE ||
|
||||
Math.abs(current.y - point.y) > READBACK_TOLERANCE
|
||||
) {
|
||||
// Cursor isn't where we last put it -> real user activity. Abort
|
||||
// Cursor isn't where we last put it -> real user activity. Stop
|
||||
// without snapping back, so we don't yank it from under the user.
|
||||
//
|
||||
// The comparison allows a small tolerance rather than demanding an
|
||||
// exact match: on scaled (fractional-DPI) or multi-monitor setups
|
||||
// the OS can place the cursor a pixel off the coordinate we
|
||||
// commanded, and the edge-seeking patterns (clamp/reflect/arc)
|
||||
// reach exactly the coordinates where that's most likely. A real
|
||||
// user moves far more than a couple of pixels, so this doesn't
|
||||
// meaningfully weaken real-user-wins.
|
||||
log.event(`User activity detected at ${timestamp()}; aborting simulation.`);
|
||||
// commanded, and edge-seeking patterns reach exactly the
|
||||
// coordinates where that's most likely. A real user moves far more
|
||||
// than a couple of pixels, so this doesn't meaningfully weaken
|
||||
// real-user-wins.
|
||||
log.event(`User activity detected at ${timestamp()}; stopping simulation.`);
|
||||
return "interrupted";
|
||||
}
|
||||
}
|
||||
|
||||
+14
-17
@@ -8,8 +8,8 @@
|
||||
* the interesting parts stay testable:
|
||||
* - `device.ts` — the nut.js I/O boundary (injected here).
|
||||
* - `strategies.ts` — pure "where to move" pattern generators.
|
||||
* - `executor.ts` — the "how to move" driver (bounds, timing,
|
||||
* interrupt detection, restore).
|
||||
* - `executor.ts` — the "how to move" driver (on-screen reflection,
|
||||
* timing, interrupt detection, restore).
|
||||
*
|
||||
* `runKeeper` takes an optional `Device` so tests can drive the loop with a
|
||||
* fake; production supplies the nut.js device. Importing this module is
|
||||
@@ -18,9 +18,9 @@
|
||||
* Logging policy:
|
||||
* - The startup banner in `runKeeper` is unconditional so the user always
|
||||
* sees the process is alive.
|
||||
* - Per-sweep / interrupt / bounds lines are gated by `config.verbose`
|
||||
* (see `makeLogger`). Errors stay on `console.error`, raised by the
|
||||
* entry point on unhandled rejection.
|
||||
* - Per-sweep / interrupt lines are gated by `config.verbose` (see
|
||||
* `makeLogger`). Errors stay on `console.error`, raised by the entry
|
||||
* point on unhandled rejection.
|
||||
*/
|
||||
|
||||
import { createNutDevice, type Device, type Point } from "./device.ts";
|
||||
@@ -55,19 +55,16 @@ function makeLogger(verbose: boolean): Logger {
|
||||
* at the CLI / config-file boundary should prevent that from ever happening.
|
||||
*
|
||||
* Single-sweep mode (`config.loop === false`) runs exactly one sweep via
|
||||
* `executePath`, which owns bounds, pacing, interrupt detection, and
|
||||
* restore-on-clean — unchanged from before loop mode existed.
|
||||
* `executePath`, which owns on-screen reflection, pacing, interrupt
|
||||
* detection, and restore-on-clean — unchanged from before loop mode existed.
|
||||
*
|
||||
* Loop mode (`config.loop === true`) keeps the cursor moving until the
|
||||
* user moves the mouse (or Ctrl+C). Two things change for every pattern:
|
||||
* the cursor is never restored between iterations (`restore: false`), and the
|
||||
* bounds policy is forced to `reflect` so edge-seeking paths bounce off the
|
||||
* screen instead of aborting (`line`) or sticking in a corner (`clamp`).
|
||||
* Patterns that define an infinite `loopPath` (`line`, `diagonal`) run it once
|
||||
* and are stopped only by interruption; the rest have their finite `path`
|
||||
* chained, re-read from the cursor's current position each cycle. Per-cycle
|
||||
* event logs are suppressed to avoid unbounded output — one line brackets the
|
||||
* run at each end.
|
||||
* user moves the mouse (or Ctrl+C). The cursor is never restored between
|
||||
* iterations (`restore: false`). Patterns that define an infinite `loopPath`
|
||||
* (`line`, `diagonal`) run it once and are stopped only by interruption; the
|
||||
* rest have their finite `path` chained, re-read from the cursor's current
|
||||
* position each cycle. Per-cycle event logs are suppressed to avoid unbounded
|
||||
* output — one line brackets the run at each end.
|
||||
*/
|
||||
async function simulateActivity(config: Config, log: Logger, device: Device): Promise<void> {
|
||||
const width: number = await device.width();
|
||||
@@ -83,7 +80,7 @@ async function simulateActivity(config: Config, log: Logger, device: Device): Pr
|
||||
|
||||
log.event(`Loop mode (${strategy.name}); repeating until you move the mouse.`);
|
||||
const cycleLog: Logger = { info: log.info, event: (): void => {} };
|
||||
const loopOpts = { restore: false, bounds: "reflect" as const, loop: true };
|
||||
const loopOpts = { restore: false, loop: true };
|
||||
|
||||
let cycles = 0;
|
||||
let outcome: SweepOutcome;
|
||||
|
||||
+30
-46
@@ -10,9 +10,10 @@
|
||||
* add (write one pure generator) and trivial to test (feed a deterministic
|
||||
* `rng`, assert the emitted points).
|
||||
*
|
||||
* Coordinates emitted here may be fractional; the executor rounds to whole
|
||||
* pixels before commanding the cursor and applies the strategy's declared
|
||||
* `BoundsPolicy` to keep everything on-screen.
|
||||
* Coordinates emitted here may be fractional and may fall past a screen
|
||||
* edge; the executor rounds to whole pixels and reflects any out-of-range
|
||||
* coordinate back inside, so a pattern bounces off the edges and keeps
|
||||
* moving. Strategies never need to bound their own output.
|
||||
*
|
||||
* 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
|
||||
@@ -25,19 +26,6 @@
|
||||
|
||||
import type { Point } from "./device.ts";
|
||||
|
||||
/**
|
||||
* How the executor keeps a strategy's targets on-screen:
|
||||
*
|
||||
* - `abort` — stop the sweep the moment a target falls out of bounds.
|
||||
* Used by `line`, whose direction is chosen so this never
|
||||
* actually fires; preserves the original straight-line
|
||||
* semantics exactly.
|
||||
* - `clamp` — pin each out-of-bounds coordinate to the nearest edge.
|
||||
* - `reflect` — mirror out-of-bounds coordinates back inside, so a roaming
|
||||
* pattern bounces off the screen edges instead of sticking.
|
||||
*/
|
||||
export type BoundsPolicy = "abort" | "clamp" | "reflect";
|
||||
|
||||
/**
|
||||
* Everything a strategy needs to generate a path. Screen dimensions and the
|
||||
* start point are snapshotted per sweep by the caller; `rng` is injected so
|
||||
@@ -59,7 +47,6 @@ export interface MoveContext {
|
||||
*
|
||||
* - `name` — registry key, also the value accepted by `--pattern` / the
|
||||
* `pattern` config key.
|
||||
* - `bounds` — how the executor confines this pattern to the screen.
|
||||
* - `path` — pure generator of ideal (possibly fractional) targets,
|
||||
* emitted in visiting order. Should not re-emit `start`.
|
||||
* - `loopPath` — optional infinite variant for loop mode (`--loop`).
|
||||
@@ -68,23 +55,26 @@ export interface MoveContext {
|
||||
* direction from the cursor's position every cycle, so chained
|
||||
* repetition oscillates in a band near an edge instead of
|
||||
* crossing the screen. An infinite generator picks its
|
||||
* direction once and ramps forever; the executor's `reflect`
|
||||
* policy (forced on in loop mode) folds the monotonic ramp
|
||||
* into an edge-to-edge bounce. Absent this, loop mode simply
|
||||
* chains `path` — correct for patterns whose finite path is a
|
||||
* self-contained cyclic unit (`jitter`, `walk`, `arc`,
|
||||
* `figureEight`). The executor stops either kind on real user
|
||||
* activity; an infinite `loopPath` therefore only ever ends
|
||||
* by interruption.
|
||||
* direction once and ramps forever; the executor reflects the
|
||||
* monotonic ramp into an edge-to-edge bounce. Absent this,
|
||||
* loop mode simply chains `path` — correct for patterns whose
|
||||
* finite path is a self-contained cyclic unit (`jitter`,
|
||||
* `walk`, `arc`, `figureEight`). The executor stops either
|
||||
* kind on real user activity; an infinite `loopPath` therefore
|
||||
* only ever ends by interruption.
|
||||
*/
|
||||
export interface MovementStrategy {
|
||||
readonly name: string;
|
||||
readonly bounds: BoundsPolicy;
|
||||
path(ctx: MoveContext): Iterable<Point>;
|
||||
loopPath?(ctx: MoveContext): Iterable<Point>;
|
||||
}
|
||||
|
||||
/** Clamp `v` into the inclusive pixel range `[0, max - 1]`. */
|
||||
/**
|
||||
* Clamp `v` into the inclusive pixel range `[0, max - 1]`. This is a geometry
|
||||
* helper for `arc` (choosing a well-formed on-screen endpoint and control
|
||||
* point), NOT an on-screen bounds policy — the executor keeps every commanded
|
||||
* point on-screen by reflecting, uniformly for all patterns.
|
||||
*/
|
||||
function clamp(v: number, max: number): number {
|
||||
if (v < 0) return 0;
|
||||
if (v > max - 1) return max - 1;
|
||||
@@ -97,20 +87,20 @@ function clamp(v: number, max: number): number {
|
||||
* Pick a horizontal direction that keeps the sweep on-screen (right if
|
||||
* 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).
|
||||
* keeper produced before movement patterns existed. The direction choice
|
||||
* keeps the finite sweep on-screen, so the executor's reflection never
|
||||
* actually engages for it.
|
||||
*
|
||||
* In loop mode `loopPath` ramps x in one direction forever (loop mode
|
||||
* forces `reflect`, so the direction never matters and the ramp bounces edge
|
||||
* to edge). `LINE_LOOP_STEP` is several pixels per step rather than one so a
|
||||
* screen crossing takes seconds, not minutes, at the default cadence.
|
||||
* In loop mode `loopPath` ramps x in one direction forever; the direction
|
||||
* never matters because the executor reflects the ramp edge to edge.
|
||||
* `LINE_LOOP_STEP` is several pixels per step rather than one so a screen
|
||||
* crossing takes seconds, not minutes, at the default cadence.
|
||||
*/
|
||||
const LINE_STEPS = 250;
|
||||
const LINE_LOOP_STEP = 4;
|
||||
|
||||
export const line: MovementStrategy = {
|
||||
name: "line",
|
||||
bounds: "abort",
|
||||
*path(ctx: MoveContext): Generator<Point> {
|
||||
const { start, width } = ctx;
|
||||
const dx: number = start.x + LINE_STEPS < width ? 1 : -1;
|
||||
@@ -134,18 +124,16 @@ export const line: MovementStrategy = {
|
||||
* roomiest corner and stays on-screen. 250 single-pixel steps per axis
|
||||
* (≈250px reach), matching `line`'s magnitude.
|
||||
*
|
||||
* In loop mode `loopPath` ramps both axes forever under the forced
|
||||
* `reflect` policy. Because the x and y travel ranges have different spans,
|
||||
* their triangle waves have different periods, so the path precesses across
|
||||
* the whole screen — the roaming-DVD bounce — rather than retracing one 45°
|
||||
* line.
|
||||
* In loop mode `loopPath` ramps both axes forever, and the executor reflects
|
||||
* them. Because the x and y travel ranges have different spans, their
|
||||
* triangle waves have different periods, so the path precesses across the
|
||||
* whole screen — the roaming-DVD bounce — rather than retracing one 45° line.
|
||||
*/
|
||||
const DIAGONAL_STEPS = 250;
|
||||
const DIAGONAL_LOOP_STEP = 4;
|
||||
|
||||
export const diagonal: MovementStrategy = {
|
||||
name: "diagonal",
|
||||
bounds: "clamp",
|
||||
*path(ctx: MoveContext): Generator<Point> {
|
||||
const { start, width, height } = ctx;
|
||||
const dx: number = start.x + DIAGONAL_STEPS < width ? 1 : -1;
|
||||
@@ -178,7 +166,6 @@ const JITTER_RADIUS = 30;
|
||||
|
||||
export const jitter: MovementStrategy = {
|
||||
name: "jitter",
|
||||
bounds: "clamp",
|
||||
*path(ctx: MoveContext): Generator<Point> {
|
||||
const { start, rng } = ctx;
|
||||
for (let i = 1; i <= JITTER_STEPS; i++) {
|
||||
@@ -194,15 +181,14 @@ export const jitter: MovementStrategy = {
|
||||
* 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.
|
||||
* position drift freely; the executor 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, rng } = ctx;
|
||||
let x: number = start.x;
|
||||
@@ -226,7 +212,6 @@ const ARC_REACH = 300;
|
||||
|
||||
export const arc: MovementStrategy = {
|
||||
name: "arc",
|
||||
bounds: "clamp",
|
||||
*path(ctx: MoveContext): Generator<Point> {
|
||||
const { start, width, height, rng } = ctx;
|
||||
|
||||
@@ -268,7 +253,6 @@ const FIG8_AMP = 125;
|
||||
|
||||
export const figureEight: MovementStrategy = {
|
||||
name: "figureEight",
|
||||
bounds: "clamp",
|
||||
*path(ctx: MoveContext): Generator<Point> {
|
||||
const { start } = ctx;
|
||||
for (let i = 1; i <= FIG8_STEPS; i++) {
|
||||
|
||||
Reference in New Issue
Block a user