fix: combine fast easing with smooth precision mode transition

**Problem:**
Previous fix prevented the zoom jump, but brought back slow easing
to the threshold (spring was targeting the capped value).

**Solution - Best of both worlds:**
1. Spring always targets UNCAPPED zoom (animates quickly to threshold)
2. Rendering clamps zoom at threshold when not in precision mode
3. When precision mode activates, clamping is removed
4. Spring continues smoothly from current value to target (no jump)

**How it works:**
- **Easing to threshold:** Spring targets 100×, but rendering shows max 20 px/px
  → Fast animation, immediate visual feedback at threshold
- **Activating precision mode:** pointerLocked becomes true
  → Clamping removed, spring continues from ~40× to 100×
  → Smooth continuation, no jarring jump

**Result:**
 Fast easing to precision mode threshold
 Smooth transition through threshold when activated
 No instantaneous zoom jumps
 Visual effects appear immediately at threshold

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock 2025-11-23 20:26:57 -06:00
parent 50f09a9a62
commit cab1fbff95
1 changed files with 23 additions and 29 deletions

View File

@ -1368,32 +1368,9 @@ export function MapRenderer({
)
}
// Cap the target zoom when not in precision mode
// When precision mode activates, the uncapped target will be set
let targetZoomValue = adaptiveZoom
if (!pointerLocked && containerRef.current && svgRef.current) {
const containerRect = containerRef.current.getBoundingClientRect()
const svgRect = svgRef.current.getBoundingClientRect()
const magnifierWidth = containerRect.width * 0.5
const viewBoxParts = mapData.viewBox.split(' ').map(Number)
const viewBoxWidth = viewBoxParts[2]
if (viewBoxWidth && !isNaN(viewBoxWidth)) {
// Calculate what the screen pixel ratio would be at this zoom
const magnifiedViewBoxWidth = viewBoxWidth / adaptiveZoom
const magnifierScreenPixelsPerSvgUnit = magnifierWidth / magnifiedViewBoxWidth
const mainMapSvgUnitsPerScreenPixel = viewBoxWidth / svgRect.width
const screenPixelRatio = mainMapSvgUnitsPerScreenPixel * magnifierScreenPixelsPerSvgUnit
// If it exceeds threshold, cap the target zoom
if (screenPixelRatio > PRECISION_MODE_THRESHOLD) {
const maxZoom = PRECISION_MODE_THRESHOLD / (magnifierWidth / svgRect.width)
targetZoomValue = Math.min(adaptiveZoom, maxZoom)
}
}
}
setTargetZoom(targetZoomValue)
// Always set the uncapped target zoom so spring animates quickly
// The capping will happen when rendering (in viewBox calculation)
setTargetZoom(adaptiveZoom)
setShowMagnifier(true)
setTargetOpacity(1)
setTargetTop(newTop)
@ -1875,9 +1852,26 @@ export function MapRenderer({
const cursorSvgX = (cursorPosition.x - svgOffsetX) * scaleX + viewBoxX
const cursorSvgY = (cursorPosition.y - svgOffsetY) * scaleY + viewBoxY
// Magnified view: use spring zoom value directly (capping happens at setTargetZoom)
const magnifiedWidth = viewBoxWidth / zoom
const magnifiedHeight = viewBoxHeight / zoom
// Clamp zoom at threshold when not in precision mode
// This creates a "pause" - spring keeps animating but rendered zoom stops at threshold
let effectiveZoom = zoom
if (!pointerLocked) {
const magnifierWidth = containerRect.width * 0.5
const magnifiedViewBoxWidth = viewBoxWidth / zoom
const magnifierScreenPixelsPerSvgUnit = magnifierWidth / magnifiedViewBoxWidth
const mainMapSvgUnitsPerScreenPixel = viewBoxWidth / svgRect.width
const screenPixelRatio =
mainMapSvgUnitsPerScreenPixel * magnifierScreenPixelsPerSvgUnit
if (screenPixelRatio > PRECISION_MODE_THRESHOLD) {
const maxZoom = PRECISION_MODE_THRESHOLD / (magnifierWidth / svgRect.width)
effectiveZoom = Math.min(zoom, maxZoom)
}
}
// Magnified view: use effective (clamped) zoom
const magnifiedWidth = viewBoxWidth / effectiveZoom
const magnifiedHeight = viewBoxHeight / effectiveZoom
// Center the magnified viewBox on the cursor
const magnifiedViewBoxX = cursorSvgX - magnifiedWidth / 2