Skip to content

Commit c1534a3

Browse files
feat: add human-like Bezier mouse movement
- Add mousetrajectory lib with Bernstein Bezier curves, jitter, easeOutQuad - MoveMouse supports smooth=true for curved path, smooth=false for instant - OpenAPI: smooth, steps (5-80), step_delay_ms (3-30) on MoveMouseRequest - Add cursor-trail demo (before/after instant vs Bezier) Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 640ca69 commit c1534a3

10 files changed

Lines changed: 1476 additions & 136 deletions

File tree

demo/mouse-movement/README.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# Mouse Movement Demo — Before/After Video
2+
3+
Create a before/after video demonstrating human-like Bezier curve mouse movement vs instant teleport, inspired by [Camoufox's stealth overview](https://camoufox.com/stealth/) and [cursor movement docs](https://camoufox.com/fingerprint/cursor-movement).
4+
5+
## What You'll Record
6+
7+
- **BEFORE**: Instant movement (`smooth: false`) — cursor jumps in straight lines between targets
8+
- **AFTER**: Human-like Bezier movement (`smooth: true`) — curved, natural trajectory
9+
10+
The cursor trail overlay makes the difference visually obvious.
11+
12+
## Prerequisites
13+
14+
- Kernel browser session running kernel-images-private (with Bezier support in `server/cmd/api/api/computer.go`)
15+
- `KERNEL_BROWSER_ID` and `KERNEL_API_KEY` (or equivalent auth)
16+
- Screen recorder (OBS, QuickTime, or `ffmpeg`)
17+
18+
## Steps
19+
20+
### 1. Start Screen Recording
21+
22+
Record the **browser live view** URL. Options:
23+
24+
- **OBS**: Add Browser source or window capture for the live view tab
25+
- **QuickTime** (macOS): File → New Screen Recording, select the live view window
26+
- **ffmpeg**:
27+
```bash
28+
ffmpeg -f avfoundation -i "1" -c:v libx264 -crf 18 mouse-demo.mp4
29+
```
30+
31+
### 2. Run the Demo Script
32+
33+
```bash
34+
cd demo/mouse-movement
35+
npm install
36+
KERNEL_BROWSER_ID=<your-browser-id> KERNEL_API_KEY=<key> npm run demo
37+
```
38+
39+
### 3. What Happens
40+
41+
1. The script loads the cursor trail demo page (`cursor-trail-demo.html`) into the browser
42+
2. **BEFORE** phase: Moves the mouse along the path with `smooth: false` — straight lines
43+
3. Pause and clear trail
44+
4. **AFTER** phase: Same path with `smooth: true` — Bezier curves
45+
5. The trail shows the curved vs straight paths
46+
47+
### 4. Edit the Video
48+
49+
- Trim to show BEFORE and AFTER clearly
50+
- Optional: split screen or side-by-side comparison
51+
- Add captions: "Instant movement" vs "Human-like Bezier movement"
52+
53+
## Files
54+
55+
| File | Purpose |
56+
|------|---------|
57+
| `cursor-trail-demo.html` | Page that draws the cursor path as the mouse moves |
58+
| `demo-mouse-movement-video.ts` | Script that runs before/after moveMouse with smooth on/off |
59+
60+
## Implementation
61+
62+
The Bezier trajectory and `smooth` movement are implemented in `server/cmd/api/api/computer.go` and `server/lib/mousetrajectory/`. When `smooth: true` is sent in the move_mouse request body, the instance uses Bernstein Bezier curves for human-like movement.
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>Mouse Movement Demo — Cursor Trail</title>
7+
<style>
8+
* { margin: 0; padding: 0; box-sizing: border-box; }
9+
html, body { width: 100%; height: 100%; overflow: hidden; }
10+
body {
11+
font-family: 'SF Mono', 'Fira Code', 'Monaco', monospace;
12+
background: #0d1117;
13+
color: #e6edf3;
14+
cursor: none;
15+
}
16+
#canvas {
17+
position: fixed;
18+
top: 0; left: 0;
19+
width: 100%;
20+
height: 100%;
21+
pointer-events: none;
22+
z-index: 1;
23+
}
24+
#ui {
25+
position: fixed;
26+
top: 24px;
27+
left: 50%;
28+
transform: translateX(-50%);
29+
z-index: 2;
30+
padding: 12px 24px;
31+
background: rgba(22, 27, 34, 0.95);
32+
border: 1px solid #30363d;
33+
border-radius: 12px;
34+
font-size: 14px;
35+
display: flex;
36+
align-items: center;
37+
gap: 24px;
38+
}
39+
#mode {
40+
font-weight: 600;
41+
color: #58a6ff;
42+
}
43+
#mode.instant { color: #f85149; }
44+
#mode.smooth { color: #3fb950; }
45+
#hint {
46+
color: #8b949e;
47+
font-size: 12px;
48+
}
49+
.cursor-dot {
50+
position: fixed;
51+
width: 12px;
52+
height: 12px;
53+
margin: -6px 0 0 -6px;
54+
background: #58a6ff;
55+
border: 2px solid #fff;
56+
border-radius: 50%;
57+
pointer-events: none;
58+
z-index: 3;
59+
box-shadow: 0 0 12px rgba(88, 166, 255, 0.6);
60+
transition: none;
61+
}
62+
</style>
63+
</head>
64+
<body>
65+
<canvas id="canvas"></canvas>
66+
<div id="ui">
67+
<span id="mode">Recording cursor trail…</span>
68+
<span id="hint">Move the mouse to see the path</span>
69+
</div>
70+
<div id="cursor-dot" class="cursor-dot"></div>
71+
72+
<script>
73+
const canvas = document.getElementById('canvas');
74+
const ctx = canvas.getContext('2d');
75+
const cursorDot = document.getElementById('cursor-dot');
76+
const modeEl = document.getElementById('mode');
77+
78+
let trail = [];
79+
const maxTrail = 2000;
80+
const trailColor = 'rgba(88, 166, 255, 0.85)';
81+
const trailWidth = 3;
82+
83+
function resize() {
84+
canvas.width = window.innerWidth;
85+
canvas.height = window.innerHeight;
86+
redraw();
87+
}
88+
89+
function redraw() {
90+
ctx.clearRect(0, 0, canvas.width, canvas.height);
91+
if (trail.length < 2) return;
92+
ctx.beginPath();
93+
ctx.moveTo(trail[0].x, trail[0].y);
94+
for (let i = 1; i < trail.length; i++) {
95+
ctx.lineTo(trail[i].x, trail[i].y);
96+
}
97+
ctx.strokeStyle = trailColor;
98+
ctx.lineWidth = trailWidth;
99+
ctx.lineCap = 'round';
100+
ctx.lineJoin = 'round';
101+
ctx.stroke();
102+
}
103+
104+
function addPoint(x, y) {
105+
trail.push({ x, y });
106+
if (trail.length > maxTrail) trail.shift();
107+
redraw();
108+
}
109+
110+
function onMove(e) {
111+
addPoint(e.clientX, e.clientY);
112+
cursorDot.style.left = e.clientX + 'px';
113+
cursorDot.style.top = e.clientY + 'px';
114+
}
115+
116+
window.addEventListener('resize', resize);
117+
document.addEventListener('mousemove', onMove);
118+
resize();
119+
120+
window.demoApi = {
121+
setMode: (label, cls) => {
122+
modeEl.textContent = label;
123+
modeEl.className = cls || '';
124+
},
125+
clear: () => {
126+
trail = [];
127+
ctx.clearRect(0, 0, canvas.width, canvas.height);
128+
}
129+
};
130+
</script>
131+
</body>
132+
</html>
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/**
2+
* Before/After mouse movement demo for video recording.
3+
*
4+
* Demonstrates:
5+
* - BEFORE: Instant mouse movement (smooth: false) — cursor teleports in straight lines
6+
* - AFTER: Human-like Bezier curve movement (smooth: true) — natural curved trajectory
7+
*
8+
* Inspired by https://camoufox.com/stealth/ and https://camoufox.com/fingerprint/cursor-movement
9+
*
10+
* Usage:
11+
* 1. Ensure KERNEL_BROWSER_ID and KERNEL_API_KEY are set
12+
* 2. Start screen recording (OBS, QuickTime, ffmpeg) on the browser live view
13+
* 3. Run: npm run demo
14+
* 4. Record: BEFORE (instant) then AFTER (smooth Bezier) segments
15+
*/
16+
17+
import Kernel from "@onkernel/sdk";
18+
import { chromium } from "playwright-core";
19+
import { readFileSync } from "fs";
20+
import { join, dirname } from "path";
21+
import { fileURLToPath } from "url";
22+
23+
const BROWSER_ID = process.env.KERNEL_BROWSER_ID!;
24+
25+
function sleep(ms: number) {
26+
return new Promise((r) => setTimeout(r, ms));
27+
}
28+
29+
const __dirname = dirname(fileURLToPath(import.meta.url));
30+
31+
// Movement path chosen to clearly show the difference: diagonal + arc
32+
const DEMO_PATH: [number, number][] = [
33+
[200, 200],
34+
[600, 350],
35+
[1000, 200],
36+
[700, 500],
37+
[400, 400],
38+
[800, 300],
39+
];
40+
41+
(async () => {
42+
if (!BROWSER_ID) {
43+
console.error("Set KERNEL_BROWSER_ID");
44+
process.exit(1);
45+
}
46+
47+
const kernel = new Kernel();
48+
const session = await kernel.browsers.retrieve(BROWSER_ID);
49+
50+
console.log("Session:", BROWSER_ID);
51+
console.log("Live view (record this):", session.browser_live_view_url);
52+
53+
const browser = await chromium.connectOverCDP(session.cdp_ws_url);
54+
const page = browser.contexts()[0].pages()[0] ?? (await browser.newPage());
55+
56+
// Load cursor trail demo page
57+
const demoHtml = readFileSync(
58+
join(__dirname, "cursor-trail-demo.html"),
59+
"utf-8"
60+
);
61+
await page.setContent(demoHtml, { waitUntil: "domcontentloaded" });
62+
await page.setViewportSize({ width: 1280, height: 720 });
63+
64+
await sleep(500);
65+
66+
// --- BEFORE: Instant movement (smooth: false) ---
67+
await page.evaluate(() => {
68+
(window as any).demoApi?.setMode("BEFORE: Instant movement (smooth: false)", "instant");
69+
(window as any).demoApi?.clear();
70+
});
71+
await sleep(800);
72+
73+
console.log("[BEFORE] Running instant mouse moves...");
74+
for (let i = 0; i < DEMO_PATH.length; i++) {
75+
const [x, y] = DEMO_PATH[i];
76+
await kernel.browsers.computer.moveMouse(BROWSER_ID, { x, y, smooth: false });
77+
await sleep(400);
78+
}
79+
await sleep(2000);
80+
81+
// --- Clear and switch to AFTER ---
82+
await page.evaluate(() => {
83+
(window as any).demoApi?.setMode("AFTER: Human-like Bezier movement (smooth: true)", "smooth");
84+
(window as any).demoApi?.clear();
85+
});
86+
await sleep(1500);
87+
88+
// --- AFTER: Smooth Bezier movement (smooth: true) ---
89+
console.log("[AFTER] Running smooth Bezier mouse moves...");
90+
for (let i = 0; i < DEMO_PATH.length; i++) {
91+
const [x, y] = DEMO_PATH[i];
92+
await kernel.browsers.computer.moveMouse(BROWSER_ID, {
93+
x,
94+
y,
95+
smooth: true,
96+
step_delay_ms: 12,
97+
});
98+
await sleep(400);
99+
}
100+
await sleep(3000);
101+
102+
console.log("Demo complete. Stop recording.");
103+
browser.close();
104+
})();

0 commit comments

Comments
 (0)