Last active
April 5, 2025 11:34
-
-
Save callmephil/f455d2f9fc6dfc319d79eaa3eb77b692 to your computer and use it in GitHub Desktop.
Revisions
-
callmephil revised this gist
Apr 3, 2025 . No changes.There are no files selected for viewing
-
callmephil revised this gist
Apr 3, 2025 . 1 changed file with 69 additions and 177 deletions.There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -1,65 +1,43 @@ 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, @@ -73,10 +51,6 @@ const List<Color> kBurstColors = [ Colors.cyan, ]; void main() { runApp(const MyApp()); } @@ -94,15 +68,13 @@ class MyApp extends StatelessWidget { } } 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; @@ -113,22 +85,17 @@ class _FireworksDisplayState extends State<FireworksDisplay> 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(() { @@ -141,12 +108,12 @@ class _FireworksDisplayState extends State<FireworksDisplay> } void _startLaunchTimer() { _launchTimer?.cancel(); _launchTimer = Timer.periodic( Duration( milliseconds: kLaunchIntervalBaseMs + _random.nextInt(kLaunchIntervalRandomMs + 1), ), (timer) { if (mounted && _screenSize != Size.zero) { @@ -159,85 +126,69 @@ class _FireworksDisplayState extends State<FireworksDisplay> 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; @@ -251,26 +202,22 @@ class _FireworksDisplayState extends State<FireworksDisplay> 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++) { @@ -280,22 +227,18 @@ class _FireworksDisplayState extends State<FireworksDisplay> _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; @@ -315,10 +258,8 @@ class _FireworksDisplayState extends State<FireworksDisplay> @override Widget build(BuildContext context) { final currentSize = MediaQuery.sizeOf(context); if (_screenSize != currentSize && currentSize != Size.zero) { WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { setState(() { @@ -328,159 +269,122 @@ class _FireworksDisplayState extends State<FireworksDisplay> }); } 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({ @@ -490,37 +394,25 @@ class _FireworksPainter extends CustomPainter { @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( -
callmephil revised this gist
Apr 3, 2025 . 1 changed file with 247 additions and 120 deletions.There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -5,6 +5,78 @@ import 'dart:async'; // Import Timer import 'dart:ui' as ui; // Import dart:ui with alias import 'dart:typed_data'; // Import typed data lists for drawRawPoints // ========================================================================== // Configuration Constants - Tweak settings here! // ========================================================================== // --- General --- const bool kDebugMode = false; // Set true to slow down for debugging // --- Launching --- const int kLaunchIntervalBaseMs = 400; // Base time between launches (milliseconds) const int kLaunchIntervalRandomMs = 300; // Random additional time (milliseconds) const double kLaunchHorizontalPaddingFactor = 0.1; // Padding from screen edges (0.0 to 0.5) const double kLaunchTargetHeightMinFactor = 0.15; // Min burst height (% of screen height) const double kLaunchTargetHeightMaxFactor = 0.45; // Max burst height (% of screen height) // --- Rocket --- const double kRocketInitialVelocityYMin = 9.0; // Min initial upward speed const double kRocketInitialVelocityYRandom = 3.0; // Random additional upward speed const double kRocketInitialVelocityXMax = 0.2; // Max horizontal deviation speed const double kRocketGravity = 0.18; // Gravity effect on rocket const double kRocketDamping = 0.99; // Speed reduction factor per frame const double kRocketAcceleration = 0.015; // Slight upward boost const int kRocketTrailMaxBaseLength = 10; // Base length of the trail const int kRocketTrailBrushDensity = 1; // How many extra points per side for trail 'brush' (0 = thin line) const double kRocketTrailSpread = 1.5; // How wide the trail 'brush' spreads const double kRocketTrailStrokeWidth = 0.8; // Painter stroke width for trail points (THINNER TRAIL) final Color kRocketTrailColor = Colors.orangeAccent.withValues( alpha: 0.4, ); // Trail color and base alpha // --- Burst --- const int kBurstParticleCountBase = 200; // Base number of particles per burst const int kBurstParticleCountRandom = 100; // Random additional particles const double kBurstParticleSpeedMin = 1.5; // Min initial speed of burst particles const double kBurstParticleSpeedRandom = 5.5; // Random additional speed const double kBurstParticleSizeMin = 1.0; // Min size of burst particles (for points) const double kBurstParticleSizeRandom = 2.0; // Random additional size const double kBurstParticleGravity = 0.15; // Gravity effect on burst particles const double kBurstParticleDamping = 0.97; // Speed reduction factor per frame const double kBurstParticleFadeRate = 0.965; // Alpha reduction factor per frame const double kBurstParticleInitialAlphaMin = 0.85; // Minimum starting alpha const double kBurstParticleInitialAlphaRandom = 0.15; // Random additional starting alpha const double kBurstStrokeWidth = 1.8; // Painter stroke width for burst points // List of possible colors for each firework burst. A single color is chosen per burst. const List<Color> kBurstColors = [ Colors.red, Colors.redAccent, Colors.orangeAccent, Colors.yellowAccent, Colors.lightGreenAccent, Colors.lightBlueAccent, Colors.purpleAccent, Colors.pinkAccent, Colors.white, Colors.cyan, ]; // ========================================================================== // Application Code // ========================================================================== void main() { runApp(const MyApp()); } @@ -17,7 +89,7 @@ class MyApp extends StatelessWidget { return MaterialApp( debugShowCheckedModeBanner: false, theme: ThemeData.dark(), home: const FireworksDisplay(), ); } } @@ -42,19 +114,21 @@ class _FireworksDisplayState extends State<FireworksDisplay> List<BurstParticle> _burstParticles = []; // Prepared lists/maps for rendering Map<ui.Color, Float32List>? _groupedBurstPoints; // Map for grouped burst points (Color -> Flat list of x,y coordinates) Float32List? _trailPointsList; // Flat list of x,y coordinates for all trail points @override void initState() { super.initState(); _ticker = createTicker(_updateAnimation)..start(); _startLaunchTimer(); // Get initial screen size after the first frame WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { // Delay slightly to ensure layout is complete Future.delayed(const Duration(milliseconds: 50), () { if (mounted) { setState(() { @@ -67,10 +141,13 @@ class _FireworksDisplayState extends State<FireworksDisplay> } void _startLaunchTimer() { _launchTimer?.cancel(); // Cancel previous timer if exists _launchTimer = Timer.periodic( Duration( milliseconds: kLaunchIntervalBaseMs + _random.nextInt(kLaunchIntervalRandomMs + 1), // Use constants ), (timer) { if (mounted && _screenSize != Size.zero) { _launchFirework(); @@ -82,116 +159,141 @@ class _FireworksDisplayState extends State<FireworksDisplay> void _updateAnimation(Duration elapsed) { if (!mounted) return; // --- Update Rockets --- final List<Map<String, dynamic>> burstsToSpawn = []; // Store origin and color _rockets.removeWhere((rocket) { rocket.update(); if (rocket.hasReachedApex()) { // Store position and the rocket's assigned burst color burstsToSpawn.add({ 'position': rocket.position, 'color': rocket.burstColor, }); return true; // Remove rocket } return false; }); // --- Spawn New Bursts --- for (var burstData in burstsToSpawn) { _spawnBurst(burstData['position'], burstData['color']); } // --- Update Burst Particles --- _burstParticles.removeWhere((particle) { particle.update(); return particle.isDead(); }); // --- Prepare data for efficient rendering --- _prepareRenderData(); // Trigger repaint setState(() {}); } void _launchFirework() { if (_screenSize == Size.zero) return; // Calculate launch position using constants final double horizontalPadding = _screenSize.width * kLaunchHorizontalPaddingFactor; final launchX = horizontalPadding + _random.nextDouble() * (_screenSize.width - 2 * horizontalPadding); final launchPosition = Offset( launchX, _screenSize.height + 10, ); // Start just below screen // Calculate target height using constants final minHeight = _screenSize.height * kLaunchTargetHeightMinFactor; final maxHeight = _screenSize.height * kLaunchTargetHeightMaxFactor; final targetHeight = minHeight + _random.nextDouble() * (maxHeight - minHeight); // *** Select a SINGLE random color for this firework's burst *** final Color selectedBurstColor = kBurstColors[_random.nextInt(kBurstColors.length)]; _rockets.add( Rocket( startPosition: launchPosition, targetHeight: targetHeight, burstColor: selectedBurstColor, // Pass the chosen color random: _random, ), ); } void _spawnBurst(Offset position, Color burstColor) { // Receive the color // Use constants for particle count int numberOfParticles = kBurstParticleCountBase + _random.nextInt(kBurstParticleCountRandom + 1); for (int i = 0; i < numberOfParticles; i++) { final angle = _random.nextDouble() * 2 * pi; // Use constants for speed and size 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, // Use the single color passed to this burst size: size, ), ); } } // Prepare data for drawRawPoints (grouping bursts by color, flattening trails) void _prepareRenderData() { // --- Prepare burst particle data (grouped by color) --- final Map<ui.Color, List<Offset>> pointsByColor = {}; for (final particle in _burstParticles) { // Group by the color's integer value for efficient map lookup (pointsByColor[particle.color] ??= []).add(particle.position); } if (pointsByColor.isNotEmpty) { _groupedBurstPoints = {}; // Initialize the map for Float32Lists pointsByColor.forEach((color, offsets) { // Optimization: Only create Float32List if there are points for this color 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; } }); // Remove the entry if list was empty (though the check above prevents this) _groupedBurstPoints!.removeWhere((key, value) => value.isEmpty); // If after filtering, the map is empty, set it to null if (_groupedBurstPoints!.isEmpty) _groupedBurstPoints = null; } else { _groupedBurstPoints = null; } // --- Prepare trail particle data (flat list for drawRawPoints) --- List<Offset> allTrailPoints = []; for (final rocket in _rockets) { allTrailPoints.addAll(rocket.trail); } if (allTrailPoints.isNotEmpty) { // Convert List<Offset> to Float32List for drawRawPoints _trailPointsList = Float32List(allTrailPoints.length * 2); @@ -213,8 +315,10 @@ class _FireworksDisplayState extends State<FireworksDisplay> @override Widget build(BuildContext context) { // Update screen size if it changes (e.g., orientation change) final currentSize = MediaQuery.sizeOf(context); if (_screenSize != currentSize && currentSize != Size.zero) { // Use addPostFrameCallback to avoid setState during build WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { setState(() { @@ -227,7 +331,7 @@ class _FireworksDisplayState extends State<FireworksDisplay> return Scaffold( backgroundColor: Colors.black, body: CustomPaint( size: Size.infinite, // Painter covers the whole screen painter: _FireworksPainter( groupedBurstPoints: _groupedBurstPoints, trailPoints: _trailPointsList, // Pass the Float32List @@ -241,143 +345,149 @@ class _FireworksDisplayState extends State<FireworksDisplay> class Rocket { Offset position; final double targetHeight; final Color burstColor; // Store the color for the eventual burst final Random random; List<Offset> trail = []; Offset velocity; // Mutable velocity Rocket({ required Offset startPosition, required this.targetHeight, required this.burstColor, // Receive burst color required this.random, }) : position = startPosition, velocity = Offset( // Use constants for initial velocity (random.nextDouble() - 0.5) * (kRocketInitialVelocityXMax * 2), // Centered around 0 -(kRocketInitialVelocityYMin + random.nextDouble() * kRocketInitialVelocityYRandom), ); void update() { // --- Trail Brush Effect --- // Use constants for density and spread // Add center point trail.insert(0, position); // Add offset points for brush effect if density > 0 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; // Less vertical jitter trail.insert(0, position + Offset(offsetX, offsetY)); trail.insert( 0, position + Offset(-offsetX, -offsetY), ); // Mirrored point } } // --- Limit Trail Length --- // Adjust max length based on density: base + density*2 points per update int dynamicMaxLength = (kRocketTrailMaxBaseLength * (1 + kRocketTrailBrushDensity * 2)); while (trail.length > dynamicMaxLength) { trail.removeLast(); } // --- Update Velocity (Mutable) --- // Use constants for gravity, acceleration, damping velocity = Offset( velocity.dx * kRocketDamping, (velocity.dy - kRocketAcceleration + kRocketGravity) * kRocketDamping, ); // --- Update Position --- position += velocity; } bool hasReachedApex() { // Apex is when upward velocity stops/reverses or target height is passed return velocity.dy >= -0.1 || position.dy <= targetHeight; } } // --- Burst Particle Class --- class BurstParticle { Offset position; Color color; // Color is now set fully at creation including alpha // final double size; // Size isn't used directly by drawRawPoints, only strokeWidth late double _vx; late double _vy; double _alpha; // Keep alpha for fading logic and isDead check BurstParticle({ required this.position, required double angle, required double speed, required Random random, required this.color, // Receive the base color for the burst required double size, // Keep size if you might want varied strokeWidth later }) : // this.size = size, // Store if needed later _alpha = kBurstParticleInitialAlphaMin + random.nextDouble() * kBurstParticleInitialAlphaRandom { _vx = speed * cos(angle); _vy = speed * sin(angle); // Set the initial color with calculated alpha // color = baseColor.withOpacity(_alpha); } void update() { // Use constants for gravity, damping, fade rate _vy += kBurstParticleGravity; _vx *= kBurstParticleDamping; _vy *= kBurstParticleDamping; position = Offset(position.dx + _vx, position.dy + _vy); // Fade the alpha value internally _alpha *= kBurstParticleFadeRate; if (_alpha < 0.01) _alpha = 0; // Clamp near zero // Update the particle's color with the new alpha // Optimization: Only update color if alpha changed significantly to avoid object creation // Note: withOpacity creates a new Color object. If performance critical, // consider passing alpha separately to the painter or using shaders. // For simplicity here, we update the color object. color = color.withValues(alpha: _alpha); } bool isDead() { return _alpha <= 0.01; // Check against internal alpha } } // --- Custom Painter --- class _FireworksPainter extends CustomPainter { final Map<ui.Color, Float32List>? groupedBurstPoints; final Float32List? trailPoints; // Use constants for stroke widths and trail color final Paint _burstPaint = Paint() ..strokeWidth = kBurstStrokeWidth // Use constant ..strokeCap = ui.StrokeCap.round; final Paint _trailPaint = Paint() ..strokeWidth = kRocketTrailStrokeWidth // Use constant (THINNER TRAIL) ..color = kRocketTrailColor // Use constant ..strokeCap = ui.StrokeCap.round; _FireworksPainter({ required this.groupedBurstPoints, required this.trailPoints, }); @override void paint(Canvas canvas, Size size) { // --- Draw Rocket Trails using drawRawPoints --- @@ -389,7 +499,7 @@ class _FireworksPainter extends CustomPainter { // --- Draw Burst Particles using multiple drawRawPoints calls --- if (groupedBurstPoints != null && groupedBurstPoints!.isNotEmpty) { groupedBurstPoints!.forEach((color, points) { // The color object already includes alpha from the particle update _burstPaint.color = color; // Draw this group's points canvas.drawRawPoints(ui.PointMode.points, points, _burstPaint); @@ -399,8 +509,25 @@ class _FireworksPainter extends CustomPainter { @override bool shouldRepaint(_FireworksPainter oldDelegate) { // Repaint whenever the points data changes. // Using identity check (is the map/list object itself different?) is usually // sufficient here because we recreate the maps/lists in _prepareRenderData. // For more complex scenarios, a deep comparison might be needed, but avoid if possible for performance. return oldDelegate.groupedBurstPoints != groupedBurstPoints || oldDelegate.trailPoints != trailPoints; } } // Helper extension to modify Color alpha without creating new objects unnecessarily in BurstParticle update // NOTE: Color is immutable, so this still creates a new object, but it's a clear way to express the intent. // For extreme optimization, you might manage alpha separately. 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(), ); } } -
callmephil created this gist
Apr 3, 2025 .There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,406 @@ import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; // Import TickerProvider import 'dart:math'; import 'dart:async'; // Import Timer import 'dart:ui' as ui; // Import dart:ui with alias import 'dart:typed_data'; // Import typed data lists for drawRawPoints 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: FireworksDisplay(), ); } } // Main Widget class FireworksDisplay extends StatefulWidget { const FireworksDisplay({Key? key}) : super(key: key); @override State<FireworksDisplay> createState() => _FireworksDisplayState(); } // State using SingleTickerProviderStateMixin for the Ticker 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 = []; // Prepared lists/maps for rendering // Map for grouped burst points (Color -> Flat list of x,y coordinates) Map<ui.Color, Float32List>? _groupedBurstPoints; // Flat list of x,y coordinates for all trail points Float32List? _trailPointsList; @override void initState() { super.initState(); _ticker = createTicker(_updateAnimation)..start(); _startLaunchTimer(); WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { // Delay getting size slightly Future.delayed(const Duration(milliseconds: 50), () { if (mounted) { setState(() { _screenSize = MediaQuery.sizeOf(context); }); } }); } }); } void _startLaunchTimer() { _launchTimer = Timer.periodic( Duration( milliseconds: 500 + _random.nextInt(10), ), // Launch more frequently? (timer) { if (mounted && _screenSize != Size.zero) { _launchFirework(); } }, ); } void _updateAnimation(Duration elapsed) { if (!mounted) return; List<Offset> newBurstOrigins = []; // Update Rockets _rockets.removeWhere((rocket) { rocket.update(); if (rocket.hasReachedApex()) { newBurstOrigins.add(rocket.position); return true; // Remove rocket } return false; }); // Spawn New Bursts for (var origin in newBurstOrigins) { _spawnBurst(origin); } // Update Burst Particles _burstParticles.removeWhere((particle) { particle.update(); return particle.isDead(); }); // Prepare data for efficient rendering _prepareRenderData(); setState(() {}); } void _launchFirework() { if (_screenSize == Size.zero) return; final launchX = _random.nextDouble() * _screenSize.width * 0.8 + _screenSize.width * 0.1; final launchPosition = Offset(launchX, _screenSize.height + 10); final targetHeight = _screenSize.height * (0.15 + _random.nextDouble() * 0.3); // Aim higher? _rockets.add( Rocket( startPosition: launchPosition, targetHeight: targetHeight, random: _random, ), ); } void _spawnBurst(Offset position) { int numberOfParticles = 200 + _random.nextInt(150); // More particles? List<Color> fireworkColors = [ Colors.red, Colors.redAccent, Colors.orange, Colors.orangeAccent, Colors.yellow, Colors.yellowAccent, Colors.white, Colors.lightBlueAccent, Colors.blue.shade300, Colors.greenAccent, Colors.purpleAccent.shade100, ]; for (int i = 0; i < numberOfParticles; i++) { final angle = _random.nextDouble() * 2 * pi; final speed = _random.nextDouble() * 6.0 + 2.0; // Increased max speed final size = _random.nextDouble() * 3 + 1.0; // Smaller base size for points final color = fireworkColors[_random.nextInt(fireworkColors.length)]; _burstParticles.add( BurstParticle( position: position, angle: angle, speed: speed, random: _random, color: color, size: size, ), ); } } // Prepare data for drawRawPoints (grouping bursts by color) void _prepareRenderData() { // --- Prepare burst particle data (grouped by color) --- final Map<ui.Color, List<Offset>> pointsByColor = {}; for (final particle in _burstParticles) { // Group by the integer value for efficient map lookup (pointsByColor[particle.color] ??= []).add(particle.position); } if (pointsByColor.isNotEmpty) { _groupedBurstPoints = {}; // Initialize the map for Float32Lists pointsByColor.forEach((color, offsets) { 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; }); } else { _groupedBurstPoints = null; } // --- Prepare trail particle data (flat list for drawPoints) --- List<Offset> allTrailPoints = []; for (final rocket in _rockets) { allTrailPoints.addAll(rocket.trail); } if (allTrailPoints.isNotEmpty) { // Convert List<Offset> to Float32List for drawRawPoints _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.of(context).size; if (_screenSize != currentSize && currentSize != Size.zero) { WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { setState(() { _screenSize = currentSize; }); } }); } return Scaffold( backgroundColor: Colors.black, body: CustomPaint( size: Size.infinite, painter: _FireworksPainter( groupedBurstPoints: _groupedBurstPoints, trailPoints: _trailPointsList, // Pass the Float32List ), ), ); } } // --- Rocket Class --- class Rocket { Offset position; final double targetHeight; final Random random; List<Offset> trail = []; // Increase trail length final int _maxTrailLengthBase = 25; final Color color = Colors.yellow.shade300; // Slightly richer yellow final double gravity = 0.20; // Reduced gravity effect final double damping = 1; // Less damping // Add upward acceleration final double acceleration = 0.01; // *** FIX: Make velocity mutable *** Offset velocity; Rocket({ required Offset startPosition, required this.targetHeight, required this.random, }) : position = startPosition, // Start with a decent initial velocity velocity = Offset( random.nextDouble() * 1.0 - 0.5, -(9.0 + random.nextDouble() * 5.0), // Stronger initial push ); void update() { // --- Trail Brush Effect --- int brushDensity = 2; // How many points per side (+ center = 2*N+1) double spread = 3; // How wide the brush spreads // Add center point trail.insert(0, position); // Add offset points for brush effect for (int i = 0; i < brushDensity; i++) { double offsetX = (random.nextDouble() - 0.5) * spread * 2; double offsetY = (random.nextDouble() - 0.5) * 0.8; // Slight vertical jitter trail.insert(0, position + Offset(offsetX, offsetY)); trail.insert( 0, position + Offset(-offsetX, -offsetY), ); // Mirror for symmetry? } // --- Limit Trail Length --- // Adjust max length based on density int dynamicMaxLength = (_maxTrailLengthBase * (1 + brushDensity * 2)); while (trail.length > dynamicMaxLength) { trail.removeLast(); } // --- Update Velocity (Mutable) --- velocity = Offset( velocity.dx * damping, // Apply acceleration upward, counter gravity slightly (velocity.dy - acceleration + gravity) * damping, ); // --- Update Position --- position += velocity; } bool hasReachedApex() { // Apex is when upward velocity stops (or when position is reached) return velocity.dy >= -0.1 || position.dy <= targetHeight; } } // --- Burst Particle Class --- (Unchanged conceptually) class BurstParticle { Offset position; double angle; double speed; Random random; Color color; double size; late double _vx; late double _vy; final double _gravity = 0.10; double _alpha; final double _damping = 0.975; final double _fadeRate = 0.96; BurstParticle({ required this.position, required this.angle, required this.speed, required this.random, required this.color, required this.size, }) : _alpha = 0.9 + random.nextDouble() * 0.1 { _vx = speed * cos(angle); _vy = speed * sin(angle); color = color.withValues(alpha: _alpha); } void update() { _vy += _gravity; _vx *= _damping; _vy *= _damping; position = Offset(position.dx + _vx, position.dy + _vy); _alpha *= _fadeRate; if (_alpha < 0.01) _alpha = 0; color = color.withValues(alpha: _alpha); } bool isDead() { return _alpha <= 0; } } // --- Custom Painter --- class _FireworksPainter extends CustomPainter { // Receive grouped burst points and flat trail points final Map<ui.Color, Float32List>? groupedBurstPoints; final Float32List? trailPoints; // Now Float32List _FireworksPainter({ required this.groupedBurstPoints, required this.trailPoints, }); // Shared paint object for bursts (color will be overridden) final Paint _burstPaint = Paint() ..strokeWidth = 1.8 // Point size ..strokeCap = ui.StrokeCap.round; // Paint object for rocket trails (using drawRawPoints now too) final Paint _trailPaint = Paint() ..strokeWidth = 1.5 // Smaller points for denser trail ..color = Colors.yellow.shade400.withValues( alpha: 0.5, ) // Base trail color+alpha ..strokeCap = ui.StrokeCap.round; @override void paint(Canvas canvas, Size size) { // --- Draw Rocket Trails using drawRawPoints --- if (trailPoints != null && trailPoints!.isNotEmpty) { // Trail uses a single base color defined in _trailPaint canvas.drawRawPoints(ui.PointMode.points, trailPoints!, _trailPaint); } // --- Draw Burst Particles using multiple drawRawPoints calls --- if (groupedBurstPoints != null && groupedBurstPoints!.isNotEmpty) { groupedBurstPoints!.forEach((color, points) { // Set the color for this group _burstPaint.color = color; // Draw this group's points canvas.drawRawPoints(ui.PointMode.points, points, _burstPaint); }); } } @override bool shouldRepaint(_FireworksPainter oldDelegate) { // Basic check - repaint if the maps/lists are different objects return oldDelegate.groupedBurstPoints != groupedBurstPoints || oldDelegate.trailPoints != trailPoints; } }