1 /* $Id$ */
2 /***************************************************************************
3  *                   (C) Copyright 2003-2010 - Stendhal                    *
4  ***************************************************************************
5  ***************************************************************************
6  *                                                                         *
7  *   This program is free software; you can redistribute it and/or modify  *
8  *   it under the terms of the GNU General Public License as published by  *
9  *   the Free Software Foundation; either version 2 of the License, or     *
10  *   (at your option) any later version.                                   *
11  *                                                                         *
12  ***************************************************************************/
13 package games.stendhal.client.sprite;
14 
15 
16 import java.awt.Graphics;
17 import java.util.Arrays;
18 
19 import org.apache.log4j.Logger;
20 
21 /**
22  * This is a sprite that transparently animates itself when drawn.
23  */
24 public class AnimatedSprite implements Sprite {
25 	private static Logger logger = Logger.getLogger(AnimatedSprite.class);
26 
27 	/**
28 	 * The identifier reference.
29 	 */
30 	private Object reference;
31 
32 	/**
33 	 * Whether the sprite is currently animating.
34 	 */
35 	private boolean animating;
36 
37 	/**
38 	 * The amount of time passed in the cycle.
39 	 */
40 	private int cycleTime;
41 
42 	/**
43 	 * The [minimum] frame durations.
44 	 */
45 	protected int[] delays;
46 
47 	/**
48 	 * The total duration of a cycle.
49 	 */
50 	private int duration;
51 
52 	/**
53 	 * The current frame index.
54 	 */
55 	private int index;
56 
57 	/**
58 	 * The frame sprites.
59 	 */
60 	protected Sprite[] frames;
61 
62 	/**
63 	 * The sprite height.
64 	 */
65 	private int height;
66 
67 	/**
68 	 * The time of the last update.
69 	 */
70 	protected long lastUpdate;
71 
72 	/**
73 	 * Whether to loop after last frame.
74 	 */
75 	protected boolean loop;
76 
77 	/**
78 	 * The current sprite.
79 	 */
80 	protected Sprite sprite;
81 
82 	/**
83 	 * The sprite width.
84 	 */
85 	private int width;
86 
87 	/**
88 	 * Create an animated sprite from a set of frame sprites.
89 	 *
90 	 * <strong>NOTE: The array of frames passed is not copied, and must not be
91 	 * modified while this instance exists (unless you are sure you know what
92 	 * you are doing).</strong>
93 	 *
94 	 * @param frames
95 	 *            The frames to animate.
96 	 * @param delay
97 	 *            The minimum delay between frames (in ms).
98 	 *
99 	 * @throws IllegalArgumentException
100 	 *             If less than one frame is given, or the delay is < 0.
101 	 */
AnimatedSprite(final Sprite[] frames, final int delay)102 	AnimatedSprite(final Sprite[] frames, final int delay) {
103 		this(frames, delay, true);
104 	}
105 
106 	/**
107 	 * Create an animated sprite from a set of frame sprites.
108 	 *
109 	 * <strong>NOTE: The array of frames passed is not copied, and must not be
110 	 * modified while this instance exists (unless you are sure you know what
111 	 * you are doing).</strong>
112 	 *
113 	 * @param frames
114 	 *            The frames to animate.
115 	 * @param delay
116 	 *            The minimum delay between frames (in ms).
117 	 * @param animating
118 	 *            If animation is enabled.
119 	 *
120 	 * @throws IllegalArgumentException
121 	 *             If less than one frame is given, or the delay is < 0.
122 	 */
AnimatedSprite(final Sprite[] frames, final int delay, final boolean animating)123 	public AnimatedSprite(final Sprite[] frames, final int delay,
124 			final boolean animating) {
125 		this(frames, delay, animating, null);
126 	}
127 
128 	/**
129 	 * Create an animated sprite from a set of frame sprites.
130 	 *
131 	 * <strong>NOTE: The array of frames passed is not copied, and must not be
132 	 * modified while this instance exists (unless you are sure you know what
133 	 * you are doing).</strong>
134 	 *
135 	 * @param frames
136 	 *            The frames to animate.
137 	 * @param delay
138 	 *            The minimum delay between frames (in ms).
139 	 * @param animating
140 	 *            If animation is enabled.
141 	 * @param reference
142 	 *            The sprite identifier reference.
143 	 *
144 	 * @throws IllegalArgumentException
145 	 *             If less than one frame is given, or the delay is < 0.
146 	 */
AnimatedSprite(final Sprite[] frames, final int delay, final boolean animating, final Object reference)147 	private AnimatedSprite(final Sprite[] frames, final int delay,
148 			final boolean animating, final Object reference) {
149 		this(frames, createDelays(delay, frames.length), animating, reference);
150 	}
151 
152 	/**
153 	 * Create an animated sprite from a set of frame sprites.
154 	 *
155 	 * <strong>NOTE: The array of frames/delays passed is not copied, and must
156 	 * not be modified while this instance exists (unless you are sure you know
157 	 * what you are doing).</strong>
158 	 *
159 	 * @param frames
160 	 *            The frames to animate.
161 	 * @param delays
162 	 *            The minimum duration for each frame (in ms).
163 	 * @param animating
164 	 *            If animation is enabled.
165 	 * @param reference
166 	 *            The sprite identifier reference.
167 	 *
168 	 * @throws IllegalArgumentException
169 	 *             If less than one frame is given, or the delay is < 0.
170 	 */
AnimatedSprite(final Sprite[] frames, final int[] delays, final boolean animating, final Object reference)171 	AnimatedSprite(final Sprite[] frames, final int[] delays,
172 			final boolean animating, final Object reference) {
173 		if (frames.length == 0) {
174 			logger.warn("AnimatedSprite needs at least one frame");
175 		}
176 
177 		if (delays.length != frames.length) {
178 			throw new IllegalArgumentException(
179 					"Mismatch between number of frame sprites and delays");
180 		}
181 
182 		/*
183 		 * Validate delay values. Calculate total cycle duration.
184 		 */
185 		duration = 0;
186 
187 		for (int i = 0; i < delays.length; i++) {
188 			if (delays[i] < 0) {
189 				throw new IllegalArgumentException("Delay < 0");
190 			}
191 
192 			duration += delays[i];
193 		}
194 
195 		this.frames = frames;
196 		this.delays = delays;
197 		this.animating = animating;
198 		this.reference = reference;
199 
200 		loop = true;
201 
202 		height = 0;
203 		width = 0;
204 
205 		for (final Sprite frame : frames) {
206 			height = Math.max(height, frame.getHeight());
207 			width = Math.max(width, frame.getWidth());
208 		}
209 
210 		index = 0;
211 		if (frames.length > 0) {
212 			sprite = frames[0];
213 		} else {
214 			sprite = null;
215 		}
216 
217 		cycleTime = 0;
218 
219 		long now = System.currentTimeMillis();
220 		if (loop) {
221 			/*
222 			 * Make all looped animations look like they were started at time 0.
223 			 * Keeps the map sprites nicely in sync.
224 			 */
225 			lastUpdate = now - (now % duration);
226 		} else {
227 			lastUpdate = now;
228 		}
229 	}
230 
231 	//
232 	// AnimatedSprite
233 	//
234 
235 	/**
236 	 * Utility method to convert a single delay to an array of delays having
237 	 * that value.
238 	 *
239 	 * @param delay
240 	 *            The delay value.
241 	 * @param count
242 	 *            The size of the array to create.
243 	 *
244 	 * @return An array of delays.
245 	 */
createDelays(final int delay, final int count)246 	private static int[] createDelays(final int delay, final int count) {
247 		final int[] delays = new int[count];
248 		Arrays.fill(delays, delay);
249 
250 		return delays;
251 	}
252 
253 	/**
254 	 * Reset the animation back to specified frame, and reset the next frame
255 	 * time.
256 	 *
257 	 * @param index the default index
258 	 */
reset(int index)259 	public void reset(int index) {
260 		setIndex(index);
261 	}
262 
263 	/**
264 	 * Set the frame index to a specific value.
265 	 *
266 	 * @param index
267 	 *            The index to use.
268 	 *
269 	 * @throws ArrayIndexOutOfBoundsException
270 	 *             If the index is less than 0 or greater than or equal to the
271 	 *             number of frames.
272 	 */
setIndex(final int index)273 	private void setIndex(final int index) {
274 		if ((index < 0) || (index >= frames.length)) {
275 			throw new ArrayIndexOutOfBoundsException("Invalid index: " + index);
276 		}
277 
278 		this.index = index;
279 		sprite = frames[index];
280 
281 		/*
282 		 * Calculate the time into this frame
283 		 */
284 		cycleTime = 0;
285 
286 		for (int i = 0; i < index; i++) {
287 			cycleTime += delays[i];
288 		}
289 
290 		lastUpdate = System.currentTimeMillis();
291 	}
292 
293 	/**
294 	 * Start the sprite animating.
295 	 *
296 	 * @see #stop()
297 	 */
start()298 	public void start() {
299 		animating = true;
300 	}
301 
302 	/**
303 	 * Stop the sprite animating. This does not change the current frame.
304 	 *
305 	 * @see #start()
306 	 */
stop()307 	public void stop() {
308 		animating = false;
309 	}
310 
311 	/**
312 	 * Update the current frame sprite.
313 	 */
update()314 	private void update() {
315 		final long now = System.currentTimeMillis();
316 		update((int) (now - lastUpdate));
317 		lastUpdate = now;
318 	}
319 
320 	//
321 	// Sprite
322 	//
323 	/**
324 	 * Create a sub-region of this sprite. <strong>NOTE: This does not use
325 	 * caching.</strong>
326 	 *
327 	 * @param x
328 	 *            The starting X coordinate.
329 	 * @param y
330 	 *            The starting Y coordinate.
331 	 * @param width
332 	 *            The region width.
333 	 * @param height
334 	 *            The region height.
335 	 * @param ref
336 	 *            The sprite reference.
337 	 *
338 	 * @return A new sprite.
339 	 */
340 	@Override
createRegion(final int x, final int y, final int width, final int height, final Object ref)341 	public Sprite createRegion(final int x, final int y, final int width,
342 			final int height, final Object ref) {
343 		return new TileSprite(this, x, y, width, height, ref);
344 	}
345 
346 	/**
347 	 * Draw the sprite onto the graphics context provided.
348 	 *
349 	 * @param g
350 	 *            The graphics context on which to draw the sprite
351 	 * @param x
352 	 *            The x location at which to draw the sprite
353 	 * @param y
354 	 *            The y location at which to draw the sprite
355 	 */
356 	@Override
draw(final Graphics g, final int x, final int y)357 	public void draw(final Graphics g, final int x, final int y) {
358 		update();
359 
360 		if (sprite != null) {
361 			sprite.draw(g, x, y);
362 		}
363 	}
364 
365 	/**
366 	 * Draws the image.
367 	 *
368 	 * @param g
369 	 *            the graphics context where to draw to
370 	 * @param destx
371 	 *            destination x
372 	 * @param desty
373 	 *            destination y
374 	 * @param x
375 	 *            the source x
376 	 * @param y
377 	 *            the source y
378 	 * @param w
379 	 *            the width
380 	 * @param h
381 	 *            the height
382 	 */
383 	@Override
draw(final Graphics g, final int destx, final int desty, final int x, final int y, final int w, final int h)384 	public void draw(final Graphics g, final int destx, final int desty,
385 			final int x, final int y, final int w, final int h) {
386 		update();
387 
388 		if (sprite != null) {
389 			sprite.draw(g, destx, desty, x, y, w, h);
390 		}
391 	}
392 
393 	/**
394 	 * Get the height of the drawn sprite.
395 	 *
396 	 * @return The height in pixels of this sprite.
397 	 */
398 	@Override
getHeight()399 	public int getHeight() {
400 		return height;
401 	}
402 
403 	/**
404 	 * Get the sprite reference. This identifier is an externally opaque object
405 	 * that implements equals() and hashCode() to uniquely/repeatably reference
406 	 * a keyed sprite.
407 	 *
408 	 * @return The reference identifier, or <code>null</code> if not
409 	 *         referencable.
410 	 */
411 	@Override
getReference()412 	public Object getReference() {
413 		return reference;
414 	}
415 
416 	/**
417 	 * Get the width of the drawn sprite.
418 	 *
419 	 * @return The width in pixels of this sprite.
420 	 */
421 	@Override
getWidth()422 	public int getWidth() {
423 		return width;
424 	}
425 
426 	/**
427 	 * Update the current frame sprite.
428 	 * <em>Idealy this would be called from a central time manager,
429 	 * instead of draw() like now.</em>
430 	 *
431 	 * @param delta
432 	 *            The time since last update (in ms).
433 	 */
update(final int delta)434 	private void update(final int delta) {
435 		if (animating) {
436 			cycleTime += delta;
437 			if (loop) {
438 				// let the non-looping sprites overflow, so that they get
439 				// properly stopped
440 				cycleTime %= duration;
441 			}
442 
443 			while (cycleTime >= delays[index]) {
444 				cycleTime -= delays[index];
445 
446 				if (++index == frames.length) {
447 					index = 0;
448 
449 					if (!loop) {
450 						sprite = null;
451 						animating = false;
452 						return;
453 					}
454 				}
455 			}
456 
457 			sprite = frames[index];
458 		}
459 	}
460 }
461