fix: preserve native cursor click interactions
This commit is contained in:
@@ -281,6 +281,12 @@ function normalizeCursorSample(sample: unknown): CursorRecordingSample | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const point = sample as Partial<CursorRecordingSample>;
|
const point = sample as Partial<CursorRecordingSample>;
|
||||||
|
const interactionType =
|
||||||
|
point.interactionType === "click" ||
|
||||||
|
point.interactionType === "mouseup" ||
|
||||||
|
point.interactionType === "move"
|
||||||
|
? point.interactionType
|
||||||
|
: "move";
|
||||||
return {
|
return {
|
||||||
timeMs:
|
timeMs:
|
||||||
typeof point.timeMs === "number" && Number.isFinite(point.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,
|
assetId: typeof point.assetId === "string" ? point.assetId : null,
|
||||||
visible: typeof point.visible === "boolean" ? point.visible : true,
|
visible: typeof point.visible === "boolean" ? point.visible : true,
|
||||||
cursorType: typeof point.cursorType === "string" ? point.cursorType : null,
|
cursorType: typeof point.cursorType === "string" ? point.cursorType : null,
|
||||||
|
interactionType,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
"test:wgc-webcam:win": "node scripts/test-windows-wgc-helper.mjs --webcam",
|
"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",
|
"test:wgc-full:win": "node scripts/test-windows-wgc-helper.mjs --webcam --system-audio --microphone",
|
||||||
"capture:openscreen-preview": "node scripts/capture-openscreen-preview.mjs",
|
"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",
|
"build-vite": "tsc && vite build",
|
||||||
"test:browser": "vitest --config vitest.browser.config.ts --run",
|
"test:browser": "vitest --config vitest.browser.config.ts --run",
|
||||||
"test:browser:install": "playwright install --with-deps chromium-headless-shell",
|
"test:browser:install": "playwright install --with-deps chromium-headless-shell",
|
||||||
|
|||||||
@@ -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 <video-or-cursor-json-path> [--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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user