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.
This commit is contained in:
@@ -3692,7 +3692,10 @@ function createThoughtPanel($message, thoughtsArray) {
|
|||||||
updateCharacterField(character, field, value);
|
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 = () => {
|
const updatePanelPosition = () => {
|
||||||
if (!$message.is(':visible')) {
|
if (!$message.is(':visible')) {
|
||||||
$thoughtPanel.hide();
|
$thoughtPanel.hide();
|
||||||
@@ -3700,47 +3703,57 @@ function createThoughtPanel($message, thoughtsArray) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newAvatarRect = $avatar[0].getBoundingClientRect();
|
// Cancel any pending RAF
|
||||||
const newTop = newAvatarRect.top + (newAvatarRect.height / 2);
|
if (positionUpdateRaf) {
|
||||||
const newIconTop = newAvatarRect.top;
|
cancelAnimationFrame(positionUpdateRaf);
|
||||||
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({
|
// Schedule update on next frame
|
||||||
top: `${newIconTop}px`,
|
positionUpdateRaf = requestAnimationFrame(() => {
|
||||||
left: `${newIconLeft}px`,
|
const newAvatarRect = $avatar[0].getBoundingClientRect();
|
||||||
right: 'auto'
|
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
|
// Update position on scroll and resize
|
||||||
|
|||||||
@@ -3409,6 +3409,9 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
|
|||||||
#rpg-thought-icon {
|
#rpg-thought-icon {
|
||||||
/* Use transform to shift icon above and to the right of avatar */
|
/* Use transform to shift icon above and to the right of avatar */
|
||||||
transform: translate(50px, -45px) !important;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ========================================
|
/* ========================================
|
||||||
|
|||||||
Reference in New Issue
Block a user