Skip to content

Instantly share code, notes, and snippets.

@callmephil
Last active April 5, 2025 11:34
Show Gist options
  • Save callmephil/f455d2f9fc6dfc319d79eaa3eb77b692 to your computer and use it in GitHub Desktop.
Save callmephil/f455d2f9fc6dfc319d79eaa3eb77b692 to your computer and use it in GitHub Desktop.
flutter fireworks (gemini)
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