feat: apply native cursor visual effects

This commit is contained in:
EtienneLescot
2026-05-05 21:13:02 +02:00
parent ab3d38d90f
commit d0341580d6
11 changed files with 340 additions and 19 deletions
@@ -1507,6 +1507,9 @@ export default function VideoEditor() {
cropRegion,
cursorRecordingData,
cursorScale: showCursor ? cursorSize : 0,
cursorSmoothing,
cursorMotionBlur,
cursorClickBounce,
annotationRegions,
webcamLayoutPreset,
webcamMaskShape,
@@ -1650,6 +1653,9 @@ export default function VideoEditor() {
cropRegion,
cursorRecordingData,
cursorScale: showCursor ? cursorSize : 0,
cursorSmoothing,
cursorMotionBlur,
cursorClickBounce,
annotationRegions,
webcamLayoutPreset,
webcamMaskShape,
@@ -1744,6 +1750,9 @@ export default function VideoEditor() {
effectiveCursorHighlight,
showCursor,
cursorSize,
cursorSmoothing,
cursorMotionBlur,
cursorClickBounce,
t,
],
);
+41 -4
View File
@@ -26,11 +26,19 @@ import {
type WebcamSizePreset,
} from "@/lib/compositeLayout";
import {
createNativeCursorMotionBlurState,
createNativeCursorSmoothingState,
getNativeCursorClickBounceProgress,
getNativeCursorClickBounceScale,
getNativeCursorMotionBlurPx,
hasNativeCursorRecordingData,
projectNativeCursorToLocal,
projectNativeCursorToStage,
resetNativeCursorMotionBlurState,
resetNativeCursorSmoothingState,
resolveInterpolatedNativeCursorFrame,
resolveNativeCursorRenderAsset,
smoothNativeCursorSample,
} from "@/lib/cursor/nativeCursor";
import { classifyWallpaper, DEFAULT_WALLPAPER, resolveImageWallpaperUrl } from "@/lib/wallpaper";
import { getCssClipPath } from "@/lib/webcamMaskShapes";
@@ -642,6 +650,8 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
useEffect(() => {
cursorRecordingDataRef.current = cursorRecordingData;
resetNativeCursorSmoothingState(nativeCursorSmoothingStateRef.current);
resetNativeCursorMotionBlurState(nativeCursorMotionBlurStateRef.current);
}, [cursorRecordingData]);
useEffect(() => {
@@ -1311,7 +1321,10 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
}
if (nativeCursorImage) {
nativeCursorImage.style.display = "none";
nativeCursorImage.style.filter = "none";
}
resetNativeCursorSmoothingState(nativeCursorSmoothingStateRef.current);
resetNativeCursorMotionBlurState(nativeCursorMotionBlurStateRef.current);
};
if (nativeCursorImage) {
if (hasNativeCursorRecordingRef.current && showCursorRef.current) {
@@ -1321,13 +1334,20 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
timeMs,
);
if (frame) {
const displaySample = smoothNativeCursorSample({
forceSnap: !isPlayingRef.current || isSeekingRef.current,
sample: frame.sample,
smoothing: cursorSmoothingRef.current,
state: nativeCursorSmoothingStateRef.current,
timeMs,
});
const cameraContainer = cameraContainerRef.current;
const videoContainer = videoContainerRef.current;
const cropRegionValue = cropRegionRef.current ?? { x: 0, y: 0, width: 1, height: 1 };
const projectedLocalPoint = projectNativeCursorToLocal({
cropRegion: cropRegionValue,
maskRect: baseMaskRef.current,
sample: frame.sample,
sample: displaySample,
});
const projectedStagePoint =
cameraContainer && videoContainer
@@ -1339,17 +1359,32 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
x: videoContainer.x,
y: videoContainer.y,
},
sample: frame.sample,
sample: displaySample,
})
: null;
if (projectedLocalPoint && projectedStagePoint) {
const renderAsset = resolveNativeCursorRenderAsset(
frame.asset,
window.devicePixelRatio || 1,
frame.sample,
displaySample,
);
const scale = Math.max(0, cursorSizeRef.current);
const bounceProgress = getNativeCursorClickBounceProgress(
cursorRecordingDataRef.current,
timeMs,
);
const scale =
Math.max(0, cursorSizeRef.current) *
getNativeCursorClickBounceScale(cursorClickBounceRef.current, bounceProgress);
const transformedScale = scale * Math.abs(cameraContainer?.scale.x || 1);
const blurPx =
!isPlayingRef.current || isSeekingRef.current
? 0
: getNativeCursorMotionBlurPx({
motionBlur: cursorMotionBlurRef.current,
point: projectedStagePoint,
state: nativeCursorMotionBlurStateRef.current,
timeMs,
});
if (nativeCursorImageIdRef.current !== renderAsset.id) {
nativeCursorImage.src = renderAsset.imageDataUrl;
nativeCursorImageIdRef.current = renderAsset.id;
@@ -1357,6 +1392,8 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
nativeCursorImage.style.display = "block";
nativeCursorImage.style.width = `${renderAsset.width * transformedScale}px`;
nativeCursorImage.style.height = `${renderAsset.height * transformedScale}px`;
nativeCursorImage.style.filter =
blurPx > 0 ? `blur(${blurPx.toFixed(2)}px)` : "none";
nativeCursorImage.style.transform = `translate3d(${
projectedStagePoint.x - renderAsset.hotspotX * transformedScale
}px, ${projectedStagePoint.y - renderAsset.hotspotY * transformedScale}px, 0)`;