diff --git a/src/draggable.attempts/6/Draggable.ts b/src/draggable.attempts/6/Draggable.ts index cfe3fc6..9969b02 100644 --- a/src/draggable.attempts/6/Draggable.ts +++ b/src/draggable.attempts/6/Draggable.ts @@ -1,4 +1,9 @@ -import { clamp, debounce, normaliseAngleDifference } from "../../util/index.ts"; +import { + clamp, + debounce, + normaliseAngleDifference, + throttle, +} from "../../util/index.ts"; interface Vec2 { x: number; @@ -27,6 +32,7 @@ export function makeDraggable(card: HTMLElement, opts: DraggableOpts = {}) { let rotation = opts.initialRotation ?? 0; let dragging = false; + const state = { dragging }; let offsetLocal: Vec2 = { x: 0, y: 0 }; let velocity: Vec2 = { x: 0, y: 0 }; @@ -36,10 +42,15 @@ export function makeDraggable(card: HTMLElement, opts: DraggableOpts = {}) { const dampingFactor = 0.7; const springFactor = 0.2; const maxAngularVelocity = 0.95; - const momentumDampening = 0.98; const RESIZE_DEBOUNCE_MS = 100; const VIEWPORT_CHECK_INTERVAL_MS = 100; + // Adjust damping factors (base + velocity-dependent part) + const baseDamping = 0.98; // Base exponential damping + const angularVelocityDecay = 0.99; + const velocityDecay = 0.005; + const maxEffectiveSpeed = 50; + // --- State --- let lastMousePosition: Vec2 = { x: 0, y: 0 }; let activePointerId: number | null = null; @@ -48,9 +59,9 @@ export function makeDraggable(card: HTMLElement, opts: DraggableOpts = {}) { // --- Helpers --- - const checkViewportExit = debounce(() => { + const checkViewportExit = throttle(() => { // Don't check if we're dragging, user may still be able to move the card back into view - if (dragging) return; + if (state.dragging) return; const rect = card.getBoundingClientRect(); const outside = @@ -70,7 +81,7 @@ export function makeDraggable(card: HTMLElement, opts: DraggableOpts = {}) { const down = (e: PointerEvent) => { if (activePointerId !== null) return; - dragging = true; + state.dragging = true; activePointerId = e.pointerId; card.style.cursor = "grabbing"; card.setPointerCapture(e.pointerId); @@ -90,7 +101,7 @@ export function makeDraggable(card: HTMLElement, opts: DraggableOpts = {}) { }; const move = (e: PointerEvent) => { - if (!dragging || e.pointerId !== activePointerId) return; + if (!state.dragging || e.pointerId !== activePointerId) return; const mx = e.pageX; const my = e.pageY; @@ -135,7 +146,7 @@ export function makeDraggable(card: HTMLElement, opts: DraggableOpts = {}) { const up = (e: PointerEvent) => { if (e.pointerId === activePointerId) { - dragging = false; + state.dragging = false; activePointerId = null; card.style.cursor = "grab"; // Momentum is handled in the render loop @@ -179,21 +190,36 @@ export function makeDraggable(card: HTMLElement, opts: DraggableOpts = {}) { // --- Render Loop --- function render() { - if (!dragging) { - if (Math.abs(angularVelocity) > 0.01) { + if (!state.dragging) { + // --- Angular Momentum --- + + if (Math.abs(angularVelocity) > 0.001) { + // Simple exponential damping rotation += angularVelocity; - angularVelocity *= momentumDampening; + angularVelocity *= angularVelocityDecay; } else angularVelocity = 0; + // --- Linear Momentum --- + const speed = Math.sqrt( velocity.x * velocity.x + velocity.y * velocity.y, ); if (speed > 0.01) { - center.x += velocity.x * 0.4; - center.y += velocity.y * 0.4; - velocity.x *= momentumDampening; - velocity.y *= momentumDampening; + // Calculate speed-dependent damping + // Clamp speed influence to avoid excessive damping + const speedInfluence = + Math.min(speed, maxEffectiveSpeed) / maxEffectiveSpeed; + const currentDamping = + baseDamping * (1 - speedInfluence * velocityDecay); + // Ensure damping doesn't go below a minimum or above 1 + const effectiveDamping = clamp(currentDamping, 0.8, 0.995); // Adjust min/max clamp + + velocity.x *= effectiveDamping; + velocity.y *= effectiveDamping; + + center.x += velocity.x; + center.y += velocity.y; } else velocity = { x: 0, y: 0 }; } diff --git a/src/util/index.ts b/src/util/index.ts index cc533de..1aad488 100644 --- a/src/util/index.ts +++ b/src/util/index.ts @@ -50,20 +50,46 @@ export const getTimeout = () => { return [timeout, clearTimers] as const; }; -export function debounce void>( - func: T, +export function debounce void>( + func: Fn, wait: number, -): T { +): Fn { let timeoutId: number | null = null; - return function (this: ThisParameterType, ...args: Parameters) { + return function (this: ThisParameterType, ...args: Parameters) { if (timeoutId !== null) clearTimeout(timeoutId); timeoutId = window.setTimeout(() => { func.apply(this, args); timeoutId = null; }, wait); - } as T; + } as Fn; } +export const throttle = void>( + fn: Fn, + wait: number, +): Fn => { + let inThrottle = false; + let lastFn: ReturnType | undefined = undefined; + let lastTime = 0; + + return function (this: ThisParameterType, ...args: Parameters) { + const context = this; + if (!inThrottle) { + fn.apply(context, args); + lastTime = Date.now(); + inThrottle = true; + } else { + clearTimeout(lastFn); + lastFn = setTimeout(function () { + if (Date.now() - lastTime >= wait) { + fn.apply(context, args); + lastTime = Date.now(); + } + }, Math.max(wait - (Date.now() - lastTime), 0)); + } + } as Fn; +}; + export const ellipses = (text: string, length: number = 100) => text.length > length ? text.slice(0, length - 3) + "..." : text;