fix(flashcards): correct pivot point to rotate around card center

Fixed issue where cards appeared to pivot around the grab point in
viewport space instead of rotating around their own center.

The problem: When dragging, the card would stay "pinned" at the grab
point in screen space, making it rotate around that viewport location.

The solution: As the card rotates, calculate the rotated grab offset
and reposition the card so that:
1. The grab point stays under the cursor
2. The card rotates around its own center
3. The visual result feels like the card is "stuck" to your finger

Implementation:
- Convert rotation angle to radians
- Apply 2D rotation matrix to grab offset vector
- Calculate card center position: cursor - rotated grab offset
- Convert center position to top-left for CSS translate positioning

This creates the natural feeling of grabbing and spinning a physical
card by its edge.

🤖 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-21 11:17:46 -05:00
parent e52d907087
commit 50fc3fdf7f
1 changed files with 39 additions and 5 deletions

View File

@ -206,11 +206,45 @@ function DraggableCard({ card }: DraggableCardProps) {
)
}
// Update card position
setPosition({
x: dragStartRef.current.cardX + deltaX,
y: dragStartRef.current.cardY + deltaY,
})
// Update card position - keep the grab point "stuck" to the cursor
// As the card rotates, the grab point rotates with it, so we need to account for rotation
const rotationRad = (clampedRotation * Math.PI) / 180
const cosRot = Math.cos(rotationRad)
const sinRot = Math.sin(rotationRad)
// Rotate the grab offset by the current rotation angle
const rotatedGrabX = grabOffsetRef.current.x * cosRot - grabOffsetRef.current.y * sinRot
const rotatedGrabY = grabOffsetRef.current.x * sinRot + grabOffsetRef.current.y * cosRot
// Current cursor position
const cursorX = e.clientX
const cursorY = e.clientY
// Card center should be at: cursor position - rotated grab offset
// But we need to position the card element (top-left), not the center
// Get card dimensions to calculate offset from center to top-left
if (cardRef.current) {
const rect = cardRef.current.getBoundingClientRect()
const cardWidth = rect.width
const cardHeight = rect.height
// Card center position in screen space
const cardCenterX = cursorX - rotatedGrabX
const cardCenterY = cursorY - rotatedGrabY
// Convert center position to top-left position (what we store in position state)
// Note: position.x/y is used in translate(), which positions the element
setPosition({
x: cardCenterX - cardWidth / 2,
y: cardCenterY - cardHeight / 2,
})
} else {
// Fallback to simple delta if we don't have card dimensions yet
setPosition({
x: dragStartRef.current.cardX + deltaX,
y: dragStartRef.current.cardY + deltaY,
})
}
}
const handlePointerUp = (e: React.PointerEvent) => {