From 9219fe3f19cb870dad35b27c39e54970575d0439 Mon Sep 17 00:00:00 2001 From: Lucas 'Paperboy' Rose-Winters Date: Thu, 16 Oct 2025 11:39:15 +1100 Subject: [PATCH] feat: add smooth animation to mobile FAB drag behavior Improved the mobile floating action button (FAB) drag experience with smooth, performant animations: - Use requestAnimationFrame to throttle position updates during drag - Add will-change CSS property to optimize rendering performance - Add dragging class to disable transitions during active drag - Change cursor to grab/grabbing for better visual feedback - Remove janky direct CSS updates in favor of RAF-based updates Technical improvements: - Position updates now synced to display refresh rate (~60fps) - Prevents layout thrashing from excessive DOM manipulation - Smooth transition animations when drag ends Result: Dragging the FAB button now feels fluid and responsive on mobile devices instead of laggy and jumpy. --- index.js | 56 +++++++++++++++++++++++++++++++++++-------------------- style.css | 11 +++++++++-- 2 files changed, 45 insertions(+), 22 deletions(-) diff --git a/index.js b/index.js index e9f8ea5..f3e92fb 100644 --- a/index.js +++ b/index.js @@ -1032,6 +1032,24 @@ function setupMobileToggle() { let buttonStartY = 0; const LONG_PRESS_DURATION = 200; // ms to hold before enabling drag const MOVE_THRESHOLD = 10; // px to move before enabling drag + let rafId = null; // RequestAnimationFrame ID for smooth updates + let pendingX = null; + let pendingY = null; + + // Update position using requestAnimationFrame for smooth rendering + function updateFabPosition() { + if (pendingX !== null && pendingY !== null) { + $mobileToggle.css({ + left: pendingX + 'px', + top: pendingY + 'px', + right: 'auto', + bottom: 'auto' + }); + pendingX = null; + pendingY = null; + } + rafId = null; + } // Touch start - begin tracking $mobileToggle.on('touchstart', function(e) { @@ -1059,7 +1077,7 @@ function setupMobileToggle() { // Start dragging if held long enough OR moved far enough if (!isDragging && (timeSinceStart > LONG_PRESS_DURATION || distance > MOVE_THRESHOLD)) { isDragging = true; - $mobileToggle.css('transition', 'none'); // Disable transitions while dragging + $mobileToggle.addClass('dragging'); // Disable transitions while dragging } if (isDragging) { @@ -1082,13 +1100,12 @@ function setupMobileToggle() { newX = Math.max(minX, Math.min(maxX, newX)); newY = Math.max(minY, Math.min(maxY, newY)); - // Apply position - $mobileToggle.css({ - left: newX + 'px', - top: newY + 'px', - right: 'auto', - bottom: 'auto' - }); + // Store pending position and request animation frame for smooth update + pendingX = newX; + pendingY = newY; + if (!rafId) { + rafId = requestAnimationFrame(updateFabPosition); + } } }); @@ -1123,7 +1140,7 @@ function setupMobileToggle() { // Start dragging if held long enough OR moved far enough if (!isDragging && (timeSinceStart > LONG_PRESS_DURATION || distance > MOVE_THRESHOLD)) { isDragging = true; - $mobileToggle.css('transition', 'none'); + $mobileToggle.addClass('dragging'); // Disable transitions while dragging } if (isDragging) { @@ -1146,13 +1163,12 @@ function setupMobileToggle() { newX = Math.max(minX, Math.min(maxX, newX)); newY = Math.max(minY, Math.min(maxY, newY)); - // Apply position - $mobileToggle.css({ - left: newX + 'px', - top: newY + 'px', - right: 'auto', - bottom: 'auto' - }); + // Store pending position and request animation frame for smooth update + pendingX = newX; + pendingY = newY; + if (!rafId) { + rafId = requestAnimationFrame(updateFabPosition); + } } }); @@ -1178,9 +1194,9 @@ function setupMobileToggle() { // Constrain to viewport bounds (now that position is saved) setTimeout(() => constrainFabToViewport(), 10); - // Re-enable transitions + // Re-enable transitions with smooth animation setTimeout(() => { - $mobileToggle.css('transition', ''); + $mobileToggle.removeClass('dragging'); }, 50); isDragging = false; @@ -1219,9 +1235,9 @@ function setupMobileToggle() { // Constrain to viewport bounds (now that position is saved) setTimeout(() => constrainFabToViewport(), 10); - // Re-enable transitions + // Re-enable transitions with smooth animation setTimeout(() => { - $mobileToggle.css('transition', ''); + $mobileToggle.removeClass('dragging'); }, 50); isDragging = false; diff --git a/style.css b/style.css index 3f0498a..4d7a3bf 100644 --- a/style.css +++ b/style.css @@ -2942,12 +2942,19 @@ body:has(.rpg-panel.rpg-position-left) #sheld { border: 2px solid var(--SmartThemeBorderColor); color: var(--rpg-text, #ecf0f1); font-size: 1.25rem; - cursor: pointer; + cursor: grab; z-index: 10002; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); - transition: opacity 0.3s ease, transform 0.2s ease; + transition: opacity 0.3s ease, transform 0.2s ease, top 0.3s ease, left 0.3s ease, right 0.3s ease, bottom 0.3s ease; user-select: none; /* Prevent text selection while dragging */ -webkit-user-select: none; + will-change: top, left; /* Optimize for position changes */ +} + +/* Disable transitions while actively dragging */ +.rpg-mobile-toggle.dragging { + transition: none; + cursor: grabbing; } .rpg-mobile-toggle:hover {