Design cursor motions
Cua Cursor Motion, the open-source library behind Cua Driver's agent cursor. Plan the six motions, design your own, and play them on a web page.
Cua Cursor Motion, the open-source library behind Cua Driver's agent cursor. Plan the six motions, design your own, and play them on a web page.
Cua Driver moves its agent cursor with Cua Cursor Motion, an MIT-licensed library you can use on its own. It comes in two packages that give the same trajectories:
cua-cursor-motion (libs/cua-driver/rust/crates/cua-cursor-motion),
the Rust crate the driver plans every move with;@trycua/cursor-motion (libs/typescript/cursor-motion), a TypeScript
port with a canvas player that draws the Cua cursor.Both are tested against the same golden trajectories, so a motion you tune on a web page plays the same way in the driver.
Cua Cursor Motion is not published to crates.io or npm. Use it straight from trycua/cua.
In Rust, add a git dependency. Cargo finds the crate inside the repository:
[dependencies]
cua-cursor-motion = { git = "https://github.com/trycua/cua" }On the web, build the single-file ES module once and copy it with its types into your project:
git clone --depth 1 https://github.com/trycua/cua
cd cua/libs/typescript
pnpm install
pnpm --filter @trycua/cursor-motion build
cp cursor-motion/dist/index.js ../../../my-app/src/vendor/cua-cursor-motion.js
cp cursor-motion/dist/index.d.ts ../../../my-app/src/vendor/cua-cursor-motion.d.tsThe file has no imports, so it also loads from a plain
<script type="module">. In a TypeScript project with a bundler you can copy
libs/typescript/cursor-motion/src/ instead.
The Cua Driver SDKs export the planner through the same UniFFI boundary as
the rest of the driver, so Python and TypeScript hosts plan moves with the
Rust code itself. A planned move is a CursorTrajectory: read its samples
once with samples() and interpolate locally each frame.
import cua_driver as cd
params = cd.default_cursor_motion_params()
params.style = cd.CursorMotionStyle.SPRING_SETTLE
trajectory = cd.plan_cursor_move(
params,
cd.CursorMoveRequest(
from_point=cd.CursorMotionPoint(x=100, y=100),
to_point=cd.CursorMotionPoint(x=700, y=400),
from_heading=None,
end_heading=None,
target=cd.CursorMotionRect(x=660, y=380, width=80, height=40),
seed="demo",
reduced_motion=False,
),
)
samples = trajectory.samples()import { CursorMotionStyle, defaultCursorMotionParams, planCursorMove } from '@trycua/cua-driver';
const trajectory = planCursorMove(
{ ...defaultCursorMotionParams(), style: CursorMotionStyle.SpringSettle },
{
fromPoint: { x: 100, y: 100 },
toPoint: { x: 700, y: 400 },
target: { x: 660, y: 380, width: 80, height: 40 },
seed: 'demo',
reducedMotion: false,
}
);
const samples = trajectory.samples();plan_cursor_spec takes a custom CursorMotionSpec, and
CursorTrajectory.effect_frame(t, blend) returns the trail, glow and magnet to
paint. For a web page without the native library, use the single-file web port
(see Use it from the repository).
A move is planned once, as samples at 120 Hz, then played back by time. It
takes a style (signature_arc, spring_settle, magnetic, comet_swoop,
adaptive or classic), a timing mode (native, fitts or fixed) and
the shape knobs of set_agent_cursor_motion.
import { planMove } from './vendor/cua-cursor-motion.js';
const trajectory = planMove(
{ style: 'spring_settle', timing: 'fitts' },
{ from: { x: 100, y: 100 }, to: { x: 700, y: 400 }, target: [660, 380, 80, 40] }
);
const { x, y, heading } = trajectory.sampleAt(0.3);use cua_cursor_motion::{plan_move, MotionParams, MotionStyle, MotionTiming, MoveRequest, Pt};
let params = MotionParams { style: MotionStyle::SpringSettle, timing: MotionTiming::Fitts, ..Default::default() };
let request = MoveRequest {
target: Some([660.0, 380.0, 80.0, 40.0]),
..MoveRequest::new(Pt::new(100.0, 100.0), Pt::new(700.0, 400.0))
};
let sample = plan_move(¶ms, &request).sample_at(0.3);arrivalT (arrival_t in Rust) is when the tip first reaches the target.
Cua Driver clicks then and lets any follow-through or bounce finish during the
click. The target rect sets the Fitts width: smaller targets take longer.
MotionPlayer draws the Cua cursor, its glow, the comet trail and the click
ripple inside a <canvas>. It never moves, hides or restyles the page's own
mouse pointer.
import { MotionPlayer } from './vendor/cua-cursor-motion.js';
const player = new MotionPlayer(document.querySelector('canvas')!);
player.place({ x: 80, y: 300 });
await player.moveTo(
{ x: 640, y: 120 },
{ params: { style: 'comet_swoop' }, target: [600, 104, 80, 32], click: true }
);A custom motion is a MotionSpec: a path shape (straight, arc, bow), a
speed curve (named easings or cubic_bezier), an overshoot or settle
(follow_through, spring), a duration model (fitts, fixed, distance),
a heading mode, effects and a trail. signature_arc, spring_settle and
comet_swoop are specs themselves, so start from one:
import { planSpec, specForStyle } from './vendor/cua-cursor-motion.js';
const spec = specForStyle('signature_arc')!;
spec.ease = { type: 'cubic_bezier', x1: 0.3, y1: 0, x2: 0.1, y2: 1 };
spec.settle = { type: 'follow_through', amount: 0.04, maxPt: 14, at: 0.75 };
spec.effects.trail = true;
const trajectory = planSpec(spec, { from: { x: 100, y: 500 }, to: { x: 900, y: 200 } });Custom specs play in your own renderer. Cua Driver itself takes one of the six styles and its knobs.
The playground lets you pick a style, drag the start and end points, tune the curve with a live speed plot, and copy the result as TypeScript or as Cua Driver config. Run it from a checkout:
cd libs/typescript
pnpm install
pnpm --filter @trycua/cursor-motion playgroundIt opens at http://127.0.0.1:4173/playground/. For a built-in style, the
exported Cua Driver config looks like this:
cua-driver config set cursor.motion.style comet_swoop
cua-driver config set cursor.motion.timing fitts
cua-driver cursor motion --session demo --style comet_swoop --timing fitts --effects trail=onThe arc knobs (arc_size, arc_flow, start_handle, end_handle) and the
classic glide's spring and turn_radius travel only through
set_agent_cursor_motion. See Choose your cursor
for how saved defaults, start_session and per-call settings combine.