Compare commits

...

2 Commits

3 changed files with 69 additions and 43 deletions
@@ -7,17 +7,26 @@
let { smoothCursor = true }: { smoothCursor?: boolean } = $props(); let { smoothCursor = true }: { smoothCursor?: boolean } = $props();
const VELOCITY_WINDOW_MS = 16;
const SPEED_THRESHOLD = 9;
const REQUIRED_CONSECUTIVE = 3;
let mouseX = $state(0); let mouseX = $state(0);
let mouseY = $state(0); let mouseY = $state(0);
let lastMouseX = $state(0); let lastMouseX = 0;
let lastMouseY = $state(0); let lastMouseY = 0;
let isMouseDown = $state(false); let isMouseDown = $state(false);
let isHoveringInteractive = $state(false); let isHoveringInteractive = $state(false);
let dragStartX = $state(0); let dragStartX = 0;
let dragStartY = $state(0); let dragStartY = 0;
let degrees = $state(0); let degrees = $state(0);
let applyRotation = $state(false); let applyRotation = $state(false);
let lastVelocityTime = 0;
let consecutiveHighSpeed = 0;
let velocityAccumX = 0;
let velocityAccumY = 0;
function isInteractive(el: Element | null): boolean { function isInteractive(el: Element | null): boolean {
while (el) { while (el) {
const tag = el.tagName.toLowerCase(); const tag = el.tagName.toLowerCase();
@@ -30,7 +39,6 @@
) { ) {
return true; return true;
} }
el = el.parentElement; el = el.parentElement;
} }
return false; return false;
@@ -43,17 +51,32 @@
const deltaX = e.pageX - window.pageXOffset - dragStartX; const deltaX = e.pageX - window.pageXOffset - dragStartX;
const deltaY = e.pageY - window.pageYOffset - dragStartY; const deltaY = e.pageY - window.pageYOffset - dragStartY;
const velocityX = mouseX - lastMouseX; velocityAccumX += mouseX - lastMouseX;
const velocityY = mouseY - lastMouseY; velocityAccumY += mouseY - lastMouseY;
const dist = Math.sqrt(velocityX * velocityX + velocityY * velocityY); const now = performance.now();
const dt = now - lastVelocityTime;
if (!applyRotation && isMouseDown && dist > 80) applyRotation = true; if (dt >= VELOCITY_WINDOW_MS) {
const speed = Math.sqrt(velocityAccumX ** 2 + velocityAccumY ** 2) / dt;
if (speed > SPEED_THRESHOLD) {
consecutiveHighSpeed++;
if (!applyRotation && isMouseDown && consecutiveHighSpeed >= REQUIRED_CONSECUTIVE) {
applyRotation = true;
}
} else {
consecutiveHighSpeed = 0;
}
velocityAccumX = 0;
velocityAccumY = 0;
lastVelocityTime = now;
}
let newDegrees = (Math.atan2(-deltaX, deltaY) * 180) / Math.PI + 24.3; let newDegrees = (Math.atan2(-deltaX, deltaY) * 180) / Math.PI + 24.3;
let diff = (newDegrees - degrees) % 360; let diff = (newDegrees - degrees) % 360;
if (diff < -180) diff += 360; if (diff < -180) diff += 360;
if (diff > 180) diff -= 360; if (diff > 180) diff -= 360;
@@ -67,26 +90,18 @@
isHoveringInteractive = isInteractive(el); isHoveringInteractive = isInteractive(el);
if (!isMouseDown) { if (!isMouseDown) {
if (isHoveringInteractive) { animate(cursorAdditive, {
animate(cursorAdditive, { opacity: isHoveringInteractive ? 1 : 0,
opacity: 1, duration: 800,
duration: 800, ease: (t: number) => (t - 1) ** 5 + 1,
ease: (t: number) => (t - 1) ** 5 + 1, });
});
} else {
animate(cursorAdditive, {
opacity: 0,
duration: 800,
ease: (t: number) => (t - 1) ** 5 + 1,
});
}
} }
animate(cursor, { animate(cursor, {
duration: smoothCursor ? $cursorSmoothness : 0, duration: smoothCursor ? $cursorSmoothness : 0,
translateX: mouseX, translateX: mouseX,
translateY: mouseY, translateY: mouseY,
ease: (t: number) => (t - 1) ** 5 + 1, ease: (t: number) => t === 1 ? 1 : 1 - Math.pow(2, -10 * t),
}); });
animate(cursor, { animate(cursor, {
@@ -107,6 +122,11 @@
dragStartX = event.clientX; dragStartX = event.clientX;
dragStartY = event.clientY; dragStartY = event.clientY;
velocityAccumX = 0;
velocityAccumY = 0;
consecutiveHighSpeed = 0;
lastVelocityTime = performance.now();
isMouseDown = true; isMouseDown = true;
animate(cursorInner, { animate(cursorInner, {
scale: 0.9, scale: 0.9,
@@ -124,6 +144,10 @@
const handleMouseUp = () => { const handleMouseUp = () => {
isMouseDown = false; isMouseDown = false;
applyRotation = false; applyRotation = false;
consecutiveHighSpeed = 0;
velocityAccumX = 0;
velocityAccumY = 0;
animate(cursorInner, { animate(cursorInner, {
scale: 1, scale: 1,
duration: 500, duration: 500,
+12 -10
View File
@@ -1,19 +1,21 @@
export function estimateRefreshRate(): Promise<number> { export async function estimateFrameRate(): Promise<number> {
return new Promise((resolve) => { return new Promise((resolve) => {
const last = performance.now(); const samples: number[] = [];
let frames = 0; let last = performance.now();
function loop() { const tick = () => {
const now = performance.now(); const now = performance.now();
frames++; samples.push(now - last);
last = now;
if (now - last >= 1000) { if (samples.length < 60) {
resolve(frames - 2); requestAnimationFrame(tick);
} else { } else {
requestAnimationFrame(loop); const avgDelta = samples.reduce((a, b) => a + b) / samples.length;
resolve(Math.round(1000 / avgDelta));
} }
} };
requestAnimationFrame(loop); requestAnimationFrame(tick);
}); });
} }
+8 -8
View File
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import Logo from '$assets/logo.png'; import Logo from '$assets/logo.png';
import { estimateRefreshRate } from '@/displayUtils'; import { estimateFrameRate } from '@/displayUtils';
import { import {
beatmapSets, beatmapSets,
currentLoadingInfo, currentLoadingInfo,
@@ -67,15 +67,15 @@
}; };
const calculateCursorSmoothness = async () => { const calculateCursorSmoothness = async () => {
const refreshRate = await estimateRefreshRate(); const refreshRate = await estimateFrameRate();
const hzMin = 60; const hzMin = 60;
const hzMax = 144; const hzMax = 500;
const durationMin = 70; const durationMin = 350;
const durationMax = 180; const durationMax = 600;
const clamped = Math.min(Math.max(refreshRate, hzMin), hzMax);
const duration = const duration =
durationMin + ((refreshRate - hzMin) / (hzMax - hzMin)) * (durationMax - durationMin); durationMin + ((clamped - hzMin) / (hzMax - hzMin)) * (durationMax - durationMin);
console.log(Math.round(duration));
cursorSmoothness.set(Math.round(duration)); cursorSmoothness.set(Math.round(duration));
}; };