feat: add adaptive zoom magnifier for Know Your World map
Add intelligent magnifying glass feature with smooth animations: Magnifier Features: - Appears when 7+ regions overlap in 50×50px box OR region <8px - Adaptive zoom (8×-24×) based on region density and size - Smooth zoom/opacity animations using react-spring - Dynamic positioning to avoid covering cursor - Visual indicator box on main map shows magnified area - Crosshair shows exact cursor position in magnified view Implementation Details: - Uses react-spring for smooth zoom and fade transitions - Position calculated per quadrant (opposite corner from cursor) - Zoom formula: base 8× + density factor + size factor - Animated SVG viewBox for seamless zooming - Dashed blue indicator rectangle tracks magnified region UI/UX Improvements: - Remove duplicate turn indicator (use player avatar dock) - Hide arrow labels feature behind flag (disabled by default) - Add Storybook stories for map renderer with tuning controls This makes clicking tiny island nations and crowded regions much easier! 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
e900e4465b
commit
1e8846cdb1
|
|
@ -0,0 +1,169 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
import { MapRenderer } from './MapRenderer'
|
||||
import { getFilteredMapData } from '../maps'
|
||||
import type { ContinentId } from '../continents'
|
||||
|
||||
const meta = {
|
||||
title: 'Arcade/KnowYourWorld/MapRenderer',
|
||||
component: MapRenderer,
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
},
|
||||
argTypes: {
|
||||
continent: {
|
||||
control: 'select',
|
||||
options: [
|
||||
'all',
|
||||
'africa',
|
||||
'asia',
|
||||
'europe',
|
||||
'north-america',
|
||||
'south-america',
|
||||
'oceania',
|
||||
'antarctica',
|
||||
],
|
||||
description: 'Select a continent to filter the map',
|
||||
},
|
||||
difficulty: {
|
||||
control: 'select',
|
||||
options: ['easy', 'hard'],
|
||||
description: 'Game difficulty',
|
||||
},
|
||||
showArrows: {
|
||||
control: 'boolean',
|
||||
description: 'Show arrow labels for small regions (experimental feature)',
|
||||
},
|
||||
centeringStrength: {
|
||||
control: { type: 'range', min: 0.1, max: 10, step: 0.1 },
|
||||
description: 'Force pulling labels back to regions (higher = stronger)',
|
||||
},
|
||||
collisionPadding: {
|
||||
control: { type: 'range', min: 0, max: 50, step: 1 },
|
||||
description: 'Extra padding around labels for collision detection',
|
||||
},
|
||||
simulationIterations: {
|
||||
control: { type: 'range', min: 0, max: 500, step: 10 },
|
||||
description: 'Number of simulation iterations (more = more settled)',
|
||||
},
|
||||
useObstacles: {
|
||||
control: 'boolean',
|
||||
description: 'Use region obstacles to push labels away from map',
|
||||
},
|
||||
obstaclePadding: {
|
||||
control: { type: 'range', min: 0, max: 50, step: 1 },
|
||||
description: 'Extra padding around region obstacles',
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof MapRenderer>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
// Mock data
|
||||
const mockPlayerMetadata = {
|
||||
'player-1': {
|
||||
id: 'player-1',
|
||||
name: 'Player 1',
|
||||
emoji: '😊',
|
||||
color: '#3b82f6',
|
||||
},
|
||||
'player-2': {
|
||||
id: 'player-2',
|
||||
name: 'Player 2',
|
||||
emoji: '🎮',
|
||||
color: '#ef4444',
|
||||
},
|
||||
}
|
||||
|
||||
// Story template
|
||||
const Template = (args: any) => {
|
||||
const mapData = getFilteredMapData('world', args.continent as ContinentId | 'all')
|
||||
|
||||
// Simulate some found regions (first 5 regions)
|
||||
const regionsFound = mapData.regions.slice(0, 5).map((r) => r.id)
|
||||
|
||||
// Mock guess history
|
||||
const guessHistory = regionsFound.map((regionId, index) => ({
|
||||
playerId: index % 2 === 0 ? 'player-1' : 'player-2',
|
||||
regionId,
|
||||
correct: true,
|
||||
}))
|
||||
|
||||
return (
|
||||
<div style={{ padding: '20px', minHeight: '100vh', background: '#111827' }}>
|
||||
<MapRenderer
|
||||
mapData={mapData}
|
||||
regionsFound={regionsFound}
|
||||
currentPrompt={mapData.regions[5]?.id || null}
|
||||
difficulty={args.difficulty}
|
||||
onRegionClick={(id, name) => console.log('Clicked:', id, name)}
|
||||
guessHistory={guessHistory}
|
||||
playerMetadata={mockPlayerMetadata}
|
||||
forceTuning={{
|
||||
showArrows: args.showArrows,
|
||||
centeringStrength: args.centeringStrength,
|
||||
collisionPadding: args.collisionPadding,
|
||||
simulationIterations: args.simulationIterations,
|
||||
useObstacles: args.useObstacles,
|
||||
obstaclePadding: args.obstaclePadding,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const Oceania: Story = {
|
||||
render: Template,
|
||||
args: {
|
||||
continent: 'oceania',
|
||||
difficulty: 'easy',
|
||||
showArrows: false,
|
||||
centeringStrength: 2.0,
|
||||
collisionPadding: 5,
|
||||
simulationIterations: 200,
|
||||
useObstacles: true,
|
||||
obstaclePadding: 10,
|
||||
},
|
||||
}
|
||||
|
||||
export const Europe: Story = {
|
||||
render: Template,
|
||||
args: {
|
||||
continent: 'europe',
|
||||
difficulty: 'easy',
|
||||
showArrows: false,
|
||||
centeringStrength: 2.0,
|
||||
collisionPadding: 5,
|
||||
simulationIterations: 200,
|
||||
useObstacles: true,
|
||||
obstaclePadding: 10,
|
||||
},
|
||||
}
|
||||
|
||||
export const Africa: Story = {
|
||||
render: Template,
|
||||
args: {
|
||||
continent: 'africa',
|
||||
difficulty: 'easy',
|
||||
showArrows: false,
|
||||
centeringStrength: 2.0,
|
||||
collisionPadding: 5,
|
||||
simulationIterations: 200,
|
||||
useObstacles: true,
|
||||
obstaclePadding: 10,
|
||||
},
|
||||
}
|
||||
|
||||
export const AllWorld: Story = {
|
||||
render: Template,
|
||||
args: {
|
||||
continent: 'all',
|
||||
difficulty: 'easy',
|
||||
showArrows: false,
|
||||
centeringStrength: 2.0,
|
||||
collisionPadding: 5,
|
||||
simulationIterations: 200,
|
||||
useObstacles: true,
|
||||
obstaclePadding: 10,
|
||||
},
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -158,6 +158,8 @@ export function PlayingPhase() {
|
|||
currentPrompt={state.currentPrompt}
|
||||
difficulty={state.difficulty}
|
||||
onRegionClick={clickRegion}
|
||||
guessHistory={state.guessHistory}
|
||||
playerMetadata={state.playerMetadata}
|
||||
/>
|
||||
|
||||
{/* Game Mode Info */}
|
||||
|
|
@ -198,26 +200,6 @@ export function PlayingPhase() {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Turn Indicator (for turn-based mode) */}
|
||||
{state.gameMode === 'turn-based' && (
|
||||
<div
|
||||
data-section="turn-indicator"
|
||||
className={css({
|
||||
textAlign: 'center',
|
||||
padding: '3',
|
||||
bg: isDark ? 'purple.900' : 'purple.50',
|
||||
rounded: 'lg',
|
||||
border: '2px solid',
|
||||
borderColor: 'purple.500',
|
||||
fontSize: 'lg',
|
||||
fontWeight: 'semibold',
|
||||
color: isDark ? 'purple.100' : 'purple.900',
|
||||
})}
|
||||
>
|
||||
Current Turn: {state.playerMetadata[state.currentPlayer]?.name || state.currentPlayer}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue