From c9eead1965c81aebb4c299da4ee58c9670a2c58d Mon Sep 17 00:00:00 2001 From: dtoro Date: Sun, 5 Apr 2026 23:49:10 +0200 Subject: [PATCH] feat: inertia --- .../src/components/shared/FloatingWindow.tsx | 63 +++++++++++++++++-- 1 file changed, 59 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/shared/FloatingWindow.tsx b/frontend/src/components/shared/FloatingWindow.tsx index 517e257..c12c87a 100644 --- a/frontend/src/components/shared/FloatingWindow.tsx +++ b/frontend/src/components/shared/FloatingWindow.tsx @@ -38,19 +38,74 @@ export default function FloatingWindow({ const ref = containerRef ?? internalRef; const dragRef = useRef<{ startX: number; startY: number; origX: number; origY: number } | null>(null); const resizeRef = useRef<{ startX: number; startY: number; origW: number; origH: number } | null>(null); + const velocityRef = useRef({ vx: 0, vy: 0, lastX: 0, lastY: 0, lastT: 0 }); + const inertiaRef = useRef(0); + const posRef = useRef({ x, y }); + posRef.current = { x, y }; useEffect(() => { if (focused) ref.current?.focus(); }, [focused]); + // Cancel any running inertia animation + const stopInertia = useCallback(() => { + if (inertiaRef.current) { cancelAnimationFrame(inertiaRef.current); inertiaRef.current = 0; } + }, []); + const onDragStart = useCallback((e: React.MouseEvent) => { if ((e.target as HTMLElement).closest("button")) return; e.preventDefault(); onFocus(id); ref.current?.focus(); + stopInertia(); dragRef.current = { startX: e.clientX, startY: e.clientY, origX: x, origY: y }; + velocityRef.current = { vx: 0, vy: 0, lastX: e.clientX, lastY: e.clientY, lastT: performance.now() }; document.documentElement.classList.add("cursor-grabbing"); - const onMove = (ev: MouseEvent) => { if (!dragRef.current) return; onUpdate(id, { x: dragRef.current.origX + (ev.clientX - dragRef.current.startX), y: Math.max(0, dragRef.current.origY + (ev.clientY - dragRef.current.startY)) }); }; - const onUp = () => { dragRef.current = null; document.documentElement.classList.remove("cursor-grabbing"); document.removeEventListener("mousemove", onMove); document.removeEventListener("mouseup", onUp); }; - document.addEventListener("mousemove", onMove); document.addEventListener("mouseup", onUp); - }, [id, x, y, onUpdate, onFocus]); + + const onMove = (ev: MouseEvent) => { + if (!dragRef.current) return; + const now = performance.now(); + const dt = now - velocityRef.current.lastT; + if (dt > 0) { + const smooth = 0.3; + const rawVx = (ev.clientX - velocityRef.current.lastX) / dt * 16; + const rawVy = (ev.clientY - velocityRef.current.lastY) / dt * 16; + velocityRef.current.vx = velocityRef.current.vx * (1 - smooth) + rawVx * smooth; + velocityRef.current.vy = velocityRef.current.vy * (1 - smooth) + rawVy * smooth; + velocityRef.current.lastX = ev.clientX; + velocityRef.current.lastY = ev.clientY; + velocityRef.current.lastT = now; + } + onUpdate(id, { + x: dragRef.current.origX + (ev.clientX - dragRef.current.startX), + y: Math.max(0, dragRef.current.origY + (ev.clientY - dragRef.current.startY)), + }); + }; + + const onUp = () => { + const { vx, vy } = velocityRef.current; + dragRef.current = null; + document.documentElement.classList.remove("cursor-grabbing"); + document.removeEventListener("mousemove", onMove); + document.removeEventListener("mouseup", onUp); + + // Kick off inertia if there's meaningful velocity + if (Math.abs(vx) > 0.5 || Math.abs(vy) > 0.5) { + let curVx = vx; + let curVy = vy; + const friction = 0.92; + const tick = () => { + curVx *= friction; + curVy *= friction; + if (Math.abs(curVx) < 0.3 && Math.abs(curVy) < 0.3) { inertiaRef.current = 0; return; } + posRef.current = { x: posRef.current.x + curVx, y: Math.max(0, posRef.current.y + curVy) }; + onUpdate(id, posRef.current); + inertiaRef.current = requestAnimationFrame(tick); + }; + inertiaRef.current = requestAnimationFrame(tick); + } + }; + + document.addEventListener("mousemove", onMove); + document.addEventListener("mouseup", onUp); + }, [id, x, y, onUpdate, onFocus, stopInertia]); const onResizeStart = useCallback((e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); onFocus(id);