136 lines
3.6 KiB
Dart
136 lines
3.6 KiB
Dart
import 'dart:math' as math;
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
import '../../../../core/theme/app_theme.dart';
|
|
|
|
/// انیمیشنِ «پروازِ سکهها به کیفپول»: چند سکه از مبدأ (مثلاً کارتِ هدیه) روی یک
|
|
/// قوس به سمتِ مقصد (چیپِ سکه) پرواز میکنند. با Overlay نمایش داده میشود.
|
|
class CoinFly {
|
|
static void play(
|
|
BuildContext context, {
|
|
required Offset from,
|
|
required Offset to,
|
|
int count = 9,
|
|
}) {
|
|
final overlay = Overlay.maybeOf(context, rootOverlay: true);
|
|
if (overlay == null) return;
|
|
late OverlayEntry entry;
|
|
entry = OverlayEntry(
|
|
builder: (_) => _CoinFlyOverlay(
|
|
from: from,
|
|
to: to,
|
|
count: count,
|
|
onDone: () => entry.remove(),
|
|
),
|
|
);
|
|
overlay.insert(entry);
|
|
}
|
|
}
|
|
|
|
class _CoinFlyOverlay extends StatefulWidget {
|
|
final Offset from, to;
|
|
final int count;
|
|
final VoidCallback onDone;
|
|
const _CoinFlyOverlay({
|
|
required this.from,
|
|
required this.to,
|
|
required this.count,
|
|
required this.onDone,
|
|
});
|
|
|
|
@override
|
|
State<_CoinFlyOverlay> createState() => _CoinFlyOverlayState();
|
|
}
|
|
|
|
class _CoinFlyOverlayState extends State<_CoinFlyOverlay>
|
|
with SingleTickerProviderStateMixin {
|
|
late final AnimationController _c;
|
|
late final List<_Coin> _coins;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
final r = math.Random();
|
|
_coins = List.generate(widget.count, (_) {
|
|
return _Coin(
|
|
jitter: Offset((r.nextDouble() - 0.5) * 46, (r.nextDouble() - 0.5) * 46),
|
|
delay: r.nextDouble() * 0.28,
|
|
arc: -60 - r.nextDouble() * 90, // ارتفاعِ قوس (به بالا)
|
|
size: 16 + r.nextDouble() * 8,
|
|
);
|
|
});
|
|
_c = AnimationController(
|
|
vsync: this,
|
|
duration: const Duration(milliseconds: 950),
|
|
)
|
|
..addStatusListener((s) {
|
|
if (s == AnimationStatus.completed) widget.onDone();
|
|
})
|
|
..forward();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_c.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return IgnorePointer(
|
|
child: AnimatedBuilder(
|
|
animation: _c,
|
|
builder: (_, __) {
|
|
return Stack(
|
|
children: [
|
|
for (final coin in _coins) _buildCoin(coin),
|
|
],
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildCoin(_Coin coin) {
|
|
final local = ((_c.value - coin.delay) / (1 - coin.delay)).clamp(0.0, 1.0);
|
|
if (local <= 0) return const SizedBox.shrink();
|
|
final start = widget.from + coin.jitter;
|
|
final end = widget.to;
|
|
// قوسِ درجهدو: نقطهی کنترل بالای میانه.
|
|
final mid = Offset((start.dx + end.dx) / 2, (start.dy + end.dy) / 2 + coin.arc);
|
|
final t = Curves.easeInCubic.transform(local);
|
|
final p = _quad(start, mid, end, t);
|
|
final opacity = local < 0.85 ? 1.0 : (1 - (local - 0.85) / 0.15);
|
|
final scale = 1.0 - 0.35 * local;
|
|
return Positioned(
|
|
left: p.dx - coin.size / 2,
|
|
top: p.dy - coin.size / 2,
|
|
child: Opacity(
|
|
opacity: opacity.clamp(0.0, 1.0),
|
|
child: Transform.scale(
|
|
scale: scale,
|
|
child: Icon(Icons.monetization_on,
|
|
color: AppColors.gold, size: coin.size),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Offset _quad(Offset a, Offset b, Offset c, double t) {
|
|
final u = 1 - t;
|
|
return a * (u * u) + b * (2 * u * t) + c * (t * t);
|
|
}
|
|
}
|
|
|
|
class _Coin {
|
|
final Offset jitter;
|
|
final double delay, arc, size;
|
|
_Coin({
|
|
required this.jitter,
|
|
required this.delay,
|
|
required this.arc,
|
|
required this.size,
|
|
});
|
|
}
|