feat: add windows cursor preview diagnostics

This commit is contained in:
EtienneLescot
2026-05-05 10:16:01 +02:00
parent 28ff0fb7bf
commit bb0dec7344
13 changed files with 1713 additions and 29 deletions
+10 -9
View File
@@ -26,10 +26,10 @@ import {
type WebcamSizePreset,
} from "@/lib/compositeLayout";
import {
getNativeCursorDisplayMetrics,
hasNativeCursorRecordingData,
projectNativeCursorToStage,
resolveInterpolatedNativeCursorFrame,
resolveNativeCursorRenderAsset,
} from "@/lib/cursor/nativeCursor";
import { classifyWallpaper, DEFAULT_WALLPAPER, resolveImageWallpaperUrl } from "@/lib/wallpaper";
import { getCssClipPath } from "@/lib/webcamMaskShapes";
@@ -1324,19 +1324,20 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
sample: frame.sample,
});
if (projectedPoint) {
const metrics = getNativeCursorDisplayMetrics(
const renderAsset = resolveNativeCursorRenderAsset(
frame.asset,
window.devicePixelRatio || 1,
frame.sample,
);
const scale = Math.max(0, cursorSizeRef.current);
if (nativeCursorImg.dataset.cursorId !== frame.asset.id) {
nativeCursorImg.src = frame.asset.imageDataUrl;
nativeCursorImg.dataset.cursorId = frame.asset.id;
if (nativeCursorImg.dataset.cursorId !== renderAsset.id) {
nativeCursorImg.src = renderAsset.imageDataUrl;
nativeCursorImg.dataset.cursorId = renderAsset.id;
}
nativeCursorImg.style.left = `${projectedPoint.x - metrics.hotspotX * scale}px`;
nativeCursorImg.style.top = `${projectedPoint.y - metrics.hotspotY * scale}px`;
nativeCursorImg.style.width = `${metrics.width * scale}px`;
nativeCursorImg.style.height = `${metrics.height * scale}px`;
nativeCursorImg.style.left = `${projectedPoint.x - renderAsset.hotspotX * scale}px`;
nativeCursorImg.style.top = `${projectedPoint.y - renderAsset.hotspotY * scale}px`;
nativeCursorImg.style.width = `${renderAsset.width * scale}px`;
nativeCursorImg.style.height = `${renderAsset.height * scale}px`;
nativeCursorImg.style.display = "block";
} else {
nativeCursorImg.style.display = "none";
+128
View File
@@ -1,9 +1,20 @@
import { type Container, Point } from "pixi.js";
import crosshairUrl from "@/assets/cursors/Cursor=Cross.svg";
import arrowUrl from "@/assets/cursors/Cursor=Default.svg";
import pointerUrl from "@/assets/cursors/Cursor=Hand-(Pointing).svg";
import notAllowedUrl from "@/assets/cursors/Cursor=Menu.svg";
import moveUrl from "@/assets/cursors/Cursor=Move.svg";
import resizeNeswUrl from "@/assets/cursors/Cursor=Resize-North-East-South-West.svg";
import resizeNsUrl from "@/assets/cursors/Cursor=Resize-North-South.svg";
import resizeNwseUrl from "@/assets/cursors/Cursor=Resize-North-West-South-East.svg";
import resizeEwUrl from "@/assets/cursors/Cursor=Resize-West-East.svg";
import textUrl from "@/assets/cursors/Cursor=Text-Cursor.svg";
import type { CropRegion } from "@/components/video-editor/types";
import type {
CursorRecordingData,
CursorRecordingSample,
NativeCursorAsset,
NativeCursorType,
} from "@/native/contracts";
export interface ActiveNativeCursorFrame {
@@ -23,6 +34,87 @@ function clamp(value: number, min: number, max: number) {
return Math.min(max, Math.max(min, value));
}
interface PrettyNativeCursorAsset {
imageDataUrl: string;
width: number;
height: number;
hotspotX: number;
hotspotY: number;
}
const PRETTY_NATIVE_CURSOR_ASSETS: Partial<Record<NativeCursorType, PrettyNativeCursorAsset>> = {
arrow: {
imageDataUrl: arrowUrl,
width: 32,
height: 32,
hotspotX: 5.8,
hotspotY: 3.2,
},
text: {
imageDataUrl: textUrl,
width: 32,
height: 32,
hotspotX: 16,
hotspotY: 16,
},
pointer: {
imageDataUrl: pointerUrl,
width: 32,
height: 32,
hotspotX: 11.8,
hotspotY: 2.6,
},
crosshair: {
imageDataUrl: crosshairUrl,
width: 32,
height: 32,
hotspotX: 16,
hotspotY: 16,
},
"resize-ew": {
imageDataUrl: resizeEwUrl,
width: 32,
height: 32,
hotspotX: 16,
hotspotY: 16,
},
"resize-ns": {
imageDataUrl: resizeNsUrl,
width: 32,
height: 32,
hotspotX: 16,
hotspotY: 16,
},
"resize-nesw": {
imageDataUrl: resizeNeswUrl,
width: 32,
height: 32,
hotspotX: 16,
hotspotY: 16,
},
"resize-nwse": {
imageDataUrl: resizeNwseUrl,
width: 32,
height: 32,
hotspotX: 16,
hotspotY: 16,
},
move: {
imageDataUrl: moveUrl,
width: 32,
height: 32,
hotspotX: 16,
hotspotY: 16,
},
"not-allowed": {
imageDataUrl: notAllowedUrl,
width: 32,
height: 32,
hotspotX: 16,
hotspotY: 16,
},
};
export function hasNativeCursorRecordingData(
recordingData: CursorRecordingData | null | undefined,
): recordingData is CursorRecordingData {
@@ -169,3 +261,39 @@ export function getNativeCursorDisplayMetrics(asset: NativeCursorAsset, deviceSc
hotspotY: asset.hotspotY / scaleFactor,
};
}
export function resolvePrettyNativeCursorAsset(
asset: NativeCursorAsset,
sample?: CursorRecordingSample,
) {
const cursorType = sample?.cursorType ?? asset.cursorType ?? null;
return cursorType ? (PRETTY_NATIVE_CURSOR_ASSETS[cursorType] ?? null) : null;
}
export function resolveNativeCursorRenderAsset(
asset: NativeCursorAsset,
deviceScaleFactor: number,
sample?: CursorRecordingSample,
) {
const prettyAsset = resolvePrettyNativeCursorAsset(asset, sample);
if (prettyAsset) {
return {
id: `pretty:${sample?.cursorType ?? asset.cursorType}`,
imageDataUrl: prettyAsset.imageDataUrl,
width: prettyAsset.width,
height: prettyAsset.height,
hotspotX: prettyAsset.hotspotX,
hotspotY: prettyAsset.hotspotY,
};
}
const metrics = getNativeCursorDisplayMetrics(asset, deviceScaleFactor);
return {
id: asset.id,
imageDataUrl: asset.imageDataUrl,
width: metrics.width,
height: metrics.height,
hotspotX: metrics.hotspotX,
hotspotY: metrics.hotspotY,
};
}
+13 -9
View File
@@ -57,13 +57,13 @@ import {
type StyledRenderRect,
} from "@/lib/compositeLayout";
import {
getNativeCursorDisplayMetrics,
projectNativeCursorToStage,
resolveInterpolatedNativeCursorFrame,
resolveNativeCursorRenderAsset,
} from "@/lib/cursor/nativeCursor";
import { BackgroundLoadError, classifyWallpaper, resolveImageWallpaperUrl } from "@/lib/wallpaper";
import { drawCanvasClipPath } from "@/lib/webcamMaskShapes";
import type { CursorRecordingData, NativeCursorAsset } from "@/native/contracts";
import type { CursorRecordingData } from "@/native/contracts";
import { renderAnnotations } from "./annotationRenderer";
import {
getLinearGradientPoints,
@@ -585,19 +585,23 @@ export class FrameRenderer {
return;
}
const image = await this.getCursorImage(activeNativeCursor.asset);
const metrics = getNativeCursorDisplayMetrics(activeNativeCursor.asset, 1);
const renderAsset = resolveNativeCursorRenderAsset(
activeNativeCursor.asset,
1,
activeNativeCursor.sample,
);
const image = await this.getCursorImage(renderAsset);
const scale = Math.max(0, this.config.cursorScale ?? 1);
this.compositeCtx.drawImage(
image,
projectedPoint.x - metrics.hotspotX * scale,
projectedPoint.y - metrics.hotspotY * scale,
metrics.width * scale,
metrics.height * scale,
projectedPoint.x - renderAsset.hotspotX * scale,
projectedPoint.y - renderAsset.hotspotY * scale,
renderAsset.width * scale,
renderAsset.height * scale,
);
}
private async getCursorImage(asset: NativeCursorAsset) {
private async getCursorImage(asset: { id: string; imageDataUrl: string }) {
const cachedImage = this.cursorImageCache.get(asset.id);
if (cachedImage) {
return cachedImage;
+17
View File
@@ -3,6 +3,21 @@ export const NATIVE_BRIDGE_VERSION = 1;
export type NativePlatform = "darwin" | "win32" | "linux";
export type CursorProviderKind = "native" | "none";
export type NativeCursorType =
| "arrow"
| "text"
| "pointer"
| "crosshair"
| "resize-ew"
| "resize-ns"
| "resize-nesw"
| "resize-nwse"
| "move"
| "not-allowed"
| "wait"
| "app-starting"
| "help"
| "up-arrow";
export interface CursorTelemetryPoint {
timeMs: number;
@@ -13,6 +28,7 @@ export interface CursorTelemetryPoint {
export interface CursorRecordingSample extends CursorTelemetryPoint {
assetId?: string | null;
visible?: boolean;
cursorType?: NativeCursorType | null;
}
export interface NativeCursorAsset {
@@ -24,6 +40,7 @@ export interface NativeCursorAsset {
hotspotX: number;
hotspotY: number;
scaleFactor?: number;
cursorType?: NativeCursorType | null;
}
export interface CursorRecordingData {