particle_system.dart 15.2 KB
Newer Older
1
part of flutter_sprites;
2 3 4 5 6

class _Particle {
  Vector2 pos;
  Vector2 startPos;

7 8
  double colorPos = 0.0;
  double deltaColorPos = 0.0;
9

10 11
  double size = 0.0;
  double deltaSize = 0.0;
12

13 14
  double rotation = 0.0;
  double deltaRotation = 0.0;
15

16
  double timeToLive = 0.0;
17 18

  Vector2 dir;
19 20 21 22

  _ParticleAccelerations accelerations;

  Float64List simpleColorSequence;
23 24 25 26

  ColorSequence colorSequence;
}

27
class _ParticleAccelerations {
28 29
  double radialAccel = 0.0;
  double tangentialAccel = 0.0;
30
}
31

32 33 34 35 36 37 38 39 40 41
/// A particle system uses a large number of sprites to draw complex effects
/// such as explosions, smoke, rain, or fire. There are a number of properties
/// that can be set to control the look of the particle system. Most of the
/// properties have a base value and a variance, these values are used when
/// creating each individual particle. For instance, by setting the [life] to
/// 1.0 and the [lifeVar] to 0.5, each particle will get a life time in the
/// range of 0.5 to 1.5.
///
/// Particles are created and added to the system at [emissionRate], but the
/// number of particles can never exceed the [maxParticles] limit.
42 43
class ParticleSystem extends Node {

44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86
  ParticleSystem(this.texture,
                 {this.life: 1.5,
                  this.lifeVar: 1.0,
                  this.posVar: Point.origin,
                  this.startSize: 2.5,
                  this.startSizeVar: 0.5,
                  this.endSize: 0.0,
                  this.endSizeVar: 0.0,
                  this.startRotation: 0.0,
                  this.startRotationVar: 0.0,
                  this.endRotation: 0.0,
                  this.endRotationVar: 0.0,
                  this.rotateToMovement : false,
                  this.direction: 0.0,
                  this.directionVar: 360.0,
                  this.speed: 100.0,
                  this.speedVar: 50.0,
                  this.radialAcceleration: 0.0,
                  this.radialAccelerationVar: 0.0,
                  this.tangentialAcceleration: 0.0,
                  this.tangentialAccelerationVar: 0.0,
                  this.maxParticles: 100,
                  this.emissionRate: 50.0,
                  this.colorSequence,
                  this.alphaVar: 0,
                  this.redVar: 0,
                  this.greenVar: 0,
                  this.blueVar: 0,
                  this.transferMode: TransferMode.plus,
                  this.numParticlesToEmit: 0,
                  this.autoRemoveOnFinish: true,
                  Offset gravity
  }) {
    this.gravity = gravity;
    _particles = new List<_Particle>();
    _emitCounter = 0.0;
    // _elapsedTime = 0.0;
    if (_gravity == null)
      _gravity = new Vector2.zero();
    if (colorSequence == null)
      colorSequence = new ColorSequence.fromStartAndEndColor(new Color(0xffffffff), new Color(0x00ffffff));
  }

87
  /// The texture used to draw each individual sprite.
88 89
  Texture texture;

90
  /// The time in seconds each particle will be alive.
91
  double life;
92 93

  /// Variance of the [life] property.
94 95
  double lifeVar;

96
  /// The variance of a particles initial position.
97 98
  Point posVar;

99
  /// The start scale of each individual particle.
100
  double startSize;
101 102

  /// Variance of the [startSize] property.
103 104
  double startSizeVar;

105
  /// The end scale of each individual particle.
106
  double endSize;
107 108

  /// Variance of the [endSize] property.
109 110
  double endSizeVar;

111
  /// The start rotation of each individual particle.
112
  double startRotation;
113 114

  /// Variance of the [startRotation] property.
115 116
  double startRotationVar;

117
  /// The end rotation of each individual particle.
118
  double endRotation;
119 120

  /// Variance of the [endRotation] property.
121 122
  double endRotationVar;

123 124 125 126
  /// If true, each particle will be rotated to the direction of the movement
  /// of the particle. The calculated rotation will be added to the current
  /// rotation as calculated by the [startRotation] and [endRotation]
  /// properties.
127 128
  bool rotateToMovement;

129
  /// The direction in which each particle will be emitted in degrees.
130
  double direction;
131 132

  /// Variance of the [direction] property.
133 134
  double directionVar;

135
  /// The speed at which each particle will be emitted.
136
  double speed;
137 138

  /// Variance of the [direction] property.
139 140
  double speedVar;

141
  /// The radial acceleration of each induvidual particle.
142
  double radialAcceleration;
143 144

  /// Variance of the [radialAcceleration] property.
145 146
  double radialAccelerationVar;

147
  /// The tangential acceleration of each individual particle.
148
  double tangentialAcceleration;
149 150

  /// Variance of the [tangentialAcceleration] property.
151 152
  double tangentialAccelerationVar;

153
  /// The gravity vector of the particle system.
154 155 156 157 158 159 160 161 162 163 164 165 166 167 168
  Offset get gravity {
    if (_gravity == null)
      return null;

    return new Offset(_gravity.x, _gravity.y);
  }

  Vector2 _gravity;

  void set gravity(Offset gravity) {
    if (gravity == null)
      _gravity = null;
    else
      _gravity = new Vector2(gravity.dx, gravity.dy);
  }
169

170
  /// The maximum number of particles the system can display at a single time.
171
  int maxParticles;
172 173 174

  /// Total number of particles to emit, if the value is set to 0 the system
  /// will continue to emit particles for an indifinte period of time.
175
  int numParticlesToEmit;
176 177

  /// The rate at which particles are emitted, defined in particles per second.
178
  double emissionRate;
179 180 181

  /// If set to true, the particle system will be automatically removed as soon
  /// as there are no more particles left to draw.
182 183
  bool autoRemoveOnFinish;

184 185 186 187
  /// The [ColorSequence] used to animate the color of each individual particle
  /// over the duration of its [life]. When applied to a particle the sequence's
  /// color stops modified in accordance with the [alphaVar], [redVar],
  /// [greenVar], and [blueVar] properties.
188
  ColorSequence colorSequence;
189 190

  /// Alpha varience of the [colorSequence] property.
191
  int alphaVar;
192 193

  /// Red varience of the [colorSequence] property.
194
  int redVar;
195 196

  /// Green varience of the [colorSequence] property.
197
  int greenVar;
198 199

  /// Blue varience of the [colorSequence] property.
200
  int blueVar;
201 202 203

  /// The transfer mode used to draw the particle system. Default is
  /// [TransferMode.plus].
204
  TransferMode transferMode;
205 206 207 208 209 210

  List<_Particle> _particles;

  double _emitCounter;
  int _numEmittedParticles = 0;

211 212
  double opacity = 1.0;

213
  static Paint _paint = new Paint()
214
    ..filterQuality = FilterQuality.low
215 216
    ..isAntiAlias = false;

217
  @override
218
  void update(double dt) {
219 220
    // TODO: Fix this (it's a temp fix for low framerates)
    if (dt > 0.1) dt = 0.1;
221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251

    // Create new particles
    double rate = 1.0 / emissionRate;

    if (_particles.length < maxParticles) {
      _emitCounter += dt;
    }

    while(_particles.length < maxParticles
       && _emitCounter > rate
       && (numParticlesToEmit == 0 || _numEmittedParticles < numParticlesToEmit)) {
      // Add a new particle
      _addParticle();
      _emitCounter -= rate;
    }

    // _elapsedTime += dt;

    // Iterate over all particles
    for (int i = _particles.length -1; i >= 0; i--) {
      _Particle particle = _particles[i];

      // Manage life time
      particle.timeToLive -= dt;
      if (particle.timeToLive <= 0) {
        _particles.removeAt(i);
        continue;
      }

      // Update the particle

252
      if (particle.accelerations != null) {
253 254
      // Radial acceleration
      Vector2 radial;
255 256 257 258 259 260 261 262 263 264 265 266 267 268 269
        if (particle.pos[0] != 0 || particle.pos[1] != 0) {
          radial = new Vector2.copy(particle.pos).normalize();
        } else {
          radial = new Vector2.zero();
        }
        Vector2 tangential = new Vector2.copy(radial);
        radial.scale(particle.accelerations.radialAccel);

        // Tangential acceleration
        double newY = tangential.x;
        tangential.x = -tangential.y;
        tangential.y = newY;
        tangential.scale(particle.accelerations.tangentialAccel);

        // (gravity + radial + tangential) * dt
270
        final Vector2 accel = (_gravity + radial + tangential).scale(dt);
271
        particle.dir += accel;
272
      } else if (_gravity[0] != 0.0 || _gravity[1] != 0) {
273
        // gravity
274
        final Vector2 accel = new Vector2.copy(_gravity).scale(dt);
275
        particle.dir += accel;
276 277
      }

278 279 280
      // Update particle position
      particle.pos[0] += particle.dir[0] * dt;
      particle.pos[1] += particle.dir[1] * dt;
281 282 283 284 285 286 287 288

      // Size
      particle.size = math.max(particle.size + particle.deltaSize * dt, 0.0);

      // Angle
      particle.rotation += particle.deltaRotation * dt;

      // Color
289 290 291 292 293 294 295
      if (particle.simpleColorSequence != null) {
        for (int i = 0; i < 4; i++) {
          particle.simpleColorSequence[i] += particle.simpleColorSequence[i + 4] * dt;
        }
      } else {
        particle.colorPos = math.min(particle.colorPos + particle.deltaColorPos * dt, 1.0);
      }
296 297 298 299 300 301 302 303 304 305 306 307
    }

    if (autoRemoveOnFinish && _particles.length == 0 && _numEmittedParticles > 0) {
      if (parent != null) removeFromParent();
    }
  }

  void _addParticle() {

    _Particle particle = new _Particle();

    // Time to live
308
    particle.timeToLive = math.max(life + lifeVar * randomSignedDouble(), 0.0);
309 310 311

    // Position
    Point srcPos = Point.origin;
312 313
    particle.pos = new Vector2(srcPos.x + posVar.x * randomSignedDouble(),
                               srcPos.y + posVar.y * randomSignedDouble());
314 315

    // Size
316 317
    particle.size = math.max(startSize + startSizeVar * randomSignedDouble(), 0.0);
    double endSizeFinal = math.max(endSize + endSizeVar * randomSignedDouble(), 0.0);
318 319 320
    particle.deltaSize = (endSizeFinal - particle.size) / particle.timeToLive;

    // Rotation
321 322
    particle.rotation = startRotation + startRotationVar * randomSignedDouble();
    double endRotationFinal = endRotation + endRotationVar * randomSignedDouble();
323 324 325
    particle.deltaRotation = (endRotationFinal - particle.rotation) / particle.timeToLive;

    // Direction
326
    double dirRadians = convertDegrees2Radians(direction + directionVar * randomSignedDouble());
327
    Vector2 dirVector = new Vector2(math.cos(dirRadians), math.sin(dirRadians));
328
    double speedFinal = speed + speedVar * randomSignedDouble();
329 330
    particle.dir = dirVector.scale(speedFinal);

331 332 333 334
    // Accelerations
    if (radialAcceleration != 0.0 || radialAccelerationVar != 0.0 ||
        tangentialAcceleration != 0.0 || tangentialAccelerationVar != 0.0) {
      particle.accelerations = new _ParticleAccelerations();
335

336 337 338 339 340 341
      // Radial acceleration
      particle.accelerations.radialAccel = radialAcceleration + radialAccelerationVar * randomSignedDouble();

      // Tangential acceleration
      particle.accelerations.tangentialAccel = tangentialAcceleration + tangentialAccelerationVar * randomSignedDouble();
    }
342 343 344 345 346 347

    // Color
    particle.colorPos = 0.0;
    particle.deltaColorPos = 1.0 / particle.timeToLive;

    if (alphaVar != 0 || redVar != 0 || greenVar != 0 || blueVar != 0) {
348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374
      particle.colorSequence = _ColorSequenceUtil.copyWithVariance(colorSequence, alphaVar, redVar, greenVar, blueVar);
    }

    // Optimizes the case where there are only two colors in the sequence
    if (colorSequence.colors.length == 2) {
      Color startColor;
      Color endColor;

      if (particle.colorSequence != null) {
        startColor = particle.colorSequence.colors[0];
        endColor = particle.colorSequence.colors[1];
      } else {
        startColor = colorSequence.colors[0];
        endColor = colorSequence.colors[1];
      }

      // First 4 elements are start ARGB, last 4 are delta ARGB
      particle.simpleColorSequence = new Float64List(8);
      particle.simpleColorSequence[0] = startColor.alpha.toDouble();
      particle.simpleColorSequence[1] = startColor.red.toDouble();
      particle.simpleColorSequence[2] = startColor.green.toDouble();
      particle.simpleColorSequence[3] = startColor.blue.toDouble();

      particle.simpleColorSequence[4] = (endColor.alpha.toDouble() - startColor.alpha.toDouble()) / particle.timeToLive;
      particle.simpleColorSequence[5] = (endColor.red.toDouble() - startColor.red.toDouble()) / particle.timeToLive;
      particle.simpleColorSequence[6] = (endColor.green.toDouble() - startColor.green.toDouble()) / particle.timeToLive;
      particle.simpleColorSequence[7] = (endColor.blue.toDouble() - startColor.blue.toDouble()) / particle.timeToLive;
375 376 377 378 379 380
    }

    _particles.add(particle);
    _numEmittedParticles++;
  }

381
  @override
Adam Barth's avatar
Adam Barth committed
382
  void paint(Canvas canvas) {
383 384
    if (opacity == 0.0)
      return;
385

386
    List<RSTransform> transforms = <RSTransform>[];
Hixie's avatar
Hixie committed
387 388
    List<Rect> rects = <Rect>[];
    List<Color> colors = <Color>[];
389

390
    _paint.transferMode = transferMode;
391

392
    for (_Particle particle in _particles) {
393 394 395 396
      // Rect
      Rect rect = texture.frame;
      rects.add(rect);

397 398 399 400
      // Transform
      double scos;
      double ssin;
      if (rotateToMovement) {
401
        double extraRotation = GameMath.atan2(particle.dir[1], particle.dir[0]);
402 403
        scos = math.cos(convertDegrees2Radians(particle.rotation) + extraRotation) * particle.size;
        ssin = math.sin(convertDegrees2Radians(particle.rotation) + extraRotation) * particle.size;
404
      } else if (particle.rotation != 0.0) {
405 406
        scos = math.cos(convertDegrees2Radians(particle.rotation)) * particle.size;
        ssin = math.sin(convertDegrees2Radians(particle.rotation)) * particle.size;
407 408 409
      } else {
        scos = particle.size;
        ssin = 0.0;
410
      }
411 412 413 414
      double ax = rect.width / 2;
      double ay = rect.height / 2;
      double tx = particle.pos[0] + -scos * ax + ssin * ay;
      double ty = particle.pos[1] + -ssin * ax - scos * ay;
415
      RSTransform transform = new RSTransform(scos, ssin, tx, ty);
416 417 418
      transforms.add(transform);

      // Color
419 420
      if (particle.simpleColorSequence != null) {
        Color particleColor = new Color.fromARGB(
421
          (particle.simpleColorSequence[0] * opacity).toInt().clamp(0, 255),
422 423 424 425
          particle.simpleColorSequence[1].toInt().clamp(0, 255),
          particle.simpleColorSequence[2].toInt().clamp(0, 255),
          particle.simpleColorSequence[3].toInt().clamp(0, 255));
        colors.add(particleColor);
426
      } else {
427 428 429 430 431 432
        Color particleColor;
        if (particle.colorSequence != null) {
          particleColor = particle.colorSequence.colorAtPosition(particle.colorPos);
        } else {
          particleColor = colorSequence.colorAtPosition(particle.colorPos);
        }
433 434 435
        if (opacity != 1.0) {
          particleColor = particleColor.withAlpha((particleColor.alpha * opacity).toInt().clamp(0, 255));
        }
436
        colors.add(particleColor);
437 438 439
      }
    }

440
    canvas.drawAtlas(texture.image, transforms, rects, colors,
441
      TransferMode.modulate, null, _paint);
442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471
  }
}

class _ColorSequenceUtil {
  static ColorSequence copyWithVariance(
    ColorSequence sequence,
    int alphaVar,
    int redVar,
    int greenVar,
    int blueVar
  ) {
    ColorSequence copy = new ColorSequence.copy(sequence);

    int i = 0;
    for (Color color in sequence.colors) {
      int aDelta = ((randomDouble() * 2.0 - 1.0) * alphaVar).toInt();
      int rDelta = ((randomDouble() * 2.0 - 1.0) * redVar).toInt();
      int gDelta = ((randomDouble() * 2.0 - 1.0) * greenVar).toInt();
      int bDelta = ((randomDouble() * 2.0 - 1.0) * blueVar).toInt();

      int aNew = (color.alpha + aDelta).clamp(0, 255);
      int rNew = (color.red + rDelta).clamp(0, 255);
      int gNew = (color.green + gDelta).clamp(0, 255);
      int bNew = (color.blue + bDelta).clamp(0, 255);

      copy.colors[i] = new Color.fromARGB(aNew, rNew, gNew, bNew);
      i++;
    }

    return copy;
472 473
  }
}