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}
|
currentPrompt={state.currentPrompt}
|
||||||
difficulty={state.difficulty}
|
difficulty={state.difficulty}
|
||||||
onRegionClick={clickRegion}
|
onRegionClick={clickRegion}
|
||||||
|
guessHistory={state.guessHistory}
|
||||||
|
playerMetadata={state.playerMetadata}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Game Mode Info */}
|
{/* Game Mode Info */}
|
||||||
|
|
@ -198,26 +200,6 @@ export function PlayingPhase() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue