Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,10 @@ export class MacNativeCursorRecordingSession implements CursorRecordingSession {
private readyReject: ((error: Error) => void) | null = null;
private readyTimer: NodeJS.Timeout | null = null;
private previousLeftButtonDown = false;
private consecutiveOutsideSamples = 0;
// Only hide after this many consecutive out-of-bounds samples (≈100ms at 33ms interval).
// Fast swipes that briefly exit the display are clipped by clip-path instead of disappearing.
private static readonly OUTSIDE_HIDE_THRESHOLD = 3;

constructor(private readonly options: MacNativeCursorRecordingSessionOptions) {}

Expand All @@ -186,6 +190,7 @@ export class MacNativeCursorRecordingSession implements CursorRecordingSession {
this.lineBuffer = "";
this.startTimeMs = this.options.startTimeMs ?? Date.now();
this.previousLeftButtonDown = false;
this.consecutiveOutsideSamples = 0;

try {
systemPreferences.isTrustedAccessibilityClient(true);
Expand Down Expand Up @@ -325,6 +330,19 @@ export class MacNativeCursorRecordingSession implements CursorRecordingSession {
const height = Math.max(1, bounds.height);
const normalizedX = (cursor.x - bounds.x) / width;
const normalizedY = (cursor.y - bounds.y) / height;
const isOutsideDisplay =
normalizedX < 0 || normalizedX > 1 || normalizedY < 0 || normalizedY > 1;
// Fast swipes that briefly exit the display (<THRESHOLD samples) are handled by
// clip-path — the cursor clips to the canvas edge instead of snapping invisible.
// Sustained exits (≥THRESHOLD samples, ≈100ms) mark visible=false to prevent
// ghost cursors and motion trails from multi-display movement.
if (isOutsideDisplay) {
this.consecutiveOutsideSamples++;
} else {
this.consecutiveOutsideSamples = 0;
}
const visible =
this.consecutiveOutsideSamples < MacNativeCursorRecordingSession.OUTSIDE_HIDE_THRESHOLD;
const interactionType =
leftButtonPressed || (leftButtonDown && !this.previousLeftButtonDown)
? "click"
Expand All @@ -337,7 +355,7 @@ export class MacNativeCursorRecordingSession implements CursorRecordingSession {
timeMs: Math.max(0, timestampMs - this.startTimeMs),
cx: clamp(normalizedX, 0, 1),
cy: clamp(normalizedY, 0, 1),
visible: true,
visible,
interactionType,
...(cursorType ? { cursorType } : {}),
});
Expand Down
147 changes: 84 additions & 63 deletions src/components/video-editor/SettingsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,8 @@ interface SettingsPanelProps {
onCursorMotionBlurChange?: (blur: number) => void;
cursorClickBounce?: number;
onCursorClickBounceChange?: (bounce: number) => void;
cursorClipToBounds?: boolean;
onCursorClipToBoundsChange?: (clip: boolean) => void;
hasCursorData?: boolean;
showCursorSettings?: boolean;
}
Expand Down Expand Up @@ -416,6 +418,8 @@ export function SettingsPanel({
onCursorMotionBlurChange,
cursorClickBounce = 2.5,
onCursorClickBounceChange,
cursorClipToBounds = true,
onCursorClipToBoundsChange,
hasCursorData = false,
showCursorSettings = true,
}: SettingsPanelProps) {
Expand Down Expand Up @@ -1381,86 +1385,103 @@ export function SettingsPanel({
{activePanelMode === "cursor" && showCursorSettings && hasCursorData && (
<div className="p-2 rounded-lg editor-control-surface mt-2 space-y-3">
<div className="flex items-center justify-between">
<div className="text-[10px] font-medium text-slate-300">Show Cursor</div>
<div className="text-[10px] font-medium text-slate-300">
{t("cursor.show")}
</div>
<Switch
checked={showCursor}
onCheckedChange={onShowCursorChange}
className="data-[state=checked]:bg-[#34B27B] scale-90"
/>
</div>
{showCursor && (
<div className="grid grid-cols-2 gap-2">
<div className="p-2 rounded-lg bg-white/5 border border-white/5">
<div className="flex items-center justify-between mb-1">
<div className="text-[10px] font-medium text-slate-300">Size</div>
<span className="text-[10px] text-slate-500 font-mono">
{cursorSize.toFixed(1)}
</span>
<>
<div className="flex items-center justify-between">
<div className="text-[10px] font-medium text-slate-300">
{t("cursor.clipToBounds")}
</div>
<Slider
value={[cursorSize]}
onValueChange={(values) => onCursorSizeChange?.(values[0])}
min={0.5}
max={10}
step={0.1}
className="w-full [&_[role=slider]]:bg-[#34B27B] [&_[role=slider]]:border-[#34B27B] [&_[role=slider]]:h-3 [&_[role=slider]]:w-3"
<Switch
checked={cursorClipToBounds}
onCheckedChange={onCursorClipToBoundsChange}
className="data-[state=checked]:bg-[#34B27B] scale-90"
aria-label={t("cursor.clipToBounds")}
/>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
</div>
<div className="p-2 rounded-lg bg-white/5 border border-white/5">
<div className="flex items-center justify-between mb-1">
<div className="text-[10px] font-medium text-slate-300">
Smoothing
<div className="grid grid-cols-2 gap-2">
<div className="p-2 rounded-lg bg-white/5 border border-white/5">
<div className="flex items-center justify-between mb-1">
<div className="text-[10px] font-medium text-slate-300">
{t("cursor.size")}
</div>
<span className="text-[10px] text-slate-500 font-mono">
{cursorSize.toFixed(1)}
</span>
</div>
<span className="text-[10px] text-slate-500 font-mono">
{Math.round(cursorSmoothing * 100)}%
</span>
<Slider
value={[cursorSize]}
onValueChange={(values) => onCursorSizeChange?.(values[0])}
min={0.5}
max={10}
step={0.1}
className="w-full [&_[role=slider]]:bg-[#34B27B] [&_[role=slider]]:border-[#34B27B] [&_[role=slider]]:h-3 [&_[role=slider]]:w-3"
/>
</div>
<Slider
value={[cursorSmoothing]}
onValueChange={(values) => onCursorSmoothingChange?.(values[0])}
min={0}
max={1}
step={0.01}
className="w-full [&_[role=slider]]:bg-[#34B27B] [&_[role=slider]]:border-[#34B27B] [&_[role=slider]]:h-3 [&_[role=slider]]:w-3"
/>
</div>
<div className="p-2 rounded-lg bg-white/5 border border-white/5">
<div className="flex items-center justify-between mb-1">
<div className="text-[10px] font-medium text-slate-300">
Motion Blur
<div className="p-2 rounded-lg bg-white/5 border border-white/5">
<div className="flex items-center justify-between mb-1">
<div className="text-[10px] font-medium text-slate-300">
{t("cursor.smoothing")}
</div>
<span className="text-[10px] text-slate-500 font-mono">
{Math.round(cursorSmoothing * 100)}%
</span>
</div>
<span className="text-[10px] text-slate-500 font-mono">
{Math.round(cursorMotionBlur * 100)}%
</span>
<Slider
value={[cursorSmoothing]}
onValueChange={(values) => onCursorSmoothingChange?.(values[0])}
min={0}
max={1}
step={0.01}
className="w-full [&_[role=slider]]:bg-[#34B27B] [&_[role=slider]]:border-[#34B27B] [&_[role=slider]]:h-3 [&_[role=slider]]:w-3"
/>
</div>
<Slider
value={[cursorMotionBlur]}
onValueChange={(values) => onCursorMotionBlurChange?.(values[0])}
min={0}
max={1}
step={0.01}
className="w-full [&_[role=slider]]:bg-[#34B27B] [&_[role=slider]]:border-[#34B27B] [&_[role=slider]]:h-3 [&_[role=slider]]:w-3"
/>
</div>
<div className="p-2 rounded-lg bg-white/5 border border-white/5">
<div className="flex items-center justify-between mb-1">
<div className="text-[10px] font-medium text-slate-300">
Click Bounce
<div className="p-2 rounded-lg bg-white/5 border border-white/5">
<div className="flex items-center justify-between mb-1">
<div className="text-[10px] font-medium text-slate-300">
{t("cursor.motionBlur")}
</div>
<span className="text-[10px] text-slate-500 font-mono">
{Math.round(cursorMotionBlur * 100)}%
</span>
</div>
<span className="text-[10px] text-slate-500 font-mono">
{cursorClickBounce.toFixed(1)}
</span>
<Slider
value={[cursorMotionBlur]}
onValueChange={(values) => onCursorMotionBlurChange?.(values[0])}
min={0}
max={1}
step={0.01}
className="w-full [&_[role=slider]]:bg-[#34B27B] [&_[role=slider]]:border-[#34B27B] [&_[role=slider]]:h-3 [&_[role=slider]]:w-3"
/>
</div>
<div className="p-2 rounded-lg bg-white/5 border border-white/5">
<div className="flex items-center justify-between mb-1">
<div className="text-[10px] font-medium text-slate-300">
{t("cursor.clickBounce")}
</div>
<span className="text-[10px] text-slate-500 font-mono">
{cursorClickBounce.toFixed(1)}
</span>
</div>
<Slider
value={[cursorClickBounce]}
onValueChange={(values) => onCursorClickBounceChange?.(values[0])}
min={0}
max={5}
step={0.1}
className="w-full [&_[role=slider]]:bg-[#34B27B] [&_[role=slider]]:border-[#34B27B] [&_[role=slider]]:h-3 [&_[role=slider]]:w-3"
/>
</div>
<Slider
value={[cursorClickBounce]}
onValueChange={(values) => onCursorClickBounceChange?.(values[0])}
min={0}
max={5}
step={0.1}
className="w-full [&_[role=slider]]:bg-[#34B27B] [&_[role=slider]]:border-[#34B27B] [&_[role=slider]]:h-3 [&_[role=slider]]:w-3"
/>
</div>
</div>
</>
)}
</div>
)}
Expand Down
8 changes: 8 additions & 0 deletions src/components/video-editor/VideoEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ import {
DEFAULT_ANNOTATION_STYLE,
DEFAULT_BLUR_DATA,
DEFAULT_CURSOR_CLICK_BOUNCE,
DEFAULT_CURSOR_CLIP_TO_BOUNDS,
DEFAULT_CURSOR_MOTION_BLUR,
DEFAULT_CURSOR_SIZE,
DEFAULT_CURSOR_SMOOTHING,
Expand Down Expand Up @@ -240,6 +241,7 @@ export default function VideoEditor() {
const [cursorSmoothing, setCursorSmoothing] = useState(DEFAULT_CURSOR_SMOOTHING);
const [cursorMotionBlur, setCursorMotionBlur] = useState(DEFAULT_CURSOR_MOTION_BLUR);
const [cursorClickBounce, setCursorClickBounce] = useState(DEFAULT_CURSOR_CLICK_BOUNCE);
const [cursorClipToBounds, setCursorClipToBounds] = useState(DEFAULT_CURSOR_CLIP_TO_BOUNDS);
const [nativePlatform, setNativePlatform] = useState<NativePlatform | null>(null);
const [recordingCursorCaptureMode, setRecordingCursorCaptureMode] =
useState<CursorCaptureMode | null>(null);
Expand Down Expand Up @@ -1612,6 +1614,7 @@ export default function VideoEditor() {
cursorSmoothing,
cursorMotionBlur,
cursorClickBounce,
cursorClipToBounds,
annotationRegions,
webcamLayoutPreset,
webcamMaskShape,
Expand Down Expand Up @@ -1769,6 +1772,7 @@ export default function VideoEditor() {
cursorSmoothing,
cursorMotionBlur,
cursorClickBounce,
cursorClipToBounds,
annotationRegions,
webcamLayoutPreset,
webcamMaskShape,
Expand Down Expand Up @@ -1884,6 +1888,7 @@ export default function VideoEditor() {
cursorSmoothing,
cursorMotionBlur,
cursorClickBounce,
cursorClipToBounds,
t,
],
);
Expand Down Expand Up @@ -2161,6 +2166,7 @@ export default function VideoEditor() {
cursorSmoothing={cursorSmoothing}
cursorMotionBlur={cursorMotionBlur}
cursorClickBounce={cursorClickBounce}
cursorClipToBounds={cursorClipToBounds}
/>
</div>
</div>
Expand Down Expand Up @@ -2313,6 +2319,8 @@ export default function VideoEditor() {
onCursorMotionBlurChange={setCursorMotionBlur}
cursorClickBounce={cursorClickBounce}
onCursorClickBounceChange={setCursorClickBounce}
cursorClipToBounds={cursorClipToBounds}
onCursorClipToBoundsChange={setCursorClipToBounds}
hasCursorData={
cursorTelemetry.length > 0 || hasNativeCursorRecordingData(cursorRecordingData)
}
Expand Down
Loading