Last active
April 5, 2025 11:34
-
-
Save callmephil/f455d2f9fc6dfc319d79eaa3eb77b692 to your computer and use it in GitHub Desktop.
flutter fireworks (gemini)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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<Color> 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<FireworksDisplay> createState() => _FireworksDisplayState(); | |
} | |
class _FireworksDisplayState extends State<FireworksDisplay> | |
with SingleTickerProviderStateMixin { | |
late Ticker _ticker; | |
Timer? _launchTimer; | |
final Random _random = Random(); | |
Size _screenSize = Size.zero; | |
List<Rocket> _rockets = []; | |
List<BurstParticle> _burstParticles = []; | |
Map<ui.Color, Float32List>? _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<Map<String, dynamic>> 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<ui.Color, List<Offset>> 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<Offset> 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<Offset> 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<ui.Color, Float32List>? 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(), | |
); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment