fluffychat/lib/pangea/analytics_misc/level_up/star_rain_widget.dart
avashilling 6e8a6f8332
rain confetti on activity finish (#4715)
* rain confetti on activity finish

also add continue button, and change copy for completed single practice activities

* fix: show confetti popup when finish all activities with grammar activity

* translations

---------

Co-authored-by: ggurdin <ggurdin@gmail.com>
2025-11-20 14:55:27 -05:00

177 lines
5.1 KiB
Dart

import 'dart:math';
import 'package:flutter/material.dart';
import 'package:confetti/confetti.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/widgets/matrix.dart';
class StarRainWidget extends StatefulWidget {
final bool showBlast;
final Duration rainDuration;
final Duration blastDuration;
final VoidCallback? onFinished;
final String? overlayKey;
const StarRainWidget({
super.key,
this.overlayKey,
this.showBlast = true,
this.rainDuration = const Duration(seconds: 8),
this.blastDuration = const Duration(seconds: 1),
this.onFinished,
});
@override
State<StarRainWidget> createState() => _StarRainWidgetState();
}
class _StarRainWidgetState extends State<StarRainWidget> {
late ConfettiController _blastController;
late ConfettiController _rainController;
int numParticles = 2;
double _fadeOpacity = 1.0;
@override
void initState() {
super.initState();
_blastController = ConfettiController(duration: widget.blastDuration);
_rainController = ConfettiController(duration: widget.rainDuration);
if (widget.showBlast) {
_blastController.play();
}
_rainController.play();
Future.delayed(const Duration(seconds: 4), () {
if (_rainController.state == ConfettiControllerState.playing) {
setState(() {
numParticles = 1;
});
}
});
_fadeOpacity = 1.0;
Future.delayed(widget.rainDuration, () async {
if (mounted) {
setState(() {
_fadeOpacity = 0.0;
});
}
await Future.delayed(const Duration(milliseconds: 800));
if (widget.overlayKey != null) {
MatrixState.pAnyState.closeOverlay(widget.overlayKey);
}
widget.onFinished?.call();
if (mounted) {
_blastController.stop();
_rainController.stop();
}
});
}
@override
void dispose() {
_blastController.dispose();
_rainController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
const count = 3;
const spawnOffsets = [0.2, 0.5, 0.8]; // Relative horizontal positions
return IgnorePointer(
ignoring: true,
child: AnimatedOpacity(
opacity: _fadeOpacity,
duration: const Duration(milliseconds: 800),
child: Stack(
fit: StackFit.expand,
children: [
// Initial center blast (top center)
Positioned(
top: 0,
left: 0,
right: 0,
child: Align(
alignment: Alignment.topCenter,
child: ConfettiWidget(
confettiController: _blastController,
blastDirectionality: BlastDirectionality.explosive,
shouldLoop: false,
emissionFrequency: .02,
numberOfParticles: 40,
minimumSize: const Size(20, 20),
maximumSize: const Size(25, 25),
minBlastForce: 10,
maxBlastForce: 40,
gravity: 0.07,
colors: const [AppConfig.goldLight, AppConfig.gold],
createParticlePath: drawStar,
),
),
),
// Rain confetti from the top (3 fixed spawn points)
...List.generate(count, (index) {
return Positioned(
top: -30,
left: null,
right: null,
child: FractionallySizedBox(
widthFactor: 0,
alignment: Alignment(spawnOffsets[index] * 2 - 1, -1),
child: ConfettiWidget(
confettiController: _rainController,
blastDirectionality: BlastDirectionality.directional,
blastDirection: 3 * pi / 2,
shouldLoop: false,
maxBlastForce: 5,
minBlastForce: 2,
minimumSize: const Size(20, 20),
maximumSize: const Size(25, 25),
gravity: 0.07,
emissionFrequency: 0.1,
numberOfParticles: numParticles,
colors: const [AppConfig.goldLight, AppConfig.gold],
createParticlePath: drawStar,
),
),
);
}),
],
),
),
);
}
}
Path drawStar(Size size) {
double degToRad(double deg) => deg * (pi / 180.0);
const numberOfPoints = 5;
final halfWidth = size.width / 2;
final externalRadius = halfWidth;
final internalRadius = halfWidth / 2.5;
final degreesPerStep = degToRad(360 / numberOfPoints);
final halfDegreesPerStep = degreesPerStep / 2;
final path = Path();
final fullAngle = degToRad(360);
path.moveTo(size.width, halfWidth);
for (double step = 0; step < fullAngle; step += degreesPerStep) {
path.lineTo(
halfWidth + externalRadius * cos(step),
halfWidth + externalRadius * sin(step),
);
path.lineTo(
halfWidth + internalRadius * cos(step + halfDegreesPerStep),
halfWidth + internalRadius * sin(step + halfDegreesPerStep),
);
}
path.close();
return path;
}