particle_system.dart 15.7 KB
Newer Older
1 2 3 4
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
part of flutter_sprites;
6 7 8 9 10

class _Particle {
  Vector2 pos;
  Vector2 startPos;

11 12
  double colorPos = 0.0;
  double deltaColorPos = 0.0;
13

14 15
  double size = 0.0;
  double deltaSize = 0.0;
16

17 18
  double rotation = 0.0;
  double deltaRotation = 0.0;
19

20
  double timeToLive = 0.0;
21 22

  Vector2 dir;
23 24 25 26

  _ParticleAccelerations accelerations;

  Float64List simpleColorSequence;
27 28 29 30

  ColorSequence colorSequence;
}

31
class _ParticleAccelerations {
32 33
  double radialAccel = 0.0;
  double tangentialAccel = 0.0;
34
}
35

36 37 38 39 40 41 42 43 44 45
/// 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.
46 47
class ParticleSystem extends Node {

48 49
  /// Creates a new particle system with the given properties. The only
  /// required parameter is the texture, all other parameters are optional.
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 87 88 89 90 91 92
  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));
  }

93
  /// The texture used to draw each individual sprite.
94 95
  Texture texture;

96
  /// The time in seconds each particle will be alive.
97
  double life;
98 99

  /// Variance of the [life] property.
100 101
  double lifeVar;

102
  /// The variance of a particles initial position.
103 104
  Point posVar;

105
  /// The start scale of each individual particle.
106
  double startSize;
107 108

  /// Variance of the [startSize] property.
109 110
  double startSizeVar;

111
  /// The end scale of each individual particle.
112
  double endSize;
113 114

  /// Variance of the [endSize] property.
115 116
  double endSizeVar;

117
  /// The start rotation of each individual particle.
118
  double startRotation;
119 120

  /// Variance of the [startRotation] property.
121 122
  double startRotationVar;

123
  /// The end rotation of each individual particle.
124
  double endRotation;
125 126

  /// Variance of the [endRotation] property.
127 128
  double endRotationVar;

129 130 131 132
  /// 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.
133 134
  bool rotateToMovement;

135
  /// The direction in which each particle will be emitted in degrees.
136
  double direction;
137 138

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

141
  /// The speed at which each particle will be emitted.
142
  double speed;
143 144

  /// Variance of the [direction] property.
145 146
  double speedVar;

147
  /// The radial acceleration of each induvidual particle.
148
  double radialAcceleration;
149 150

  /// Variance of the [radialAcceleration] property.
151 152
  double radialAccelerationVar;

153
  /// The tangential acceleration of each individual particle.
154
  double tangentialAcceleration;
155 156

  /// Variance of the [tangentialAcceleration] property.
157 158
  double tangentialAccelerationVar;

159
  /// The gravity vector of the particle system.
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;

169
  set gravity(Offset gravity) {
170 171 172 173 174
    if (gravity == null)
      _gravity = null;
    else
      _gravity = new Vector2(gravity.dx, gravity.dy);
  }
175

176
  /// The maximum number of particles the system can display at a single time.
177
  int maxParticles;
178 179 180

  /// 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.
181
  int numParticlesToEmit;
182 183

  /// The rate at which particles are emitted, defined in particles per second.
184
  double emissionRate;
185 186 187

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

190 191 192 193
  /// 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.
194
  ColorSequence colorSequence;
195 196

  /// Alpha varience of the [colorSequence] property.
197
  int alphaVar;
198 199

  /// Red varience of the [colorSequence] property.
200
  int redVar;
201 202

  /// Green varience of the [colorSequence] property.
203
  int greenVar;
204 205

  /// Blue varience of the [colorSequence] property.
206
  int blueVar;
207 208 209

  /// The transfer mode used to draw the particle system. Default is
  /// [TransferMode.plus].
210
  TransferMode transferMode;
211 212 213 214 215 216

  List<_Particle> _particles;

  double _emitCounter;
  int _numEmittedParticles = 0;

217 218
  /// The over all opacity of the particle system. This value is multiplied by
  /// the opacity of the individual particles.
219 220
  double opacity = 1.0;

221
  static Paint _paint = new Paint()
222
    ..filterQuality = FilterQuality.low
223 224
    ..isAntiAlias = false;

225
  @override
226
  void update(double dt) {
227 228
    // TODO: Fix this (it's a temp fix for low framerates)
    if (dt > 0.1) dt = 0.1;
229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259

    // 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

260
      if (particle.accelerations != null) {
261 262
      // Radial acceleration
      Vector2 radial;
263
        if (particle.pos[0] != 0 || particle.pos[1] != 0) {
264
          radial = new Vector2.copy(particle.pos)..normalize();
265 266 267 268 269 270 271 272 273 274 275 276 277
        } 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
278
        final Vector2 accel = (_gravity + radial + tangential)..scale(dt);
279
        particle.dir += accel;
280
      } else if (_gravity[0] != 0.0 || _gravity[1] != 0) {
281
        // gravity
282
        final Vector2 accel = _gravity.clone()..scale(dt);
283
        particle.dir += accel;
284 285
      }

286 287 288
      // Update particle position
      particle.pos[0] += particle.dir[0] * dt;
      particle.pos[1] += particle.dir[1] * dt;
289 290 291 292 293 294 295 296

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

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

      // Color
297 298 299 300 301 302 303
      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);
      }
304 305 306 307 308 309 310 311 312 313 314 315
    }

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

  void _addParticle() {

    _Particle particle = new _Particle();

    // Time to live
316
    particle.timeToLive = math.max(life + lifeVar * randomSignedDouble(), 0.0);
317 318 319

    // Position
    Point srcPos = Point.origin;
320 321
    particle.pos = new Vector2(srcPos.x + posVar.x * randomSignedDouble(),
                               srcPos.y + posVar.y * randomSignedDouble());
322 323

    // Size
324 325
    particle.size = math.max(startSize + startSizeVar * randomSignedDouble(), 0.0);
    double endSizeFinal = math.max(endSize + endSizeVar * randomSignedDouble(), 0.0);
326 327 328
    particle.deltaSize = (endSizeFinal - particle.size) / particle.timeToLive;

    // Rotation
329 330
    particle.rotation = startRotation + startRotationVar * randomSignedDouble();
    double endRotationFinal = endRotation + endRotationVar * randomSignedDouble();
331 332 333
    particle.deltaRotation = (endRotationFinal - particle.rotation) / particle.timeToLive;

    // Direction
334
    double dirRadians = convertDegrees2Radians(direction + directionVar * randomSignedDouble());
335
    Vector2 dirVector = new Vector2(math.cos(dirRadians), math.sin(dirRadians));
336
    double speedFinal = speed + speedVar * randomSignedDouble();
337
    particle.dir = dirVector..scale(speedFinal);
338

339 340 341 342
    // Accelerations
    if (radialAcceleration != 0.0 || radialAccelerationVar != 0.0 ||
        tangentialAcceleration != 0.0 || tangentialAccelerationVar != 0.0) {
      particle.accelerations = new _ParticleAccelerations();
343

344 345 346 347 348 349
      // Radial acceleration
      particle.accelerations.radialAccel = radialAcceleration + radialAccelerationVar * randomSignedDouble();

      // Tangential acceleration
      particle.accelerations.tangentialAccel = tangentialAcceleration + tangentialAccelerationVar * randomSignedDouble();
    }
350 351 352 353 354 355

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

    if (alphaVar != 0 || redVar != 0 || greenVar != 0 || blueVar != 0) {
356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382
      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;
383 384 385 386 387 388
    }

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

389
  @override
Adam Barth's avatar
Adam Barth committed
390
  void paint(Canvas canvas) {
391 392
    if (opacity == 0.0)
      return;
393

394
    List<RSTransform> transforms = <RSTransform>[];
Hixie's avatar
Hixie committed
395 396
    List<Rect> rects = <Rect>[];
    List<Color> colors = <Color>[];
397

398
    _paint.transferMode = transferMode;
399

400
    for (_Particle particle in _particles) {
401 402 403 404
      // Rect
      Rect rect = texture.frame;
      rects.add(rect);

405 406 407 408
      // Transform
      double scos;
      double ssin;
      if (rotateToMovement) {
409
        double extraRotation = GameMath.atan2(particle.dir[1], particle.dir[0]);
410 411
        scos = math.cos(convertDegrees2Radians(particle.rotation) + extraRotation) * particle.size;
        ssin = math.sin(convertDegrees2Radians(particle.rotation) + extraRotation) * particle.size;
412
      } else if (particle.rotation != 0.0) {
413 414
        scos = math.cos(convertDegrees2Radians(particle.rotation)) * particle.size;
        ssin = math.sin(convertDegrees2Radians(particle.rotation)) * particle.size;
415 416 417
      } else {
        scos = particle.size;
        ssin = 0.0;
418
      }
419 420 421 422
      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;
423
      RSTransform transform = new RSTransform(scos, ssin, tx, ty);
424 425 426
      transforms.add(transform);

      // Color
427 428
      if (particle.simpleColorSequence != null) {
        Color particleColor = new Color.fromARGB(
429
          (particle.simpleColorSequence[0] * opacity).toInt().clamp(0, 255),
430 431 432 433
          particle.simpleColorSequence[1].toInt().clamp(0, 255),
          particle.simpleColorSequence[2].toInt().clamp(0, 255),
          particle.simpleColorSequence[3].toInt().clamp(0, 255));
        colors.add(particleColor);
434
      } else {
435 436 437 438 439 440
        Color particleColor;
        if (particle.colorSequence != null) {
          particleColor = particle.colorSequence.colorAtPosition(particle.colorPos);
        } else {
          particleColor = colorSequence.colorAtPosition(particle.colorPos);
        }
441 442 443
        if (opacity != 1.0) {
          particleColor = particleColor.withAlpha((particleColor.alpha * opacity).toInt().clamp(0, 255));
        }
444
        colors.add(particleColor);
445 446 447
      }
    }

448
    canvas.drawAtlas(texture.image, transforms, rects, colors,
449
      TransferMode.modulate, null, _paint);
450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479
  }
}

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;
480 481
  }
}