From aef9cf812da9f784fc41cccaa997466cac6ede45 Mon Sep 17 00:00:00 2001 From: Lucas 'Paperboy' Rose-Winters Date: Thu, 16 Oct 2025 12:16:43 +1100 Subject: [PATCH] feat: add smooth animation to thought icon scroll tracking Improved thought icon behavior on mobile with smooth 60fps animations matching the FAB button drag experience: CSS changes: - Add transition properties for top/left position changes - Use 0.2s ease-out timing for smooth, natural movement - Add will-change: top, left for browser rendering optimization - Applied in mobile media query (@media max-width: 1000px) JavaScript changes: - Wrap position updates in requestAnimationFrame() - Cancel pending RAF before scheduling new update (debouncing) - Sync position updates with display refresh rate - Same pattern as FAB button smooth drag implementation Technical details: - RAF throttling prevents layout thrashing - CSS transitions handle the actual animation - Combined approach gives 60fps smooth tracking - Icon follows avatar smoothly during scroll on mobile Result: Thought icon smoothly tracks avatar position during scroll instead of jumping around, with buttery smooth 60fps animation. --- index.js | 91 +++++++++++++++++++++++++++++++------------------------ style.css | 3 ++ 2 files changed, 55 insertions(+), 39 deletions(-) diff --git a/index.js b/index.js index f3e92fb..762cfe9 100644 --- a/index.js +++ b/index.js @@ -3692,7 +3692,10 @@ function createThoughtPanel($message, thoughtsArray) { updateCharacterField(character, field, value); }); - // Update position on scroll + // RAF throttling for smooth position updates + let positionUpdateRaf = null; + + // Update position on scroll with RAF throttling const updatePanelPosition = () => { if (!$message.is(':visible')) { $thoughtPanel.hide(); @@ -3700,47 +3703,57 @@ function createThoughtPanel($message, thoughtsArray) { return; } - const newAvatarRect = $avatar[0].getBoundingClientRect(); - const newTop = newAvatarRect.top + (newAvatarRect.height / 2); - const newIconTop = newAvatarRect.top; - let newLeft, newIconLeft; - - if (panelPosition === 'left') { - // Position at chat's right edge, extending right - const chatContainer = $('#chat')[0]; - const chatRect = chatContainer ? chatContainer.getBoundingClientRect() : { right: window.innerWidth }; - newLeft = chatRect.right + panelMargin; - newIconLeft = chatRect.right + 10; - - $thoughtPanel.css({ - top: `${newTop}px`, - left: `${newLeft}px`, - right: 'auto' - }); - } else { - // Left position relative to avatar - newLeft = newAvatarRect.left - panelWidth - panelMargin; - newIconLeft = newAvatarRect.left - 40; - - $thoughtPanel.css({ - top: `${newTop}px`, - left: `${newLeft}px`, - right: 'auto' - }); + // Cancel any pending RAF + if (positionUpdateRaf) { + cancelAnimationFrame(positionUpdateRaf); } - $thoughtIcon.css({ - top: `${newIconTop}px`, - left: `${newIconLeft}px`, - right: 'auto' + // Schedule update on next frame + positionUpdateRaf = requestAnimationFrame(() => { + const newAvatarRect = $avatar[0].getBoundingClientRect(); + const newTop = newAvatarRect.top + (newAvatarRect.height / 2); + const newIconTop = newAvatarRect.top; + let newLeft, newIconLeft; + + if (panelPosition === 'left') { + // Position at chat's right edge, extending right + const chatContainer = $('#chat')[0]; + const chatRect = chatContainer ? chatContainer.getBoundingClientRect() : { right: window.innerWidth }; + newLeft = chatRect.right + panelMargin; + newIconLeft = chatRect.right + 10; + + $thoughtPanel.css({ + top: `${newTop}px`, + left: `${newLeft}px`, + right: 'auto' + }); + } else { + // Left position relative to avatar + newLeft = newAvatarRect.left - panelWidth - panelMargin; + newIconLeft = newAvatarRect.left - 40; + + $thoughtPanel.css({ + top: `${newTop}px`, + left: `${newLeft}px`, + right: 'auto' + }); + } + + $thoughtIcon.css({ + top: `${newIconTop}px`, + left: `${newIconLeft}px`, + right: 'auto' + }); + + if ($thoughtPanel.is(':visible')) { + $thoughtPanel.show(); + } + if ($thoughtIcon.is(':visible')) { + $thoughtIcon.show(); + } + + positionUpdateRaf = null; }); - - if ($thoughtPanel.is(':visible')) { - $thoughtPanel.show(); - } - if ($thoughtIcon.is(':visible')) { - $thoughtIcon.show(); - } }; // Update position on scroll and resize diff --git a/style.css b/style.css index 4d7a3bf..c20e960 100644 --- a/style.css +++ b/style.css @@ -3409,6 +3409,9 @@ body:has(.rpg-panel.rpg-position-left) #sheld { #rpg-thought-icon { /* Use transform to shift icon above and to the right of avatar */ transform: translate(50px, -45px) !important; + /* Smooth animation for position changes during scroll */ + transition: top 0.2s ease-out, left 0.2s ease-out !important; + will-change: top, left; } /* ========================================