From de420c3784881150510f5877104d37667d2ec3d4 Mon Sep 17 00:00:00 2001 From: Chirag Date: Wed, 20 May 2026 03:13:03 +0530 Subject: [PATCH] feat(editor): add always-use-default-cursor toggle Adds an "Always use default cursor" switch to the editor Cursor settings. When on, every cursor sample renders as the default arrow at playback and export time, removing arrow / I-beam / hand flicker on existing recordings too. Applied at render time, not capture. Rebuilt on current main after the editor refactor. Per review on #539, the forced shape is the default arrow rather than the hand. Closes #515 --- src/components/video-editor/SettingsPanel.tsx | 13 +++++++ src/components/video-editor/VideoPlayback.tsx | 10 ++++++ .../video-editor/editorPreferences.ts | 5 +++ .../export/buildExportRenderOptions.ts | 1 + .../layout/EditorVideoPreview.tsx | 1 + .../layout/useEditorSettingsPanelProps.ts | 2 ++ .../useEditorPreferencesPersistence.ts | 2 ++ .../presets/useVideoEditorPresets.ts | 2 ++ .../project/useProjectLibraryController.ts | 3 ++ .../project/useProjectLifecycle.ts | 1 + .../project/useProjectSnapshotModel.ts | 2 ++ .../video-editor/projectPersistence.ts | 5 +++ .../video-editor/state/useAppearanceState.ts | 5 +++ .../videoPlayback/cursorRenderer.ts | 36 ++++++++++++++----- src/i18n/locales/de/settings.json | 1 + src/i18n/locales/en/settings.json | 1 + src/i18n/locales/es/settings.json | 1 + src/i18n/locales/fr/settings.json | 1 + src/i18n/locales/it/settings.json | 1 + src/i18n/locales/ko/settings.json | 1 + src/i18n/locales/nl/settings.json | 1 + src/i18n/locales/pt-BR/settings.json | 1 + src/i18n/locales/ru/settings.json | 1 + src/i18n/locales/zh-CN/settings.json | 1 + src/i18n/locales/zh-TW/settings.json | 1 + src/lib/exporter/frameRenderer.ts | 2 ++ src/lib/exporter/gifExporter.ts | 2 ++ src/lib/exporter/modernFrameRenderer.ts | 2 ++ src/lib/exporter/modernVideoExporter.ts | 3 ++ .../nativeStaticLayoutTelemetry.test.ts | 18 ++++++++++ .../exporter/nativeStaticLayoutTelemetry.ts | 10 ++++++ src/lib/exporter/videoExporter.ts | 2 ++ 32 files changed, 130 insertions(+), 8 deletions(-) diff --git a/src/components/video-editor/SettingsPanel.tsx b/src/components/video-editor/SettingsPanel.tsx index 89974d144..eb6bb09da 100644 --- a/src/components/video-editor/SettingsPanel.tsx +++ b/src/components/video-editor/SettingsPanel.tsx @@ -477,6 +477,8 @@ interface SettingsPanelProps { onShowCursorChange?: (enabled: boolean) => void; loopCursor?: boolean; onLoopCursorChange?: (enabled: boolean) => void; + alwaysUsePointerCursor?: boolean; + onAlwaysUsePointerCursorChange?: (enabled: boolean) => void; cursorStyle?: CursorStyle; onCursorStyleChange?: (style: CursorStyle) => void; cursorSize?: number; @@ -923,6 +925,8 @@ export function SettingsPanel({ onShowCursorChange, loopCursor = false, onLoopCursorChange, + alwaysUsePointerCursor = false, + onAlwaysUsePointerCursorChange, cursorStyle = DEFAULT_CURSOR_STYLE, onCursorStyleChange, cursorSize = 5, @@ -1435,6 +1439,7 @@ export function SettingsPanel({ const resetCursorSection = () => { onShowCursorChange?.(initialEditorPreferences.showCursor); onLoopCursorChange?.(initialEditorPreferences.loopCursor); + onAlwaysUsePointerCursorChange?.(initialEditorPreferences.alwaysUsePointerCursor); onCursorStyleChange?.(initialEditorPreferences.cursorStyle); onCursorSizeChange?.(initialEditorPreferences.cursorSize); onCursorSmoothingChange?.(initialEditorPreferences.cursorSmoothing); @@ -2837,6 +2842,14 @@ export function SettingsPanel({ onCheckedChange={onLoopCursorChange} /> + )} diff --git a/src/components/video-editor/VideoPlayback.tsx b/src/components/video-editor/VideoPlayback.tsx index 47a841013..024ac954b 100644 --- a/src/components/video-editor/VideoPlayback.tsx +++ b/src/components/video-editor/VideoPlayback.tsx @@ -261,6 +261,7 @@ interface VideoPlaybackProps { onAnnotationSizeChange?: (id: string, size: { width: number; height: number }) => void; cursorTelemetry?: CursorTelemetryPoint[]; showCursor?: boolean; + alwaysUsePointerCursor?: boolean; cursorStyle?: CursorStyle; cursorSize?: number; cursorSmoothing?: number; @@ -347,6 +348,7 @@ const VideoPlayback = forwardRef( onAnnotationSizeChange, cursorTelemetry = [], showCursor = false, + alwaysUsePointerCursor = false, cursorStyle = DEFAULT_CURSOR_STYLE, cursorSize = DEFAULT_CURSOR_SIZE, cursorSmoothing = DEFAULT_CURSOR_SMOOTHING, @@ -492,6 +494,7 @@ const VideoPlayback = forwardRef( const cursorOverlayRef = useRef(null); const cursorTelemetryRef = useRef([]); const showCursorRef = useRef(showCursor); + const alwaysUsePointerCursorRef = useRef(alwaysUsePointerCursor); const cursorSizeRef = useRef(cursorSize); const cursorStyleRef = useRef(cursorStyle); const cursorSmoothingRef = useRef(cursorSmoothing); @@ -1427,6 +1430,12 @@ const VideoPlayback = forwardRef( requestPausedFrameRefresh(); }, [cursorStyle, requestPausedFrameRefresh]); + useEffect(() => { + alwaysUsePointerCursorRef.current = alwaysUsePointerCursor; + cursorOverlayRef.current?.setAlwaysUsePointerCursor(alwaysUsePointerCursor); + requestPausedFrameRefresh(); + }, [alwaysUsePointerCursor, requestPausedFrameRefresh]); + useEffect(() => { cursorSizeRef.current = cursorSize; requestPausedFrameRefresh(); @@ -1794,6 +1803,7 @@ const VideoPlayback = forwardRef( dotRadius: DEFAULT_CURSOR_CONFIG.dotRadius * cursorSizeRef.current, minViewportScale: 0, style: cursorStyleRef.current, + alwaysUsePointerCursor: alwaysUsePointerCursorRef.current, smoothingFactor: cursorSmoothingRef.current, springTuning: { stiffnessMultiplier: cursorSpringStiffnessMultiplierRef.current, diff --git a/src/components/video-editor/editorPreferences.ts b/src/components/video-editor/editorPreferences.ts index 61ade506f..61109e31d 100644 --- a/src/components/video-editor/editorPreferences.ts +++ b/src/components/video-editor/editorPreferences.ts @@ -26,6 +26,7 @@ type PersistedEditorControls = Pick< | "connectedZoomEasing" | "showCursor" | "loopCursor" + | "alwaysUsePointerCursor" | "cursorStyle" | "cursorSize" | "cursorSmoothing" @@ -103,6 +104,7 @@ export const DEFAULT_EDITOR_PREFERENCES: EditorPreferences = { connectedZoomEasing: DEFAULT_EDITOR_CONTROLS.connectedZoomEasing, showCursor: DEFAULT_EDITOR_CONTROLS.showCursor, loopCursor: DEFAULT_EDITOR_CONTROLS.loopCursor, + alwaysUsePointerCursor: DEFAULT_EDITOR_CONTROLS.alwaysUsePointerCursor, cursorStyle: DEFAULT_EDITOR_CONTROLS.cursorStyle, cursorSize: DEFAULT_EDITOR_CONTROLS.cursorSize, cursorSmoothing: DEFAULT_EDITOR_CONTROLS.cursorSmoothing, @@ -287,6 +289,8 @@ function normalizeEditorControls( connectedZoomEasing: sanitizedRaw.connectedZoomEasing ?? fallback.connectedZoomEasing, showCursor: sanitizedRaw.showCursor ?? fallback.showCursor, loopCursor: sanitizedRaw.loopCursor ?? fallback.loopCursor, + alwaysUsePointerCursor: + sanitizedRaw.alwaysUsePointerCursor ?? fallback.alwaysUsePointerCursor, cursorStyle: sanitizedRaw.cursorStyle ?? fallback.cursorStyle, cursorSize: sanitizedRaw.cursorSize ?? fallback.cursorSize, cursorSmoothing: sanitizedRaw.cursorSmoothing ?? fallback.cursorSmoothing, @@ -343,6 +347,7 @@ function normalizeEditorControls( connectedZoomEasing: normalized.connectedZoomEasing, showCursor: normalized.showCursor, loopCursor: normalized.loopCursor, + alwaysUsePointerCursor: normalized.alwaysUsePointerCursor, cursorStyle: normalized.cursorStyle, cursorSize: normalized.cursorSize, cursorSmoothing: normalized.cursorSmoothing, diff --git a/src/components/video-editor/export/buildExportRenderOptions.ts b/src/components/video-editor/export/buildExportRenderOptions.ts index ae836c5a7..a0cef7eea 100644 --- a/src/components/video-editor/export/buildExportRenderOptions.ts +++ b/src/components/video-editor/export/buildExportRenderOptions.ts @@ -64,6 +64,7 @@ export function buildExportRenderOptions({ zoomRegions: effectiveZoomRegions, cursorTelemetry: effectiveCursorTelemetry, showCursor: effectiveShowCursor, + alwaysUsePointerCursor: appearance.alwaysUsePointerCursor, cursorStyle: appearance.cursorStyle, cursorSize: appearance.cursorSize, cursorSmoothing: appearance.cursorSmoothing, diff --git a/src/components/video-editor/layout/EditorVideoPreview.tsx b/src/components/video-editor/layout/EditorVideoPreview.tsx index 06ff3e1ed..f833c4700 100644 --- a/src/components/video-editor/layout/EditorVideoPreview.tsx +++ b/src/components/video-editor/layout/EditorVideoPreview.tsx @@ -104,6 +104,7 @@ export function EditorVideoPreview({ selectedAnnotationId={timeline.selectedAnnotationId} cursorTelemetry={effectiveCursorTelemetry} showCursor={effectiveShowCursor} + alwaysUsePointerCursor={appearance.alwaysUsePointerCursor} cursorStyle={appearance.cursorStyle} cursorSize={appearance.cursorSize} cursorSmoothing={appearance.cursorSmoothing} diff --git a/src/components/video-editor/layout/useEditorSettingsPanelProps.ts b/src/components/video-editor/layout/useEditorSettingsPanelProps.ts index 4c4c8c696..5730a0927 100644 --- a/src/components/video-editor/layout/useEditorSettingsPanelProps.ts +++ b/src/components/video-editor/layout/useEditorSettingsPanelProps.ts @@ -133,6 +133,8 @@ export function useEditorSettingsPanelProps(input: Input): ComponentProps): Pro ), showCursor: typeof editor.showCursor === "boolean" ? editor.showCursor : true, loopCursor: typeof editor.loopCursor === "boolean" ? editor.loopCursor : false, + alwaysUsePointerCursor: + typeof editor.alwaysUsePointerCursor === "boolean" + ? editor.alwaysUsePointerCursor + : false, cursorStyle: normalizedCursorStyle, cursorClickEffect: normalizeCursorClickEffectStyle( (editor as Partial).cursorClickEffect, diff --git a/src/components/video-editor/state/useAppearanceState.ts b/src/components/video-editor/state/useAppearanceState.ts index 6dbfd4b16..8a5f07dbe 100644 --- a/src/components/video-editor/state/useAppearanceState.ts +++ b/src/components/video-editor/state/useAppearanceState.ts @@ -61,6 +61,9 @@ export function useAppearanceState(preferences: EditorPreferences) { ); const [showCursor, setShowCursor] = useState(preferences.showCursor); const [loopCursor, setLoopCursor] = useState(preferences.loopCursor); + const [alwaysUsePointerCursor, setAlwaysUsePointerCursor] = useState( + preferences.alwaysUsePointerCursor, + ); const [cursorStyle, setCursorStyle] = useState( preferences.cursorStyle ?? DEFAULT_CURSOR_STYLE, ); @@ -150,6 +153,8 @@ export function useAppearanceState(preferences: EditorPreferences) { setShowCursor, loopCursor, setLoopCursor, + alwaysUsePointerCursor, + setAlwaysUsePointerCursor, cursorStyle, setCursorStyle, cursorSize, diff --git a/src/components/video-editor/videoPlayback/cursorRenderer.ts b/src/components/video-editor/videoPlayback/cursorRenderer.ts index bde9bfd66..d1586683f 100644 --- a/src/components/video-editor/videoPlayback/cursorRenderer.ts +++ b/src/components/video-editor/videoPlayback/cursorRenderer.ts @@ -112,6 +112,8 @@ export interface CursorRenderConfig { sway: number; /** Cursor visual style. */ style: CursorStyle; + /** Render every sample as the default arrow cursor. */ + alwaysUsePointerCursor: boolean; } const MIN_CURSOR_VIEWPORT_SCALE = 0; @@ -154,6 +156,7 @@ export const DEFAULT_CURSOR_CONFIG: CursorRenderConfig = { clickEffectDurationMs: DEFAULT_CURSOR_CLICK_EFFECT_DURATION_MS, sway: 0, style: DEFAULT_CURSOR_STYLE, + alwaysUsePointerCursor: false, }; let cursorAssetsPromise: Promise | null = null; let cursorPackAssetsPromise: Promise | null = null; @@ -1236,6 +1239,10 @@ export class PixiCursorOverlay { this.config.sway = clamp(sway, 0, 2); } + setAlwaysUsePointerCursor(enabled: boolean) { + this.config.alwaysUsePointerCursor = enabled; + } + setStyle(style: CursorStyle) { this.config.style = style; if (isStatefulCursorStyle(style)) { @@ -1321,13 +1328,20 @@ export class PixiCursorOverlay { const h = this.config.dotRadius * getCursorViewportScale(viewport.width, this.config.minViewportScale); - const { cursorType, clickSample, clickBounceProgress, clickProgress } = - getCursorVisualState( - samples, - timeMs, - this.config.clickBounceDuration, - this.config.clickEffectDurationMs, - ); + const { + cursorType: rawCursorType, + clickSample, + clickBounceProgress, + clickProgress, + } = getCursorVisualState( + samples, + timeMs, + this.config.clickBounceDuration, + this.config.clickEffectDurationMs, + ); + const cursorType: CursorAssetKey = this.config.alwaysUsePointerCursor + ? "arrow" + : rawCursorType; const projectedClickSample = clickSample ? projectCursorPositionToViewport(clickSample, viewport.sourceCrop) : null; @@ -1630,12 +1644,18 @@ export function drawCursorOnCanvas( const px = viewport.x + smoothedState.x * viewport.width; const py = viewport.y + smoothedState.y * viewport.height; const h = config.dotRadius * getCursorViewportScale(viewport.width, config.minViewportScale); - const { cursorType, clickSample, clickBounceProgress, clickProgress } = getCursorVisualState( + const { + cursorType: rawCursorType, + clickSample, + clickBounceProgress, + clickProgress, + } = getCursorVisualState( samples, timeMs, config.clickBounceDuration, config.clickEffectDurationMs, ); + const cursorType: CursorAssetKey = config.alwaysUsePointerCursor ? "arrow" : rawCursorType; const projectedClickSample = clickSample ? projectCursorPositionToViewport(clickSample, viewport.sourceCrop) : null; diff --git a/src/i18n/locales/de/settings.json b/src/i18n/locales/de/settings.json index 3dae35b70..a531ec15b 100644 --- a/src/i18n/locales/de/settings.json +++ b/src/i18n/locales/de/settings.json @@ -31,6 +31,7 @@ "show": "Anzeigen", "showCursor": "Cursor anzeigen", "loopCursor": "Cursor wiederholen", + "alwaysUsePointerCursor": "Immer Standardcursor verwenden", "cursorStyle": "Cursorstil", "cursorStyleOptions": { "macos": "macOS", diff --git a/src/i18n/locales/en/settings.json b/src/i18n/locales/en/settings.json index 9db0e74eb..54161c143 100644 --- a/src/i18n/locales/en/settings.json +++ b/src/i18n/locales/en/settings.json @@ -37,6 +37,7 @@ "show": "Show", "showCursor": "Show Cursor", "loopCursor": "Loop cursor", + "alwaysUsePointerCursor": "Always use default cursor", "cursorStyle": "Cursor Style", "cursorStyleOptions": { "macos": "macOS", diff --git a/src/i18n/locales/es/settings.json b/src/i18n/locales/es/settings.json index 5eafdfdde..c99d69245 100644 --- a/src/i18n/locales/es/settings.json +++ b/src/i18n/locales/es/settings.json @@ -31,6 +31,7 @@ "show": "Mostrar", "showCursor": "Mostrar cursor", "loopCursor": "Cursor en bucle", + "alwaysUsePointerCursor": "Usar siempre el cursor predeterminado", "cursorStyle": "Estilo del cursor", "cursorStyleOptions": { "macos": "macOS", diff --git a/src/i18n/locales/fr/settings.json b/src/i18n/locales/fr/settings.json index 26283e789..ce2493dd6 100644 --- a/src/i18n/locales/fr/settings.json +++ b/src/i18n/locales/fr/settings.json @@ -31,6 +31,7 @@ "show": "Afficher", "showCursor": "Afficher le curseur", "loopCursor": "Boucler le curseur", + "alwaysUsePointerCursor": "Toujours utiliser le curseur par défaut", "cursorStyle": "Style du curseur", "cursorStyleOptions": { "macos": "macOS", diff --git a/src/i18n/locales/it/settings.json b/src/i18n/locales/it/settings.json index b43fdba9a..988f562a2 100644 --- a/src/i18n/locales/it/settings.json +++ b/src/i18n/locales/it/settings.json @@ -31,6 +31,7 @@ "show": "Mostra", "showCursor": "Mostra cursore", "loopCursor": "Cursore in loop", + "alwaysUsePointerCursor": "Usa sempre il cursore predefinito", "cursorStyle": "Stile cursore", "cursorStyleOptions": { "macos": "macOS", diff --git a/src/i18n/locales/ko/settings.json b/src/i18n/locales/ko/settings.json index 33488fc0b..b2e8ce59e 100644 --- a/src/i18n/locales/ko/settings.json +++ b/src/i18n/locales/ko/settings.json @@ -31,6 +31,7 @@ "show": "표시", "showCursor": "커서 표시", "loopCursor": "커서 반복", + "alwaysUsePointerCursor": "항상 기본 커서 사용", "cursorStyle": "커서 스타일", "cursorStyleOptions": { "macos": "macOS", diff --git a/src/i18n/locales/nl/settings.json b/src/i18n/locales/nl/settings.json index 9d863cb53..b7ea48977 100644 --- a/src/i18n/locales/nl/settings.json +++ b/src/i18n/locales/nl/settings.json @@ -31,6 +31,7 @@ "show": "Tonen", "showCursor": "Cursor tonen", "loopCursor": "Cursor herhalen", + "alwaysUsePointerCursor": "Altijd standaardcursor gebruiken", "cursorStyle": "Cursorstijl", "cursorStyleOptions": { "macos": "macOS", diff --git a/src/i18n/locales/pt-BR/settings.json b/src/i18n/locales/pt-BR/settings.json index 7541913a7..dd972559b 100644 --- a/src/i18n/locales/pt-BR/settings.json +++ b/src/i18n/locales/pt-BR/settings.json @@ -31,6 +31,7 @@ "show": "Mostrar", "showCursor": "Mostrar cursor", "loopCursor": "Loop do cursor", + "alwaysUsePointerCursor": "Usar sempre o cursor padrão", "cursorStyle": "Estilo do cursor", "cursorStyleOptions": { "macos": "macOS", diff --git a/src/i18n/locales/ru/settings.json b/src/i18n/locales/ru/settings.json index aadb70e02..26dc12df7 100644 --- a/src/i18n/locales/ru/settings.json +++ b/src/i18n/locales/ru/settings.json @@ -31,6 +31,7 @@ "show": "Показать", "showCursor": "Показать курсор", "loopCursor": "Зациклить курсор", + "alwaysUsePointerCursor": "Всегда использовать курсор по умолчанию", "cursorStyle": "Стиль курсора", "cursorStyleOptions": { "macos": "macOS", diff --git a/src/i18n/locales/zh-CN/settings.json b/src/i18n/locales/zh-CN/settings.json index ba4a68c6a..55db861fe 100644 --- a/src/i18n/locales/zh-CN/settings.json +++ b/src/i18n/locales/zh-CN/settings.json @@ -31,6 +31,7 @@ "show": "显示", "showCursor": "显示光标", "loopCursor": "循环光标", + "alwaysUsePointerCursor": "始终使用默认光标", "cursorStyle": "光标样式", "cursorStyleOptions": { "macos": "macOS", diff --git a/src/i18n/locales/zh-TW/settings.json b/src/i18n/locales/zh-TW/settings.json index b7b944030..646e480ae 100644 --- a/src/i18n/locales/zh-TW/settings.json +++ b/src/i18n/locales/zh-TW/settings.json @@ -31,6 +31,7 @@ "show": "顯示", "showCursor": "顯示游標", "loopCursor": "游標循環", + "alwaysUsePointerCursor": "始終使用預設游標", "cursorStyle": "游標樣式", "cursorStyleOptions": { "macos": "macOS", diff --git a/src/lib/exporter/frameRenderer.ts b/src/lib/exporter/frameRenderer.ts index d42f6a69b..ce8ceb40c 100644 --- a/src/lib/exporter/frameRenderer.ts +++ b/src/lib/exporter/frameRenderer.ts @@ -113,6 +113,7 @@ interface FrameRenderConfig { previewHeight?: number; cursorTelemetry?: CursorTelemetryPoint[]; showCursor?: boolean; + alwaysUsePointerCursor?: boolean; cursorStyle?: CursorStyle; cursorSize?: number; cursorSmoothing?: number; @@ -399,6 +400,7 @@ export class FrameRenderer { this.cursorOverlay = new PixiCursorOverlay({ dotRadius: DEFAULT_CURSOR_CONFIG.dotRadius * (this.config.cursorSize ?? 1.4), style: this.config.cursorStyle ?? "tahoe", + alwaysUsePointerCursor: this.config.alwaysUsePointerCursor ?? false, smoothingFactor: this.config.cursorSmoothing ?? DEFAULT_CURSOR_CONFIG.smoothingFactor, springTuning: { diff --git a/src/lib/exporter/gifExporter.ts b/src/lib/exporter/gifExporter.ts index 2df730000..eadb73110 100644 --- a/src/lib/exporter/gifExporter.ts +++ b/src/lib/exporter/gifExporter.ts @@ -66,6 +66,7 @@ interface GifExporterConfig { autoCaptionSettings?: AutoCaptionSettings; cursorTelemetry?: CursorTelemetryPoint[]; showCursor?: boolean; + alwaysUsePointerCursor?: boolean; cursorStyle?: CursorStyle; cursorSize?: number; cursorSmoothing?: number; @@ -170,6 +171,7 @@ export function buildGifFrameRendererConfig( previewHeight: config.previewHeight, cursorTelemetry: config.cursorTelemetry, showCursor: config.showCursor, + alwaysUsePointerCursor: config.alwaysUsePointerCursor, cursorStyle: config.cursorStyle, cursorSize: config.cursorSize, cursorSmoothing: config.cursorSmoothing, diff --git a/src/lib/exporter/modernFrameRenderer.ts b/src/lib/exporter/modernFrameRenderer.ts index 805af27c0..9f9a01068 100644 --- a/src/lib/exporter/modernFrameRenderer.ts +++ b/src/lib/exporter/modernFrameRenderer.ts @@ -132,6 +132,7 @@ interface FrameRenderConfig { previewHeight?: number; cursorTelemetry?: CursorTelemetryPoint[]; showCursor?: boolean; + alwaysUsePointerCursor?: boolean; cursorStyle?: CursorStyle; cursorSize?: number; cursorSmoothing?: number; @@ -556,6 +557,7 @@ export class FrameRenderer { this.cursorOverlay = new PixiCursorOverlay({ dotRadius: DEFAULT_CURSOR_CONFIG.dotRadius * (this.config.cursorSize ?? 1.4), style: this.config.cursorStyle ?? "tahoe", + alwaysUsePointerCursor: this.config.alwaysUsePointerCursor ?? false, smoothingFactor: this.config.cursorSmoothing ?? DEFAULT_CURSOR_CONFIG.smoothingFactor, springTuning: { diff --git a/src/lib/exporter/modernVideoExporter.ts b/src/lib/exporter/modernVideoExporter.ts index 10fdf2755..78e8ad15c 100644 --- a/src/lib/exporter/modernVideoExporter.ts +++ b/src/lib/exporter/modernVideoExporter.ts @@ -125,6 +125,7 @@ interface VideoExporterConfig extends ExportConfig { autoCaptionSettings?: AutoCaptionSettings; cursorTelemetry?: CursorTelemetryPoint[]; showCursor?: boolean; + alwaysUsePointerCursor?: boolean; cursorStyle?: CursorStyle; cursorSize?: number; cursorSmoothing?: number; @@ -638,6 +639,7 @@ export class ModernVideoExporter { previewHeight: this.config.previewHeight, cursorTelemetry: this.config.cursorTelemetry, showCursor: this.config.showCursor, + alwaysUsePointerCursor: this.config.alwaysUsePointerCursor, cursorStyle: this.config.cursorStyle, cursorSize: this.config.cursorSize, cursorSmoothing: this.config.cursorSmoothing, @@ -2283,6 +2285,7 @@ export class ModernVideoExporter { clickBounce: this.config.cursorClickBounce, clickBounceDurationMs: this.config.cursorClickBounceDuration, sourceCrop: this.config.cropRegion, + alwaysUsePointerCursor: this.config.alwaysUsePointerCursor, }); } diff --git a/src/lib/exporter/nativeStaticLayoutTelemetry.test.ts b/src/lib/exporter/nativeStaticLayoutTelemetry.test.ts index 5157b5205..999203ab5 100644 --- a/src/lib/exporter/nativeStaticLayoutTelemetry.test.ts +++ b/src/lib/exporter/nativeStaticLayoutTelemetry.test.ts @@ -110,6 +110,24 @@ describe("buildNativeStaticLayoutCursorTelemetry", () => { expect(resampled?.some((sample) => (sample.bounceScale ?? 1) < 1)).toBe(true); }); + it("keeps recorded cursor type boundaries when forcing the pointer cursor", () => { + const samples = [ + { timeMs: 0, cx: 0.4, cy: 0.6, cursorType: "arrow" as const }, + { timeMs: 500, cx: 0.4, cy: 0.6, cursorType: "text" as const }, + ]; + const options = { frameRate: 10, durationSec: 1 }; + const plain = buildNativeStaticLayoutCursorTelemetry(samples, options); + const forced = buildNativeStaticLayoutCursorTelemetry(samples, { + ...options, + alwaysUsePointerCursor: true, + }); + + expect(forced?.map((sample) => sample.timeMs)).toEqual(plain?.map((sample) => sample.timeMs)); + expect(forced).toHaveLength(2); + expect(forced?.every((sample) => sample.cursorType === "arrow")).toBe(true); + expect(forced?.every((sample) => sample.cursorTypeIndex === 0)).toBe(true); + }); + it("projects cursor samples into the cropped viewport and marks out-of-crop samples hidden", () => { const resampled = buildNativeStaticLayoutCursorTelemetry( [ diff --git a/src/lib/exporter/nativeStaticLayoutTelemetry.ts b/src/lib/exporter/nativeStaticLayoutTelemetry.ts index 7a8cda118..01954fe96 100644 --- a/src/lib/exporter/nativeStaticLayoutTelemetry.ts +++ b/src/lib/exporter/nativeStaticLayoutTelemetry.ts @@ -18,6 +18,7 @@ export type NativeStaticLayoutCursorTelemetryOptions = { clickBounce?: number; clickBounceDurationMs?: number; sourceCrop?: CropRegion; + alwaysUsePointerCursor?: boolean; }; const CURSOR_POSITION_EPSILON = 0.00001; @@ -204,5 +205,14 @@ export function buildNativeStaticLayoutCursorTelemetry( pushCursorSample(resampled, sampleCursorAt(durationMs)); } + if (options.alwaysUsePointerCursor) { + const arrowIndex = getCursorTypeIndex("arrow"); + return resampled.map((sample) => ({ + ...sample, + cursorType: "arrow" as const, + cursorTypeIndex: arrowIndex, + })); + } + return resampled; } diff --git a/src/lib/exporter/videoExporter.ts b/src/lib/exporter/videoExporter.ts index 27e48bb44..9d06d5a2e 100644 --- a/src/lib/exporter/videoExporter.ts +++ b/src/lib/exporter/videoExporter.ts @@ -75,6 +75,7 @@ interface VideoExporterConfig extends ExportConfig { autoCaptionSettings?: AutoCaptionSettings; cursorTelemetry?: CursorTelemetryPoint[]; showCursor?: boolean; + alwaysUsePointerCursor?: boolean; cursorStyle?: CursorStyle; cursorSize?: number; cursorSmoothing?: number; @@ -249,6 +250,7 @@ export class VideoExporter { previewHeight: this.config.previewHeight, cursorTelemetry: this.config.cursorTelemetry, showCursor: this.config.showCursor, + alwaysUsePointerCursor: this.config.alwaysUsePointerCursor, cursorStyle: this.config.cursorStyle, cursorSize: this.config.cursorSize, cursorSmoothing: this.config.cursorSmoothing,