import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'dart:math'; import 'dart:async'; import 'dart:ui' as ui; import 'dart:typed_data'; const bool kDebugMode = false; const int kLaunchIntervalBaseMs = 400; const int kLaunchIntervalRandomMs = 300; const double kLaunchHorizontalPaddingFactor = 0.1; const double kLaunchTargetHeightMinFactor = 0.05; // lower value const double kLaunchTargetHeightMaxFactor = 0.1; // lower value const double kRocketInitialVelocityYMin = 12.0; const double kRocketInitialVelocityYRandom = 4.0; const double kRocketInitialVelocityXMax = 0.2; const double kRocketGravity = 0.16; const double kRocketDamping = 0.99; const double kRocketAcceleration = 0.015; const int kRocketTrailMaxBaseLength = 10; const int kRocketTrailBrushDensity = 1; const double kRocketTrailSpread = 1.5; const double kRocketTrailStrokeWidth = 0.8; final Color kRocketTrailColor = Colors.orangeAccent.withValues(alpha: 0.4); const int kBurstParticleCountBase = 500; const int kBurstParticleCountRandom = 100; const double kBurstParticleSpeedMin = 1.5; const double kBurstParticleSpeedRandom = 5.5; const double kBurstParticleSizeMin = 1.0; const double kBurstParticleSizeRandom = 2.0; const double kBurstParticleGravity = 0.15; const double kBurstParticleDamping = 0.97; const double kBurstParticleFadeRate = 0.95; const double kBurstParticleInitialAlphaMin = 0.85; const double kBurstParticleInitialAlphaRandom = 0.15; const double kBurstStrokeWidth = 1.8; const List kBurstColors = [ Colors.red, Colors.redAccent, Colors.orangeAccent, Colors.yellowAccent, Colors.lightGreenAccent, Colors.lightBlueAccent, Colors.purpleAccent, Colors.pinkAccent, Colors.white, Colors.cyan, ]; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, theme: ThemeData.dark(), home: const FireworksDisplay(), ); } } class FireworksDisplay extends StatefulWidget { const FireworksDisplay({Key? key}) : super(key: key); @override State createState() => _FireworksDisplayState(); } class _FireworksDisplayState extends State with SingleTickerProviderStateMixin { late Ticker _ticker; Timer? _launchTimer; final Random _random = Random(); Size _screenSize = Size.zero; List _rockets = []; List _burstParticles = []; Map? _groupedBurstPoints; Float32List? _trailPointsList; @override void initState() { super.initState(); _ticker = createTicker(_updateAnimation)..start(); _startLaunchTimer(); WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { Future.delayed(const Duration(milliseconds: 50), () { if (mounted) { setState(() { _screenSize = MediaQuery.sizeOf(context); }); } }); } }); } void _startLaunchTimer() { _launchTimer?.cancel(); _launchTimer = Timer.periodic( Duration( milliseconds: kLaunchIntervalBaseMs + _random.nextInt(kLaunchIntervalRandomMs + 1), ), (timer) { if (mounted && _screenSize != Size.zero) { _launchFirework(); } }, ); } void _updateAnimation(Duration elapsed) { if (!mounted) return; final List> burstsToSpawn = []; _rockets.removeWhere((rocket) { rocket.update(); if (rocket.hasReachedApex()) { burstsToSpawn.add({ 'position': rocket.position, 'color': rocket.burstColor, }); return true; } return false; }); for (var burstData in burstsToSpawn) { _spawnBurst(burstData['position'], burstData['color']); } _burstParticles.removeWhere((particle) { particle.update(); return particle.isDead(); }); _prepareRenderData(); setState(() {}); } void _launchFirework() { if (_screenSize == Size.zero) return; final double horizontalPadding = _screenSize.width * kLaunchHorizontalPaddingFactor; final launchX = horizontalPadding + _random.nextDouble() * (_screenSize.width - 2 * horizontalPadding); final launchPosition = Offset(launchX, _screenSize.height + 10); final minHeight = _screenSize.height * kLaunchTargetHeightMinFactor; final maxHeight = _screenSize.height * kLaunchTargetHeightMaxFactor; final targetHeight = minHeight + _random.nextDouble() * (maxHeight - minHeight); final Color selectedBurstColor = kBurstColors[_random.nextInt(kBurstColors.length)]; _rockets.add( Rocket( startPosition: launchPosition, targetHeight: targetHeight, burstColor: selectedBurstColor, random: _random, ), ); } void _spawnBurst(Offset position, Color burstColor) { int numberOfParticles = kBurstParticleCountBase + _random.nextInt(kBurstParticleCountRandom + 1); for (int i = 0; i < numberOfParticles; i++) { final angle = _random.nextDouble() * 2 * pi; final speed = kBurstParticleSpeedMin + _random.nextDouble() * kBurstParticleSpeedRandom; final size = kBurstParticleSizeMin + _random.nextDouble() * kBurstParticleSizeRandom; _burstParticles.add( BurstParticle( position: position, angle: angle, speed: speed, random: _random, color: burstColor, size: size, ), ); } } void _prepareRenderData() { final Map> pointsByColor = {}; for (final particle in _burstParticles) { (pointsByColor[particle.color] ??= []).add(particle.position); } if (pointsByColor.isNotEmpty) { _groupedBurstPoints = {}; pointsByColor.forEach((color, offsets) { if (offsets.isNotEmpty) { final pointsList = Float32List(offsets.length * 2); for (int i = 0; i < offsets.length; i++) { pointsList[i * 2] = offsets[i].dx; pointsList[i * 2 + 1] = offsets[i].dy; } _groupedBurstPoints![color] = pointsList; } }); _groupedBurstPoints!.removeWhere((key, value) => value.isEmpty); if (_groupedBurstPoints!.isEmpty) _groupedBurstPoints = null; } else { _groupedBurstPoints = null; } List allTrailPoints = []; for (final rocket in _rockets) { allTrailPoints.addAll(rocket.trail); } if (allTrailPoints.isNotEmpty) { _trailPointsList = Float32List(allTrailPoints.length * 2); for (int i = 0; i < allTrailPoints.length; i++) { _trailPointsList![i * 2] = allTrailPoints[i].dx; _trailPointsList![i * 2 + 1] = allTrailPoints[i].dy; } } else { _trailPointsList = null; } } @override void dispose() { _ticker.dispose(); _launchTimer?.cancel(); super.dispose(); } @override Widget build(BuildContext context) { final currentSize = MediaQuery.sizeOf(context); if (_screenSize != currentSize && currentSize != Size.zero) { WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { setState(() { _screenSize = currentSize; }); } }); } return Material( color: Colors.transparent, child: CustomPaint( size: Size.infinite, painter: _FireworksPainter( groupedBurstPoints: _groupedBurstPoints, trailPoints: _trailPointsList, ), ), ); } } class Rocket { Offset position; final double targetHeight; final Color burstColor; final Random random; List trail = []; Offset velocity; Rocket({ required Offset startPosition, required this.targetHeight, required this.burstColor, required this.random, }) : position = startPosition, velocity = Offset( (random.nextDouble() - 0.5) * (kRocketInitialVelocityXMax * 2), -(kRocketInitialVelocityYMin + random.nextDouble() * kRocketInitialVelocityYRandom), ); void update() { trail.insert(0, position); if (kRocketTrailBrushDensity > 0) { for (int i = 0; i < kRocketTrailBrushDensity; i++) { double offsetX = (random.nextDouble() - 0.5) * kRocketTrailSpread * 2; double offsetY = (random.nextDouble() - 0.5) * kRocketTrailSpread * 0.5; trail.insert(0, position + Offset(offsetX, offsetY)); trail.insert(0, position + Offset(-offsetX, -offsetY)); } } int dynamicMaxLength = (kRocketTrailMaxBaseLength * (1 + kRocketTrailBrushDensity * 2)); while (trail.length > dynamicMaxLength) { trail.removeLast(); } velocity = Offset( velocity.dx * kRocketDamping, (velocity.dy - kRocketAcceleration + kRocketGravity) * kRocketDamping, ); position += velocity; } bool hasReachedApex() { return velocity.dy >= -0.1 || position.dy <= targetHeight; } } class BurstParticle { Offset position; Color color; late double _vx; late double _vy; double _alpha; BurstParticle({ required this.position, required double angle, required double speed, required Random random, required this.color, required double size, }) : _alpha = kBurstParticleInitialAlphaMin + random.nextDouble() * kBurstParticleInitialAlphaRandom { _vx = speed * cos(angle); _vy = speed * sin(angle); } void update() { _vy += kBurstParticleGravity; _vx *= kBurstParticleDamping; _vy *= kBurstParticleDamping; position = Offset(position.dx + _vx, position.dy + _vy); _alpha *= kBurstParticleFadeRate; if (_alpha < 0.01) _alpha = 0; color = color.withValues(alpha: _alpha); } bool isDead() { return _alpha <= 0.01; } } class _FireworksPainter extends CustomPainter { final Map? groupedBurstPoints; final Float32List? trailPoints; final Paint _burstPaint = Paint() ..strokeWidth = kBurstStrokeWidth ..strokeCap = ui.StrokeCap.round; final Paint _trailPaint = Paint() ..strokeWidth = kRocketTrailStrokeWidth ..color = kRocketTrailColor ..strokeCap = ui.StrokeCap.round; _FireworksPainter({ required this.groupedBurstPoints, required this.trailPoints, }); @override void paint(Canvas canvas, Size size) { if (trailPoints != null && trailPoints!.isNotEmpty) { canvas.drawRawPoints(ui.PointMode.points, trailPoints!, _trailPaint); } if (groupedBurstPoints != null && groupedBurstPoints!.isNotEmpty) { groupedBurstPoints!.forEach((color, points) { _burstPaint.color = color; canvas.drawRawPoints(ui.PointMode.points, points, _burstPaint); }); } } @override bool shouldRepaint(_FireworksPainter oldDelegate) { return oldDelegate.groupedBurstPoints != groupedBurstPoints || oldDelegate.trailPoints != trailPoints; } } extension ColorAlpha on Color { Color withValues({double? alpha, int? r, int? g, int? b}) { return Color.fromARGB( ((alpha ?? this.a / 255.0) * 255).clamp(0, 255).toInt(), r ?? this.r.toInt(), g ?? this.g.toInt(), b ?? this.b.toInt(), ); } }