This commit is contained in:
@@ -2,6 +2,7 @@ import 'dart:async';
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
@@ -328,6 +329,8 @@ class _MapboxMapLayer extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _MapboxMapLayerState extends State<_MapboxMapLayer> {
|
class _MapboxMapLayerState extends State<_MapboxMapLayer> {
|
||||||
|
static const _worldCenter = LatLng(8.0, 32.0);
|
||||||
|
|
||||||
mbx.MapboxMap? _mapboxMap;
|
mbx.MapboxMap? _mapboxMap;
|
||||||
var _mapLoaded = false;
|
var _mapLoaded = false;
|
||||||
var _didInitialFlyIn = false;
|
var _didInitialFlyIn = false;
|
||||||
@@ -351,12 +354,19 @@ class _MapboxMapLayerState extends State<_MapboxMapLayer> {
|
|||||||
mbx.MapWidget(
|
mbx.MapWidget(
|
||||||
styleUri: _mapboxStyleUri,
|
styleUri: _mapboxStyleUri,
|
||||||
viewport: mbx.CameraViewportState(
|
viewport: mbx.CameraViewportState(
|
||||||
center: _mapboxPoint(widget.initialCenter),
|
center: _mapboxPoint(
|
||||||
zoom: _didInitialFlyIn ? 14.8 : 1.25,
|
_didInitialFlyIn ? widget.initialCenter : _worldCenter,
|
||||||
bearing: _didInitialFlyIn ? -16 : 0,
|
),
|
||||||
|
zoom: _didInitialFlyIn ? 14.8 : 0.58,
|
||||||
|
bearing: _didInitialFlyIn ? -16 : -24,
|
||||||
pitch: _didInitialFlyIn ? 62 : 0,
|
pitch: _didInitialFlyIn ? 62 : 0,
|
||||||
),
|
),
|
||||||
onMapCreated: _onMapCreated,
|
onMapCreated: _onMapCreated,
|
||||||
|
onStyleLoadedListener: (_) {
|
||||||
|
if (!kIsWeb) {
|
||||||
|
unawaited(_enableGlobeProjection());
|
||||||
|
}
|
||||||
|
},
|
||||||
onMapLoadedListener: (_) {
|
onMapLoadedListener: (_) {
|
||||||
_mapLoaded = true;
|
_mapLoaded = true;
|
||||||
_updateCameraForFocus();
|
_updateCameraForFocus();
|
||||||
@@ -392,6 +402,16 @@ class _MapboxMapLayerState extends State<_MapboxMapLayer> {
|
|||||||
_mapboxMap = mapboxMap;
|
_mapboxMap = mapboxMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _enableGlobeProjection() async {
|
||||||
|
final mapboxMap = _mapboxMap;
|
||||||
|
if (mapboxMap == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await mapboxMap.style.setProjection(
|
||||||
|
mbx.StyleProjection(name: mbx.StyleProjectionName.globe),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
void _updateCameraForFocus() {
|
void _updateCameraForFocus() {
|
||||||
final mapboxMap = _mapboxMap;
|
final mapboxMap = _mapboxMap;
|
||||||
final focusCenter = widget.focusCenter;
|
final focusCenter = widget.focusCenter;
|
||||||
@@ -417,7 +437,12 @@ class _MapboxMapLayerState extends State<_MapboxMapLayer> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_didInitialFlyIn = true;
|
_didInitialFlyIn = true;
|
||||||
unawaited(mapboxMap.flyTo(camera, mbx.MapAnimationOptions(duration: 2600)));
|
unawaited(
|
||||||
|
mapboxMap.flyTo(
|
||||||
|
camera,
|
||||||
|
mbx.MapAnimationOptions(duration: 4200, startDelay: 350),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _projectMarkers() async {
|
Future<void> _projectMarkers() async {
|
||||||
@@ -968,8 +993,14 @@ class _TelegramLoginScreenState extends State<_TelegramLoginScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final tokens = context.mapflowTokens;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: SafeArea(
|
backgroundColor: tokens.darkSurface,
|
||||||
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
const Positioned.fill(child: _AnimatedEarthBackdrop()),
|
||||||
|
SafeArea(
|
||||||
child: Center(
|
child: Center(
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: 320,
|
width: 320,
|
||||||
@@ -978,19 +1009,26 @@ class _TelegramLoginScreenState extends State<_TelegramLoginScreen> {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'MapFlow',
|
'MapFlow',
|
||||||
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
style: Theme.of(context).textTheme.headlineMedium
|
||||||
|
?.copyWith(
|
||||||
|
color: tokens.onDark,
|
||||||
fontWeight: FontWeight.w900,
|
fontWeight: FontWeight.w900,
|
||||||
letterSpacing: 0,
|
letterSpacing: 0,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
TelegramLoginButton(onPressed: _startLogin, loading: _loading),
|
TelegramLoginButton(
|
||||||
|
onPressed: _startLogin,
|
||||||
|
loading: _loading,
|
||||||
|
),
|
||||||
if (_message.isNotEmpty) ...[
|
if (_message.isNotEmpty) ...[
|
||||||
const SizedBox(height: 14),
|
const SizedBox(height: 14),
|
||||||
Text(
|
Text(
|
||||||
_message,
|
_message,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: Theme.of(context).textTheme.bodyMedium,
|
style: Theme.of(
|
||||||
|
context,
|
||||||
|
).textTheme.bodyMedium?.copyWith(color: tokens.onDark),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@@ -998,7 +1036,421 @@ class _TelegramLoginScreenState extends State<_TelegramLoginScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AnimatedEarthBackdrop extends StatefulWidget {
|
||||||
|
const _AnimatedEarthBackdrop({this.compact = false});
|
||||||
|
|
||||||
|
final bool compact;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_AnimatedEarthBackdrop> createState() => _AnimatedEarthBackdropState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AnimatedEarthBackdropState extends State<_AnimatedEarthBackdrop>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late final AnimationController _controller;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: const Duration(seconds: 18),
|
||||||
|
)..repeat();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return RepaintBoundary(
|
||||||
|
child: AnimatedBuilder(
|
||||||
|
animation: _controller,
|
||||||
|
builder: (context, _) {
|
||||||
|
return CustomPaint(
|
||||||
|
painter: _EarthBackdropPainter(
|
||||||
|
progress: _controller.value,
|
||||||
|
tokens: context.mapflowTokens,
|
||||||
|
compact: widget.compact,
|
||||||
|
),
|
||||||
|
child: const SizedBox.expand(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _EarthBackdropPainter extends CustomPainter {
|
||||||
|
const _EarthBackdropPainter({
|
||||||
|
required this.progress,
|
||||||
|
required this.tokens,
|
||||||
|
required this.compact,
|
||||||
|
});
|
||||||
|
|
||||||
|
final double progress;
|
||||||
|
final MapflowThemeTokens tokens;
|
||||||
|
final bool compact;
|
||||||
|
|
||||||
|
static const _landMasses = <({double lon, double lat, double sx, double sy})>[
|
||||||
|
(lon: -106, lat: 42, sx: 0.30, sy: 0.18),
|
||||||
|
(lon: -82, lat: 18, sx: 0.20, sy: 0.14),
|
||||||
|
(lon: -62, lat: -14, sx: 0.22, sy: 0.25),
|
||||||
|
(lon: 16, lat: 8, sx: 0.23, sy: 0.31),
|
||||||
|
(lon: 46, lat: 48, sx: 0.34, sy: 0.18),
|
||||||
|
(lon: 78, lat: 25, sx: 0.32, sy: 0.20),
|
||||||
|
(lon: 116, lat: -23, sx: 0.19, sy: 0.13),
|
||||||
|
(lon: 137, lat: 36, sx: 0.10, sy: 0.07),
|
||||||
|
(lon: -42, lat: 73, sx: 0.16, sy: 0.08),
|
||||||
|
];
|
||||||
|
|
||||||
|
static const _routes =
|
||||||
|
<({double fromLon, double fromLat, double toLon, double toLat})>[
|
||||||
|
(fromLon: -74, fromLat: 41, toLon: 2, toLat: 49),
|
||||||
|
(fromLon: 2, fromLat: 49, toLon: 105, toLat: 16),
|
||||||
|
(fromLon: 139, fromLat: 35, toLon: 105, toLat: 16),
|
||||||
|
(fromLon: -122, fromLat: 37, toLon: 139, toLat: 35),
|
||||||
|
];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
_paintSpace(canvas, size);
|
||||||
|
_paintOrbits(canvas, size);
|
||||||
|
_paintEarth(canvas, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _paintSpace(Canvas canvas, Size size) {
|
||||||
|
final rect = Offset.zero & size;
|
||||||
|
canvas.drawRect(
|
||||||
|
rect,
|
||||||
|
Paint()
|
||||||
|
..shader = LinearGradient(
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
colors: [
|
||||||
|
tokens.darkSurface,
|
||||||
|
const Color(0xFF07151B),
|
||||||
|
const Color(0xFF150918),
|
||||||
|
tokens.darkSurface,
|
||||||
|
],
|
||||||
|
).createShader(rect),
|
||||||
|
);
|
||||||
|
|
||||||
|
final glowCenter = Offset(size.width * 0.77, size.height * 0.18);
|
||||||
|
canvas.drawCircle(
|
||||||
|
glowCenter,
|
||||||
|
math.max(size.width, size.height) * 0.48,
|
||||||
|
Paint()
|
||||||
|
..shader =
|
||||||
|
RadialGradient(
|
||||||
|
colors: [
|
||||||
|
const Color(0xFFFFB86B).withValues(alpha: 0.20),
|
||||||
|
tokens.voiceAccent.withValues(alpha: 0.08),
|
||||||
|
Colors.transparent,
|
||||||
|
],
|
||||||
|
stops: const [0, 0.34, 1],
|
||||||
|
).createShader(
|
||||||
|
Rect.fromCircle(
|
||||||
|
center: glowCenter,
|
||||||
|
radius: math.max(size.width, size.height) * 0.48,
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final stars = Paint()..color = Colors.white;
|
||||||
|
for (var index = 0; index < 96; index++) {
|
||||||
|
final xSeed = math.sin(index * 12.9898) * 43758.5453;
|
||||||
|
final ySeed = math.sin(index * 78.233) * 24634.6345;
|
||||||
|
final x = (xSeed - xSeed.floor()) * size.width;
|
||||||
|
final y = (ySeed - ySeed.floor()) * size.height;
|
||||||
|
final twinkle = 0.35 + 0.65 * math.sin(progress * math.pi * 2 + index);
|
||||||
|
stars.color = Colors.white.withValues(alpha: 0.10 + twinkle * 0.28);
|
||||||
|
canvas.drawCircle(Offset(x, y), 0.7 + (index % 3) * 0.35, stars);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _paintOrbits(Canvas canvas, Size size) {
|
||||||
|
final center = _earthCenter(size);
|
||||||
|
final radius = _earthRadius(size);
|
||||||
|
final orbitPaint = Paint()
|
||||||
|
..style = PaintingStyle.stroke
|
||||||
|
..strokeWidth = 1.1
|
||||||
|
..color = const Color(0xFF5BE7C4).withValues(alpha: 0.15);
|
||||||
|
|
||||||
|
for (final scale in const [1.28, 1.48, 1.72]) {
|
||||||
|
canvas.save();
|
||||||
|
canvas.translate(center.dx, center.dy);
|
||||||
|
canvas.rotate(-0.46 + progress * math.pi * 0.18);
|
||||||
|
canvas.scale(1, 0.34);
|
||||||
|
canvas.drawCircle(Offset.zero, radius * scale, orbitPaint);
|
||||||
|
canvas.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
final satelliteAngle = progress * math.pi * 2;
|
||||||
|
final satellite = Offset(
|
||||||
|
center.dx + math.cos(satelliteAngle) * radius * 1.72,
|
||||||
|
center.dy + math.sin(satelliteAngle) * radius * 0.58,
|
||||||
|
);
|
||||||
|
canvas.drawCircle(
|
||||||
|
satellite,
|
||||||
|
3.6,
|
||||||
|
Paint()..color = const Color(0xFFFFD38A).withValues(alpha: 0.92),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _paintEarth(Canvas canvas, Size size) {
|
||||||
|
final center = _earthCenter(size);
|
||||||
|
final radius = _earthRadius(size);
|
||||||
|
final sphere = Rect.fromCircle(center: center, radius: radius);
|
||||||
|
|
||||||
|
canvas.drawCircle(
|
||||||
|
center.translate(radius * 0.10, radius * 0.12),
|
||||||
|
radius * 1.06,
|
||||||
|
Paint()
|
||||||
|
..shader = RadialGradient(
|
||||||
|
colors: [Colors.black.withValues(alpha: 0.46), Colors.transparent],
|
||||||
|
).createShader(sphere.inflate(radius * 0.18)),
|
||||||
|
);
|
||||||
|
|
||||||
|
canvas.drawCircle(
|
||||||
|
center,
|
||||||
|
radius * 1.02,
|
||||||
|
Paint()
|
||||||
|
..shader = RadialGradient(
|
||||||
|
center: const Alignment(-0.48, -0.62),
|
||||||
|
colors: [
|
||||||
|
const Color(0xFF8AF8FF).withValues(alpha: 0.32),
|
||||||
|
const Color(0xFF124968).withValues(alpha: 0.72),
|
||||||
|
Colors.transparent,
|
||||||
|
],
|
||||||
|
stops: const [0, 0.64, 1],
|
||||||
|
).createShader(sphere.inflate(radius * 0.18)),
|
||||||
|
);
|
||||||
|
|
||||||
|
canvas.save();
|
||||||
|
canvas.clipPath(Path()..addOval(sphere));
|
||||||
|
canvas.drawOval(
|
||||||
|
sphere,
|
||||||
|
Paint()
|
||||||
|
..shader = RadialGradient(
|
||||||
|
center: const Alignment(-0.42, -0.50),
|
||||||
|
radius: 1.08,
|
||||||
|
colors: const [
|
||||||
|
Color(0xFF7AF6FF),
|
||||||
|
Color(0xFF1589B0),
|
||||||
|
Color(0xFF073456),
|
||||||
|
Color(0xFF030A1A),
|
||||||
|
],
|
||||||
|
stops: [0, 0.34, 0.72, 1],
|
||||||
|
).createShader(sphere),
|
||||||
|
);
|
||||||
|
|
||||||
|
_paintGrid(canvas, center, radius);
|
||||||
|
_paintLand(canvas, center, radius);
|
||||||
|
_paintRoutes(canvas, center, radius);
|
||||||
|
_paintTerminator(canvas, center, radius, sphere);
|
||||||
|
canvas.restore();
|
||||||
|
|
||||||
|
canvas.drawOval(
|
||||||
|
sphere,
|
||||||
|
Paint()
|
||||||
|
..style = PaintingStyle.stroke
|
||||||
|
..strokeWidth = 1.5
|
||||||
|
..color = const Color(0xFFB7FFF5).withValues(alpha: 0.36),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _paintGrid(Canvas canvas, Offset center, double radius) {
|
||||||
|
final paint = Paint()
|
||||||
|
..style = PaintingStyle.stroke
|
||||||
|
..strokeWidth = 0.8
|
||||||
|
..color = Colors.white.withValues(alpha: 0.11);
|
||||||
|
|
||||||
|
for (final lat in const [-60, -30, 0, 30, 60]) {
|
||||||
|
final y = center.dy - math.sin(_radians(lat.toDouble())) * radius * 0.72;
|
||||||
|
final width = math.cos(_radians(lat.toDouble())) * radius;
|
||||||
|
canvas.drawOval(
|
||||||
|
Rect.fromCenter(
|
||||||
|
center: Offset(center.dx, y),
|
||||||
|
width: width * 2,
|
||||||
|
height: width * 0.38,
|
||||||
|
),
|
||||||
|
paint,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var meridian = 0; meridian < 6; meridian++) {
|
||||||
|
canvas.save();
|
||||||
|
canvas.translate(center.dx, center.dy);
|
||||||
|
canvas.rotate((meridian / 6) * math.pi + progress * math.pi * 2);
|
||||||
|
canvas.scale(0.20, 1);
|
||||||
|
canvas.drawCircle(Offset.zero, radius, paint);
|
||||||
|
canvas.restore();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _paintLand(Canvas canvas, Offset center, double radius) {
|
||||||
|
final landPaint = Paint()
|
||||||
|
..color = const Color(0xFF4EDC9B).withValues(alpha: 0.86);
|
||||||
|
final highlightPaint = Paint()
|
||||||
|
..color = const Color(0xFFFFD38A).withValues(alpha: 0.34);
|
||||||
|
|
||||||
|
for (final land in _landMasses) {
|
||||||
|
final projected = _project(land.lon, land.lat, center, radius);
|
||||||
|
if (projected == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final (:point, :depth) = projected;
|
||||||
|
final alpha = (0.30 + depth * 0.70).clamp(0.0, 1.0);
|
||||||
|
landPaint.color = const Color(0xFF4EDC9B).withValues(alpha: alpha * 0.88);
|
||||||
|
highlightPaint.color = const Color(
|
||||||
|
0xFFFFD38A,
|
||||||
|
).withValues(alpha: alpha * 0.26);
|
||||||
|
final landRect = Rect.fromCenter(
|
||||||
|
center: point,
|
||||||
|
width: radius * land.sx * (0.62 + depth * 0.58),
|
||||||
|
height: radius * land.sy * (0.56 + depth * 0.36),
|
||||||
|
);
|
||||||
|
canvas.save();
|
||||||
|
canvas.translate(point.dx, point.dy);
|
||||||
|
canvas.rotate(math.sin(land.lon) * 0.65);
|
||||||
|
canvas.translate(-point.dx, -point.dy);
|
||||||
|
canvas.drawOval(landRect, landPaint);
|
||||||
|
canvas.drawOval(landRect.deflate(radius * 0.018), highlightPaint);
|
||||||
|
canvas.restore();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _paintRoutes(Canvas canvas, Offset center, double radius) {
|
||||||
|
final paint = Paint()
|
||||||
|
..style = PaintingStyle.stroke
|
||||||
|
..strokeCap = StrokeCap.round
|
||||||
|
..strokeWidth = 1.4
|
||||||
|
..color = tokens.voiceAccentSoft.withValues(alpha: 0.38);
|
||||||
|
final pulsePaint = Paint()
|
||||||
|
..style = PaintingStyle.stroke
|
||||||
|
..strokeCap = StrokeCap.round
|
||||||
|
..strokeWidth = 2.8
|
||||||
|
..color = tokens.voiceAccent.withValues(alpha: 0.44);
|
||||||
|
|
||||||
|
for (var index = 0; index < _routes.length; index++) {
|
||||||
|
final route = _routes[index];
|
||||||
|
final from = _project(route.fromLon, route.fromLat, center, radius);
|
||||||
|
final to = _project(route.toLon, route.toLat, center, radius);
|
||||||
|
if (from == null || to == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final path = Path()
|
||||||
|
..moveTo(from.point.dx, from.point.dy)
|
||||||
|
..quadraticBezierTo(
|
||||||
|
(from.point.dx + to.point.dx) / 2,
|
||||||
|
math.min(from.point.dy, to.point.dy) - radius * (0.18 + index * 0.03),
|
||||||
|
to.point.dx,
|
||||||
|
to.point.dy,
|
||||||
|
);
|
||||||
|
canvas.drawPath(path, paint);
|
||||||
|
final metric = path.computeMetrics().first;
|
||||||
|
final head = ((progress + index * 0.19) % 1) * metric.length;
|
||||||
|
final segment = metric.extractPath(
|
||||||
|
math.max(0, head - metric.length * 0.12),
|
||||||
|
head,
|
||||||
|
);
|
||||||
|
canvas.drawPath(segment, pulsePaint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _paintTerminator(
|
||||||
|
Canvas canvas,
|
||||||
|
Offset center,
|
||||||
|
double radius,
|
||||||
|
Rect sphere,
|
||||||
|
) {
|
||||||
|
canvas.drawOval(
|
||||||
|
sphere.shift(Offset(radius * 0.22, radius * 0.06)),
|
||||||
|
Paint()
|
||||||
|
..shader = LinearGradient(
|
||||||
|
begin: Alignment.centerLeft,
|
||||||
|
end: Alignment.centerRight,
|
||||||
|
colors: [
|
||||||
|
Colors.transparent,
|
||||||
|
Colors.black.withValues(alpha: 0.18),
|
||||||
|
Colors.black.withValues(alpha: 0.68),
|
||||||
|
],
|
||||||
|
stops: const [0, 0.52, 1],
|
||||||
|
).createShader(sphere),
|
||||||
|
);
|
||||||
|
canvas.drawCircle(
|
||||||
|
center.translate(-radius * 0.28, -radius * 0.30),
|
||||||
|
radius * 0.22,
|
||||||
|
Paint()
|
||||||
|
..shader =
|
||||||
|
RadialGradient(
|
||||||
|
colors: [
|
||||||
|
Colors.white.withValues(alpha: 0.22),
|
||||||
|
Colors.transparent,
|
||||||
|
],
|
||||||
|
).createShader(
|
||||||
|
Rect.fromCircle(
|
||||||
|
center: center.translate(-radius * 0.28, -radius * 0.30),
|
||||||
|
radius: radius * 0.22,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
({Offset point, double depth})? _project(
|
||||||
|
double lon,
|
||||||
|
double lat,
|
||||||
|
Offset center,
|
||||||
|
double radius,
|
||||||
|
) {
|
||||||
|
final longitude = _radians(lon + progress * 360);
|
||||||
|
final latitude = _radians(lat);
|
||||||
|
final x = math.cos(latitude) * math.sin(longitude);
|
||||||
|
final z = math.cos(latitude) * math.cos(longitude);
|
||||||
|
if (z < -0.18) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final y = -math.sin(latitude) * 0.72;
|
||||||
|
final depthScale = 0.72 + z * 0.28;
|
||||||
|
return (
|
||||||
|
point: Offset(
|
||||||
|
center.dx + x * radius * depthScale,
|
||||||
|
center.dy + y * radius * depthScale,
|
||||||
|
),
|
||||||
|
depth: ((z + 0.18) / 1.18).clamp(0.0, 1.0),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Offset _earthCenter(Size size) {
|
||||||
|
if (compact) {
|
||||||
|
return Offset(size.width * 0.50, size.height * 0.39);
|
||||||
|
}
|
||||||
|
return Offset(size.width * 0.50, size.height * 0.40);
|
||||||
|
}
|
||||||
|
|
||||||
|
double _earthRadius(Size size) {
|
||||||
|
final base = math.min(size.width, size.height);
|
||||||
|
final radius = base * (compact ? 0.18 : 0.31);
|
||||||
|
return radius.clamp(116.0, compact ? 180.0 : 310.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
double _radians(double degrees) => degrees * math.pi / 180;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRepaint(covariant _EarthBackdropPainter oldDelegate) {
|
||||||
|
return oldDelegate.progress != progress ||
|
||||||
|
oldDelegate.tokens != tokens ||
|
||||||
|
oldDelegate.compact != compact;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1010,7 +1462,10 @@ class _MapLoading extends StatelessWidget {
|
|||||||
final tokens = context.mapflowTokens;
|
final tokens = context.mapflowTokens;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: tokens.darkSurface,
|
backgroundColor: tokens.darkSurface,
|
||||||
body: Center(
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
const Positioned.fill(child: _AnimatedEarthBackdrop(compact: true)),
|
||||||
|
Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
@@ -1048,6 +1503,8 @@ class _MapLoading extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user