From f4fc7fab9e1d950fb92db69a1044e0fc30bf7a39 Mon Sep 17 00:00:00 2001 From: EtienneLescot Date: Tue, 5 May 2026 22:55:33 +0200 Subject: [PATCH] fix: preserve native cursor click interactions --- electron/ipc/handlers.ts | 7 ++ package.json | 1 + .../inspect-native-cursor-click-bounce.mjs | 110 ++++++++++++++++++ 3 files changed, 118 insertions(+) create mode 100644 scripts/inspect-native-cursor-click-bounce.mjs diff --git a/electron/ipc/handlers.ts b/electron/ipc/handlers.ts index a8ea362..3a53ba9 100644 --- a/electron/ipc/handlers.ts +++ b/electron/ipc/handlers.ts @@ -281,6 +281,12 @@ function normalizeCursorSample(sample: unknown): CursorRecordingSample | null { } const point = sample as Partial; + const interactionType = + point.interactionType === "click" || + point.interactionType === "mouseup" || + point.interactionType === "move" + ? point.interactionType + : "move"; return { timeMs: typeof point.timeMs === "number" && Number.isFinite(point.timeMs) @@ -291,6 +297,7 @@ function normalizeCursorSample(sample: unknown): CursorRecordingSample | null { assetId: typeof point.assetId === "string" ? point.assetId : null, visible: typeof point.visible === "boolean" ? point.visible : true, cursorType: typeof point.cursorType === "string" ? point.cursorType : null, + interactionType, }; } diff --git a/package.json b/package.json index dd65ebe..47b7ec3 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "test:wgc-webcam:win": "node scripts/test-windows-wgc-helper.mjs --webcam", "test:wgc-full:win": "node scripts/test-windows-wgc-helper.mjs --webcam --system-audio --microphone", "capture:openscreen-preview": "node scripts/capture-openscreen-preview.mjs", + "inspect:cursor-click-bounce": "node scripts/inspect-native-cursor-click-bounce.mjs", "build-vite": "tsc && vite build", "test:browser": "vitest --config vitest.browser.config.ts --run", "test:browser:install": "playwright install --with-deps chromium-headless-shell", diff --git a/scripts/inspect-native-cursor-click-bounce.mjs b/scripts/inspect-native-cursor-click-bounce.mjs new file mode 100644 index 0000000..870ee8d --- /dev/null +++ b/scripts/inspect-native-cursor-click-bounce.mjs @@ -0,0 +1,110 @@ +#!/usr/bin/env node +import fs from "node:fs"; +import path from "node:path"; + +const CLICK_ANIMATION_MS = 260; + +function usage() { + console.error( + "Usage: node scripts/inspect-native-cursor-click-bounce.mjs [--bounce=5]", + ); + process.exit(1); +} + +function getCursorJsonPath(inputPath) { + if (!inputPath) { + usage(); + } + + const resolved = path.resolve(inputPath); + if (resolved.endsWith(".cursor.json")) { + return resolved; + } + return `${resolved}.cursor.json`; +} + +function getBounceValue() { + const arg = process.argv.find((value) => value.startsWith("--bounce=")); + const parsed = Number(arg?.slice("--bounce=".length) ?? 5); + return Number.isFinite(parsed) ? Math.min(5, Math.max(0, parsed)) : 5; +} + +function clickBounceProgress(samples, timeMs) { + for (let index = samples.length - 1; index >= 0; index -= 1) { + const sample = samples[index]; + if (sample.timeMs > timeMs) { + continue; + } + + const ageMs = timeMs - sample.timeMs; + if (ageMs > CLICK_ANIMATION_MS) { + return 0; + } + + if (sample.interactionType === "click") { + return 1 - ageMs / CLICK_ANIMATION_MS; + } + } + + return 0; +} + +function clickBounceScale(clickBounce, progress) { + if (progress <= 0 || clickBounce <= 0) { + return 1; + } + + const intensity = Math.min(5, Math.max(0, clickBounce)) / 5; + const elapsed = 1 - Math.min(1, Math.max(0, progress)); + 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; +} + +const cursorJsonPath = getCursorJsonPath(process.argv[2]); +const clickBounce = getBounceValue(); +const parsed = JSON.parse(fs.readFileSync(cursorJsonPath, "utf8")); +const samples = (Array.isArray(parsed) ? parsed : (parsed.samples ?? [])).sort( + (a, b) => (a.timeMs ?? 0) - (b.timeMs ?? 0), +); +const clicks = samples.filter((sample) => sample.interactionType === "click"); + +const windows = clicks.slice(0, 8).map((click) => { + const times = [0, 33, 66, 100, 133, 166, 200, 233, 260].map( + (offsetMs) => click.timeMs + offsetMs, + ); + return { + clickTimeMs: click.timeMs, + cursorType: click.cursorType ?? null, + assetId: click.assetId ?? null, + scales: times.map((timeMs) => ({ + timeMs, + progress: Number(clickBounceProgress(samples, timeMs).toFixed(3)), + scale: Number(clickBounceScale(clickBounce, clickBounceProgress(samples, timeMs)).toFixed(3)), + })), + }; +}); + +const report = { + cursorJsonPath, + provider: parsed.provider ?? (Array.isArray(parsed) ? "legacy-array" : null), + sampleCount: samples.length, + assetCount: Array.isArray(parsed.assets) ? parsed.assets.length : 0, + clickCount: clicks.length, + interactionCounts: samples.reduce((counts, sample) => { + const key = sample.interactionType ?? "missing"; + counts[key] = (counts[key] ?? 0) + 1; + return counts; + }, {}), + clickBounce, + windows, +}; + +console.log(JSON.stringify(report, null, 2)); +if (clicks.length === 0) { + process.exitCode = 2; +}