Created
January 16, 2023 14:02
-
-
Save HayesGordon/fd36cf8a02d2f24982b5ac9e1ccbbbc4 to your computer and use it in GitHub Desktop.
Gist to support Flutter normalisation crash
This file contains 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
// Gist to support https://github.com/flutter/flutter/issues/118559 | |
import 'dart:math'; | |
import 'dart:ui'; | |
import 'package:flutter/material.dart'; | |
import 'package:flutter/foundation.dart'; | |
import 'package:vector_math/vector_math.dart' as vmath; | |
enum BlastDirectionality { | |
directional, | |
explosive, | |
} | |
const double kLowLimit = 1 / 60; | |
const desiredSpeed = 1 / kLowLimit; | |
void main() => runApp(const MyApp()); | |
class MyApp extends StatefulWidget { | |
const MyApp({super.key}); | |
@override | |
State<MyApp> createState() => _MyAppState(); | |
} | |
class _MyAppState extends State<MyApp> { | |
late ParticleSystem particleSystem; | |
@override | |
void initState() { | |
super.initState(); | |
particleSystem = ParticleSystem( | |
emissionFrequency: 0.02, | |
numberOfParticles: 20, | |
maxBlastForce: 5, | |
minBlastForce: 20, | |
gravity: 0.1, | |
blastDirection: 0, | |
blastDirectionality: BlastDirectionality.explosive, | |
colors: [Colors.red, Colors.blue], | |
minimumSize: const Size(20, 10), | |
maximumSize: const Size(30, 15), | |
particleDrag: 0.05, | |
// createParticlePath: widget.createParticlePath, | |
); | |
particleSystem.screenSize = const Size(1000, 1000); | |
particleSystem.startParticleEmission(); | |
for (var i = 0; i < 100; i++) { | |
particleSystem.update(1 / 60); | |
} | |
} | |
@override | |
Widget build(BuildContext context) { | |
return MaterialApp( | |
title: 'Material App', | |
home: Scaffold( | |
appBar: AppBar( | |
title: const Text('Material App Bar'), | |
), | |
body: const Center( | |
child: Text('Tester!'), | |
), | |
), | |
); | |
} | |
} | |
enum ParticleSystemStatus { | |
started, | |
finished, | |
stopped, | |
} | |
class ParticleSystem extends ChangeNotifier { | |
ParticleSystem({ | |
required double emissionFrequency, | |
required int numberOfParticles, | |
required double maxBlastForce, | |
required double minBlastForce, | |
required double blastDirection, | |
required BlastDirectionality blastDirectionality, | |
required List<Color>? colors, | |
required Size minimumSize, | |
required Size maximumSize, | |
required double particleDrag, | |
required double gravity, | |
Path Function(Size size)? createParticlePath, | |
}) : assert(maxBlastForce > 0 && | |
minBlastForce > 0 && | |
emissionFrequency >= 0 && | |
emissionFrequency <= 1 && | |
numberOfParticles > 0 && | |
minimumSize.width > 0 && | |
minimumSize.height > 0 && | |
maximumSize.width > 0 && | |
maximumSize.height > 0 && | |
minimumSize.width <= maximumSize.width && | |
minimumSize.height <= maximumSize.height && | |
particleDrag >= 0.0 && | |
particleDrag <= 1 && | |
minimumSize.height <= maximumSize.height), | |
assert(gravity >= 0 && gravity <= 1), | |
_blastDirection = blastDirection, | |
_blastDirectionality = blastDirectionality, | |
_gravity = gravity, | |
_maxBlastForce = maxBlastForce, | |
_minBlastForce = minBlastForce, | |
_frequency = emissionFrequency, | |
_numberOfParticles = numberOfParticles, | |
_colors = colors, | |
_minimumSize = minimumSize, | |
_maximumSize = maximumSize, | |
_particleDrag = particleDrag, | |
_rand = Random(), | |
_createParticlePath = createParticlePath; | |
ParticleSystemStatus? _particleSystemStatus; | |
final List<Particle> _particles = []; | |
/// A frequency between 0 and 1 to determine how often the emitter | |
/// should emit new particles. | |
final double _frequency; | |
final int _numberOfParticles; | |
final double _maxBlastForce; | |
final double _minBlastForce; | |
final double _blastDirection; | |
final BlastDirectionality _blastDirectionality; | |
final double _gravity; | |
final List<Color>? _colors; | |
final Size _minimumSize; | |
final Size _maximumSize; | |
final double _particleDrag; | |
final Path Function(Size size)? _createParticlePath; | |
Offset _particleSystemPosition = Offset.zero; | |
Size _screenSize = Size.zero; | |
late double _bottomBorder; | |
late double _rightBorder; | |
late double _leftBorder; | |
final Random _rand; | |
set particleSystemPosition(Offset position) { | |
_particleSystemPosition = position; | |
} | |
set screenSize(Size size) { | |
_screenSize = size; | |
// needs to be called here to only set the borders once | |
_setScreenBorderPositions(); | |
} | |
void stopParticleEmission({bool clearAllParticles = false}) { | |
_particleSystemStatus = ParticleSystemStatus.stopped; | |
if (clearAllParticles) { | |
_particles.clear(); | |
} | |
} | |
void startParticleEmission() { | |
_particleSystemStatus = ParticleSystemStatus.started; | |
} | |
void finishParticleEmission() { | |
_particles.clear(); | |
_particleSystemStatus = ParticleSystemStatus.finished; | |
} | |
List<Particle> get particles => _particles; | |
int get numberOfParticles => _particles.length; | |
int get activeNumberOfParticles => _particles.fold( | |
0, | |
(previousValue, element) { | |
if (element.active) { | |
return previousValue + 1; | |
} else { | |
return previousValue; | |
} | |
}, | |
); | |
ParticleSystemStatus? get particleSystemStatus => _particleSystemStatus; | |
void update(double deltaTime, {bool pauseEmission = false}) { | |
if (_particleSystemStatus != ParticleSystemStatus.finished) { | |
_updateParticles(deltaTime); | |
} | |
if ((_particleSystemStatus == ParticleSystemStatus.stopped) && | |
_particles.isEmpty) { | |
finishParticleEmission(); | |
notifyListeners(); | |
} | |
if (pauseEmission) return; | |
if (_particleSystemStatus == ParticleSystemStatus.started) { | |
if (particles.isEmpty) { | |
_addParticles(_particles, number: _numberOfParticles); | |
return; | |
} | |
final chanceToGenerate = _rand.nextDouble(); | |
if (chanceToGenerate < _frequency) { | |
_addParticles(_particles, number: _numberOfParticles); | |
} | |
} | |
} | |
void _setScreenBorderPositions() { | |
_bottomBorder = _screenSize.height * 1.1; | |
_rightBorder = _screenSize.width * 1.1; | |
_leftBorder = _screenSize.width - _rightBorder; | |
} | |
void _updateParticles(double deltaTime) { | |
if (_particleSystemStatus == ParticleSystemStatus.stopped) { | |
_particles | |
.removeWhere((particle) => _isOutsideOfBorder(particle.location)); | |
for (final particle in _particles) { | |
particle.update(deltaTime); | |
} | |
return; | |
} | |
for (final particle in _particles) { | |
if (_isOutsideOfBorder(particle.location)) { | |
particle.deactivate(); | |
continue; | |
} | |
particle.update(deltaTime); | |
} | |
} | |
bool _isOutsideOfBorder(Offset particleLocation) { | |
final globalParticlePosition = particleLocation + _particleSystemPosition; | |
return (globalParticlePosition.dy >= _bottomBorder) || | |
(globalParticlePosition.dx >= _rightBorder) || | |
(globalParticlePosition.dx <= _leftBorder); | |
} | |
void _addParticles(List<Particle> particles, {int number = 1}) { | |
int count = 0; | |
for (final particle in particles) { | |
if (!particle.active) { | |
particle.reactivate(); | |
count++; | |
if (count == number) { | |
return; // exit early, no need to generate more particles | |
} | |
} | |
} | |
for (var i = 0; i < number - count; i++) { | |
particles.add( | |
Particle( | |
_randomColor(), | |
_randomSize(), | |
_gravity, | |
_particleDrag, | |
_createParticlePath, | |
generateParticleForceCallback: _generateParticleForce, | |
), | |
); | |
} | |
} | |
double get _randomBlastDirection => | |
vmath.radians(Random().nextInt(359).toDouble()); | |
vmath.Vector2 _generateParticleForce() { | |
var blastDirection = _blastDirection; | |
if (_blastDirectionality == BlastDirectionality.explosive) { | |
blastDirection = _randomBlastDirection; | |
} | |
final blastRadius = Helper.randomize(_minBlastForce, _maxBlastForce); | |
final y = blastRadius * sin(blastDirection); | |
final x = blastRadius * cos(blastDirection); | |
return vmath.Vector2(x, y); | |
} | |
Color _randomColor() { | |
if (_colors != null) { | |
if (_colors!.length == 1) { | |
return _colors![0]; | |
} | |
final index = _rand.nextInt(_colors!.length); | |
return _colors![index]; | |
} | |
return Helper.randomColor(); | |
} | |
Size _randomSize() { | |
return Size( | |
Helper.randomize(_minimumSize.width, _maximumSize.width), | |
Helper.randomize(_minimumSize.height, _maximumSize.height), | |
); | |
} | |
} | |
typedef GenerateParticleForceCallback = vmath.Vector2 Function(); | |
class Particle { | |
Particle( | |
Color color, | |
Size size, | |
this.gravity, | |
double particleDrag, | |
Path Function(Size size)? createParticlePath, { | |
required this.generateParticleForceCallback, | |
}) : _startUpForce = generateParticleForceCallback(), | |
_color = color, | |
_mass = Helper.randomize(1, 11), | |
_particleDrag = particleDrag, | |
_location = vmath.Vector2.zero(), | |
_acceleration = vmath.Vector2.zero(), | |
_velocity = | |
vmath.Vector2(Helper.randomize(-3, 3), Helper.randomize(-3, 3)), | |
_pathShape = createParticlePath != null | |
? createParticlePath(size) | |
: createPath(size), | |
_aVelocityX = Helper.randomize(-0.1, 0.1), | |
_aVelocityY = Helper.randomize(-0.1, 0.1), | |
_aVelocityZ = Helper.randomize(-0.1, 0.1), | |
_rotateZ = Helper.randomBool(), | |
gravityVector = vmath.Vector2( | |
0, | |
lerpDouble(0.1, 5, gravity)!, | |
), | |
_active = true; | |
final double gravity; | |
final vmath.Vector2 _startUpForce; | |
final GenerateParticleForceCallback generateParticleForceCallback; | |
final vmath.Vector2 _location; | |
final vmath.Vector2 _velocity; | |
final vmath.Vector2 _acceleration; | |
final double _particleDrag; | |
double _aX = 0; | |
double _aVelocityX; | |
double _aY = 0; | |
double _aVelocityY; | |
double _aZ = 0; | |
double _aVelocityZ; | |
final vmath.Vector2 gravityVector; | |
late final _aAcceleration = 0.0001 / _mass; | |
final Color _color; | |
final double _mass; | |
final Path _pathShape; | |
bool _active; | |
bool get active => _active; | |
final bool _rotateZ; | |
double _timeAlive = 0; | |
vmath.Vector2 windforceUp = vmath.Vector2(0, -1); | |
static Path createPath(Size size) { | |
final pathShape = Path() | |
..moveTo(0, 0) | |
..lineTo(-size.width, 0) | |
..lineTo(-size.width, size.height) | |
..lineTo(0, size.height) | |
..close(); | |
// TODO: remove when this is fixed: https://github.com/funwithflutter/flutter_confetti/issues/66 | |
if (kIsWeb) { | |
pathShape | |
..lineTo(-size.width, 0) | |
..lineTo(-size.width, size.height) | |
..lineTo(0, size.height) | |
..close(); | |
} | |
return pathShape; | |
} | |
void reactivate() { | |
_timeAlive = 0; | |
final f = generateParticleForceCallback(); | |
_startUpForce.setValues(f.x, f.y); | |
_location.setValues(0, 0); | |
_acceleration.setValues(0, 0); | |
_velocity.setValues(Helper.randomize(-3, 3), Helper.randomize(-3, 3)); | |
_aX = 0; | |
_aY = 0; | |
_aZ = 0; | |
_aVelocityX = Helper.randomize(-0.1, 0.1); | |
_aVelocityY = Helper.randomize(-0.1, 0.1); | |
_aVelocityZ = Helper.randomize(-0.1, 0.1); | |
gravityVector.setValues( | |
0, | |
lerpDouble(0.1, 5, gravity)!, | |
); | |
_active = true; | |
} | |
void deactivate() { | |
_active = false; | |
} | |
void applyForce(vmath.Vector2 force, double deltaTimeSpeed) { | |
final f = force.clone()..divide(vmath.Vector2.all(_mass)); | |
_acceleration.add(f * deltaTimeSpeed); | |
} | |
void drag(double deltaTimeSpeed) { | |
final speed = sqrt(pow(_velocity.x, 2) + pow(_velocity.y, 2)); | |
final dragMagnitude = _particleDrag * speed * speed; | |
final drag = _velocity.clone() | |
..multiply(vmath.Vector2.all(-1)) | |
..normalize() | |
..multiply(vmath.Vector2.all(dragMagnitude)); | |
applyForce(drag, deltaTimeSpeed); | |
} | |
void update(double deltaTime) { | |
final deltaTimeSpeed = deltaTime * desiredSpeed; | |
drag(deltaTimeSpeed); | |
if (_timeAlive < 5) { | |
applyForce(_startUpForce, deltaTimeSpeed); | |
} | |
if (_timeAlive < 25) { | |
applyForce(windforceUp, deltaTimeSpeed); | |
_timeAlive += 1; | |
} | |
applyForce(gravityVector, deltaTimeSpeed); | |
_velocity.add(_acceleration * deltaTimeSpeed); | |
_location.add(_velocity * deltaTimeSpeed); | |
_acceleration.setZero(); | |
_aVelocityX += _aAcceleration; | |
_aX += _aVelocityX * deltaTimeSpeed; | |
_aVelocityY += _aAcceleration; | |
_aY += _aVelocityY * deltaTimeSpeed; | |
if (_rotateZ) { | |
_aZ += _aVelocityZ * deltaTimeSpeed; | |
_aVelocityZ += _aAcceleration; | |
} | |
} | |
Offset get location { | |
if (_location.x.isNaN || _location.y.isNaN) { | |
return const Offset(0, 0); | |
} | |
return Offset(_location.x, _location.y); | |
} | |
Color get color => _color; | |
Path get path => _pathShape; | |
double get angleX => _aX; | |
double get angleY => _aY; | |
double get angleZ => _aZ; | |
bool get rotateZ => _rotateZ; | |
} | |
final _rand = Random(); | |
abstract class Helper { | |
static double randomize(double min, double max) => | |
lerpDouble(min, max, _rand.nextDouble())!; | |
static Color randomColor() => | |
Colors.primaries[_rand.nextInt(Colors.primaries.length)]; | |
static bool randomBool() => _rand.nextBool(); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment