Keep cursor size and sharpness consistent in previews

This commit is contained in:
young
2026-08-25 19:05:34 +10:00
parent 72688f4fcb
commit 20596b6a30
6 changed files with 36 additions and 15 deletions
@@ -1084,6 +1084,7 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
motionBlurFilter.resolution = filterResolution;
zoomBlurFilter.resolution = filterResolution;
cursorOverlayRef.current?.setFilterResolution(filterResolution);
videoEffectsContainer.filterArea = new Rectangle(0, 0, stageWidth, stageHeight);
}, []);
@@ -2173,6 +2174,9 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
sway: cursorSwayRef.current,
});
cursorOverlayRef.current = cursorOverlay;
cursorOverlay.setFilterResolution(
app.renderer.resolution || window.devicePixelRatio || 1,
);
cursorContainer.addChild(cursorOverlay.container);
} else {
cursorOverlayRef.current = null;
@@ -16,6 +16,7 @@ import {
DEFAULT_CURSOR_STYLE,
normalizeCursorClickEffectColor,
} from "../types";
import { getCursorViewportScale } from "./cursorScale";
import { computeCursorSwayRotation } from "./cursorSway";
import { type CursorViewportRect, projectCursorPositionToViewport } from "./cursorViewport";
import {
@@ -110,8 +111,7 @@ export interface CursorRenderConfig {
style: CursorStyle;
}
const REFERENCE_WIDTH = 1920;
const MIN_CURSOR_VIEWPORT_SCALE = 0.55;
const MIN_CURSOR_VIEWPORT_SCALE = 0;
const CURSOR_MOTION_BLUR_BASE_MULTIPLIER = 0.08;
const CURSOR_TIME_DISCONTINUITY_MS = 100;
const CURSOR_SWAY_SMOOTHING_MULTIPLIER = 0.7;
@@ -807,13 +807,6 @@ function findLatestStableCursorType(samples: CursorTelemetryPoint[], timeMs: num
return findLatestSample(samples, timeMs)?.cursorType ?? "arrow";
}
function getCursorViewportScale(
viewport: CursorViewportRect,
minViewportScale = MIN_CURSOR_VIEWPORT_SCALE,
) {
return Math.max(minViewportScale, viewport.width / REFERENCE_WIDTH);
}
function getCursorSwaySpringConfig(smoothingFactor: number, springTuning: CursorSpringTuning) {
const baseConfig = getCursorSpringConfig(
Math.min(
@@ -1231,6 +1224,10 @@ export class PixiCursorOverlay {
}
}
setFilterResolution(resolution: number) {
this.cursorMotionBlurFilter.resolution = Math.max(1, resolution);
}
setClickBounce(clickBounce: number) {
this.config.clickBounce = Math.max(0, clickBounce);
}
@@ -1346,7 +1343,7 @@ export class PixiCursorOverlay {
const h =
this.config.dotRadius *
getCursorViewportScale(viewport, this.config.minViewportScale);
getCursorViewportScale(viewport.width, this.config.minViewportScale);
const { cursorType, clickSample, clickBounceProgress, clickProgress } =
getCursorVisualState(
samples,
@@ -1642,7 +1639,7 @@ 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, config.minViewportScale);
const h = config.dotRadius * getCursorViewportScale(viewport.width, config.minViewportScale);
const { cursorType, clickSample, clickBounceProgress, clickProgress } = getCursorVisualState(
samples,
timeMs,
@@ -0,0 +1,14 @@
import { describe, expect, it } from "vitest";
import { getCursorViewportScale } from "./cursorScale";
describe("cursor preview/export scale", () => {
it("preserves the same cursor-to-video ratio at preview and export sizes", () => {
const baseCursorHeight = 28 * 2.5;
const previewWidth = 720;
const exportWidth = 2940;
const previewCursorHeight = baseCursorHeight * getCursorViewportScale(previewWidth);
const exportCursorHeight = baseCursorHeight * getCursorViewportScale(exportWidth);
expect(previewCursorHeight / previewWidth).toBeCloseTo(exportCursorHeight / exportWidth, 8);
});
});
@@ -0,0 +1,5 @@
export const CURSOR_REFERENCE_VIEWPORT_WIDTH = 1920;
export function getCursorViewportScale(viewportWidth: number, minimumScale = 0): number {
return Math.max(minimumScale, Math.max(0, viewportWidth) / CURSOR_REFERENCE_VIEWPORT_WIDTH);
}
@@ -236,12 +236,12 @@ describe("ModernVideoExporter native static-layout eligibility", () => {
).toBeNull();
});
it("scales native static-layout cursor size with a minimum visible floor", () => {
it("preserves the cursor-to-video ratio at every native static-layout size", () => {
const exporter = createExporter({ cursorSize: 3, cursorStyle: "tahoe" });
expect(exporter.getNativeStaticLayoutCursorSize(1920)).toBeCloseTo(84, 6);
expect(exporter.getNativeStaticLayoutCursorSize(960)).toBeCloseTo(46.2, 6);
expect(exporter.getNativeStaticLayoutCursorSize(480)).toBeCloseTo(46.2, 6);
expect(exporter.getNativeStaticLayoutCursorSize(960)).toBeCloseTo(42, 6);
expect(exporter.getNativeStaticLayoutCursorSize(480)).toBeCloseTo(21, 6);
});
it("skips native static-layout when cursor click effects are enabled", () => {
+2 -1
View File
@@ -25,6 +25,7 @@ import {
SNAP_TO_EDGES_RATIO_AUTO,
} from "@/components/video-editor/videoPlayback/cursorFollowCamera";
import { buildNativeCursorAtlas } from "@/components/video-editor/videoPlayback/cursorRenderer";
import { getCursorViewportScale } from "@/components/video-editor/videoPlayback/cursorScale";
import {
computePaddedLayout,
scalePreviewBorderRadius,
@@ -2089,7 +2090,7 @@ export class ModernVideoExporter {
private getNativeStaticLayoutCursorSize(contentWidth: number) {
const cursorStyle = this.config.cursorStyle ?? "tahoe";
const viewportScale = Math.max(0.55, contentWidth / 1920);
const viewportScale = getCursorViewportScale(contentWidth);
return (
28 *
(this.config.cursorSize ?? 3) *