Compare commits

..

6 Commits

Author SHA1 Message Date
semantic-release-bot
1fd0474cd5 chore(release): 4.63.0 [skip ci]
## [4.63.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.62.1...v4.63.0) (2025-10-21)

### Features

* **flashcards:** add grab point physics for realistic rotation ([bf37eb1](bf37eb1928))
2025-10-21 16:05:21 +00:00
Thomas Hallock
bf37eb1928 feat(flashcards): add grab point physics for realistic rotation
Implements physics-based rotation that responds to where the user grabs
the card. When you grab a card off-center and drag it, it rotates
naturally based on the grab point and drag direction.

Features:
- Calculate grab offset from card center on pointer down
- Apply rotation using cross product of grab offset and drag direction
- Rotation clamped to ±45° to prevent excessive spinning
- Final rotation preserved when card is released
- Console logging for grab point coordinates and rotation changes

Physics details:
- Cross product (grabOffset.x * deltaY - grabOffset.y * deltaX) determines
  rotation direction and magnitude
- Grabbing left side + dragging right = clockwise rotation
- Grabbing right side + dragging left = counter-clockwise rotation
- Scale factor of 5000 provides smooth, realistic rotation feel

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-21 11:04:07 -05:00
semantic-release-bot
9f56c9728c chore(release): 4.62.1 [skip ci]
## [4.62.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.62.0...v4.62.1) (2025-10-21)

### Bug Fixes

* **flashcards:** improve shadow speed logging with separate throttling ([0f51366](0f51366fd5))
2025-10-21 15:59:26 +00:00
Thomas Hallock
0f51366fd5 fix(flashcards): improve shadow speed logging with separate throttling
Fixed logging issue where speed logs weren't showing during drag:
- Added separate lastLogTimeRef for logging throttle
- Logs now appear every ~200ms during drag (was never logging before)
- Velocity calculation unchanged, only logging throttle fixed

Now you can see speed values in console during drag to verify
shadow responsiveness.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-21 10:58:07 -05:00
semantic-release-bot
fe1e8979c8 chore(release): 4.62.0 [skip ci]
## [4.62.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.61.3...v4.62.0) (2025-10-21)

### Features

* **flashcards:** add dynamic shadow based on drag speed ([92148a4](92148a4cf8))
2025-10-21 15:52:28 +00:00
Thomas Hallock
92148a4cf8 feat(flashcards): add dynamic shadow based on drag speed
First physics enhancement - shadow changes based on how fast you drag:
- Tracks drag velocity (distance/time) during pointer move
- Shadow grows larger and darker with faster dragging
- Base: 8px offset, 24px blur, 0.3 opacity
- Fast: 32px offset, 64px blur, 0.6 opacity
- Smooth decay when released

Console logging included:
- [Shadow] logs on drag start/release
- Speed/distance/time logged during drag (throttled to ~100ms)

Test: Drag cards slowly vs fast and watch shadow change

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-21 10:51:08 -05:00
3 changed files with 127 additions and 6 deletions

View File

@@ -1,3 +1,24 @@
## [4.63.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.62.1...v4.63.0) (2025-10-21)
### Features
* **flashcards:** add grab point physics for realistic rotation ([bf37eb1](https://github.com/antialias/soroban-abacus-flashcards/commit/bf37eb1928de8d07673234e2faa1fa6268c45686))
## [4.62.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.62.0...v4.62.1) (2025-10-21)
### Bug Fixes
* **flashcards:** improve shadow speed logging with separate throttling ([0f51366](https://github.com/antialias/soroban-abacus-flashcards/commit/0f51366fd56540e691df4931b6350c03043484f1))
## [4.62.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.61.3...v4.62.0) (2025-10-21)
### Features
* **flashcards:** add dynamic shadow based on drag speed ([92148a4](https://github.com/antialias/soroban-abacus-flashcards/commit/92148a4cf87e828ba2e5ec1740fb51d9667c1d73))
## [4.61.3](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.61.2...v4.61.3) (2025-10-21)

View File

@@ -94,16 +94,24 @@ interface DraggableCardProps {
function DraggableCard({ card }: DraggableCardProps) {
// Track position - starts at initial, updates when dragged
const [position, setPosition] = useState({ x: card.initialX, y: card.initialY })
const [rotation] = useState(card.initialRotation)
const [rotation, setRotation] = useState(card.initialRotation) // Now dynamic!
const [zIndex, setZIndex] = useState(card.zIndex)
const [isDragging, setIsDragging] = useState(false)
const [dragSpeed, setDragSpeed] = useState(0) // Speed for dynamic shadow
// Track drag state
const dragStartRef = useRef<{ x: number; y: number; cardX: number; cardY: number } | null>(null)
const grabOffsetRef = useRef<{ x: number; y: number }>({ x: 0, y: 0 }) // Offset from card center where grabbed
const baseRotationRef = useRef(card.initialRotation) // Starting rotation
const lastMoveTimeRef = useRef<number>(0)
const lastMovePositionRef = useRef<{ x: number; y: number }>({ x: 0, y: 0 })
const lastLogTimeRef = useRef<number>(0) // Separate throttling for logging
const cardRef = useRef<HTMLDivElement>(null) // Reference to card element
const handlePointerDown = (e: React.PointerEvent) => {
setIsDragging(true)
setZIndex(1000) // Bring to front
setDragSpeed(0)
// Capture the pointer
e.currentTarget.setPointerCapture(e.pointerId)
@@ -115,6 +123,31 @@ function DraggableCard({ card }: DraggableCardProps) {
cardX: position.x,
cardY: position.y,
}
// Calculate grab offset from card center
if (cardRef.current) {
const rect = cardRef.current.getBoundingClientRect()
const cardCenterX = rect.left + rect.width / 2
const cardCenterY = rect.top + rect.height / 2
grabOffsetRef.current = {
x: e.clientX - cardCenterX,
y: e.clientY - cardCenterY,
}
console.log(
`[GrabPoint] Grabbed at offset: (${grabOffsetRef.current.x.toFixed(0)}, ${grabOffsetRef.current.y.toFixed(0)})px from center`
)
}
// Store the current rotation as the base for this drag
baseRotationRef.current = rotation
// Initialize velocity tracking
const now = Date.now()
lastMoveTimeRef.current = now
lastMovePositionRef.current = { x: e.clientX, y: e.clientY }
lastLogTimeRef.current = now
console.log('[Shadow] Drag started, speed reset to 0')
}
const handlePointerMove = (e: React.PointerEvent) => {
@@ -124,6 +157,47 @@ function DraggableCard({ card }: DraggableCardProps) {
const deltaX = e.clientX - dragStartRef.current.x
const deltaY = e.clientY - dragStartRef.current.y
// Calculate velocity for dynamic shadow
const now = Date.now()
const timeDelta = now - lastMoveTimeRef.current
if (timeDelta > 0) {
// Distance moved since last frame
const distX = e.clientX - lastMovePositionRef.current.x
const distY = e.clientY - lastMovePositionRef.current.y
const distance = Math.sqrt(distX * distX + distY * distY)
// Speed in pixels per millisecond, then convert to reasonable scale
const speed = distance / timeDelta
const scaledSpeed = Math.min(speed * 100, 100) // Cap at 100 for reasonable shadow size
setDragSpeed(scaledSpeed)
// Log occasionally (every ~200ms) to avoid console spam
const timeSinceLastLog = now - lastLogTimeRef.current
if (timeSinceLastLog > 200) {
console.log(
`[Shadow] Speed: ${scaledSpeed.toFixed(1)}, distance: ${distance.toFixed(0)}px, timeDelta: ${timeDelta}ms`
)
lastLogTimeRef.current = now
}
lastMoveTimeRef.current = now
lastMovePositionRef.current = { x: e.clientX, y: e.clientY }
}
// Calculate rotation based on grab point physics
// Cross product of grab offset and drag direction determines rotation
// If grabbed on left and dragged right → clockwise rotation
// If grabbed on right and dragged left → counter-clockwise rotation
const crossProduct = grabOffsetRef.current.x * deltaY - grabOffsetRef.current.y * deltaX
const rotationInfluence = crossProduct / 5000 // Scale factor for reasonable rotation (adjust as needed)
const newRotation = baseRotationRef.current + rotationInfluence
// Clamp rotation to prevent excessive spinning
const clampedRotation = Math.max(-45, Math.min(45, newRotation))
setRotation(clampedRotation)
// Update card position
setPosition({
x: dragStartRef.current.cardX + deltaX,
@@ -135,12 +209,38 @@ function DraggableCard({ card }: DraggableCardProps) {
setIsDragging(false)
dragStartRef.current = null
console.log('[Shadow] Drag released, speed decaying to 0')
console.log(
`[GrabPoint] Final rotation: ${rotation.toFixed(1)}° (base was ${baseRotationRef.current.toFixed(1)}°)`
)
// Gradually decay speed back to 0 for smooth shadow transition
const decayInterval = setInterval(() => {
setDragSpeed((prev) => {
const newSpeed = prev * 0.8 // Decay by 20% each frame
if (newSpeed < 1) {
clearInterval(decayInterval)
return 0
}
return newSpeed
})
}, 50) // Update every 50ms
// Release the pointer capture
e.currentTarget.releasePointerCapture(e.pointerId)
}
// Calculate dynamic shadow based on drag speed
// Base shadow: 0 8px 24px rgba(0, 0, 0, 0.3)
// Fast drag: 0 32px 64px rgba(0, 0, 0, 0.6)
const shadowY = 8 + (dragSpeed / 100) * 24 // 8px to 32px
const shadowBlur = 24 + (dragSpeed / 100) * 40 // 24px to 64px
const shadowOpacity = 0.3 + (dragSpeed / 100) * 0.3 // 0.3 to 0.6
const boxShadow = `0 ${shadowY}px ${shadowBlur}px rgba(0, 0, 0, ${shadowOpacity})`
return (
<div
ref={cardRef}
onPointerDown={handlePointerDown}
onPointerMove={handlePointerMove}
onPointerUp={handlePointerUp}
@@ -159,20 +259,20 @@ function DraggableCard({ card }: DraggableCardProps) {
})}
>
<div
style={{
boxShadow, // Dynamic shadow based on drag speed
}}
className={css({
bg: 'white',
rounded: 'lg',
p: '4',
boxShadow: isDragging
? '0 16px 48px rgba(0, 0, 0, 0.5)'
: '0 8px 24px rgba(0, 0, 0, 0.3)',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '2',
minW: '120px',
border: '2px solid rgba(0, 0, 0, 0.1)',
transition: 'box-shadow 0.2s',
transition: 'box-shadow 0.1s', // Quick transition for responsive feel
})}
>
{/* Abacus visualization */}

View File

@@ -1,6 +1,6 @@
{
"name": "soroban-monorepo",
"version": "4.61.3",
"version": "4.63.0",
"private": true,
"description": "Beautiful Soroban Flashcard Generator - Monorepo",
"workspaces": [