Merge pull request #465 from webadderallorg/fix/issue395-zoom-independent-cursor

fix(editor): keep cursor size stable during zoom
This commit is contained in:
Phạm Thị Minh Hồng
2026-05-09 09:59:36 +07:00
committed by GitHub
5 changed files with 59 additions and 16 deletions
@@ -2182,6 +2182,7 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
baseMaskRef.current,
showCursorRef.current,
!isPlayingRef.current || isSeekingRef.current,
animationStateRef.current.appliedScale || 1,
);
smoothedCursorForHooks = mapSmoothedCursorToCanvasNormalized(
@@ -0,0 +1,27 @@
import { describe, expect, it } from "vitest";
import { resolveCursorDrawHeight } from "./cursorRenderer";
describe("resolveCursorDrawHeight", () => {
const viewport = {
x: 100,
y: 50,
width: 960,
height: 540,
};
it("keeps cursor output size stable when the zoom parent scales up", () => {
const baseHeight = resolveCursorDrawHeight(viewport, 70, 1);
const zoomedLocalHeight = resolveCursorDrawHeight(viewport, 70, 2);
expect(zoomedLocalHeight).toBeCloseTo(baseHeight / 2, 6);
expect(zoomedLocalHeight * 2).toBeCloseTo(baseHeight, 6);
});
it("falls back to unscaled sizing for invalid parent scale values", () => {
const baseHeight = resolveCursorDrawHeight(viewport, 70, 1);
expect(resolveCursorDrawHeight(viewport, 70, 0)).toBe(baseHeight);
expect(resolveCursorDrawHeight(viewport, 70, Number.NaN)).toBe(baseHeight);
});
});
@@ -1,8 +1,8 @@
import { Assets, BlurFilter, Container, Graphics, Sprite, Texture } from "pixi.js";
import { MotionBlurFilter } from "pixi-filters/motion-blur";
import minimalCursorUrl from "@/assets/cursors/custom/minimal-cursor.svg";
import { getRenderableAssetUrl } from "@/lib/assetPath";
import { extensionHost } from "@/lib/extensions";
import minimalCursorUrl from "@/assets/cursors/custom/minimal-cursor.svg";
import {
type CursorStyle,
type CursorTelemetryPoint,
@@ -12,11 +12,11 @@ import {
import { computeCursorSwayRotation } from "./cursorSway";
import { type CursorViewportRect, projectCursorPositionToViewport } from "./cursorViewport";
import {
type CursorSpringTuning,
createSpringState,
getCursorSpringConfig,
resetSpringState,
stepSpringValue,
type CursorSpringTuning,
} from "./motionSmoothing";
import { cursorSetAssets, getCursorStyleSizeMultiplier } from "./uploadedCursorAssets";
@@ -745,6 +745,15 @@ function getCursorViewportScale(viewport: CursorViewportRect) {
return Math.max(MIN_CURSOR_VIEWPORT_SCALE, viewport.width / REFERENCE_WIDTH);
}
export function resolveCursorDrawHeight(
viewport: CursorViewportRect,
dotRadius: number,
parentScale = 1,
) {
const safeParentScale = Number.isFinite(parentScale) && parentScale > 0 ? parentScale : 1;
return (dotRadius * getCursorViewportScale(viewport)) / safeParentScale;
}
function getCursorSwaySpringConfig(
smoothingFactor: number,
springTuning: CursorSpringTuning,
@@ -1061,6 +1070,7 @@ export class PixiCursorOverlay {
viewport: CursorViewportRect,
visible: boolean,
freeze = false,
parentScale = 1,
): void {
if (!visible || samples.length === 0 || viewport.width <= 0 || viewport.height <= 0) {
this.container.visible = false;
@@ -1107,7 +1117,7 @@ export class PixiCursorOverlay {
const px = viewport.x + this.state.x * viewport.width;
const py = viewport.y + this.state.y * viewport.height;
const h = this.config.dotRadius * getCursorViewportScale(viewport);
const h = resolveCursorDrawHeight(viewport, this.config.dotRadius, parentScale);
const { cursorType, clickBounceProgress } = getCursorVisualState(
samples,
timeMs,
@@ -1303,6 +1313,7 @@ export function drawCursorOnCanvas(
viewport: CursorViewportRect,
smoothedState: SmoothedCursorState,
config: CursorRenderConfig = DEFAULT_CURSOR_CONFIG,
parentScale = 1,
): void {
if (samples.length === 0 || viewport.width <= 0 || viewport.height <= 0) return;
@@ -1316,7 +1327,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);
const h = resolveCursorDrawHeight(viewport, config.dotRadius, parentScale);
const { cursorType, clickBounceProgress } = getCursorVisualState(
samples,
timeMs,
+10 -8
View File
@@ -1626,6 +1626,12 @@ export class FrameRenderer {
const timeMs = this.currentVideoTime * 1000;
const cursorTimeMs = cursorTimestamp / 1000;
const TICKS_PER_FRAME = 1;
for (let i = 0; i < TICKS_PER_FRAME; i++) {
this.updateAnimationState(timeMs);
}
if (this.cursorOverlay) {
this.cursorOverlay.update(
this.config.cursorTelemetry ?? [],
@@ -1633,6 +1639,7 @@ export class FrameRenderer {
layoutCache.maskRect,
this.config.showCursor ?? true,
false,
this.animationState.appliedScale || 1,
);
}
@@ -1655,12 +1662,6 @@ export class FrameRenderer {
: null,
);
const TICKS_PER_FRAME = 1;
for (let i = 0; i < TICKS_PER_FRAME; i++) {
this.updateAnimationState(timeMs);
}
applyZoomTransform({
cameraContainer: this.cameraContainer,
zoomBlurFilter: this.zoomBlurFilter,
@@ -2110,6 +2111,8 @@ export class FrameRenderer {
const timeMs = this.currentVideoTime * 1000;
const cursorTimeMs = cursorTimestamp / 1000;
this.updateAnimationState(timeMs);
if (this.cursorOverlay) {
this.cursorOverlay.update(
this.config.cursorTelemetry ?? [],
@@ -2117,6 +2120,7 @@ export class FrameRenderer {
layoutCache.maskRect,
this.config.showCursor ?? true,
false,
this.animationState.appliedScale || 1,
);
}
@@ -2129,8 +2133,6 @@ export class FrameRenderer {
},
);
this.updateAnimationState(timeMs);
applyZoomTransform({
cameraContainer: this.cameraContainer,
zoomBlurFilter: this.zoomBlurFilter,
+6 -4
View File
@@ -2885,6 +2885,8 @@ export class FrameRenderer {
const timeMs = this.currentVideoTime * 1000;
const cursorTimeMs = cursorTimestamp / 1000;
this.updateAnimationState(timeMs);
if (this.cursorOverlay) {
this.cursorOverlay.update(
this.config.cursorTelemetry ?? [],
@@ -2892,11 +2894,10 @@ export class FrameRenderer {
layoutCache.maskRect,
this.config.showCursor ?? true,
false,
this.animationState.appliedScale || 1,
);
}
this.updateAnimationState(timeMs);
applyZoomTransform({
cameraContainer: this.cameraContainer,
zoomBlurFilter: this.zoomBlurFilter,
@@ -3132,6 +3133,8 @@ export class FrameRenderer {
const timeMs = this.currentVideoTime * 1000;
const cursorTimeMs = cursorTimestamp / 1000;
this.updateAnimationState(timeMs);
if (this.cursorOverlay) {
this.cursorOverlay.update(
this.config.cursorTelemetry ?? [],
@@ -3139,11 +3142,10 @@ export class FrameRenderer {
layoutCache.maskRect,
this.config.showCursor ?? true,
false,
this.animationState.appliedScale || 1,
);
}
this.updateAnimationState(timeMs);
applyZoomTransform({
cameraContainer: this.cameraContainer,
zoomBlurFilter: this.zoomBlurFilter,