Skip to content
location-ar-experience logo

Location-Based AR Experience

location-ar-experience

Designs location-based augmented reality experiences with geospatial anchoring, GPS integration, and real-world interactive overlays.

SKILL.md

Full skill instructions

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

ExperienceRequired AccuracyPositioning Method
City navigation5-15 metersGPS
POI discovery3-5 metersGPS + Wi-Fi
Building entrance1-2 metersGPS + Vision
Indoor navigation0.5-1 meterVision + beacons
Object placement1-10 cmPure 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

  1. Respect physical space: Don't place AR content blocking paths or in dangerous locations
  2. Consider lighting: Outdoor AR needs to handle bright sun and shadows
  3. Provide fallbacks: Show 2D map when AR isn't possible
  4. Clear affordances: Users should know what's interactive
  5. 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

  • references/​arcore-geospatial.md - ARCore Geospatial API guide
  • references/​arkit-location.md - ARKit location anchoring
  • references/​coordinate-systems.md - Geospatial coordinate conversions

More Other skills

Delegate independent tasks to specialized agents working concurrently with isolated context.

295.4K 206.3K
View

Applies Anthropic's official brand colors and typography to any sort of artifact that may benefit from having Anthropic's look-and-feel. Use it when brand colors or style guidelines, visual formatting, or company design standards apply.

179.7K 100.1K
View

Toolkit for styling artifacts with a theme. These artifacts can be slides, docs, reportings, HTML landing pages, etc. There are 10 pre-set themes with colors/fonts that you can apply to any artifact that has been creating, or can generate a new theme on-the-fly.

179.7K 89.5K
View

A set of resources to help me write all kinds of internal communications, using the formats that my company likes to use. Claude should use this skill whenever asked to write some sort of internal communications (status reports, leadership updates, 3P updates, company newsletters, FAQs, incident reports, project updates, etc.).

179.7K 76.1K
View

Fundamental design principles for writing maintainable, testable Python code.

40.2K 21.9K
View

Modern responsive layouts using container queries, fluid typography, CSS Grid, and mobile-first strategies.

40.2K 20K
View

Generate Playwright tests from scenarios using interactive browser exploration and validation.

39.7K 18.2K
View

Modern React state management with Redux Toolkit, Zustand, Jotai, and React Query for every state category.

40.2K 14.7K
View

GDPR-compliant data handling with consent management, data subject rights, and privacy controls.

40.2K 14.3K
View

Python error handling patterns including input validation, exception hierarchies, and partial failure handling. Use when implementing validation logic, designing exception strategies, handling batch processing failures, or building robust APIs.

40.2K 13.5K
View

Detailed multi-file refactor planning with safety checks and rollback guidance.

39.7K 13K
View