flutter_globe 1.0.0 copy "flutter_globe: ^1.0.0" to clipboard
flutter_globe: ^1.0.0 copied to clipboard

Interactive Canvas globes for Flutter with geographic markers, animated arcs, configurable themes, data layers, and camera controls.

example/lib/main.dart

import 'dart:ui' as ui;
import 'dart:math' as math;
import 'benchmark.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_globe/flutter_globe.dart';

void main() {
  runApp(const GlobeProShowcaseApp());
}

/// The main entry point for the Flutter Globe open-source showcase application.
class GlobeProShowcaseApp extends StatelessWidget {
  const GlobeProShowcaseApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter 3D Globe Showcase',
      debugShowCheckedModeBanner: false,
      theme: ThemeData.dark().copyWith(
        scaffoldBackgroundColor: const Color(0xFF020617),
        colorScheme: const ColorScheme.dark(
          primary: Color(0xFF38BDF8),
          surface: Color(0xFF0F172A),
          secondary: Color(0xFF06B6D4),
        ),
      ),
      home: const GlobeShowcaseScreen(),
    );
  }
}

/// Main destination tabs in the showcase application (strictly no tour tab).
enum ShowcaseTab {
  showcase('Showcase', Icons.public_rounded),
  skins('Skins', Icons.palette_outlined),
  arcs('Arcs', Icons.all_inclusive_rounded),
  markers('Markers', Icons.place_outlined),
  layers('Data Layers', Icons.layers_outlined),
  playground('Playground', Icons.tune_rounded),
  benchmarks('Benchmarks', Icons.speed_rounded);

  const ShowcaseTab(this.label, this.icon);
  final String label;
  final IconData icon;
}

class GlobeShowcaseScreen extends StatefulWidget {
  const GlobeShowcaseScreen({super.key});

  @override
  State<GlobeShowcaseScreen> createState() => _GlobeShowcaseScreenState();
}

class _GlobeShowcaseScreenState extends State<GlobeShowcaseScreen>
    with TickerProviderStateMixin {
  late GlobeController _controller;
  ShowcaseTab _activeTab = ShowcaseTab.showcase;

  // Active theme and skin selection
  int _selectedSkinIndex = 0;
  GlobeTheme _activeTheme = GlobeSkins.reference;
  bool _isFullscreen = false;
  Key _globeKey = UniqueKey();

  // Intro animation settings
  final GlobeIntroAnimation _introConfig = GlobeIntroAnimations.reference;

  // Interactive & visual state
  bool _autoRotate = true;
  double _autoRotateSpeed = 0.22;
  GlobeProjection _projection = GlobeProjection.perspective;
  GlobeQuality _quality = GlobeQuality.auto;
  double _radiusMultiplier = 0.82;
  double _pointSize = 1.65;
  double _pointOpacity = 0.95;
  double _globeOpacity = 1.0;
  bool _showAtmosphere = true;
  double _atmosphereAltitude = 0.22;
  double _atmosphereGlow = 0.75;
  double _ambientLight = 0.35;
  double _directionalLight = 0.65;
  bool _enableZoom = true;
  bool _inertiaEnabled = true;
  double _rotationSensitivity = 0.005;
  bool _showGrid = false;
  bool _showDayNight = false;
  bool _showHeatmap = false;
  bool _showRoutes = true;
  bool _showParticles = false;

  // Arc Showcase settings
  double _arcAltitude = 0.28;
  double _arcStrokeWidth = 2.0;
  double _arcSpeedMs = 2400.0;
  final bool _arcShowBaseLine = true;
  double _markerSize = 5.0;
  bool _markerPulse = true;

  // Inspection HUD
  GlobeMarker? _inspectedMarker;
  GlobeCoordinate? _inspectedCoordinate;

  @override
  void initState() {
    super.initState();
    _controller = GlobeController(
      autoRotate: _autoRotate,
      autoRotateSpeed: _autoRotateSpeed,
    );
    _controller.addListener(() {
      if (mounted && _autoRotate != _controller.autoRotate) {
        setState(() {
          _autoRotate = _controller.autoRotate;
        });
      }
    });
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  void _replayIntroAnimation() {
    setState(() {
      _globeKey = UniqueKey();
    });
  }

  void _selectSkin(int index) {
    setState(() {
      _selectedSkinIndex = index;
      _activeTheme = GlobeSkins.all[index];
      _pointSize = _activeTheme.surface.pointSize;
      _pointOpacity = _activeTheme.surface.pointOpacity;
      _globeOpacity = _activeTheme.surface.globeOpacity;
      _showAtmosphere = _activeTheme.atmosphere.visible;
      _atmosphereAltitude = _activeTheme.atmosphere.altitude;
      _atmosphereGlow = _activeTheme.atmosphere.glowIntensity;
      _ambientLight = _activeTheme.lighting.ambientIntensity;
      _directionalLight = _activeTheme.lighting.directionalIntensity;
    });
  }

  GlobeTheme get _configuredTheme => _activeTheme.copyWith(
        surface: _activeTheme.surface.copyWith(
          pointSize: _pointSize,
          pointOpacity: _pointOpacity,
          globeOpacity: _globeOpacity,
        ),
        atmosphere: _activeTheme.atmosphere.copyWith(
          visible: _showAtmosphere,
          altitude: _atmosphereAltitude,
          glowIntensity: _atmosphereGlow,
        ),
        lighting: _activeTheme.lighting.copyWith(
          ambientIntensity: _ambientLight,
          directionalIntensity: _directionalLight,
        ),
        interaction: _activeTheme.interaction.copyWith(
          zoomEnabled: _enableZoom,
          inertiaEnabled: _inertiaEnabled,
          rotationSensitivity: _rotationSensitivity,
          autoRotate: _autoRotate,
          autoRotateSpeed: _autoRotateSpeed,
        ),
        quality: _quality,
      );

  List<GlobeMarker> _getStandardMarkers() {
    final color = _activeTheme.accentColor;
    return [
      GlobeMarker.latLng(
        latitude: 37.7749,
        longitude: -122.4194,
        label: 'San Francisco',
        color: color,
        size: _markerSize,
        pulse: _markerPulse,
      ),
      GlobeMarker.latLng(
        latitude: 51.5074,
        longitude: -0.1278,
        label: 'London',
        color: color,
        size: _markerSize,
        pulse: _markerPulse,
      ),
      GlobeMarker.latLng(
        latitude: 35.6762,
        longitude: 139.6503,
        label: 'Tokyo',
        color: color,
        size: _markerSize,
        pulse: _markerPulse,
      ),
      GlobeMarker.latLng(
        latitude: 25.2048,
        longitude: 55.2708,
        label: 'Dubai',
        color: color,
        size: _markerSize,
        pulse: _markerPulse,
      ),
      GlobeMarker.latLng(
        latitude: -33.8688,
        longitude: 151.2093,
        label: 'Sydney',
        color: color,
        size: _markerSize,
        pulse: _markerPulse,
      ),
      GlobeMarker.latLng(
        latitude: -22.9068,
        longitude: -43.1729,
        label: 'Rio de Janeiro',
        color: color,
        size: _markerSize,
        pulse: _markerPulse,
      ),
      GlobeMarker.latLng(
        latitude: 1.3521,
        longitude: 103.8198,
        label: 'Singapore',
        color: color,
        size: _markerSize,
        pulse: _markerPulse,
      ),
    ];
  }

  List<GlobeArc> _getStandardArcs() {
    final color = _activeTheme.accentColor;
    return [
      GlobeArc(
        start: const GlobeCoordinate(latitude: 37.7749, longitude: -122.4194),
        end: const GlobeCoordinate(latitude: 51.5074, longitude: -0.1278),
        color: color,
        altitude: _arcAltitude,
        strokeWidth: _arcStrokeWidth,
        duration: Duration(milliseconds: _arcSpeedMs.round()),
        showBaseLine: _arcShowBaseLine,
      ),
      GlobeArc(
        start: const GlobeCoordinate(latitude: 51.5074, longitude: -0.1278),
        end: const GlobeCoordinate(latitude: 25.2048, longitude: 55.2708),
        color: color,
        altitude: _arcAltitude + 0.04,
        strokeWidth: _arcStrokeWidth,
        duration: Duration(milliseconds: (_arcSpeedMs * 0.9).round()),
        delay: const Duration(milliseconds: 300),
        showBaseLine: _arcShowBaseLine,
      ),
      GlobeArc(
        start: const GlobeCoordinate(latitude: 25.2048, longitude: 55.2708),
        end: const GlobeCoordinate(latitude: 35.6762, longitude: 139.6503),
        color: color,
        altitude: _arcAltitude + 0.08,
        strokeWidth: _arcStrokeWidth,
        duration: Duration(milliseconds: (_arcSpeedMs * 1.1).round()),
        delay: const Duration(milliseconds: 600),
        showBaseLine: _arcShowBaseLine,
      ),
      GlobeArc(
        start: const GlobeCoordinate(latitude: 35.6762, longitude: 139.6503),
        end: const GlobeCoordinate(latitude: -33.8688, longitude: 151.2093),
        color: color,
        altitude: _arcAltitude + 0.02,
        strokeWidth: _arcStrokeWidth,
        duration: Duration(milliseconds: (_arcSpeedMs * 0.85).round()),
        delay: const Duration(milliseconds: 900),
        showBaseLine: _arcShowBaseLine,
      ),
      GlobeArc(
        start: const GlobeCoordinate(latitude: 37.7749, longitude: -122.4194),
        end: const GlobeCoordinate(latitude: 35.6762, longitude: 139.6503),
        color: color,
        altitude: _arcAltitude + 0.12,
        strokeWidth: _arcStrokeWidth,
        duration: Duration(milliseconds: (_arcSpeedMs * 1.25).round()),
        delay: const Duration(milliseconds: 400),
        showBaseLine: _arcShowBaseLine,
      ),
    ];
  }

  String _generateDartCode({bool fullConfig = false}) {
    final skinName =
        GlobeSkins.named.entries.firstWhere((e) => e.value == _activeTheme).key;
    int colorValue(Color color) {
      // `value` keeps the declared Flutter 3.27 compatibility.
      // ignore: deprecated_member_use
      return color.value;
    }

    String color(Color value) =>
        'const Color(0x${colorValue(value).toRadixString(16).padLeft(8, '0')})';
    String coordinate(GlobeCoordinate c) =>
        'GlobeCoordinate(latitude: ${c.latitude}, longitude: ${c.longitude})';
    final markers = _getStandardMarkers()
        .map((m) =>
            "GlobeMarker(coordinate: ${coordinate(m.coordinate)}, label: '${m.label}', color: ${color(m.color)}, size: $_markerSize, pulse: $_markerPulse)")
        .join(',\n    ');
    final arcs = (_activeTab == ShowcaseTab.markers
            ? <GlobeArc>[]
            : _getStandardArcs())
        .map((a) =>
            'GlobeArc(start: ${coordinate(a.start)}, end: ${coordinate(a.end)}, color: ${color(a.color)}, altitude: ${a.altitude}, strokeWidth: ${a.strokeWidth}, duration: Duration(microseconds: ${a.duration.inMicroseconds}), delay: Duration(microseconds: ${a.delay.inMicroseconds}))')
        .join(',\n    ');
    final globe = """Globe(
  theme: GlobeSkins.$skinName.copyWith(
    surface: GlobeSkins.$skinName.surface.copyWith(pointSize: $_pointSize, pointOpacity: $_pointOpacity, globeOpacity: $_globeOpacity),
    atmosphere: GlobeSkins.$skinName.atmosphere.copyWith(visible: $_showAtmosphere, altitude: $_atmosphereAltitude, glowIntensity: $_atmosphereGlow),
    lighting: GlobeSkins.$skinName.lighting.copyWith(ambientIntensity: $_ambientLight, directionalIntensity: $_directionalLight),
    interaction: GlobeSkins.$skinName.interaction.copyWith(zoomEnabled: $_enableZoom, inertiaEnabled: $_inertiaEnabled, rotationSensitivity: $_rotationSensitivity, autoRotate: $_autoRotate, autoRotateSpeed: $_autoRotateSpeed),
    quality: GlobeQuality.${_quality.name},
  ),
  projection: GlobeProjection.${_projection.name},
  radiusMultiplier: $_radiusMultiplier,
  markers: [$markers],
  arcs: [$arcs],
)""";
    if (!fullConfig) return globe;
    return """import 'package:flutter/material.dart';
import 'package:flutter_globe/flutter_globe.dart';

void main() => runApp(MaterialApp(home: Scaffold(
  backgroundColor: const Color(0xFF020617),
  body: Center(child: SizedBox.square(dimension: 400, child: $globe)),
)));
""";
  }

  void _showCodeExportDialog() {
    var isFullConfig = false;
    showModalBottomSheet<void>(
      context: context,
      isScrollControlled: true,
      backgroundColor: const Color(0xFF0F172A),
      shape: const RoundedRectangleBorder(
        borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
      ),
      builder: (ctx) {
        return StatefulBuilder(
          builder: (context, setSheetState) {
            final code = _generateDartCode(fullConfig: isFullConfig);
            return DraggableScrollableSheet(
              initialChildSize: 0.70,
              minChildSize: 0.45,
              maxChildSize: 0.95,
              expand: false,
              builder: (_, scrollController) {
                return Padding(
                  padding: const EdgeInsets.all(24),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Center(
                        child: Container(
                          width: 44,
                          height: 4,
                          decoration: BoxDecoration(
                            color: const Color(0xFF334155),
                            borderRadius: BorderRadius.circular(2),
                          ),
                        ),
                      ),
                      const SizedBox(height: 18),
                      Row(
                        mainAxisAlignment: MainAxisAlignment.spaceBetween,
                        children: [
                          Expanded(
                              child: Column(
                            crossAxisAlignment: CrossAxisAlignment.start,
                            children: [
                              const Text(
                                'Scene code',
                                style: TextStyle(
                                  fontSize: 18,
                                  fontWeight: FontWeight.bold,
                                  color: Colors.white,
                                ),
                              ),
                              const SizedBox(height: 2),
                              Text(
                                isFullConfig
                                    ? 'Runnable app with current scene'
                                    : 'Current globe configuration',
                                style: const TextStyle(
                                  fontSize: 12,
                                  color: Colors.white60,
                                ),
                              ),
                            ],
                          )),
                          ElevatedButton.icon(
                            icon: const Icon(Icons.copy_rounded, size: 16),
                            label: const Text('Copy Code'),
                            style: ElevatedButton.styleFrom(
                              backgroundColor: const Color(0xFF38BDF8),
                              foregroundColor: Colors.black,
                              padding: const EdgeInsets.symmetric(
                                  horizontal: 16, vertical: 10),
                              shape: RoundedRectangleBorder(
                                  borderRadius: BorderRadius.circular(10)),
                            ),
                            onPressed: () async {
                              try {
                                await Clipboard.setData(
                                    ClipboardData(text: code));
                              } on PlatformException {
                                if (context.mounted) {
                                  ScaffoldMessenger.of(context).showSnackBar(
                                      const SnackBar(
                                          content:
                                              Text('Clipboard unavailable')));
                                }
                                return;
                              }
                              if (!context.mounted || !ctx.mounted) return;
                              Navigator.pop(ctx);
                              ScaffoldMessenger.of(context).showSnackBar(
                                const SnackBar(
                                  content: Text(
                                      'Copied generated Dart code to clipboard!'),
                                  backgroundColor: Color(0xFF0284C7),
                                ),
                              );
                            },
                          ),
                        ],
                      ),
                      const SizedBox(height: 14),
                      Row(
                        children: [
                          ChoiceChip(
                            label: const Text('Minimal Config'),
                            selected: !isFullConfig,
                            onSelected: (val) {
                              if (val) {
                                setSheetState(() => isFullConfig = false);
                              }
                            },
                          ),
                          const SizedBox(width: 8),
                          ChoiceChip(
                            label: const Text('Runnable app'),
                            selected: isFullConfig,
                            onSelected: (val) {
                              if (val) {
                                setSheetState(() => isFullConfig = true);
                              }
                            },
                          ),
                        ],
                      ),
                      const SizedBox(height: 14),
                      Expanded(
                        child: Container(
                          padding: const EdgeInsets.all(16),
                          decoration: BoxDecoration(
                            color: const Color(0xFF020617),
                            borderRadius: BorderRadius.circular(14),
                            border: Border.all(color: const Color(0xFF1E293B)),
                          ),
                          child: SingleChildScrollView(
                            controller: scrollController,
                            child: SelectableText(
                              code,
                              style: const TextStyle(
                                fontFamily: 'monospace',
                                fontSize: 12.5,
                                height: 1.5,
                                color: Color(0xFFE2E8F0),
                              ),
                            ),
                          ),
                        ),
                      ),
                    ],
                  ),
                );
              },
            );
          },
        );
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Stack(
        children: [
          // Background Gradient with atmospheric depth
          Positioned.fill(
            child: Container(
              decoration: const BoxDecoration(
                gradient: RadialGradient(
                  center: Alignment.center,
                  radius: 1.35,
                  colors: [
                    Color(0xFF0B132B),
                    Color(0xFF020617),
                  ],
                ),
              ),
            ),
          ),

          // Central 3D Globe Render View
          Positioned.fill(
            child: Center(
              child: AspectRatio(
                aspectRatio: 1.0,
                child: _buildActiveGlobe(),
              ),
            ),
          ),

          if (_activeTab == ShowcaseTab.benchmarks)
            const Positioned.fill(
                top: 130,
                child: ColoredBox(
                    color: Color(0xFF020617), child: BenchmarkPage())),

          // Top Header & Performance Monitor HUD
          if (!_isFullscreen)
            Positioned(
              top: 0,
              left: 0,
              right: 0,
              child: SafeArea(
                child: _buildTopHeader(),
              ),
            ),

          // Inspected Marker / Telemetry HUD Card
          if (_inspectedMarker != null || _inspectedCoordinate != null)
            Positioned(
              top: 140,
              right: 20,
              child: _buildInspectionHUDCard(),
            ),

          // Bottom Control Panel / Navigation Dock
          if (!_isFullscreen)
            Positioned(
              left: 16,
              right: 16,
              bottom: 16,
              child: SafeArea(
                child: ConstrainedBox(
                    constraints: BoxConstraints(
                        maxHeight: MediaQuery.sizeOf(context).height * 0.4),
                    child: SingleChildScrollView(child: _buildBottomPanel())),
              ),
            ),

          // Exit Fullscreen Floating Button
          if (_isFullscreen)
            Positioned(
              top: 24,
              right: 24,
              child: FloatingActionButton.small(
                backgroundColor: const Color(0xFF0F172A),
                foregroundColor: Colors.white,
                tooltip: 'Exit fullscreen preview',
                child: const Icon(Icons.fullscreen_exit_rounded),
                onPressed: () => setState(() => _isFullscreen = false),
              ),
            ),
        ],
      ),
    );
  }

  Widget _buildActiveGlobe() {
    switch (_activeTab) {
      case ShowcaseTab.benchmarks:
        return const SizedBox.shrink();
      case ShowcaseTab.playground:
      case ShowcaseTab.showcase:
        return Globe(
          key: _globeKey,
          controller: _controller,
          theme: _configuredTheme,
          introAnimation: _introConfig,
          projection: _projection,
          radiusMultiplier: _radiusMultiplier,
          markers: _getStandardMarkers(),
          arcs: _getStandardArcs(),
          onMarkerTap: (m) => setState(() => _inspectedMarker = m),
          onGlobeTap: (c) => setState(() => _inspectedCoordinate = c),
        );

      case ShowcaseTab.skins:
        return GlobeThemeTransition(
          theme: _activeTheme,
          builder: (context, interpolatedTheme) {
            return Globe(
              key: _globeKey,
              controller: _controller,
              theme: interpolatedTheme,
              projection: _projection,
              radiusMultiplier: _radiusMultiplier,
              markers: _getStandardMarkers(),
              arcs: _getStandardArcs(),
              onMarkerTap: (m) => setState(() => _inspectedMarker = m),
              onGlobeTap: (c) => setState(() => _inspectedCoordinate = c),
            );
          },
        );

      case ShowcaseTab.arcs:
        return Globe(
          key: _globeKey,
          controller: _controller,
          theme: _configuredTheme,
          projection: _projection,
          radiusMultiplier: _radiusMultiplier,
          markers: _getStandardMarkers(),
          arcs: _getStandardArcs(),
          onMarkerTap: (m) => setState(() => _inspectedMarker = m),
          onGlobeTap: (c) => setState(() => _inspectedCoordinate = c),
        );

      case ShowcaseTab.markers:
        return Globe(
          key: _globeKey,
          controller: _controller,
          theme: _configuredTheme,
          projection: _projection,
          radiusMultiplier: _radiusMultiplier,
          markers: _getStandardMarkers(),
          onMarkerTap: (m) => setState(() => _inspectedMarker = m),
          onGlobeTap: (c) => setState(() => _inspectedCoordinate = c),
        );

      case ShowcaseTab.layers:
        return Globe(
          key: _globeKey,
          controller: _controller,
          theme: _configuredTheme,
          projection: _projection,
          radiusMultiplier: _radiusMultiplier,
          markers: _getStandardMarkers(),
          arcs: _getStandardArcs(),
          layers: [
            if (_showGrid) const GlobeGridLayer(),
            if (_showDayNight) const GlobeDayNightLayer(),
            if (_showHeatmap)
              const GlobeHeatmapLayer(
                points: [
                  GlobeHeatPoint(
                    coordinate: GlobeCoordinate(
                        latitude: 37.7749, longitude: -122.4194),
                    intensity: 0.9,
                    radius: 28,
                  ),
                  GlobeHeatPoint(
                    coordinate:
                        GlobeCoordinate(latitude: 51.5074, longitude: -0.1278),
                    intensity: 0.85,
                    radius: 26,
                  ),
                  GlobeHeatPoint(
                    coordinate:
                        GlobeCoordinate(latitude: 35.6762, longitude: 139.6503),
                    intensity: 0.95,
                    radius: 30,
                  ),
                ],
              ),
            if (_showRoutes)
              const GlobeRouteLayer(
                routes: [
                  // Transatlantic corridor (SFO -> London -> Dubai)
                  GlobeRoute(
                    waypoints: [
                      GlobeCoordinate(latitude: 37.7749, longitude: -122.4194),
                      GlobeCoordinate(latitude: 51.5074, longitude: -0.1278),
                      GlobeCoordinate(latitude: 25.2048, longitude: 55.2708),
                    ],
                    color: Color(0xFF38BDF8),
                    altitude: 0.28,
                    strokeWidth: 2.0,
                    duration: Duration(seconds: 7),
                    showVehicle: true,
                    vehicleType: GlobeVehicleType.airplane,
                    vehicleColor: Colors.white,
                    vehicleSize: 18.0,
                  ),
                  // Asia-Pacific corridor (Tokyo -> Singapore -> Sydney)
                  GlobeRoute(
                    waypoints: [
                      GlobeCoordinate(latitude: 35.6762, longitude: 139.6503),
                      GlobeCoordinate(latitude: 1.3521, longitude: 103.8198),
                      GlobeCoordinate(latitude: -33.8688, longitude: 151.2093),
                    ],
                    color: Color(0xFFFBBF24),
                    altitude: 0.24,
                    strokeWidth: 2.0,
                    duration: Duration(seconds: 6),
                    delay: Duration(milliseconds: 1500),
                    showVehicle: true,
                    vehicleType: GlobeVehicleType.airplane,
                    vehicleColor: Color(0xFFFEF08A),
                    vehicleSize: 18.0,
                  ),
                  // Americas to Europe corridor (Rio -> London -> Tokyo)
                  GlobeRoute(
                    waypoints: [
                      GlobeCoordinate(latitude: -22.9068, longitude: -43.1729),
                      GlobeCoordinate(latitude: 51.5074, longitude: -0.1278),
                      GlobeCoordinate(latitude: 35.6762, longitude: 139.6503),
                    ],
                    color: Color(0xFF34D399),
                    altitude: 0.32,
                    strokeWidth: 2.0,
                    duration: Duration(seconds: 8),
                    delay: Duration(milliseconds: 3000),
                    showVehicle: true,
                    vehicleType: GlobeVehicleType.airplane,
                    vehicleColor: Color(0xFFA7F3D0),
                    vehicleSize: 18.0,
                  ),
                ],
              ),
            if (_showParticles)
              GlobeParticleLayer(
                particleCount: 40,
                color: _activeTheme.accentColor,
              ),
          ],
          onMarkerTap: (m) => setState(() => _inspectedMarker = m),
          onGlobeTap: (c) => setState(() => _inspectedCoordinate = c),
        );
    }
  }

  Widget _buildTopHeader() {
    return Padding(
      padding: const EdgeInsets.all(16),
      child: Column(mainAxisSize: MainAxisSize.min, children: [
        Row(children: [
          const Icon(Icons.public, color: Color(0xFF38BDF8)),
          const SizedBox(width: 8),
          const Expanded(
              child: Text('Flutter Globe',
                  style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold))),
          IconButton(
              tooltip: 'Replay intro',
              onPressed: _replayIntroAnimation,
              icon: const Icon(Icons.replay)),
          IconButton(
              tooltip: 'Copy code',
              onPressed: _activeTab == ShowcaseTab.layers ||
                      _activeTab == ShowcaseTab.benchmarks
                  ? null
                  : _showCodeExportDialog,
              icon: const Icon(Icons.code)),
          IconButton(
              tooltip: 'Fullscreen preview',
              onPressed: () => setState(() => _isFullscreen = true),
              icon: const Icon(Icons.fullscreen)),
        ]),
        const SizedBox(height: 12),
        SingleChildScrollView(
            scrollDirection: Axis.horizontal,
            child: Row(
              children: ShowcaseTab.values
                  .map((tab) => Padding(
                        padding: const EdgeInsets.only(right: 4),
                        child: TextButton.icon(
                          style: TextButton.styleFrom(
                            backgroundColor: _activeTab == tab
                                ? const Color(0xFF38BDF8)
                                : const Color(0xFF0F172A),
                            foregroundColor: _activeTab == tab
                                ? Colors.black
                                : Colors.white70,
                          ),
                          onPressed: () => setState(() {
                            _activeTab = tab;
                            _inspectedMarker = null;
                            _inspectedCoordinate = null;
                          }),
                          icon: Icon(tab.icon, size: 18),
                          label: Text(tab.label),
                        ),
                      ))
                  .toList(),
            )),
      ]),
    );
  }

  Widget _buildInspectionHUDCard() {
    final label = _inspectedMarker?.label ?? 'Surface Coordinate';
    final coord = _inspectedMarker?.coordinate ?? _inspectedCoordinate!;

    return ClipRRect(
      borderRadius: BorderRadius.circular(16),
      child: BackdropFilter(
        filter: ui.ImageFilter.blur(sigmaX: 16, sigmaY: 16),
        child: Container(
          width: 220,
          padding: const EdgeInsets.all(14),
          decoration: BoxDecoration(
            color: const Color(0xFF0F172A).withValues(alpha: 0.90),
            borderRadius: BorderRadius.circular(16),
            border: Border.all(
                color: const Color(0xFF38BDF8).withValues(alpha: 0.5)),
            boxShadow: const [
              BoxShadow(color: Color(0x66000000), blurRadius: 16),
            ],
          ),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            mainAxisSize: MainAxisSize.min,
            children: [
              Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: [
                  const Row(
                    children: [
                      Icon(Icons.gps_fixed_rounded,
                          size: 12, color: Color(0xFF38BDF8)),
                      SizedBox(width: 4),
                      Text(
                        'COORDINATE HUD',
                        style: TextStyle(
                          fontSize: 9,
                          fontWeight: FontWeight.bold,
                          letterSpacing: 1.1,
                          color: Color(0xFF38BDF8),
                        ),
                      ),
                    ],
                  ),
                  GestureDetector(
                    onTap: () => setState(() {
                      _inspectedMarker = null;
                      _inspectedCoordinate = null;
                    }),
                    child: const Icon(Icons.close_rounded,
                        size: 14, color: Colors.white54),
                  ),
                ],
              ),
              const SizedBox(height: 6),
              Text(label,
                  style: const TextStyle(
                      fontSize: 14, fontWeight: FontWeight.bold)),
              const SizedBox(height: 2),
              Text(
                '${coord.latitude.toStringAsFixed(4)}°, ${coord.longitude.toStringAsFixed(4)}°',
                style: const TextStyle(
                    fontSize: 11,
                    color: Colors.white70,
                    fontFamily: 'monospace'),
              ),
            ],
          ),
        ),
      ),
    );
  }

  Widget _buildBottomPanel() {
    switch (_activeTab) {
      case ShowcaseTab.benchmarks:
        return const SizedBox.shrink();
      case ShowcaseTab.playground:
        return _buildPlaygroundControls();
      case ShowcaseTab.showcase:
        return _buildShowcaseQuickControls();
      case ShowcaseTab.skins:
        return _buildSkinsGallerySheet();
      case ShowcaseTab.arcs:
        return _buildArcsControlSheet();
      case ShowcaseTab.markers:
        return _buildMarkersControlSheet();
      case ShowcaseTab.layers:
        return _buildLayersControlSheet();
    }
  }

  Widget _buildShowcaseQuickControls() {
    return ClipRRect(
      borderRadius: BorderRadius.circular(20),
      child: BackdropFilter(
        filter: ui.ImageFilter.blur(sigmaX: 16, sigmaY: 16),
        child: Container(
          padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
          decoration: BoxDecoration(
            color: const Color(0xFF0F172A).withValues(alpha: 0.85),
            borderRadius: BorderRadius.circular(20),
            border: Border.all(color: Colors.white.withValues(alpha: 0.1)),
          ),
          child: Wrap(
            spacing: 12,
            runSpacing: 8,
            crossAxisAlignment: WrapCrossAlignment.center,
            children: [
              Row(
                children: [
                  IconButton(
                    icon: Icon(
                      _autoRotate
                          ? Icons.pause_circle_filled_rounded
                          : Icons.play_circle_fill_rounded,
                      color: const Color(0xFF38BDF8),
                      size: 24,
                    ),
                    tooltip: _autoRotate ? 'Pause Rotation' : 'Resume Rotation',
                    onPressed: () {
                      setState(() {
                        _autoRotate = !_autoRotate;
                        _controller.autoRotate = _autoRotate;
                      });
                    },
                  ),
                  IconButton(
                    icon: const Icon(Icons.refresh_rounded, size: 22),
                    tooltip: 'Reset Camera View',
                    onPressed: () => _controller.resetView(vsync: this),
                  ),
                ],
              ),
              // Skin Quick Chips
              SingleChildScrollView(
                scrollDirection: Axis.horizontal,
                child: Row(
                  children: [
                    _buildQuickSkinChip(
                        0, 'Reference', const Color(0xFF38BDF8)),
                    _buildQuickSkinChip(
                        1, 'Realistic Earth', const Color(0xFF40916C)),
                    _buildQuickSkinChip(
                        2, 'Topographic', const Color(0xFFD97706)),
                    _buildQuickSkinChip(
                        6, 'Cyberpunk', const Color(0xFF06B6D4)),
                    _buildQuickSkinChip(7, 'Hologram', const Color(0xFF38BDF8)),
                    _buildQuickSkinChip(5, 'Midnight', const Color(0xFFA855F7)),
                    _buildQuickSkinChip(8, 'Neon', const Color(0xFF10B981)),
                    _buildQuickSkinChip(4, 'Minimal', Colors.white),
                  ],
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }

  Widget _buildQuickSkinChip(int index, String label, Color dotColor) {
    final isSelected = _selectedSkinIndex == index;
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 4),
      child: FilterChip(
        avatar: Container(
          width: 8,
          height: 8,
          decoration: BoxDecoration(
            shape: BoxShape.circle,
            color: dotColor,
          ),
        ),
        label: Text(label,
            style: const TextStyle(fontSize: 11, color: Colors.white)),
        selected: isSelected,
        selectedColor: const Color(0xFF38BDF8).withValues(alpha: 0.25),
        checkmarkColor: const Color(0xFF38BDF8),
        onSelected: (val) {
          if (val) _selectSkin(index);
        },
      ),
    );
  }

  Widget _buildSkinsGallerySheet() {
    return ClipRRect(
      borderRadius: BorderRadius.circular(20),
      child: BackdropFilter(
        filter: ui.ImageFilter.blur(sigmaX: 16, sigmaY: 16),
        child: Container(
          height: 120,
          padding: const EdgeInsets.all(12),
          decoration: BoxDecoration(
            color: const Color(0xFF0F172A).withValues(alpha: 0.85),
            borderRadius: BorderRadius.circular(20),
            border: Border.all(color: Colors.white.withValues(alpha: 0.1)),
          ),
          child: ListView.builder(
            scrollDirection: Axis.horizontal,
            itemCount: GlobeSkins.all.length,
            itemBuilder: (context, index) {
              final skin = GlobeSkins.all[index];
              final skinKey = GlobeSkins.named.entries
                  .firstWhere(
                    (e) => e.value == skin,
                    orElse: () => MapEntry('Skin $index', skin),
                  )
                  .key;
              final isSelected = _selectedSkinIndex == index;

              return TextButton(
                onPressed: () => _selectSkin(index),
                child: Container(
                  width: 100,
                  margin: const EdgeInsets.symmetric(horizontal: 6),
                  padding: const EdgeInsets.all(8),
                  decoration: BoxDecoration(
                    color: isSelected
                        ? const Color(0xFF1E293B)
                        : const Color(0xFF020617).withValues(alpha: 0.7),
                    borderRadius: BorderRadius.circular(14),
                    border: Border.all(
                      color: isSelected
                          ? const Color(0xFF38BDF8)
                          : const Color(0xFF1E293B),
                      width: isSelected ? 2 : 1,
                    ),
                  ),
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      Container(
                        width: 28,
                        height: 28,
                        decoration: BoxDecoration(
                          shape: BoxShape.circle,
                          color: skin.surface.surfaceColor,
                          border: Border.all(color: skin.accentColor, width: 2),
                          boxShadow: [
                            BoxShadow(
                              color: skin.accentColor.withValues(alpha: 0.4),
                              blurRadius: 6,
                            ),
                          ],
                        ),
                      ),
                      const SizedBox(height: 6),
                      Text(
                        skinKey.toUpperCase(),
                        maxLines: 1,
                        overflow: TextOverflow.ellipsis,
                        style: TextStyle(
                          fontSize: 10,
                          fontWeight: FontWeight.bold,
                          color: isSelected
                              ? const Color(0xFF38BDF8)
                              : Colors.white70,
                        ),
                      ),
                    ],
                  ),
                ),
              );
            },
          ),
        ),
      ),
    );
  }

  Widget _buildArcsControlSheet() {
    return Card(
        child: Padding(
            padding: const EdgeInsets.all(16),
            child: Column(
              mainAxisSize: MainAxisSize.min,
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                const Text('Arc controls',
                    style:
                        TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
                Text('Altitude: ${_arcAltitude.toStringAsFixed(2)}'),
                Slider(
                    value: _arcAltitude,
                    min: 0.1,
                    max: 0.55,
                    label: _arcAltitude.toStringAsFixed(2),
                    onChanged: (v) => setState(() => _arcAltitude = v)),
                Text(
                    'Duration: ${(_arcSpeedMs / 1000).toStringAsFixed(1)} seconds'),
                Slider(
                    value: _arcSpeedMs,
                    min: 1000,
                    max: 6000,
                    label: '${(_arcSpeedMs / 1000).toStringAsFixed(1)} seconds',
                    onChanged: (v) => setState(() => _arcSpeedMs = v)),
              ],
            )));
  }

  Widget _buildMarkersControlSheet() {
    return ClipRRect(
      borderRadius: BorderRadius.circular(20),
      child: BackdropFilter(
        filter: ui.ImageFilter.blur(sigmaX: 16, sigmaY: 16),
        child: Container(
          padding: const EdgeInsets.all(16),
          decoration: BoxDecoration(
            color: const Color(0xFF0F172A).withValues(alpha: 0.85),
            borderRadius: BorderRadius.circular(20),
            border: Border.all(color: Colors.white.withValues(alpha: 0.1)),
          ),
          child: Wrap(
            spacing: 12,
            runSpacing: 8,
            crossAxisAlignment: WrapCrossAlignment.center,
            children: [
              const Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                mainAxisSize: MainAxisSize.min,
                children: [
                  Row(
                    children: [
                      Icon(Icons.place_outlined,
                          size: 16, color: Color(0xFF38BDF8)),
                      SizedBox(width: 6),
                      Text('Location markers',
                          style: TextStyle(
                              fontSize: 13, fontWeight: FontWeight.bold)),
                    ],
                  ),
                  SizedBox(height: 2),
                  Text('Tap a marker to inspect its coordinates',
                      style: TextStyle(fontSize: 11, color: Colors.white54)),
                ],
              ),
              ElevatedButton.icon(
                icon: const Icon(Icons.gps_fixed_rounded, size: 16),
                label: const Text('Focus London'),
                style: ElevatedButton.styleFrom(
                  backgroundColor: const Color(0xFF38BDF8),
                  foregroundColor: Colors.black,
                  shape: RoundedRectangleBorder(
                      borderRadius: BorderRadius.circular(10)),
                ),
                onPressed: () {
                  _controller.flyTo(
                    coordinate: const GlobeCoordinate(
                        latitude: 51.5074, longitude: -0.1278),
                    vsync: this,
                  );
                },
              ),
            ],
          ),
        ),
      ),
    );
  }

  Widget _buildLayersControlSheet() {
    return ClipRRect(
      borderRadius: BorderRadius.circular(20),
      child: BackdropFilter(
        filter: ui.ImageFilter.blur(sigmaX: 16, sigmaY: 16),
        child: Container(
          padding: const EdgeInsets.all(16),
          decoration: BoxDecoration(
            color: const Color(0xFF0F172A).withValues(alpha: 0.85),
            borderRadius: BorderRadius.circular(20),
            border: Border.all(color: Colors.white.withValues(alpha: 0.1)),
          ),
          child: Wrap(
            spacing: 8,
            runSpacing: 8,
            children: [
              FilterChip(
                avatar: const Icon(Icons.grid_4x4_rounded, size: 14),
                label: const Text('Graticule Grid'),
                selected: _showGrid,
                onSelected: (v) => setState(() => _showGrid = v),
              ),
              FilterChip(
                avatar: const Icon(Icons.wb_sunny_outlined, size: 14),
                label: const Text('Day/Night Terminator'),
                selected: _showDayNight,
                onSelected: (v) => setState(() => _showDayNight = v),
              ),
              FilterChip(
                avatar: const Icon(Icons.whatshot_rounded, size: 14),
                label: const Text('Thermal Heatmap'),
                selected: _showHeatmap,
                onSelected: (v) => setState(() => _showHeatmap = v),
              ),
              FilterChip(
                avatar: const Icon(Icons.flight_takeoff_rounded, size: 14),
                label: const Text('Flight Routes & Airplanes'),
                selected: _showRoutes,
                onSelected: (v) => setState(() => _showRoutes = v),
              ),
              FilterChip(
                avatar: const Icon(Icons.auto_awesome_rounded, size: 14),
                label: const Text('Orbital Particles'),
                selected: _showParticles,
                onSelected: (v) => setState(() => _showParticles = v),
              ),
            ],
          ),
        ),
      ),
    );
  }

  Widget _buildPlaygroundControls() {
    return Card(
        child: Padding(
            padding: const EdgeInsets.all(16),
            child: Column(
              mainAxisSize: MainAxisSize.min,
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                const Text('Make it yours',
                    style:
                        TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
                Wrap(spacing: 8, runSpacing: 8, children: [
                  ActionChip(
                      label: const Text('Reset'), onPressed: _resetPlayground),
                  ActionChip(
                      label: const Text('Randomize'),
                      onPressed: () => _selectSkin(
                          math.Random().nextInt(GlobeSkins.all.length))),
                  ActionChip(
                      label: const Text('Copy code'),
                      onPressed: _showCodeExportDialog),
                  ActionChip(
                      label: const Text('Fullscreen'),
                      onPressed: () => setState(() => _isFullscreen = true)),
                  ...[0, 6, 7].map((index) => ChoiceChip(
                      label: Text(GlobeSkins.named.entries
                          .firstWhere(
                              (entry) => entry.value == GlobeSkins.all[index])
                          .key),
                      selected: _selectedSkinIndex == index,
                      onSelected: (_) => _selectSkin(index))),
                ]),
                ExpansionTile(
                  tilePadding: EdgeInsets.zero,
                  title: const Text('Globe and interaction'),
                  initiallyExpanded: true,
                  children: [
                    Wrap(spacing: 8, runSpacing: 8, children: [
                      for (final projection in GlobeProjection.values)
                        ChoiceChip(
                          label: Text(projection.name),
                          selected: _projection == projection,
                          onSelected: (_) =>
                              setState(() => _projection = projection),
                        ),
                      for (final quality in GlobeQuality.values)
                        ChoiceChip(
                          label: Text(quality.name),
                          selected: _quality == quality,
                          onSelected: (_) => setState(() => _quality = quality),
                        ),
                    ]),
                    SwitchListTile(
                      contentPadding: EdgeInsets.zero,
                      title: const Text('Auto rotation'),
                      value: _autoRotate,
                      onChanged: (value) => setState(() => _autoRotate = value),
                    ),
                    SwitchListTile(
                      contentPadding: EdgeInsets.zero,
                      title: const Text('Pinch zoom'),
                      value: _enableZoom,
                      onChanged: (value) => setState(() => _enableZoom = value),
                    ),
                    SwitchListTile(
                      contentPadding: EdgeInsets.zero,
                      title: const Text('Drag inertia'),
                      value: _inertiaEnabled,
                      onChanged: (value) =>
                          setState(() => _inertiaEnabled = value),
                    ),
                    _playgroundSlider('Globe scale', _radiusMultiplier, 0.55,
                        0.95, (value) => _radiusMultiplier = value),
                    _playgroundSlider('Zoom', _controller.zoom, 0.5, 2.0,
                        (value) => _controller.zoom = value),
                    _playgroundSlider('Rotation speed', _autoRotateSpeed, 0,
                        1.5, (value) => _autoRotateSpeed = value),
                    _playgroundSlider('Drag sensitivity', _rotationSensitivity,
                        0.001, 0.012, (value) => _rotationSensitivity = value),
                  ],
                ),
                ExpansionTile(
                  tilePadding: EdgeInsets.zero,
                  title: const Text('Surface and lighting'),
                  children: [
                    _playgroundSlider('Point size', _pointSize, 0.5, 4,
                        (value) => _pointSize = value),
                    _playgroundSlider('Land opacity', _pointOpacity, 0, 1,
                        (value) => _pointOpacity = value),
                    _playgroundSlider('Globe opacity', _globeOpacity, 0.2, 1,
                        (value) => _globeOpacity = value),
                    _playgroundSlider('Ambient light', _ambientLight, 0, 1,
                        (value) => _ambientLight = value),
                    _playgroundSlider('Directional light', _directionalLight, 0,
                        1, (value) => _directionalLight = value),
                  ],
                ),
                ExpansionTile(
                  tilePadding: EdgeInsets.zero,
                  title: const Text('Atmosphere'),
                  children: [
                    SwitchListTile(
                      contentPadding: EdgeInsets.zero,
                      title: const Text('Enabled'),
                      value: _showAtmosphere,
                      onChanged: (value) =>
                          setState(() => _showAtmosphere = value),
                    ),
                    _playgroundSlider('Altitude', _atmosphereAltitude, 0, 0.5,
                        (value) => _atmosphereAltitude = value),
                    _playgroundSlider('Glow', _atmosphereGlow, 0, 1,
                        (value) => _atmosphereGlow = value),
                  ],
                ),
                ExpansionTile(
                  tilePadding: EdgeInsets.zero,
                  title: const Text('Arcs and markers'),
                  children: [
                    _playgroundSlider('Arc altitude', _arcAltitude, 0, 0.7,
                        (value) => _arcAltitude = value),
                    _playgroundSlider('Arc width', _arcStrokeWidth, 0.5, 6,
                        (value) => _arcStrokeWidth = value),
                    _playgroundSlider('Arc duration', _arcSpeedMs, 500, 6000,
                        (value) => _arcSpeedMs = value,
                        digits: 0),
                    _playgroundSlider('Marker size', _markerSize, 2, 12,
                        (value) => _markerSize = value),
                    SwitchListTile(
                      contentPadding: EdgeInsets.zero,
                      title: const Text('Marker pulse'),
                      value: _markerPulse,
                      onChanged: (value) =>
                          setState(() => _markerPulse = value),
                    ),
                  ],
                ),
              ],
            )));
  }

  Widget _playgroundSlider(
    String label,
    double value,
    double min,
    double max,
    ValueChanged<double> update, {
    int digits = 2,
  }) {
    return Row(children: [
      SizedBox(width: 120, child: Text(label)),
      Expanded(
          child: Slider(
        value: value.clamp(min, max),
        min: min,
        max: max,
        label: value.toStringAsFixed(digits),
        onChanged: (next) => setState(() => update(next)),
      )),
    ]);
  }

  void _resetPlayground() {
    _controller.zoom = 1;
    setState(() {
      _selectedSkinIndex = 0;
      _activeTheme = GlobeSkins.reference;
      _autoRotate = true;
      _autoRotateSpeed = 0.22;
      _projection = GlobeProjection.perspective;
      _quality = GlobeQuality.auto;
      _radiusMultiplier = 0.82;
      _pointSize = _activeTheme.surface.pointSize;
      _pointOpacity = _activeTheme.surface.pointOpacity;
      _globeOpacity = _activeTheme.surface.globeOpacity;
      _showAtmosphere = _activeTheme.atmosphere.visible;
      _atmosphereAltitude = _activeTheme.atmosphere.altitude;
      _atmosphereGlow = _activeTheme.atmosphere.glowIntensity;
      _ambientLight = _activeTheme.lighting.ambientIntensity;
      _directionalLight = _activeTheme.lighting.directionalIntensity;
      _enableZoom = true;
      _inertiaEnabled = true;
      _rotationSensitivity = 0.005;
      _arcAltitude = 0.28;
      _arcStrokeWidth = 2;
      _arcSpeedMs = 2400;
      _markerSize = 5;
      _markerPulse = true;
    });
  }
}
2
likes
0
points
129
downloads

Publisher

unverified uploader

Weekly Downloads

Interactive Canvas globes for Flutter with geographic markers, animated arcs, configurable themes, data layers, and camera controls.

Repository (GitHub)
View/report issues

Topics

#globe #visualization #animation #maps #graphics

License

unknown (license)

Dependencies

flutter, meta

More

Packages that depend on flutter_globe