From f91300a1b7b96acdc78cbbf0430e28d7b3f3e1ee Mon Sep 17 00:00:00 2001 From: EtienneLescot Date: Tue, 5 May 2026 22:39:23 +0200 Subject: [PATCH] fix: make native cursor click bounce visible --- src/lib/cursor/nativeCursor.test.ts | 34 +++++++++++++++++++++++++++++ src/lib/cursor/nativeCursor.ts | 16 +++++++++----- 2 files changed, 45 insertions(+), 5 deletions(-) create mode 100644 src/lib/cursor/nativeCursor.test.ts diff --git a/src/lib/cursor/nativeCursor.test.ts b/src/lib/cursor/nativeCursor.test.ts new file mode 100644 index 0000000..1b919d6 --- /dev/null +++ b/src/lib/cursor/nativeCursor.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from "vitest"; +import { + getNativeCursorClickBounceProgress, + getNativeCursorClickBounceScale, +} from "./nativeCursor"; + +describe("native cursor click bounce", () => { + it("keeps click progress visible across several frames", () => { + const recordingData = { + version: 2, + provider: "native" as const, + assets: [], + samples: [ + { timeMs: 0, cx: 0.5, cy: 0.5, interactionType: "move" as const }, + { timeMs: 100, cx: 0.5, cy: 0.5, interactionType: "click" as const }, + { timeMs: 133, cx: 0.5, cy: 0.5, interactionType: "move" as const }, + { timeMs: 166, cx: 0.5, cy: 0.5, interactionType: "move" as const }, + { timeMs: 200, cx: 0.5, cy: 0.5, interactionType: "move" as const }, + { timeMs: 300, cx: 0.5, cy: 0.5, interactionType: "move" as const }, + ], + }; + + expect(getNativeCursorClickBounceProgress(recordingData, 133)).toBeGreaterThan(0); + expect(getNativeCursorClickBounceProgress(recordingData, 200)).toBeGreaterThan(0); + expect(getNativeCursorClickBounceProgress(recordingData, 400)).toBe(0); + }); + + it("applies a visible press and rebound scale at high intensity", () => { + expect(getNativeCursorClickBounceScale(5, 1)).toBe(1); + expect(getNativeCursorClickBounceScale(5, 0.82)).toBeLessThan(0.9); + expect(getNativeCursorClickBounceScale(5, 0.28)).toBeGreaterThan(1.05); + expect(getNativeCursorClickBounceScale(5, 0)).toBe(1); + }); +}); diff --git a/src/lib/cursor/nativeCursor.ts b/src/lib/cursor/nativeCursor.ts index 02f4d06..9ce308a 100644 --- a/src/lib/cursor/nativeCursor.ts +++ b/src/lib/cursor/nativeCursor.ts @@ -57,7 +57,7 @@ function clamp(value: number, min: number, max: number) { return Math.min(max, Math.max(min, value)); } -const NATIVE_CURSOR_CLICK_ANIMATION_MS = 140; +const NATIVE_CURSOR_CLICK_ANIMATION_MS = 260; const NATIVE_CURSOR_MOTION_BLUR_MAX_PX = 6; const nativeCursorAssetMapCache = new WeakMap< CursorRecordingData, @@ -329,7 +329,7 @@ export function getNativeCursorClickBounceProgress( recordingData: CursorRecordingData | null | undefined, timeMs: number, ) { - if (!hasNativeCursorRecordingData(recordingData)) { + if (!recordingData || recordingData.provider !== "native" || recordingData.samples.length === 0) { return 0; } @@ -357,9 +357,15 @@ export function getNativeCursorClickBounceScale(clickBounce: number, progress: n return 1; } - const bounceAmount = Math.sin(progress * Math.PI); - const amplitude = clamp(clickBounce, 0, 4) * 0.08; - return Math.max(0.72, 1 - bounceAmount * amplitude); + const intensity = clamp(clickBounce, 0, 5) / 5; + const elapsed = 1 - clamp(progress, 0, 1); + if (elapsed < 0.38) { + const pressProgress = Math.sin((elapsed / 0.38) * Math.PI); + return 1 - pressProgress * intensity * 0.24; + } + + const reboundProgress = Math.sin(((elapsed - 0.38) / 0.62) * Math.PI); + return 1 + reboundProgress * intensity * 0.16; } export function getNativeCursorMotionBlurPx({