Claude-skill-registry location-ar-experience
Designs location-based augmented reality experiences with geospatial anchoring, GPS integration, and real-world interactive overlays.
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/location-ar-experience" ~/.claude/skills/majiayu000-claude-skill-registry-location-ar-experience && rm -rf "$T"
manifest:
skills/data/location-ar-experience/SKILL.mdsource content
Location-Based AR Experience
This skill provides guidance for designing augmented reality experiences anchored to real-world locations, combining GPS, computer vision, and spatial computing.
Core Competencies
- Geospatial Anchoring: GPS, geofencing, coordinate systems
- Visual Positioning: SLAM, image recognition, cloud anchors
- Content Placement: World-scale AR, occlusion, persistence
- Mobile AR Platforms: ARKit, ARCore, WebXR
Location-Based AR Fundamentals
AR Experience Types
┌─────────────────────────────────────────────────────────────────────┐ │ Location-Based AR Spectrum │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ GPS-Only Hybrid Vision-Based │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │ ~10m │ │ ~1m │ │ ~1cm │ │ │ │accuracy │ │accuracy │ │accuracy │ │ │ └─────────┘ └─────────┘ └─────────┘ │ │ │ │ │ │ │ ▼ ▼ ▼ │ │ City-scale Building-scale Room-scale │ │ navigation experiences precision │ │ games POI overlays installations │ │ │ └─────────────────────────────────────────────────────────────────────┘
Accuracy Requirements by Experience Type
| Experience | Required Accuracy | Positioning Method |
|---|---|---|
| City navigation | 5-15 meters | GPS |
| POI discovery | 3-5 meters | GPS + Wi-Fi |
| Building entrance | 1-2 meters | GPS + Vision |
| Indoor navigation | 0.5-1 meter | Vision + beacons |
| Object placement | 1-10 cm | Pure visual SLAM |
Geospatial Systems
Coordinate Systems
from dataclasses import dataclass import math @dataclass class GeoCoordinate: """WGS84 coordinate""" latitude: float # -90 to 90 longitude: float # -180 to 180 altitude: float = 0 # meters above sea level def distance_to(self, other: 'GeoCoordinate') -> float: """Haversine distance in meters""" R = 6371000 # Earth radius in meters lat1, lat2 = math.radians(self.latitude), math.radians(other.latitude) dlat = math.radians(other.latitude - self.latitude) dlon = math.radians(other.longitude - self.longitude) a = (math.sin(dlat/2)**2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlon/2)**2) c = 2 * math.atan2(math.sqrt(a), math.sqrt(1-a)) return R * c def bearing_to(self, other: 'GeoCoordinate') -> float: """Bearing in degrees (0-360)""" lat1 = math.radians(self.latitude) lat2 = math.radians(other.latitude) dlon = math.radians(other.longitude - self.longitude) x = math.sin(dlon) * math.cos(lat2) y = (math.cos(lat1) * math.sin(lat2) - math.sin(lat1) * math.cos(lat2) * math.cos(dlon)) bearing = math.degrees(math.atan2(x, y)) return (bearing + 360) % 360 @dataclass class ARWorldCoordinate: """Local AR coordinate (meters from origin)""" x: float # East y: float # Up z: float # North def geo_to_local(geo: GeoCoordinate, origin: GeoCoordinate) -> ARWorldCoordinate: """Convert geographic to local AR coordinates""" # Simplified planar projection (accurate for small areas) lat_scale = 111320 # meters per degree latitude lon_scale = 111320 * math.cos(math.radians(origin.latitude)) x = (geo.longitude - origin.longitude) * lon_scale # East z = (geo.latitude - origin.latitude) * lat_scale # North y = geo.altitude - origin.altitude # Up return ARWorldCoordinate(x, y, z)
Geofencing
from enum import Enum from typing import List, Callable class GeofenceShape(Enum): CIRCLE = "circle" POLYGON = "polygon" class Geofence: """Define trigger zones for AR content""" def __init__(self, id: str, center: GeoCoordinate, radius: float): self.id = id self.center = center self.radius = radius # meters self.shape = GeofenceShape.CIRCLE self.on_enter: List[Callable] = [] self.on_exit: List[Callable] = [] self.on_dwell: List[Callable] = [] self.dwell_time = 30 # seconds def contains(self, point: GeoCoordinate) -> bool: """Check if point is inside geofence""" return self.center.distance_to(point) <= self.radius def add_enter_handler(self, callback: Callable): self.on_enter.append(callback) class GeofenceManager: """Manage multiple geofences""" def __init__(self): self.fences: dict[str, Geofence] = {} self.active_fences: set[str] = set() self.entry_times: dict[str, float] = {} def add_fence(self, fence: Geofence): self.fences[fence.id] = fence def update_position(self, position: GeoCoordinate, timestamp: float): """Check geofences and trigger callbacks""" currently_inside = set() for fence_id, fence in self.fences.items(): if fence.contains(position): currently_inside.add(fence_id) # Trigger enter event if fence_id not in self.active_fences: self.entry_times[fence_id] = timestamp for callback in fence.on_enter: callback(fence, position) # Check dwell time elif timestamp - self.entry_times[fence_id] >= fence.dwell_time: for callback in fence.on_dwell: callback(fence, position) # Trigger exit events for fence_id in self.active_fences - currently_inside: fence = self.fences[fence_id] for callback in fence.on_exit: callback(fence, position) del self.entry_times[fence_id] self.active_fences = currently_inside
Visual Positioning
ARCore Geospatial API Pattern
// Android/Kotlin with ARCore Geospatial API class GeospatialManager(private val session: Session) { fun placeAnchorAtLocation( latitude: Double, longitude: Double, altitude: Double, heading: Float ): Anchor? { val earth = session.earth ?: return null // Check tracking quality if (earth.trackingState != TrackingState.TRACKING) { return null } // Check horizontal accuracy val pose = earth.cameraGeospatialPose if (pose.horizontalAccuracy > 10) { // meters return null // Not accurate enough } // Create geospatial anchor val quaternion = Quaternion.axisAngle( Vector3(0f, 1f, 0f), Math.toRadians(heading.toDouble()).toFloat() ) return earth.createAnchor( latitude, longitude, altitude, quaternion.x, quaternion.y, quaternion.z, quaternion.w ) } fun resolveTerrainAnchor( latitude: Double, longitude: Double, heading: Float, callback: (Anchor?) -> Unit ) { val earth = session.earth ?: return callback(null) // Terrain anchor automatically determines altitude val future = earth.resolveAnchorOnTerrainAsync( latitude, longitude, 0.0, // altitude above terrain /* quaternion */ 0f, 0f, 0f, 1f, { anchor, state -> when (state) { Anchor.TerrainAnchorState.SUCCESS -> callback(anchor) else -> callback(null) } } ) } }
Cloud Anchors for Persistence
// iOS/Swift with ARKit Cloud Anchors class CloudAnchorManager { var arView: ARView var anchorStore: [String: ARAnchor] = [:] func saveAnchor(_ anchor: ARAnchor, completion: @escaping (String?) -> Void) { // Upload anchor to cloud service let anchorData = AnchorData( transform: anchor.transform, identifier: anchor.identifier.uuidString ) CloudService.shared.uploadAnchor(anchorData) { cloudId in if let id = cloudId { self.anchorStore[id] = anchor } completion(cloudId) } } func resolveAnchor(cloudId: String, completion: @escaping (ARAnchor?) -> Void) { CloudService.shared.downloadAnchor(cloudId) { anchorData in guard let data = anchorData else { completion(nil) return } let anchor = ARAnchor(transform: data.transform) self.arView.session.add(anchor: anchor) completion(anchor) } } }
Content Management
AR Content Data Model
from dataclasses import dataclass, field from typing import Optional, List from enum import Enum class ContentType(Enum): MODEL_3D = "model_3d" IMAGE = "image" VIDEO = "video" AUDIO = "audio" TEXT = "text" INTERACTIVE = "interactive" @dataclass class ARContent: """AR content anchored to location""" id: str content_type: ContentType location: GeoCoordinate # Asset references asset_url: str thumbnail_url: Optional[str] = None # Spatial properties scale: float = 1.0 rotation_y: float = 0.0 # Heading in degrees offset_y: float = 0.0 # Height offset from ground # Visibility rules min_distance: float = 0.0 max_distance: float = 100.0 visible_hours: Optional[tuple[int, int]] = None # Start, end hour # Interaction interactive: bool = False trigger_radius: float = 5.0 # Metadata title: str = "" description: str = "" tags: List[str] = field(default_factory=list) @dataclass class ARExperience: """Collection of AR content forming an experience""" id: str name: str description: str # Bounds center: GeoCoordinate radius: float # meters # Content contents: List[ARContent] = field(default_factory=list) # Experience flow ordered: bool = False # Sequential vs free exploration start_content_id: Optional[str] = None def get_visible_content( self, user_position: GeoCoordinate, current_hour: int ) -> List[ARContent]: """Filter content visible from user position""" visible = [] for content in self.contents: distance = user_position.distance_to(content.location) # Distance check if distance < content.min_distance or distance > content.max_distance: continue # Time check if content.visible_hours: start, end = content.visible_hours if not (start <= current_hour < end): continue visible.append(content) return visible
Spatial Data Loading Strategy
class SpatialContentLoader: """Efficiently load content near user""" def __init__(self, api_client): self.api = api_client self.cache: dict[str, ARContent] = {} self.loaded_tiles: set[str] = set() self.tile_size = 0.001 # ~111 meters at equator def get_tile_key(self, coord: GeoCoordinate) -> str: """Get tile identifier for coordinate""" lat_tile = int(coord.latitude / self.tile_size) lon_tile = int(coord.longitude / self.tile_size) return f"{lat_tile}:{lon_tile}" async def update_position(self, position: GeoCoordinate): """Load content for position and surrounding tiles""" # Get 3x3 grid of tiles around user tiles_needed = set() for dlat in [-1, 0, 1]: for dlon in [-1, 0, 1]: adjusted = GeoCoordinate( position.latitude + dlat * self.tile_size, position.longitude + dlon * self.tile_size ) tiles_needed.add(self.get_tile_key(adjusted)) # Load new tiles new_tiles = tiles_needed - self.loaded_tiles for tile in new_tiles: content = await self.api.get_content_for_tile(tile) for item in content: self.cache[item.id] = item self.loaded_tiles.add(tile) # Unload distant tiles (keep only 5x5 grid) # ... cleanup logic def get_nearby_content( self, position: GeoCoordinate, radius: float ) -> List[ARContent]: """Get cached content within radius""" return [ content for content in self.cache.values() if position.distance_to(content.location) <= radius ]
User Experience Patterns
Wayfinding
class ARWayfinder: """Guide user to AR content""" def __init__(self): self.current_target: Optional[ARContent] = None self.path: List[GeoCoordinate] = [] def set_target(self, content: ARContent, user_position: GeoCoordinate): """Set navigation target""" self.current_target = content self.path = self._calculate_path(user_position, content.location) def get_direction_indicator( self, user_position: GeoCoordinate, user_heading: float ) -> dict: """Get AR direction indicator data""" if not self.current_target: return None target_bearing = user_position.bearing_to(self.current_target.location) relative_bearing = (target_bearing - user_heading + 360) % 360 distance = user_position.distance_to(self.current_target.location) return { 'type': 'direction_arrow', 'bearing': relative_bearing, # 0 = straight ahead 'distance': distance, 'distance_text': self._format_distance(distance), 'in_view': -30 <= relative_bearing <= 30 or relative_bearing >= 330 } def _format_distance(self, meters: float) -> str: if meters < 100: return f"{int(meters)}m" elif meters < 1000: return f"{int(meters/10)*10}m" else: return f"{meters/1000:.1f}km"
Content Discovery
User Approaching Content: 100m away: [Map indicator only] "5 AR experiences nearby" 50m away: [Floating label appears] "Historic Building - 50m" ▼ 20m away: [Label grows, thumbnail shows] ┌──────────────┐ │ [image] │ │ Historic │ │ Building │ │ 20m → │ └──────────────┘ 5m away: [Full AR content triggers] 3D model appears at location Info panel available
Interaction Zones
class InteractionZone: """Define how users interact with AR content""" def __init__(self, content: ARContent): self.content = content # Zone radii (meters) self.awareness_radius = 100 # Show on map self.preview_radius = 50 # Show floating label self.activation_radius = 20 # Show full AR self.interaction_radius = 5 # Can interact def get_interaction_state( self, user_position: GeoCoordinate ) -> str: """Determine current interaction state""" distance = user_position.distance_to(self.content.location) if distance <= self.interaction_radius: return "interactive" elif distance <= self.activation_radius: return "active" elif distance <= self.preview_radius: return "preview" elif distance <= self.awareness_radius: return "aware" else: return "hidden"
Performance Optimization
LOD (Level of Detail)
class LODManager: """Manage content detail based on distance""" LOD_LEVELS = { 'full': {'max_distance': 10, 'quality': 'high'}, 'medium': {'max_distance': 30, 'quality': 'medium'}, 'low': {'max_distance': 100, 'quality': 'low'}, 'billboard': {'max_distance': float('inf'), 'quality': 'billboard'} } def get_lod_for_distance(self, distance: float) -> str: for level, config in self.LOD_LEVELS.items(): if distance <= config['max_distance']: return level return 'billboard' def get_asset_url(self, content: ARContent, lod: str) -> str: """Get appropriate asset for LOD level""" base_url = content.asset_url.rsplit('.', 1)[0] if lod == 'billboard': return content.thumbnail_url elif lod == 'low': return f"{base_url}_low.glb" elif lod == 'medium': return f"{base_url}_med.glb" else: return content.asset_url
Battery and Data Conservation
class LocationOptimizer: """Optimize location updates for battery life""" def __init__(self): self.high_accuracy_mode = False self.last_position: Optional[GeoCoordinate] = None self.movement_threshold = 5 # meters def should_update_ar(self, new_position: GeoCoordinate) -> bool: """Determine if AR scene needs update""" if not self.last_position: self.last_position = new_position return True distance_moved = self.last_position.distance_to(new_position) if distance_moved > self.movement_threshold: self.last_position = new_position return True return False def get_location_config(self, near_content: bool) -> dict: """Get GPS configuration based on context""" if near_content: return { 'accuracy': 'high', 'interval_ms': 1000, 'distance_filter': 1 } else: return { 'accuracy': 'balanced', 'interval_ms': 5000, 'distance_filter': 10 }
Best Practices
Design Guidelines
- Respect physical space: Don't place AR content blocking paths or in dangerous locations
- Consider lighting: Outdoor AR needs to handle bright sun and shadows
- Provide fallbacks: Show 2D map when AR isn't possible
- Clear affordances: Users should know what's interactive
- Graceful degradation: Work with varying GPS accuracy
Testing Considerations
- Test with real GPS, not just simulated coordinates
- Test in different weather and lighting conditions
- Test battery drain over extended sessions
- Test with poor network connectivity
References
- ARCore Geospatial API guidereferences/arcore-geospatial.md
- ARKit location anchoringreferences/arkit-location.md
- Geospatial coordinate conversionsreferences/coordinate-systems.md