Skip to content

feat: cursor follow crop with text cursor focus mode#640

Open
walters954 wants to merge 1 commit into
webadderallorg:mainfrom
walters954:feature/cursor-follow-crop
Open

feat: cursor follow crop with text cursor focus mode#640
walters954 wants to merge 1 commit into
webadderallorg:mainfrom
walters954:feature/cursor-follow-crop

Conversation

@walters954
Copy link
Copy Markdown

@walters954 walters954 commented Jun 1, 2026

When recording screen captures with a crop applied (e.g. 1080p output from a 1440p source), the viewport tracks the mouse cursor — which works great when you're moving the mouse, but causes the frame to drift away from the typing area whenever you stop to type. This PR adds cursor-follow crop: a per-frame viewport pan that keeps the cursor inside a configurable safe zone, plus an opt-in text cursor focus mode that locks the viewport to the typing area when the I-beam is active and the mouse is stationary.

What's in the PR

Core algorithm (videoPlayback/cursorFollowCrop.ts) — stateful per-frame computation that reads cursor telemetry, interpolates position, and eases the viewport top-left so the cursor stays inside the safe-zone inset. No dependencies on anything outside the existing telemetry pipeline.

Text cursor focus — optional mode that detects mouse velocity over a 400ms look-back window and the cursorType field already present in cursor.json telemetry. When the mouse has been still for 700ms and the cursor type is "text", the viewport locks to the typing area with a high smoothness floor (0.92). Mouse movement immediately snaps back to tracking mode. Debounced to avoid jarring transitions.

UI — a "Track cursor" toggle in the crop panel with safe zone and smoothness sliders, plus the text cursor focus checkbox. All settings persist with the project.

Export — wired into FrameRenderer/modernFrameRenderer so the same crop behavior applies during export.

Tests — 9 unit tests covering: edge clamping, safe zone hold, scrub reinit, telemetry fallback, mouse/text mode switching, and mode reset on scrub backwards.

Files changed

  • types.tsCursorFollowCropSettings, DEFAULT_CURSOR_FOLLOW_CROP
  • videoPlayback/cursorFollowCrop.ts — new file, core algorithm
  • videoPlayback/cursorFollowCrop.test.ts — new file, tests
  • CropControl.tsx — crop panel UI
  • VideoPlayback.tsx — preview integration + dep array update
  • projectPersistence.ts — load/save normalization
  • frameRenderer.ts, modernFrameRenderer.ts — export pipeline
  • videoExporter.ts, modernVideoExporter.ts, gifExporter.ts — config threading

Testing

Enable Track cursor on a 1440p recording cropped to 1080p. The viewport should pan to keep the cursor in frame during playback and export. With Text cursor focus on, scrubbing to a typing section should show the viewport staying stable on the text field; moving the mouse should immediately resume tracking.

@github-actions github-actions Bot added the Slop label Jun 1, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 1, 2026

⚠️ This pull request has been flagged by Anti-Slop.
Our automated checks detected patterns commonly associated with
low-quality or automated/AI submissions (failure count reached).
No automatic closure — a maintainer will review it.
If this is legitimate work, please add more context, link issues, or ping us.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 1, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds cursor-follow crop mode to the video editor and exporters. When enabled, the crop viewport dynamically repositions to keep the cursor framed within a configurable safe zone while applying smoothed panning. It includes text-cursor awareness for stable typing, project persistence, and comprehensive test coverage.

Changes

Cursor-follow crop feature

Layer / File(s) Summary
Type definitions and persistence schema
src/components/video-editor/types.ts, src/components/video-editor/projectPersistence.ts
CursorFollowCropPreviewMode, CursorFollowCropSettings (with enabled, safeZoneRatio, smoothness, trackTextCursor, previewMode), and DEFAULT_CURSOR_FOLLOW_CROP constant are defined. ProjectEditorState is extended to persist the setting.
Cursor-follow crop computation engine
src/components/video-editor/videoPlayback/cursorFollowCrop.ts, src/components/video-editor/videoPlayback/cursorFollowCrop.test.ts
Core algorithm computes per-frame effective crop from telemetry and time: clamps viewport within bounds, applies safe-zone inset, debounces mouse/text-cursor mode switching, applies higher smoothness while typing, smoothly pans toward target, and handles scrubbing. Tests verify viewport clamping, safe-zone stillness, re-initialization, focus-mode transitions, and fallback behavior.
Project persistence normalization
src/components/video-editor/projectPersistence.ts
Normalizes cursor-follow crop during project load: derives preview mode from persisted data, clamps numeric fields, validates booleans, and falls back to defaults.
Video preview rendering integration
src/components/video-editor/VideoPlayback.tsx
Syncs cursor-follow settings into refs, maintains base and effective crop regions, recomputes effective crop each Pixi ticker frame, repositions video sprite to keep cursor framed, and updates mask source crop.
Crop control editing interface
src/components/video-editor/CropControl.tsx
Adds UI controls for enable toggle, safe-zone ratio, smoothness, text-cursor focus, preview mode (source/output), and output resolution presets with active matching. Renders conditional darkened overlay and draggable edge handles. When follow is enabled, drag-resize adjusts only width/height; when disabled, drag updates position.
Editor state management and wiring
src/components/video-editor/VideoEditor.tsx
Central state for cursorFollowCrop setting, persisted to/restored from project state, threaded to preview and crop UI, and passed to all exporters. Export hook dependencies updated for consistency.
Export frame rendering
src/lib/exporter/frameRenderer.ts, src/lib/exporter/modernFrameRenderer.ts
Both frame renderers initialize cursor-follow crop state, invoke per-frame computation early in the render pipeline (before cursor overlays), update video sprite position and mask source crop, or fall back to base crop when disabled.
GIF and MP4 exporter wiring
src/lib/exporter/gifExporter.ts, src/lib/exporter/videoExporter.ts, src/lib/exporter/modernVideoExporter.ts
Extend exporter configs to accept cursor-follow crop setting and wire into frame renderer initialization. Modern exporter adds native skip reason when dynamic cursor-follow crop is enabled.

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested labels

Checked

Suggested reviewers

  • meiiie

🐰 Cursor framed with care,
Safe zones smooth the dancing stare,
Text held still while we prepare,
Follow flows through export's air!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The pull request title clearly and concisely summarizes the two main features being added: cursor follow crop functionality and text cursor focus mode.
Description check ✅ Passed The pull request description is well-structured with a clear summary, files changed table, detailed how-it-works explanation, and comprehensive test plan covering all major features and integration points.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch feature/cursor-follow-crop

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 1, 2026

⚠️ This pull request has been flagged by Anti-Slop.
Our automated checks detected patterns commonly associated with
low-quality or automated/AI submissions (failure count reached).
No automatic closure — a maintainer will review it.
If this is legitimate work, please add more context, link issues, or ping us.

Adds a "Track cursor" crop mode that pans the viewport to keep the mouse
cursor inside a configurable safe zone during playback and export. A new
"Text cursor focus" toggle locks the viewport over the typing area when
the I-beam cursor is active and the mouse is stationary, then smoothly
switches back to mouse tracking once movement is detected. Mouse always
wins; a 700ms debounce prevents jarring transitions between modes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@walters954 walters954 force-pushed the feature/cursor-follow-crop branch from 2bfe0d4 to 75ee15c Compare June 1, 2026 03:44
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 1, 2026

⚠️ This pull request has been flagged by Anti-Slop.
Our automated checks detected patterns commonly associated with
low-quality or automated/AI submissions (failure count reached).
No automatic closure — a maintainer will review it.
If this is legitimate work, please add more context, link issues, or ping us.

1 similar comment
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 1, 2026

⚠️ This pull request has been flagged by Anti-Slop.
Our automated checks detected patterns commonly associated with
low-quality or automated/AI submissions (failure count reached).
No automatic closure — a maintainer will review it.
If this is legitimate work, please add more context, link issues, or ping us.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/components/video-editor/VideoEditor.tsx (1)

1078-1104: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add cursorFollowCrop to the thumbnail callback deps.

captureProjectThumbnail() now closes over cursorFollowCrop here, but the useCallback dependency list below doesn't include it. Saving right after changing Track cursor can therefore bake the previous cursor-follow config into the project thumbnail.

💡 Minimal fix
 	}, [
 		annotationRegions,
 		autoCaptionSettings,
 		autoCaptions,
 		backgroundBlur,
 		borderRadius,
 		connectZooms,
 		connectedZoomDurationMs,
 		connectedZoomEasing,
 		connectedZoomGapMs,
 		cropRegion,
+		cursorFollowCrop,
 		currentTime,
 		cursorClickBounce,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/video-editor/VideoEditor.tsx` around lines 1078 - 1104, The
captureProjectThumbnail callback in VideoEditor closes over cursorFollowCrop but
its useCallback dependency list is missing that variable; update the useCallback
that defines captureProjectThumbnail to include cursorFollowCrop (alongside the
other deps such as wallpaper, zoomRegions, targetWidth/Height, etc.) so the
thumbnail logic uses the current cursor-follow setting when saved.
src/lib/exporter/gifExporter.ts (1)

143-201: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Forward cursorFollowCrop into the GIF frame renderer config.

GifExporterConfig accepts the new setting, but buildGifFrameRendererConfig() drops it. As written, GIF exports won't apply Track cursor even though preview and MP4 exports do.

Suggested fix
 		borderRadius: config.borderRadius,
 		padding: config.padding,
 		cropRegion: config.cropRegion,
+		cursorFollowCrop: config.cursorFollowCrop,
 		webcam: config.webcam,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/exporter/gifExporter.ts` around lines 143 - 201, The
buildGifFrameRendererConfig() return object is missing the cursorFollowCrop
option from GifExporterConfig, so GIFs ignore Track cursor; update the object
returned by buildGifFrameRendererConfig to include cursorFollowCrop:
config.cursorFollowCrop (ensuring the key matches the renderer's expected name)
so the GIF frame renderer receives and applies the setting.
🧹 Nitpick comments (1)
src/components/video-editor/VideoPlayback.tsx (1)

1551-1554: 💤 Low value

Consider splitting ref update and state reset into separate effects.

The current effect updates cursorFollowCropRef.current and resets state, but the dependency array only includes specific properties. If cursorFollowCrop object reference changes but these properties remain the same, the ref won't be updated. While this works functionally (since the properties are identical), it could be clearer to separate concerns:

+useEffect(() => {
+  cursorFollowCropRef.current = cursorFollowCrop;
+}, [cursorFollowCrop]);
+
 useEffect(() => {
-  cursorFollowCropRef.current = cursorFollowCrop;
   resetCursorFollowCropState(cursorFollowCropStateRef.current);
 }, [cursorFollowCrop?.enabled, cursorFollowCrop?.safeZoneRatio, cursorFollowCrop?.smoothness, cursorFollowCrop?.trackTextCursor]);

This makes the intent clearer: always keep the ref synced, but only reset state when computation-affecting properties change.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/video-editor/VideoPlayback.tsx` around lines 1551 - 1554,
Split the current useEffect into two: one effect that always syncs the ref by
assigning cursorFollowCropRef.current = cursorFollowCrop and depends on the
cursorFollowCrop object reference (so it runs whenever the object changes), and
a separate effect that calls
resetCursorFollowCropState(cursorFollowCropStateRef.current) with dependencies
only on the computation-affecting properties (cursorFollowCrop?.enabled,
cursorFollowCrop?.safeZoneRatio, cursorFollowCrop?.smoothness,
cursorFollowCrop?.trackTextCursor); keep references to
resetCursorFollowCropState, cursorFollowCropRef, and cursorFollowCropStateRef
as-is to locate the code.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/video-editor/CropControl.tsx`:
- Around line 115-118: The preview is being stretched because
canvas.width/canvas.height are always set to sourceWidth/sourceHeight; when in
output mode use the cropped region's dimensions instead of the full source to
preserve aspect ratio—compute the targetWidth/targetHeight from the current crop
rectangle (e.g., cropRect.width/cropRect.height or cropWidth/cropHeight) or
scale them to the configured output resolution, and set canvas.width and
canvas.height to those values before drawing; update the same logic where
sourceWidth/sourceHeight are used (the blocks around canvas.width/canvas.height
at the given ranges) so the canvas matches the crop/output aspect rather than
the full video.

In `@src/components/video-editor/VideoEditor.tsx`:
- Around line 6357-6358: The crop modal is being passed raw cursorTelemetry
which can differ from the normalized/loop-aware path used by VideoPlayback and
export; change the prop passed to the crop editor from cursorTelemetry to the
already-computed effectiveCursorTelemetry (the same value used by
VideoPlayback/export) so the crop preview uses the identical telemetry variant;
locate the prop usage in VideoEditor (prop name cursorTelemetry/currentTimeMs)
and replace it to supply effectiveCursorTelemetry (or call the helper that
computes it) so preview/export paths match.
- Around line 6355-6356: The cancel path only restores cropRegion but not the
mutable cursor-follow state, so when the crop modal is opened you should
snapshot the current cursorFollowCrop (e.g., prevCursorFollowCrop) and any
related fields (safe zone, smoothness) and then in handleCancelCropEditor()
restore cursorFollowCrop via setCursorFollowCrop(prevCursorFollowCrop) (and
clear the snapshot after). Add the snapshot creation where the editor opens and
use the snapshot in handleCancelCropEditor to roll back cursorFollowCrop
changes.

In `@src/lib/exporter/modernFrameRenderer.ts`:
- Line 2984: The call to applyCursorFollowCrop(timeMs, layoutCache) uses
source/output timeMs when sampling cursor-follow crop but cursor sampling must
use the cursor timeline; update applyCursorFollowCrop (and any callers at the
other site) to accept and pass the timeline-aligned cursor time (cursorTimeMs)
or convert timeMs to cursorTimeMs via the same remapping used by
cursorOverlay.update before calling computeCursorFollowCrop so the crop sampling
uses cursorTimeMs (ensure references to applyCursorFollowCrop,
computeCursorFollowCrop, and cursorOverlay.update are updated accordingly).

---

Outside diff comments:
In `@src/components/video-editor/VideoEditor.tsx`:
- Around line 1078-1104: The captureProjectThumbnail callback in VideoEditor
closes over cursorFollowCrop but its useCallback dependency list is missing that
variable; update the useCallback that defines captureProjectThumbnail to include
cursorFollowCrop (alongside the other deps such as wallpaper, zoomRegions,
targetWidth/Height, etc.) so the thumbnail logic uses the current cursor-follow
setting when saved.

In `@src/lib/exporter/gifExporter.ts`:
- Around line 143-201: The buildGifFrameRendererConfig() return object is
missing the cursorFollowCrop option from GifExporterConfig, so GIFs ignore Track
cursor; update the object returned by buildGifFrameRendererConfig to include
cursorFollowCrop: config.cursorFollowCrop (ensuring the key matches the
renderer's expected name) so the GIF frame renderer receives and applies the
setting.

---

Nitpick comments:
In `@src/components/video-editor/VideoPlayback.tsx`:
- Around line 1551-1554: Split the current useEffect into two: one effect that
always syncs the ref by assigning cursorFollowCropRef.current = cursorFollowCrop
and depends on the cursorFollowCrop object reference (so it runs whenever the
object changes), and a separate effect that calls
resetCursorFollowCropState(cursorFollowCropStateRef.current) with dependencies
only on the computation-affecting properties (cursorFollowCrop?.enabled,
cursorFollowCrop?.safeZoneRatio, cursorFollowCrop?.smoothness,
cursorFollowCrop?.trackTextCursor); keep references to
resetCursorFollowCropState, cursorFollowCropRef, and cursorFollowCropStateRef
as-is to locate the code.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 735219a8-1b54-4121-be56-645a82bfc981

📥 Commits

Reviewing files that changed from the base of the PR and between 1f9912b and 75ee15c.

📒 Files selected for processing (12)
  • src/components/video-editor/CropControl.tsx
  • src/components/video-editor/VideoEditor.tsx
  • src/components/video-editor/VideoPlayback.tsx
  • src/components/video-editor/projectPersistence.ts
  • src/components/video-editor/types.ts
  • src/components/video-editor/videoPlayback/cursorFollowCrop.test.ts
  • src/components/video-editor/videoPlayback/cursorFollowCrop.ts
  • src/lib/exporter/frameRenderer.ts
  • src/lib/exporter/gifExporter.ts
  • src/lib/exporter/modernFrameRenderer.ts
  • src/lib/exporter/modernVideoExporter.ts
  • src/lib/exporter/videoExporter.ts

Comment on lines +115 to +118
const sourceWidth = videoElement.videoWidth || 1920;
const sourceHeight = videoElement.videoHeight || 1080;
canvas.width = sourceWidth;
canvas.height = sourceHeight;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't stretch the output preview to the source aspect ratio.

In output mode the cropped region is still rendered into a full-source canvas and wrapped in a container sized from the full video. Any crop whose aspect differs from the source gets geometrically distorted, so the new Output preview can show the wrong framing.

Also applies to: 130-137, 271-276

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/video-editor/CropControl.tsx` around lines 115 - 118, The
preview is being stretched because canvas.width/canvas.height are always set to
sourceWidth/sourceHeight; when in output mode use the cropped region's
dimensions instead of the full source to preserve aspect ratio—compute the
targetWidth/targetHeight from the current crop rectangle (e.g.,
cropRect.width/cropRect.height or cropWidth/cropHeight) or scale them to the
configured output resolution, and set canvas.width and canvas.height to those
values before drawing; update the same logic where sourceWidth/sourceHeight are
used (the blocks around canvas.width/canvas.height at the given ranges) so the
canvas matches the crop/output aspect rather than the full video.

Comment on lines +6355 to +6356
cursorFollow={cursorFollowCrop}
onCursorFollowChange={setCursorFollowCrop}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Cancel should roll back cursor-follow edits too.

These props let the crop modal mutate cursorFollowCrop immediately, but handleCancelCropEditor() only restores cropRegion. If the user toggles Track cursor or changes safe zone/smoothness and then cancels, those edits still stick.

💡 Minimal fix
+	const cursorFollowSnapshotRef = useRef<CursorFollowCropSettings | null>(null);
+
 	const handleOpenCropEditor = useCallback(() => {
 		cropSnapshotRef.current = { ...cropRegion };
+		cursorFollowSnapshotRef.current = { ...cursorFollowCrop };
 		setShowCropModal(true);
-	}, [cropRegion]);
+	}, [cropRegion, cursorFollowCrop]);

 	const handleCancelCropEditor = useCallback(() => {
 		if (cropSnapshotRef.current) {
 			setCropRegion(cropSnapshotRef.current);
 		}
+		if (cursorFollowSnapshotRef.current) {
+			setCursorFollowCrop(cursorFollowSnapshotRef.current);
+		}
 		setShowCropModal(false);
-	}, []);
+	}, []);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/video-editor/VideoEditor.tsx` around lines 6355 - 6356, The
cancel path only restores cropRegion but not the mutable cursor-follow state, so
when the crop modal is opened you should snapshot the current cursorFollowCrop
(e.g., prevCursorFollowCrop) and any related fields (safe zone, smoothness) and
then in handleCancelCropEditor() restore cursorFollowCrop via
setCursorFollowCrop(prevCursorFollowCrop) (and clear the snapshot after). Add
the snapshot creation where the editor opens and use the snapshot in
handleCancelCropEditor to roll back cursorFollowCrop changes.

Comment on lines +6357 to +6358
cursorTelemetry={cursorTelemetry}
currentTimeMs={currentTime * 1000}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use the same telemetry variant as preview/export.

The crop modal gets raw cursorTelemetry, while VideoPlayback and both export paths use effectiveCursorTelemetry after normalization/loop handling. With looped cursor playback or timeline-adjusted telemetry, the crop editor can preview a different camera path than the actual render.

💡 Minimal fix
-							cursorTelemetry={cursorTelemetry}
+							cursorTelemetry={effectiveCursorTelemetry}
 							currentTimeMs={currentTime * 1000}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
cursorTelemetry={cursorTelemetry}
currentTimeMs={currentTime * 1000}
cursorTelemetry={effectiveCursorTelemetry}
currentTimeMs={currentTime * 1000}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/video-editor/VideoEditor.tsx` around lines 6357 - 6358, The
crop modal is being passed raw cursorTelemetry which can differ from the
normalized/loop-aware path used by VideoPlayback and export; change the prop
passed to the crop editor from cursorTelemetry to the already-computed
effectiveCursorTelemetry (the same value used by VideoPlayback/export) so the
crop preview uses the identical telemetry variant; locate the prop usage in
VideoEditor (prop name cursorTelemetry/currentTimeMs) and replace it to supply
effectiveCursorTelemetry (or call the helper that computes it) so preview/export
paths match.

const timeMs = this.currentVideoTime * 1000;
const cursorTimeMs = cursorTimestamp / 1000;

this.applyCursorFollowCrop(timeMs, layoutCache);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use the cursor timeline when sampling cursor-follow crop.

These call sites pass source-media time into computeCursorFollowCrop, but the crop logic is driven by cursor telemetry. When trims or speed regions remap output time, the crop will follow the wrong cursor sample while cursorOverlay.update(...) still uses cursorTimeMs, so exported framing diverges from the visible cursor.

Suggested fix
-		this.applyCursorFollowCrop(timeMs, layoutCache);
+		this.applyCursorFollowCrop(cursorTimeMs, layoutCache);

Also applies to: 3235-3235

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/exporter/modernFrameRenderer.ts` at line 2984, The call to
applyCursorFollowCrop(timeMs, layoutCache) uses source/output timeMs when
sampling cursor-follow crop but cursor sampling must use the cursor timeline;
update applyCursorFollowCrop (and any callers at the other site) to accept and
pass the timeline-aligned cursor time (cursorTimeMs) or convert timeMs to
cursorTimeMs via the same remapping used by cursorOverlay.update before calling
computeCursorFollowCrop so the crop sampling uses cursorTimeMs (ensure
references to applyCursorFollowCrop, computeCursorFollowCrop, and
cursorOverlay.update are updated accordingly).

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/components/video-editor/VideoEditor.tsx (1)

1175-1222: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Missing cursorFollowCrop in dependency array.

The captureProjectThumbnail callback uses cursorFollowCrop on line 1073 but does not include it in its dependency array. This can cause thumbnails to be generated with stale cursor-follow-crop settings if the setting changes after the callback is created.

🔧 Proposed fix

Add cursorFollowCrop to the dependency array:

 	}, [
 		annotationRegions,
 		autoCaptionSettings,
 		autoCaptions,
 		backgroundBlur,
 		borderRadius,
 		connectZooms,
 		connectedZoomDurationMs,
 		connectedZoomEasing,
 		connectedZoomGapMs,
 		cropRegion,
+		cursorFollowCrop,
 		currentTime,
 		cursorClickBounce,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/video-editor/VideoEditor.tsx` around lines 1175 - 1222, The
captureProjectThumbnail callback uses cursorFollowCrop but the effect's
dependency array (the useCallback/useEffect that defines
captureProjectThumbnail) is missing cursorFollowCrop, causing stale values;
update the dependency array that currently lists annotationRegions,
autoCaptionSettings, ..., zoomClassicMode to include cursorFollowCrop so
captureProjectThumbnail re-creates when cursorFollowCrop changes (look for the
captureProjectThumbnail function reference and the large dependency array near
it).
🧹 Nitpick comments (1)
src/components/video-editor/CropControl.tsx (1)

318-318: 💤 Low value

Redundant conditional expression.

(!followEnabled ? true : true) always evaluates to true, so this line simplifies to !showOutputMode.

♻️ Suggested simplification
-	const showHandles = !showOutputMode && (!followEnabled ? true : true);
+	const showHandles = !showOutputMode;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/video-editor/CropControl.tsx` at line 318, The assignment to
showHandles in CropControl.tsx uses a redundant ternary "(!followEnabled ? true
: true)" which always evaluates to true; replace the entire expression with a
simplified value by setting showHandles to "!showOutputMode" (remove the
unnecessary ternary and redundant checks around followEnabled) so the variable
reflects only the showOutputMode condition.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/video-editor/VideoPlayback.tsx`:
- Around line 2135-2137: The code currently only assigns to
baseMaskRef.current.sourceCrop when it already exists, so when cursor-follow
starts enabled that field may be undefined and downstream rendering misses the
effective crop; update the logic around baseMaskRef (referencing
baseMaskRef.current, sourceCrop, and effectiveCrop) to always initialize
sourceCrop to the effectiveCrop (e.g., set baseMaskRef.current.sourceCrop = {
...effectiveCrop } unconditionally or ensure initialization before cursor-follow
usage), ensuring the property exists whenever cursor-follow or rendering logic
reads it; keep the assignment in the same function where effectiveCrop is
computed so you don't change call sites.

---

Outside diff comments:
In `@src/components/video-editor/VideoEditor.tsx`:
- Around line 1175-1222: The captureProjectThumbnail callback uses
cursorFollowCrop but the effect's dependency array (the useCallback/useEffect
that defines captureProjectThumbnail) is missing cursorFollowCrop, causing stale
values; update the dependency array that currently lists annotationRegions,
autoCaptionSettings, ..., zoomClassicMode to include cursorFollowCrop so
captureProjectThumbnail re-creates when cursorFollowCrop changes (look for the
captureProjectThumbnail function reference and the large dependency array near
it).

---

Nitpick comments:
In `@src/components/video-editor/CropControl.tsx`:
- Line 318: The assignment to showHandles in CropControl.tsx uses a redundant
ternary "(!followEnabled ? true : true)" which always evaluates to true; replace
the entire expression with a simplified value by setting showHandles to
"!showOutputMode" (remove the unnecessary ternary and redundant checks around
followEnabled) so the variable reflects only the showOutputMode condition.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 083ab8d0-6c60-4020-bfcf-827871a42fad

📥 Commits

Reviewing files that changed from the base of the PR and between 1f9912b and 2bfe0d4.

📒 Files selected for processing (12)
  • src/components/video-editor/CropControl.tsx
  • src/components/video-editor/VideoEditor.tsx
  • src/components/video-editor/VideoPlayback.tsx
  • src/components/video-editor/projectPersistence.ts
  • src/components/video-editor/types.ts
  • src/components/video-editor/videoPlayback/cursorFollowCrop.test.ts
  • src/components/video-editor/videoPlayback/cursorFollowCrop.ts
  • src/lib/exporter/frameRenderer.ts
  • src/lib/exporter/gifExporter.ts
  • src/lib/exporter/modernFrameRenderer.ts
  • src/lib/exporter/modernVideoExporter.ts
  • src/lib/exporter/videoExporter.ts

Comment on lines +2135 to +2137
if (baseMaskRef.current.sourceCrop) {
baseMaskRef.current.sourceCrop = { ...effectiveCrop };
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

sourceCrop may never be initialized when cursor-follow is enabled.

The code only mutates baseMaskRef.current.sourceCrop if it already exists, but nothing initializes it. If cursor-follow is enabled from the start, downstream rendering logic expecting sourceCrop won't receive the effective crop coordinates.

Consider initializing sourceCrop unconditionally:

🐛 Proposed fix
-				if (baseMaskRef.current.sourceCrop) {
-					baseMaskRef.current.sourceCrop = { ...effectiveCrop };
-				}
+				baseMaskRef.current.sourceCrop = { ...effectiveCrop };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (baseMaskRef.current.sourceCrop) {
baseMaskRef.current.sourceCrop = { ...effectiveCrop };
}
baseMaskRef.current.sourceCrop = { ...effectiveCrop };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/video-editor/VideoPlayback.tsx` around lines 2135 - 2137, The
code currently only assigns to baseMaskRef.current.sourceCrop when it already
exists, so when cursor-follow starts enabled that field may be undefined and
downstream rendering misses the effective crop; update the logic around
baseMaskRef (referencing baseMaskRef.current, sourceCrop, and effectiveCrop) to
always initialize sourceCrop to the effectiveCrop (e.g., set
baseMaskRef.current.sourceCrop = { ...effectiveCrop } unconditionally or ensure
initialization before cursor-follow usage), ensuring the property exists
whenever cursor-follow or rendering logic reads it; keep the assignment in the
same function where effectiveCrop is computed so you don't change call sites.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants