fix: correct avatar positioning to prevent fly-in animation

Fixed positioning model to work properly with react-spring:

ISSUE:
- Avatar was still flying in from top-right
- Using translate(-50%, -50%) with animated left/top caused positioning issues
- Spring was animating from wrong initial position

SOLUTION:
- Remove transform: translate(-50%, -50%)
- Use negative margins instead: marginLeft: -24px, marginTop: -24px
- Calculate exact center position from getBoundingClientRect
- Set 'from' position in spring to match 'to' on first render
- Use x.to() and y.to() to convert spring values to px strings

POSITIONING MODEL:
1. Get card's bounding rect
2. Calculate avatar center point: (rect.right - 12, rect.top - 12)
3. Apply via left/top with negative margins for centering
4. Spring animates between positions smoothly
5. On first hover: from/to are same (no animation)
6. On subsequent hovers: smooth glide between cards

RESULT:
Avatar appears exactly at the card position with no fly-in animation,
then smoothly glides to new positions as user moves between cards.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock
2025-10-09 16:18:37 -05:00
parent d312969747
commit 573d0df20d

View File

@@ -98,25 +98,35 @@ function HoverAvatar({
useEffect(() => {
if (cardElement) {
const rect = cardElement.getBoundingClientRect()
// Calculate the actual position we want the avatar centered at (top-right of card)
// Since we're using translate(-50%, -50%), we need the center point
const avatarSize = 48
const avatarCenterX = rect.right - 12 // 12px from right edge
const avatarCenterY = rect.top - 12 // 12px from top edge
setPosition({
x: rect.right - 12, // Position at top-right of card
y: rect.top - 12,
x: avatarCenterX,
y: avatarCenterY,
})
}
}, [cardElement])
// Smooth spring animation for position changes
// Only animate if we have a position (prevents fly-in from 0,0)
// Use 'from' to set initial position when avatar first appears
const springProps = useSpring({
x: position?.x ?? 0,
y: position?.y ?? 0,
opacity: position ? 1 : 0, // Fade in when position is set
from: position
? { x: position.x, y: position.y, opacity: 0 }
: { x: 0, y: 0, opacity: 0 },
to: {
x: position?.x ?? 0,
y: position?.y ?? 0,
opacity: position ? 1 : 0,
},
config: {
tension: 280,
friction: 60,
mass: 1,
},
immediate: !position, // No animation on first render
})
// Don't render until we have a position
@@ -126,11 +136,14 @@ function HoverAvatar({
<animated.div
style={{
position: 'fixed',
left: springProps.x,
top: springProps.y,
opacity: springProps.opacity, // Fade in smoothly
// Don't use translate, just position directly at the calculated point
left: springProps.x.to((x) => `${x}px`),
top: springProps.y.to((y) => `${y}px`),
opacity: springProps.opacity,
width: '48px',
height: '48px',
marginLeft: '-24px', // Center horizontally (half of width)
marginTop: '-24px', // Center vertically (half of height)
borderRadius: '50%',
background: playerInfo.color || 'linear-gradient(135deg, #667eea, #764ba2)',
display: 'flex',
@@ -143,7 +156,6 @@ function HoverAvatar({
border: '3px solid white',
zIndex: 1000,
pointerEvents: 'none',
transform: 'translate(-50%, -50%)',
filter: 'drop-shadow(0 0 8px rgba(102, 126, 234, 0.8))',
}}
className={css({