Home | History | Annotate | Download | only in g2d
      1 /*******************************************************************************
      2  * Copyright 2011 See AUTHORS file.
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *   http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  ******************************************************************************/
     16 
     17 package com.badlogic.gdx.graphics.g2d;
     18 
     19 import java.io.BufferedReader;
     20 import java.io.IOException;
     21 import java.io.Writer;
     22 
     23 import com.badlogic.gdx.graphics.GL20;
     24 import com.badlogic.gdx.graphics.Texture;
     25 import com.badlogic.gdx.math.MathUtils;
     26 import com.badlogic.gdx.math.Rectangle;
     27 import com.badlogic.gdx.math.collision.BoundingBox;
     28 
     29 public class ParticleEmitter {
     30 	static private final int UPDATE_SCALE = 1 << 0;
     31 	static private final int UPDATE_ANGLE = 1 << 1;
     32 	static private final int UPDATE_ROTATION = 1 << 2;
     33 	static private final int UPDATE_VELOCITY = 1 << 3;
     34 	static private final int UPDATE_WIND = 1 << 4;
     35 	static private final int UPDATE_GRAVITY = 1 << 5;
     36 	static private final int UPDATE_TINT = 1 << 6;
     37 
     38 	private RangedNumericValue delayValue = new RangedNumericValue();
     39 	private ScaledNumericValue lifeOffsetValue = new ScaledNumericValue();
     40 	private RangedNumericValue durationValue = new RangedNumericValue();
     41 	private ScaledNumericValue lifeValue = new ScaledNumericValue();
     42 	private ScaledNumericValue emissionValue = new ScaledNumericValue();
     43 	private ScaledNumericValue scaleValue = new ScaledNumericValue();
     44 	private ScaledNumericValue rotationValue = new ScaledNumericValue();
     45 	private ScaledNumericValue velocityValue = new ScaledNumericValue();
     46 	private ScaledNumericValue angleValue = new ScaledNumericValue();
     47 	private ScaledNumericValue windValue = new ScaledNumericValue();
     48 	private ScaledNumericValue gravityValue = new ScaledNumericValue();
     49 	private ScaledNumericValue transparencyValue = new ScaledNumericValue();
     50 	private GradientColorValue tintValue = new GradientColorValue();
     51 	private RangedNumericValue xOffsetValue = new ScaledNumericValue();
     52 	private RangedNumericValue yOffsetValue = new ScaledNumericValue();
     53 	private ScaledNumericValue spawnWidthValue = new ScaledNumericValue();
     54 	private ScaledNumericValue spawnHeightValue = new ScaledNumericValue();
     55 	private SpawnShapeValue spawnShapeValue = new SpawnShapeValue();
     56 
     57 	private float accumulator;
     58 	private Sprite sprite;
     59 	private Particle[] particles;
     60 	private int minParticleCount, maxParticleCount = 4;
     61 	private float x, y;
     62 	private String name;
     63 	private String imagePath;
     64 	private int activeCount;
     65 	private boolean[] active;
     66 	private boolean firstUpdate;
     67 	private boolean flipX, flipY;
     68 	private int updateFlags;
     69 	private boolean allowCompletion;
     70 	private BoundingBox bounds;
     71 
     72 	private int emission, emissionDiff, emissionDelta;
     73 	private int lifeOffset, lifeOffsetDiff;
     74 	private int life, lifeDiff;
     75 	private float spawnWidth, spawnWidthDiff;
     76 	private float spawnHeight, spawnHeightDiff;
     77 	public float duration = 1, durationTimer;
     78 	private float delay, delayTimer;
     79 
     80 	private boolean attached;
     81 	private boolean continuous;
     82 	private boolean aligned;
     83 	private boolean behind;
     84 	private boolean additive = true;
     85 	private boolean premultipliedAlpha = false;
     86 	boolean cleansUpBlendFunction = true;
     87 
     88 	public ParticleEmitter () {
     89 		initialize();
     90 	}
     91 
     92 	public ParticleEmitter (BufferedReader reader) throws IOException {
     93 		initialize();
     94 		load(reader);
     95 	}
     96 
     97 	public ParticleEmitter (ParticleEmitter emitter) {
     98 		sprite = emitter.sprite;
     99 		name = emitter.name;
    100 		imagePath = emitter.imagePath;
    101 		setMaxParticleCount(emitter.maxParticleCount);
    102 		minParticleCount = emitter.minParticleCount;
    103 		delayValue.load(emitter.delayValue);
    104 		durationValue.load(emitter.durationValue);
    105 		emissionValue.load(emitter.emissionValue);
    106 		lifeValue.load(emitter.lifeValue);
    107 		lifeOffsetValue.load(emitter.lifeOffsetValue);
    108 		scaleValue.load(emitter.scaleValue);
    109 		rotationValue.load(emitter.rotationValue);
    110 		velocityValue.load(emitter.velocityValue);
    111 		angleValue.load(emitter.angleValue);
    112 		windValue.load(emitter.windValue);
    113 		gravityValue.load(emitter.gravityValue);
    114 		transparencyValue.load(emitter.transparencyValue);
    115 		tintValue.load(emitter.tintValue);
    116 		xOffsetValue.load(emitter.xOffsetValue);
    117 		yOffsetValue.load(emitter.yOffsetValue);
    118 		spawnWidthValue.load(emitter.spawnWidthValue);
    119 		spawnHeightValue.load(emitter.spawnHeightValue);
    120 		spawnShapeValue.load(emitter.spawnShapeValue);
    121 		attached = emitter.attached;
    122 		continuous = emitter.continuous;
    123 		aligned = emitter.aligned;
    124 		behind = emitter.behind;
    125 		additive = emitter.additive;
    126 		premultipliedAlpha = emitter.premultipliedAlpha;
    127 		cleansUpBlendFunction = emitter.cleansUpBlendFunction;
    128 	}
    129 
    130 	private void initialize () {
    131 		durationValue.setAlwaysActive(true);
    132 		emissionValue.setAlwaysActive(true);
    133 		lifeValue.setAlwaysActive(true);
    134 		scaleValue.setAlwaysActive(true);
    135 		transparencyValue.setAlwaysActive(true);
    136 		spawnShapeValue.setAlwaysActive(true);
    137 		spawnWidthValue.setAlwaysActive(true);
    138 		spawnHeightValue.setAlwaysActive(true);
    139 	}
    140 
    141 	public void setMaxParticleCount (int maxParticleCount) {
    142 		this.maxParticleCount = maxParticleCount;
    143 		active = new boolean[maxParticleCount];
    144 		activeCount = 0;
    145 		particles = new Particle[maxParticleCount];
    146 	}
    147 
    148 	public void addParticle () {
    149 		int activeCount = this.activeCount;
    150 		if (activeCount == maxParticleCount) return;
    151 		boolean[] active = this.active;
    152 		for (int i = 0, n = active.length; i < n; i++) {
    153 			if (!active[i]) {
    154 				activateParticle(i);
    155 				active[i] = true;
    156 				this.activeCount = activeCount + 1;
    157 				break;
    158 			}
    159 		}
    160 	}
    161 
    162 	public void addParticles (int count) {
    163 		count = Math.min(count, maxParticleCount - activeCount);
    164 		if (count == 0) return;
    165 		boolean[] active = this.active;
    166 		int index = 0, n = active.length;
    167 		outer:
    168 		for (int i = 0; i < count; i++) {
    169 			for (; index < n; index++) {
    170 				if (!active[index]) {
    171 					activateParticle(index);
    172 					active[index++] = true;
    173 					continue outer;
    174 				}
    175 			}
    176 			break;
    177 		}
    178 		this.activeCount += count;
    179 	}
    180 
    181 	public void update (float delta) {
    182 		accumulator += delta * 1000;
    183 		if (accumulator < 1) return;
    184 		int deltaMillis = (int)accumulator;
    185 		accumulator -= deltaMillis;
    186 
    187 		if (delayTimer < delay) {
    188 			delayTimer += deltaMillis;
    189 		} else {
    190 			boolean done = false;
    191 			if (firstUpdate) {
    192 				firstUpdate = false;
    193 				addParticle();
    194 			}
    195 
    196 			if (durationTimer < duration)
    197 				durationTimer += deltaMillis;
    198 			else {
    199 				if (!continuous || allowCompletion)
    200 					done = true;
    201 				else
    202 					restart();
    203 			}
    204 
    205 			if (!done) {
    206 				emissionDelta += deltaMillis;
    207 				float emissionTime = emission + emissionDiff * emissionValue.getScale(durationTimer / (float)duration);
    208 				if (emissionTime > 0) {
    209 					emissionTime = 1000 / emissionTime;
    210 					if (emissionDelta >= emissionTime) {
    211 						int emitCount = (int)(emissionDelta / emissionTime);
    212 						emitCount = Math.min(emitCount, maxParticleCount - activeCount);
    213 						emissionDelta -= emitCount * emissionTime;
    214 						emissionDelta %= emissionTime;
    215 						addParticles(emitCount);
    216 					}
    217 				}
    218 				if (activeCount < minParticleCount) addParticles(minParticleCount - activeCount);
    219 			}
    220 		}
    221 
    222 		boolean[] active = this.active;
    223 		int activeCount = this.activeCount;
    224 		Particle[] particles = this.particles;
    225 		for (int i = 0, n = active.length; i < n; i++) {
    226 			if (active[i] && !updateParticle(particles[i], delta, deltaMillis)) {
    227 				active[i] = false;
    228 				activeCount--;
    229 			}
    230 		}
    231 		this.activeCount = activeCount;
    232 	}
    233 
    234 	public void draw (Batch batch) {
    235 		if (premultipliedAlpha) {
    236 			batch.setBlendFunction(GL20.GL_ONE, GL20.GL_ONE_MINUS_SRC_ALPHA);
    237 		} else if (additive) {
    238 			batch.setBlendFunction(GL20.GL_SRC_ALPHA, GL20.GL_ONE);
    239 		} else {
    240 			batch.setBlendFunction(GL20.GL_SRC_ALPHA, GL20.GL_ONE_MINUS_SRC_ALPHA);
    241 		}
    242 		Particle[] particles = this.particles;
    243 		boolean[] active = this.active;
    244 
    245 		for (int i = 0, n = active.length; i < n; i++) {
    246 			if (active[i]) particles[i].draw(batch);
    247 		}
    248 
    249 		if (cleansUpBlendFunction && (additive || premultipliedAlpha))
    250 			batch.setBlendFunction(GL20.GL_SRC_ALPHA, GL20.GL_ONE_MINUS_SRC_ALPHA);
    251 
    252 	}
    253 
    254 	/** Updates and draws the particles. This is slightly more efficient than calling {@link #update(float)} and
    255 	 * {@link #draw(Batch)} separately. */
    256 	public void draw (Batch batch, float delta) {
    257 		accumulator += delta * 1000;
    258 		if (accumulator < 1) {
    259 			draw(batch);
    260 			return;
    261 		}
    262 		int deltaMillis = (int)accumulator;
    263 		accumulator -= deltaMillis;
    264 
    265 		if (premultipliedAlpha) {
    266 			batch.setBlendFunction(GL20.GL_ONE, GL20.GL_ONE_MINUS_SRC_ALPHA);
    267 		} else if (additive) {
    268 			batch.setBlendFunction(GL20.GL_SRC_ALPHA, GL20.GL_ONE);
    269 		} else {
    270 			batch.setBlendFunction(GL20.GL_SRC_ALPHA, GL20.GL_ONE_MINUS_SRC_ALPHA);
    271 		}
    272 
    273 		Particle[] particles = this.particles;
    274 		boolean[] active = this.active;
    275 		int activeCount = this.activeCount;
    276 		for (int i = 0, n = active.length; i < n; i++) {
    277 			if (active[i]) {
    278 				Particle particle = particles[i];
    279 				if (updateParticle(particle, delta, deltaMillis))
    280 					particle.draw(batch);
    281 				else {
    282 					active[i] = false;
    283 					activeCount--;
    284 				}
    285 			}
    286 		}
    287 		this.activeCount = activeCount;
    288 
    289 		if (cleansUpBlendFunction && (additive || premultipliedAlpha))
    290 			batch.setBlendFunction(GL20.GL_SRC_ALPHA, GL20.GL_ONE_MINUS_SRC_ALPHA);
    291 
    292 		if (delayTimer < delay) {
    293 			delayTimer += deltaMillis;
    294 			return;
    295 		}
    296 
    297 		if (firstUpdate) {
    298 			firstUpdate = false;
    299 			addParticle();
    300 		}
    301 
    302 		if (durationTimer < duration)
    303 			durationTimer += deltaMillis;
    304 		else {
    305 			if (!continuous || allowCompletion) return;
    306 			restart();
    307 		}
    308 
    309 		emissionDelta += deltaMillis;
    310 		float emissionTime = emission + emissionDiff * emissionValue.getScale(durationTimer / (float)duration);
    311 		if (emissionTime > 0) {
    312 			emissionTime = 1000 / emissionTime;
    313 			if (emissionDelta >= emissionTime) {
    314 				int emitCount = (int)(emissionDelta / emissionTime);
    315 				emitCount = Math.min(emitCount, maxParticleCount - activeCount);
    316 				emissionDelta -= emitCount * emissionTime;
    317 				emissionDelta %= emissionTime;
    318 				addParticles(emitCount);
    319 			}
    320 		}
    321 		if (activeCount < minParticleCount) addParticles(minParticleCount - activeCount);
    322 	}
    323 
    324 	public void start () {
    325 		firstUpdate = true;
    326 		allowCompletion = false;
    327 		restart();
    328 	}
    329 
    330 	public void reset () {
    331 		emissionDelta = 0;
    332 		durationTimer = duration;
    333 		boolean[] active = this.active;
    334 		for (int i = 0, n = active.length; i < n; i++)
    335 			active[i] = false;
    336 		activeCount = 0;
    337 		start();
    338 	}
    339 
    340 	private void restart () {
    341 		delay = delayValue.active ? delayValue.newLowValue() : 0;
    342 		delayTimer = 0;
    343 
    344 		durationTimer -= duration;
    345 		duration = durationValue.newLowValue();
    346 
    347 		emission = (int)emissionValue.newLowValue();
    348 		emissionDiff = (int)emissionValue.newHighValue();
    349 		if (!emissionValue.isRelative()) emissionDiff -= emission;
    350 
    351 		life = (int)lifeValue.newLowValue();
    352 		lifeDiff = (int)lifeValue.newHighValue();
    353 		if (!lifeValue.isRelative()) lifeDiff -= life;
    354 
    355 		lifeOffset = lifeOffsetValue.active ? (int)lifeOffsetValue.newLowValue() : 0;
    356 		lifeOffsetDiff = (int)lifeOffsetValue.newHighValue();
    357 		if (!lifeOffsetValue.isRelative()) lifeOffsetDiff -= lifeOffset;
    358 
    359 		spawnWidth = spawnWidthValue.newLowValue();
    360 		spawnWidthDiff = spawnWidthValue.newHighValue();
    361 		if (!spawnWidthValue.isRelative()) spawnWidthDiff -= spawnWidth;
    362 
    363 		spawnHeight = spawnHeightValue.newLowValue();
    364 		spawnHeightDiff = spawnHeightValue.newHighValue();
    365 		if (!spawnHeightValue.isRelative()) spawnHeightDiff -= spawnHeight;
    366 
    367 		updateFlags = 0;
    368 		if (angleValue.active && angleValue.timeline.length > 1) updateFlags |= UPDATE_ANGLE;
    369 		if (velocityValue.active) updateFlags |= UPDATE_VELOCITY;
    370 		if (scaleValue.timeline.length > 1) updateFlags |= UPDATE_SCALE;
    371 		if (rotationValue.active && rotationValue.timeline.length > 1) updateFlags |= UPDATE_ROTATION;
    372 		if (windValue.active) updateFlags |= UPDATE_WIND;
    373 		if (gravityValue.active) updateFlags |= UPDATE_GRAVITY;
    374 		if (tintValue.timeline.length > 1) updateFlags |= UPDATE_TINT;
    375 	}
    376 
    377 	protected Particle newParticle (Sprite sprite) {
    378 		return new Particle(sprite);
    379 	}
    380 
    381 	private void activateParticle (int index) {
    382 		Particle particle = particles[index];
    383 		if (particle == null) {
    384 			particles[index] = particle = newParticle(sprite);
    385 			particle.flip(flipX, flipY);
    386 		}
    387 
    388 		float percent = durationTimer / (float)duration;
    389 		int updateFlags = this.updateFlags;
    390 
    391 		particle.currentLife = particle.life = life + (int)(lifeDiff * lifeValue.getScale(percent));
    392 
    393 		if (velocityValue.active) {
    394 			particle.velocity = velocityValue.newLowValue();
    395 			particle.velocityDiff = velocityValue.newHighValue();
    396 			if (!velocityValue.isRelative()) particle.velocityDiff -= particle.velocity;
    397 		}
    398 
    399 		particle.angle = angleValue.newLowValue();
    400 		particle.angleDiff = angleValue.newHighValue();
    401 		if (!angleValue.isRelative()) particle.angleDiff -= particle.angle;
    402 		float angle = 0;
    403 		if ((updateFlags & UPDATE_ANGLE) == 0) {
    404 			angle = particle.angle + particle.angleDiff * angleValue.getScale(0);
    405 			particle.angle = angle;
    406 			particle.angleCos = MathUtils.cosDeg(angle);
    407 			particle.angleSin = MathUtils.sinDeg(angle);
    408 		}
    409 
    410 		float spriteWidth = sprite.getWidth();
    411 		particle.scale = scaleValue.newLowValue() / spriteWidth;
    412 		particle.scaleDiff = scaleValue.newHighValue() / spriteWidth;
    413 		if (!scaleValue.isRelative()) particle.scaleDiff -= particle.scale;
    414 		particle.setScale(particle.scale + particle.scaleDiff * scaleValue.getScale(0));
    415 
    416 		if (rotationValue.active) {
    417 			particle.rotation = rotationValue.newLowValue();
    418 			particle.rotationDiff = rotationValue.newHighValue();
    419 			if (!rotationValue.isRelative()) particle.rotationDiff -= particle.rotation;
    420 			float rotation = particle.rotation + particle.rotationDiff * rotationValue.getScale(0);
    421 			if (aligned) rotation += angle;
    422 			particle.setRotation(rotation);
    423 		}
    424 
    425 		if (windValue.active) {
    426 			particle.wind = windValue.newLowValue();
    427 			particle.windDiff = windValue.newHighValue();
    428 			if (!windValue.isRelative()) particle.windDiff -= particle.wind;
    429 		}
    430 
    431 		if (gravityValue.active) {
    432 			particle.gravity = gravityValue.newLowValue();
    433 			particle.gravityDiff = gravityValue.newHighValue();
    434 			if (!gravityValue.isRelative()) particle.gravityDiff -= particle.gravity;
    435 		}
    436 
    437 		float[] color = particle.tint;
    438 		if (color == null) particle.tint = color = new float[3];
    439 		float[] temp = tintValue.getColor(0);
    440 		color[0] = temp[0];
    441 		color[1] = temp[1];
    442 		color[2] = temp[2];
    443 
    444 		particle.transparency = transparencyValue.newLowValue();
    445 		particle.transparencyDiff = transparencyValue.newHighValue() - particle.transparency;
    446 
    447 		// Spawn.
    448 		float x = this.x;
    449 		if (xOffsetValue.active) x += xOffsetValue.newLowValue();
    450 		float y = this.y;
    451 		if (yOffsetValue.active) y += yOffsetValue.newLowValue();
    452 		switch (spawnShapeValue.shape) {
    453 		case square: {
    454 			float width = spawnWidth + (spawnWidthDiff * spawnWidthValue.getScale(percent));
    455 			float height = spawnHeight + (spawnHeightDiff * spawnHeightValue.getScale(percent));
    456 			x += MathUtils.random(width) - width / 2;
    457 			y += MathUtils.random(height) - height / 2;
    458 			break;
    459 		}
    460 		case ellipse: {
    461 			float width = spawnWidth + (spawnWidthDiff * spawnWidthValue.getScale(percent));
    462 			float height = spawnHeight + (spawnHeightDiff * spawnHeightValue.getScale(percent));
    463 			float radiusX = width / 2;
    464 			float radiusY = height / 2;
    465 			if (radiusX == 0 || radiusY == 0) break;
    466 			float scaleY = radiusX / (float)radiusY;
    467 			if (spawnShapeValue.edges) {
    468 				float spawnAngle;
    469 				switch (spawnShapeValue.side) {
    470 				case top:
    471 					spawnAngle = -MathUtils.random(179f);
    472 					break;
    473 				case bottom:
    474 					spawnAngle = MathUtils.random(179f);
    475 					break;
    476 				default:
    477 					spawnAngle = MathUtils.random(360f);
    478 					break;
    479 				}
    480 				float cosDeg = MathUtils.cosDeg(spawnAngle);
    481 				float sinDeg = MathUtils.sinDeg(spawnAngle);
    482 				x += cosDeg * radiusX;
    483 				y += sinDeg * radiusX / scaleY;
    484 				if ((updateFlags & UPDATE_ANGLE) == 0) {
    485 					particle.angle = spawnAngle;
    486 					particle.angleCos = cosDeg;
    487 					particle.angleSin = sinDeg;
    488 				}
    489 			} else {
    490 				float radius2 = radiusX * radiusX;
    491 				while (true) {
    492 					float px = MathUtils.random(width) - radiusX;
    493 					float py = MathUtils.random(height) - radiusY;
    494 					if (px * px + py * py <= radius2) {
    495 						x += px;
    496 						y += py / scaleY;
    497 						break;
    498 					}
    499 				}
    500 			}
    501 			break;
    502 		}
    503 		case line: {
    504 			float width = spawnWidth + (spawnWidthDiff * spawnWidthValue.getScale(percent));
    505 			float height = spawnHeight + (spawnHeightDiff * spawnHeightValue.getScale(percent));
    506 			if (width != 0) {
    507 				float lineX = width * MathUtils.random();
    508 				x += lineX;
    509 				y += lineX * (height / (float)width);
    510 			} else
    511 				y += height * MathUtils.random();
    512 			break;
    513 		}
    514 		}
    515 
    516 		float spriteHeight = sprite.getHeight();
    517 		particle.setBounds(x - spriteWidth / 2, y - spriteHeight / 2, spriteWidth, spriteHeight);
    518 
    519 		int offsetTime = (int)(lifeOffset + lifeOffsetDiff * lifeOffsetValue.getScale(percent));
    520 		if (offsetTime > 0) {
    521 			if (offsetTime >= particle.currentLife) offsetTime = particle.currentLife - 1;
    522 			updateParticle(particle, offsetTime / 1000f, offsetTime);
    523 		}
    524 	}
    525 
    526 	private boolean updateParticle (Particle particle, float delta, int deltaMillis) {
    527 		int life = particle.currentLife - deltaMillis;
    528 		if (life <= 0) return false;
    529 		particle.currentLife = life;
    530 
    531 		float percent = 1 - particle.currentLife / (float)particle.life;
    532 		int updateFlags = this.updateFlags;
    533 
    534 		if ((updateFlags & UPDATE_SCALE) != 0)
    535 			particle.setScale(particle.scale + particle.scaleDiff * scaleValue.getScale(percent));
    536 
    537 		if ((updateFlags & UPDATE_VELOCITY) != 0) {
    538 			float velocity = (particle.velocity + particle.velocityDiff * velocityValue.getScale(percent)) * delta;
    539 
    540 			float velocityX, velocityY;
    541 			if ((updateFlags & UPDATE_ANGLE) != 0) {
    542 				float angle = particle.angle + particle.angleDiff * angleValue.getScale(percent);
    543 				velocityX = velocity * MathUtils.cosDeg(angle);
    544 				velocityY = velocity * MathUtils.sinDeg(angle);
    545 				if ((updateFlags & UPDATE_ROTATION) != 0) {
    546 					float rotation = particle.rotation + particle.rotationDiff * rotationValue.getScale(percent);
    547 					if (aligned) rotation += angle;
    548 					particle.setRotation(rotation);
    549 				}
    550 			} else {
    551 				velocityX = velocity * particle.angleCos;
    552 				velocityY = velocity * particle.angleSin;
    553 				if (aligned || (updateFlags & UPDATE_ROTATION) != 0) {
    554 					float rotation = particle.rotation + particle.rotationDiff * rotationValue.getScale(percent);
    555 					if (aligned) rotation += particle.angle;
    556 					particle.setRotation(rotation);
    557 				}
    558 			}
    559 
    560 			if ((updateFlags & UPDATE_WIND) != 0)
    561 				velocityX += (particle.wind + particle.windDiff * windValue.getScale(percent)) * delta;
    562 
    563 			if ((updateFlags & UPDATE_GRAVITY) != 0)
    564 				velocityY += (particle.gravity + particle.gravityDiff * gravityValue.getScale(percent)) * delta;
    565 
    566 			particle.translate(velocityX, velocityY);
    567 		} else {
    568 			if ((updateFlags & UPDATE_ROTATION) != 0)
    569 				particle.setRotation(particle.rotation + particle.rotationDiff * rotationValue.getScale(percent));
    570 		}
    571 
    572 		float[] color;
    573 		if ((updateFlags & UPDATE_TINT) != 0)
    574 			color = tintValue.getColor(percent);
    575 		else
    576 			color = particle.tint;
    577 
    578 		if (premultipliedAlpha) {
    579 			float alphaMultiplier = additive ? 0 : 1;
    580 			float a = particle.transparency + particle.transparencyDiff * transparencyValue.getScale(percent);
    581 			particle.setColor(color[0] * a, color[1] * a, color[2] * a, a * alphaMultiplier);
    582 		} else {
    583 			particle.setColor(color[0], color[1], color[2],
    584 				particle.transparency + particle.transparencyDiff * transparencyValue.getScale(percent));
    585 		}
    586 		return true;
    587 	}
    588 
    589 	public void setPosition (float x, float y) {
    590 		if (attached) {
    591 			float xAmount = x - this.x;
    592 			float yAmount = y - this.y;
    593 			boolean[] active = this.active;
    594 			for (int i = 0, n = active.length; i < n; i++)
    595 				if (active[i]) particles[i].translate(xAmount, yAmount);
    596 		}
    597 		this.x = x;
    598 		this.y = y;
    599 	}
    600 
    601 	public void setSprite (Sprite sprite) {
    602 		this.sprite = sprite;
    603 		if (sprite == null) return;
    604 		float originX = sprite.getOriginX();
    605 		float originY = sprite.getOriginY();
    606 		Texture texture = sprite.getTexture();
    607 		for (int i = 0, n = particles.length; i < n; i++) {
    608 			Particle particle = particles[i];
    609 			if (particle == null) break;
    610 			particle.setTexture(texture);
    611 			particle.setOrigin(originX, originY);
    612 		}
    613 	}
    614 
    615 	/** Ignores the {@link #setContinuous(boolean) continuous} setting until the emitter is started again. This allows the emitter
    616 	 * to stop smoothly. */
    617 	public void allowCompletion () {
    618 		allowCompletion = true;
    619 		durationTimer = duration;
    620 	}
    621 
    622 	public Sprite getSprite () {
    623 		return sprite;
    624 	}
    625 
    626 	public String getName () {
    627 		return name;
    628 	}
    629 
    630 	public void setName (String name) {
    631 		this.name = name;
    632 	}
    633 
    634 	public ScaledNumericValue getLife () {
    635 		return lifeValue;
    636 	}
    637 
    638 	public ScaledNumericValue getScale () {
    639 		return scaleValue;
    640 	}
    641 
    642 	public ScaledNumericValue getRotation () {
    643 		return rotationValue;
    644 	}
    645 
    646 	public GradientColorValue getTint () {
    647 		return tintValue;
    648 	}
    649 
    650 	public ScaledNumericValue getVelocity () {
    651 		return velocityValue;
    652 	}
    653 
    654 	public ScaledNumericValue getWind () {
    655 		return windValue;
    656 	}
    657 
    658 	public ScaledNumericValue getGravity () {
    659 		return gravityValue;
    660 	}
    661 
    662 	public ScaledNumericValue getAngle () {
    663 		return angleValue;
    664 	}
    665 
    666 	public ScaledNumericValue getEmission () {
    667 		return emissionValue;
    668 	}
    669 
    670 	public ScaledNumericValue getTransparency () {
    671 		return transparencyValue;
    672 	}
    673 
    674 	public RangedNumericValue getDuration () {
    675 		return durationValue;
    676 	}
    677 
    678 	public RangedNumericValue getDelay () {
    679 		return delayValue;
    680 	}
    681 
    682 	public ScaledNumericValue getLifeOffset () {
    683 		return lifeOffsetValue;
    684 	}
    685 
    686 	public RangedNumericValue getXOffsetValue () {
    687 		return xOffsetValue;
    688 	}
    689 
    690 	public RangedNumericValue getYOffsetValue () {
    691 		return yOffsetValue;
    692 	}
    693 
    694 	public ScaledNumericValue getSpawnWidth () {
    695 		return spawnWidthValue;
    696 	}
    697 
    698 	public ScaledNumericValue getSpawnHeight () {
    699 		return spawnHeightValue;
    700 	}
    701 
    702 	public SpawnShapeValue getSpawnShape () {
    703 		return spawnShapeValue;
    704 	}
    705 
    706 	public boolean isAttached () {
    707 		return attached;
    708 	}
    709 
    710 	public void setAttached (boolean attached) {
    711 		this.attached = attached;
    712 	}
    713 
    714 	public boolean isContinuous () {
    715 		return continuous;
    716 	}
    717 
    718 	public void setContinuous (boolean continuous) {
    719 		this.continuous = continuous;
    720 	}
    721 
    722 	public boolean isAligned () {
    723 		return aligned;
    724 	}
    725 
    726 	public void setAligned (boolean aligned) {
    727 		this.aligned = aligned;
    728 	}
    729 
    730 	public boolean isAdditive () {
    731 		return additive;
    732 	}
    733 
    734 	public void setAdditive (boolean additive) {
    735 		this.additive = additive;
    736 	}
    737 
    738 	/** @return Whether this ParticleEmitter automatically returns the {@link com.badlogic.gdx.graphics.g2d.Batch Batch}'s blend
    739 	 *         function to the alpha-blending default (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) when done drawing. */
    740 	public boolean cleansUpBlendFunction () {
    741 		return cleansUpBlendFunction;
    742 	}
    743 
    744 	/** Set whether to automatically return the {@link com.badlogic.gdx.graphics.g2d.Batch Batch}'s blend function to the
    745 	 * alpha-blending default (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) when done drawing. Is true by default. If set to false, the
    746 	 * Batch's blend function is left as it was for drawing this ParticleEmitter, which prevents the Batch from being flushed
    747 	 * repeatedly if consecutive ParticleEmitters with the same additive or pre-multiplied alpha state are drawn in a row.
    748 	 * <p>
    749 	 * IMPORTANT: If set to false and if the next object to use this Batch expects alpha blending, you are responsible for setting
    750 	 * the Batch's blend function to (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) before that next object is drawn.
    751 	 * @param cleansUpBlendFunction */
    752 	public void setCleansUpBlendFunction (boolean cleansUpBlendFunction) {
    753 		this.cleansUpBlendFunction = cleansUpBlendFunction;
    754 	}
    755 
    756 	public boolean isBehind () {
    757 		return behind;
    758 	}
    759 
    760 	public void setBehind (boolean behind) {
    761 		this.behind = behind;
    762 	}
    763 
    764 	public boolean isPremultipliedAlpha () {
    765 		return premultipliedAlpha;
    766 	}
    767 
    768 	public void setPremultipliedAlpha (boolean premultipliedAlpha) {
    769 		this.premultipliedAlpha = premultipliedAlpha;
    770 	}
    771 
    772 	public int getMinParticleCount () {
    773 		return minParticleCount;
    774 	}
    775 
    776 	public void setMinParticleCount (int minParticleCount) {
    777 		this.minParticleCount = minParticleCount;
    778 	}
    779 
    780 	public int getMaxParticleCount () {
    781 		return maxParticleCount;
    782 	}
    783 
    784 	public boolean isComplete () {
    785 		if (continuous) return false;
    786 		if (delayTimer < delay) return false;
    787 		return durationTimer >= duration && activeCount == 0;
    788 	}
    789 
    790 	public float getPercentComplete () {
    791 		if (delayTimer < delay) return 0;
    792 		return Math.min(1, durationTimer / (float)duration);
    793 	}
    794 
    795 	public float getX () {
    796 		return x;
    797 	}
    798 
    799 	public float getY () {
    800 		return y;
    801 	}
    802 
    803 	public int getActiveCount () {
    804 		return activeCount;
    805 	}
    806 
    807 	public String getImagePath () {
    808 		return imagePath;
    809 	}
    810 
    811 	public void setImagePath (String imagePath) {
    812 		this.imagePath = imagePath;
    813 	}
    814 
    815 	public void setFlip (boolean flipX, boolean flipY) {
    816 		this.flipX = flipX;
    817 		this.flipY = flipY;
    818 		if (particles == null) return;
    819 		for (int i = 0, n = particles.length; i < n; i++) {
    820 			Particle particle = particles[i];
    821 			if (particle != null) particle.flip(flipX, flipY);
    822 		}
    823 	}
    824 
    825 	public void flipY () {
    826 		angleValue.setHigh(-angleValue.getHighMin(), -angleValue.getHighMax());
    827 		angleValue.setLow(-angleValue.getLowMin(), -angleValue.getLowMax());
    828 
    829 		gravityValue.setHigh(-gravityValue.getHighMin(), -gravityValue.getHighMax());
    830 		gravityValue.setLow(-gravityValue.getLowMin(), -gravityValue.getLowMax());
    831 
    832 		windValue.setHigh(-windValue.getHighMin(), -windValue.getHighMax());
    833 		windValue.setLow(-windValue.getLowMin(), -windValue.getLowMax());
    834 
    835 		rotationValue.setHigh(-rotationValue.getHighMin(), -rotationValue.getHighMax());
    836 		rotationValue.setLow(-rotationValue.getLowMin(), -rotationValue.getLowMax());
    837 
    838 		yOffsetValue.setLow(-yOffsetValue.getLowMin(), -yOffsetValue.getLowMax());
    839 	}
    840 
    841 	/** Returns the bounding box for all active particles. z axis will always be zero. */
    842 	public BoundingBox getBoundingBox () {
    843 		if (bounds == null) bounds = new BoundingBox();
    844 
    845 		Particle[] particles = this.particles;
    846 		boolean[] active = this.active;
    847 		BoundingBox bounds = this.bounds;
    848 
    849 		bounds.inf();
    850 		for (int i = 0, n = active.length; i < n; i++)
    851 			if (active[i]) {
    852 				Rectangle r = particles[i].getBoundingRectangle();
    853 				bounds.ext(r.x, r.y, 0);
    854 				bounds.ext(r.x + r.width, r.y + r.height, 0);
    855 			}
    856 
    857 		return bounds;
    858 	}
    859 
    860 	public void save (Writer output) throws IOException {
    861 		output.write(name + "\n");
    862 		output.write("- Delay -\n");
    863 		delayValue.save(output);
    864 		output.write("- Duration - \n");
    865 		durationValue.save(output);
    866 		output.write("- Count - \n");
    867 		output.write("min: " + minParticleCount + "\n");
    868 		output.write("max: " + maxParticleCount + "\n");
    869 		output.write("- Emission - \n");
    870 		emissionValue.save(output);
    871 		output.write("- Life - \n");
    872 		lifeValue.save(output);
    873 		output.write("- Life Offset - \n");
    874 		lifeOffsetValue.save(output);
    875 		output.write("- X Offset - \n");
    876 		xOffsetValue.save(output);
    877 		output.write("- Y Offset - \n");
    878 		yOffsetValue.save(output);
    879 		output.write("- Spawn Shape - \n");
    880 		spawnShapeValue.save(output);
    881 		output.write("- Spawn Width - \n");
    882 		spawnWidthValue.save(output);
    883 		output.write("- Spawn Height - \n");
    884 		spawnHeightValue.save(output);
    885 		output.write("- Scale - \n");
    886 		scaleValue.save(output);
    887 		output.write("- Velocity - \n");
    888 		velocityValue.save(output);
    889 		output.write("- Angle - \n");
    890 		angleValue.save(output);
    891 		output.write("- Rotation - \n");
    892 		rotationValue.save(output);
    893 		output.write("- Wind - \n");
    894 		windValue.save(output);
    895 		output.write("- Gravity - \n");
    896 		gravityValue.save(output);
    897 		output.write("- Tint - \n");
    898 		tintValue.save(output);
    899 		output.write("- Transparency - \n");
    900 		transparencyValue.save(output);
    901 		output.write("- Options - \n");
    902 		output.write("attached: " + attached + "\n");
    903 		output.write("continuous: " + continuous + "\n");
    904 		output.write("aligned: " + aligned + "\n");
    905 		output.write("additive: " + additive + "\n");
    906 		output.write("behind: " + behind + "\n");
    907 		output.write("premultipliedAlpha: " + premultipliedAlpha + "\n");
    908 		output.write("- Image Path -\n");
    909 		output.write(imagePath + "\n");
    910 	}
    911 
    912 	public void load (BufferedReader reader) throws IOException {
    913 		try {
    914 			name = readString(reader, "name");
    915 			reader.readLine();
    916 			delayValue.load(reader);
    917 			reader.readLine();
    918 			durationValue.load(reader);
    919 			reader.readLine();
    920 			setMinParticleCount(readInt(reader, "minParticleCount"));
    921 			setMaxParticleCount(readInt(reader, "maxParticleCount"));
    922 			reader.readLine();
    923 			emissionValue.load(reader);
    924 			reader.readLine();
    925 			lifeValue.load(reader);
    926 			reader.readLine();
    927 			lifeOffsetValue.load(reader);
    928 			reader.readLine();
    929 			xOffsetValue.load(reader);
    930 			reader.readLine();
    931 			yOffsetValue.load(reader);
    932 			reader.readLine();
    933 			spawnShapeValue.load(reader);
    934 			reader.readLine();
    935 			spawnWidthValue.load(reader);
    936 			reader.readLine();
    937 			spawnHeightValue.load(reader);
    938 			reader.readLine();
    939 			scaleValue.load(reader);
    940 			reader.readLine();
    941 			velocityValue.load(reader);
    942 			reader.readLine();
    943 			angleValue.load(reader);
    944 			reader.readLine();
    945 			rotationValue.load(reader);
    946 			reader.readLine();
    947 			windValue.load(reader);
    948 			reader.readLine();
    949 			gravityValue.load(reader);
    950 			reader.readLine();
    951 			tintValue.load(reader);
    952 			reader.readLine();
    953 			transparencyValue.load(reader);
    954 			reader.readLine();
    955 			attached = readBoolean(reader, "attached");
    956 			continuous = readBoolean(reader, "continuous");
    957 			aligned = readBoolean(reader, "aligned");
    958 			additive = readBoolean(reader, "additive");
    959 			behind = readBoolean(reader, "behind");
    960 
    961 			// Backwards compatibility
    962 			String line = reader.readLine();
    963 			if (line.startsWith("premultipliedAlpha")) {
    964 				premultipliedAlpha = readBoolean(line);
    965 				reader.readLine();
    966 			}
    967 			setImagePath(reader.readLine());
    968 		} catch (RuntimeException ex) {
    969 			if (name == null) throw ex;
    970 			throw new RuntimeException("Error parsing emitter: " + name, ex);
    971 		}
    972 	}
    973 
    974 	static String readString (String line) throws IOException {
    975 		return line.substring(line.indexOf(":") + 1).trim();
    976 	}
    977 
    978 	static String readString (BufferedReader reader, String name) throws IOException {
    979 		String line = reader.readLine();
    980 		if (line == null) throw new IOException("Missing value: " + name);
    981 		return readString(line);
    982 	}
    983 
    984 	static boolean readBoolean (String line) throws IOException {
    985 		return Boolean.parseBoolean(readString(line));
    986 	}
    987 
    988 	static boolean readBoolean (BufferedReader reader, String name) throws IOException {
    989 		return Boolean.parseBoolean(readString(reader, name));
    990 	}
    991 
    992 	static int readInt (BufferedReader reader, String name) throws IOException {
    993 		return Integer.parseInt(readString(reader, name));
    994 	}
    995 
    996 	static float readFloat (BufferedReader reader, String name) throws IOException {
    997 		return Float.parseFloat(readString(reader, name));
    998 	}
    999 
   1000 	public static class Particle extends Sprite {
   1001 		protected int life, currentLife;
   1002 		protected float scale, scaleDiff;
   1003 		protected float rotation, rotationDiff;
   1004 		protected float velocity, velocityDiff;
   1005 		protected float angle, angleDiff;
   1006 		protected float angleCos, angleSin;
   1007 		protected float transparency, transparencyDiff;
   1008 		protected float wind, windDiff;
   1009 		protected float gravity, gravityDiff;
   1010 		protected float[] tint;
   1011 
   1012 		public Particle (Sprite sprite) {
   1013 			super(sprite);
   1014 		}
   1015 	}
   1016 
   1017 	static public class ParticleValue {
   1018 		boolean active;
   1019 		boolean alwaysActive;
   1020 
   1021 		public void setAlwaysActive (boolean alwaysActive) {
   1022 			this.alwaysActive = alwaysActive;
   1023 		}
   1024 
   1025 		public boolean isAlwaysActive () {
   1026 			return alwaysActive;
   1027 		}
   1028 
   1029 		public boolean isActive () {
   1030 			return alwaysActive || active;
   1031 		}
   1032 
   1033 		public void setActive (boolean active) {
   1034 			this.active = active;
   1035 		}
   1036 
   1037 		public void save (Writer output) throws IOException {
   1038 			if (!alwaysActive)
   1039 				output.write("active: " + active + "\n");
   1040 			else
   1041 				active = true;
   1042 		}
   1043 
   1044 		public void load (BufferedReader reader) throws IOException {
   1045 			if (!alwaysActive)
   1046 				active = readBoolean(reader, "active");
   1047 			else
   1048 				active = true;
   1049 		}
   1050 
   1051 		public void load (ParticleValue value) {
   1052 			active = value.active;
   1053 			alwaysActive = value.alwaysActive;
   1054 		}
   1055 	}
   1056 
   1057 	static public class NumericValue extends ParticleValue {
   1058 		private float value;
   1059 
   1060 		public float getValue () {
   1061 			return value;
   1062 		}
   1063 
   1064 		public void setValue (float value) {
   1065 			this.value = value;
   1066 		}
   1067 
   1068 		public void save (Writer output) throws IOException {
   1069 			super.save(output);
   1070 			if (!active) return;
   1071 			output.write("value: " + value + "\n");
   1072 		}
   1073 
   1074 		public void load (BufferedReader reader) throws IOException {
   1075 			super.load(reader);
   1076 			if (!active) return;
   1077 			value = readFloat(reader, "value");
   1078 		}
   1079 
   1080 		public void load (NumericValue value) {
   1081 			super.load(value);
   1082 			this.value = value.value;
   1083 		}
   1084 	}
   1085 
   1086 	static public class RangedNumericValue extends ParticleValue {
   1087 		private float lowMin, lowMax;
   1088 
   1089 		public float newLowValue () {
   1090 			return lowMin + (lowMax - lowMin) * MathUtils.random();
   1091 		}
   1092 
   1093 		public void setLow (float value) {
   1094 			lowMin = value;
   1095 			lowMax = value;
   1096 		}
   1097 
   1098 		public void setLow (float min, float max) {
   1099 			lowMin = min;
   1100 			lowMax = max;
   1101 		}
   1102 
   1103 		public float getLowMin () {
   1104 			return lowMin;
   1105 		}
   1106 
   1107 		public void setLowMin (float lowMin) {
   1108 			this.lowMin = lowMin;
   1109 		}
   1110 
   1111 		public float getLowMax () {
   1112 			return lowMax;
   1113 		}
   1114 
   1115 		public void setLowMax (float lowMax) {
   1116 			this.lowMax = lowMax;
   1117 		}
   1118 
   1119 		public void save (Writer output) throws IOException {
   1120 			super.save(output);
   1121 			if (!active) return;
   1122 			output.write("lowMin: " + lowMin + "\n");
   1123 			output.write("lowMax: " + lowMax + "\n");
   1124 		}
   1125 
   1126 		public void load (BufferedReader reader) throws IOException {
   1127 			super.load(reader);
   1128 			if (!active) return;
   1129 			lowMin = readFloat(reader, "lowMin");
   1130 			lowMax = readFloat(reader, "lowMax");
   1131 		}
   1132 
   1133 		public void load (RangedNumericValue value) {
   1134 			super.load(value);
   1135 			lowMax = value.lowMax;
   1136 			lowMin = value.lowMin;
   1137 		}
   1138 	}
   1139 
   1140 	static public class ScaledNumericValue extends RangedNumericValue {
   1141 		private float[] scaling = {1};
   1142 		float[] timeline = {0};
   1143 		private float highMin, highMax;
   1144 		private boolean relative;
   1145 
   1146 		public float newHighValue () {
   1147 			return highMin + (highMax - highMin) * MathUtils.random();
   1148 		}
   1149 
   1150 		public void setHigh (float value) {
   1151 			highMin = value;
   1152 			highMax = value;
   1153 		}
   1154 
   1155 		public void setHigh (float min, float max) {
   1156 			highMin = min;
   1157 			highMax = max;
   1158 		}
   1159 
   1160 		public float getHighMin () {
   1161 			return highMin;
   1162 		}
   1163 
   1164 		public void setHighMin (float highMin) {
   1165 			this.highMin = highMin;
   1166 		}
   1167 
   1168 		public float getHighMax () {
   1169 			return highMax;
   1170 		}
   1171 
   1172 		public void setHighMax (float highMax) {
   1173 			this.highMax = highMax;
   1174 		}
   1175 
   1176 		public float[] getScaling () {
   1177 			return scaling;
   1178 		}
   1179 
   1180 		public void setScaling (float[] values) {
   1181 			this.scaling = values;
   1182 		}
   1183 
   1184 		public float[] getTimeline () {
   1185 			return timeline;
   1186 		}
   1187 
   1188 		public void setTimeline (float[] timeline) {
   1189 			this.timeline = timeline;
   1190 		}
   1191 
   1192 		public boolean isRelative () {
   1193 			return relative;
   1194 		}
   1195 
   1196 		public void setRelative (boolean relative) {
   1197 			this.relative = relative;
   1198 		}
   1199 
   1200 		public float getScale (float percent) {
   1201 			int endIndex = -1;
   1202 			float[] timeline = this.timeline;
   1203 			int n = timeline.length;
   1204 			for (int i = 1; i < n; i++) {
   1205 				float t = timeline[i];
   1206 				if (t > percent) {
   1207 					endIndex = i;
   1208 					break;
   1209 				}
   1210 			}
   1211 			if (endIndex == -1) return scaling[n - 1];
   1212 			float[] scaling = this.scaling;
   1213 			int startIndex = endIndex - 1;
   1214 			float startValue = scaling[startIndex];
   1215 			float startTime = timeline[startIndex];
   1216 			return startValue + (scaling[endIndex] - startValue) * ((percent - startTime) / (timeline[endIndex] - startTime));
   1217 		}
   1218 
   1219 		public void save (Writer output) throws IOException {
   1220 			super.save(output);
   1221 			if (!active) return;
   1222 			output.write("highMin: " + highMin + "\n");
   1223 			output.write("highMax: " + highMax + "\n");
   1224 			output.write("relative: " + relative + "\n");
   1225 			output.write("scalingCount: " + scaling.length + "\n");
   1226 			for (int i = 0; i < scaling.length; i++)
   1227 				output.write("scaling" + i + ": " + scaling[i] + "\n");
   1228 			output.write("timelineCount: " + timeline.length + "\n");
   1229 			for (int i = 0; i < timeline.length; i++)
   1230 				output.write("timeline" + i + ": " + timeline[i] + "\n");
   1231 		}
   1232 
   1233 		public void load (BufferedReader reader) throws IOException {
   1234 			super.load(reader);
   1235 			if (!active) return;
   1236 			highMin = readFloat(reader, "highMin");
   1237 			highMax = readFloat(reader, "highMax");
   1238 			relative = readBoolean(reader, "relative");
   1239 			scaling = new float[readInt(reader, "scalingCount")];
   1240 			for (int i = 0; i < scaling.length; i++)
   1241 				scaling[i] = readFloat(reader, "scaling" + i);
   1242 			timeline = new float[readInt(reader, "timelineCount")];
   1243 			for (int i = 0; i < timeline.length; i++)
   1244 				timeline[i] = readFloat(reader, "timeline" + i);
   1245 		}
   1246 
   1247 		public void load (ScaledNumericValue value) {
   1248 			super.load(value);
   1249 			highMax = value.highMax;
   1250 			highMin = value.highMin;
   1251 			scaling = new float[value.scaling.length];
   1252 			System.arraycopy(value.scaling, 0, scaling, 0, scaling.length);
   1253 			timeline = new float[value.timeline.length];
   1254 			System.arraycopy(value.timeline, 0, timeline, 0, timeline.length);
   1255 			relative = value.relative;
   1256 		}
   1257 	}
   1258 
   1259 	static public class GradientColorValue extends ParticleValue {
   1260 		static private float[] temp = new float[4];
   1261 
   1262 		private float[] colors = {1, 1, 1};
   1263 		float[] timeline = {0};
   1264 
   1265 		public GradientColorValue () {
   1266 			alwaysActive = true;
   1267 		}
   1268 
   1269 		public float[] getTimeline () {
   1270 			return timeline;
   1271 		}
   1272 
   1273 		public void setTimeline (float[] timeline) {
   1274 			this.timeline = timeline;
   1275 		}
   1276 
   1277 		/** @return the r, g and b values for every timeline position */
   1278 		public float[] getColors () {
   1279 			return colors;
   1280 		}
   1281 
   1282 		/** @param colors the r, g and b values for every timeline position */
   1283 		public void setColors (float[] colors) {
   1284 			this.colors = colors;
   1285 		}
   1286 
   1287 		public float[] getColor (float percent) {
   1288 			int startIndex = 0, endIndex = -1;
   1289 			float[] timeline = this.timeline;
   1290 			int n = timeline.length;
   1291 			for (int i = 1; i < n; i++) {
   1292 				float t = timeline[i];
   1293 				if (t > percent) {
   1294 					endIndex = i;
   1295 					break;
   1296 				}
   1297 				startIndex = i;
   1298 			}
   1299 			float startTime = timeline[startIndex];
   1300 			startIndex *= 3;
   1301 			float r1 = colors[startIndex];
   1302 			float g1 = colors[startIndex + 1];
   1303 			float b1 = colors[startIndex + 2];
   1304 			if (endIndex == -1) {
   1305 				temp[0] = r1;
   1306 				temp[1] = g1;
   1307 				temp[2] = b1;
   1308 				return temp;
   1309 			}
   1310 			float factor = (percent - startTime) / (timeline[endIndex] - startTime);
   1311 			endIndex *= 3;
   1312 			temp[0] = r1 + (colors[endIndex] - r1) * factor;
   1313 			temp[1] = g1 + (colors[endIndex + 1] - g1) * factor;
   1314 			temp[2] = b1 + (colors[endIndex + 2] - b1) * factor;
   1315 			return temp;
   1316 		}
   1317 
   1318 		public void save (Writer output) throws IOException {
   1319 			super.save(output);
   1320 			if (!active) return;
   1321 			output.write("colorsCount: " + colors.length + "\n");
   1322 			for (int i = 0; i < colors.length; i++)
   1323 				output.write("colors" + i + ": " + colors[i] + "\n");
   1324 			output.write("timelineCount: " + timeline.length + "\n");
   1325 			for (int i = 0; i < timeline.length; i++)
   1326 				output.write("timeline" + i + ": " + timeline[i] + "\n");
   1327 		}
   1328 
   1329 		public void load (BufferedReader reader) throws IOException {
   1330 			super.load(reader);
   1331 			if (!active) return;
   1332 			colors = new float[readInt(reader, "colorsCount")];
   1333 			for (int i = 0; i < colors.length; i++)
   1334 				colors[i] = readFloat(reader, "colors" + i);
   1335 			timeline = new float[readInt(reader, "timelineCount")];
   1336 			for (int i = 0; i < timeline.length; i++)
   1337 				timeline[i] = readFloat(reader, "timeline" + i);
   1338 		}
   1339 
   1340 		public void load (GradientColorValue value) {
   1341 			super.load(value);
   1342 			colors = new float[value.colors.length];
   1343 			System.arraycopy(value.colors, 0, colors, 0, colors.length);
   1344 			timeline = new float[value.timeline.length];
   1345 			System.arraycopy(value.timeline, 0, timeline, 0, timeline.length);
   1346 		}
   1347 	}
   1348 
   1349 	static public class SpawnShapeValue extends ParticleValue {
   1350 		SpawnShape shape = SpawnShape.point;
   1351 		boolean edges;
   1352 		SpawnEllipseSide side = SpawnEllipseSide.both;
   1353 
   1354 		public SpawnShape getShape () {
   1355 			return shape;
   1356 		}
   1357 
   1358 		public void setShape (SpawnShape shape) {
   1359 			this.shape = shape;
   1360 		}
   1361 
   1362 		public boolean isEdges () {
   1363 			return edges;
   1364 		}
   1365 
   1366 		public void setEdges (boolean edges) {
   1367 			this.edges = edges;
   1368 		}
   1369 
   1370 		public SpawnEllipseSide getSide () {
   1371 			return side;
   1372 		}
   1373 
   1374 		public void setSide (SpawnEllipseSide side) {
   1375 			this.side = side;
   1376 		}
   1377 
   1378 		public void save (Writer output) throws IOException {
   1379 			super.save(output);
   1380 			if (!active) return;
   1381 			output.write("shape: " + shape + "\n");
   1382 			if (shape == SpawnShape.ellipse) {
   1383 				output.write("edges: " + edges + "\n");
   1384 				output.write("side: " + side + "\n");
   1385 			}
   1386 		}
   1387 
   1388 		public void load (BufferedReader reader) throws IOException {
   1389 			super.load(reader);
   1390 			if (!active) return;
   1391 			shape = SpawnShape.valueOf(readString(reader, "shape"));
   1392 			if (shape == SpawnShape.ellipse) {
   1393 				edges = readBoolean(reader, "edges");
   1394 				side = SpawnEllipseSide.valueOf(readString(reader, "side"));
   1395 			}
   1396 		}
   1397 
   1398 		public void load (SpawnShapeValue value) {
   1399 			super.load(value);
   1400 			shape = value.shape;
   1401 			edges = value.edges;
   1402 			side = value.side;
   1403 		}
   1404 	}
   1405 
   1406 	static public enum SpawnShape {
   1407 		point, line, square, ellipse
   1408 	}
   1409 
   1410 	static public enum SpawnEllipseSide {
   1411 		both, top, bottom
   1412 	}
   1413 }
   1414