/* * Animation - An entire sequence of sprites, sounds and effects which are * united to produce a "movie clip". * * Author: Phillip Piper * Date: 19/10/2009 1:01 AM * * Change log: * 2010-02-05 JPP - Made animation system independent of any control. * 2009-10-19 JPP - Initial version * * To do: * - Animation wide effects?? Freeze. Fade. * - FramesPerSecond setting * * Copyright (C) 20009-2014 Phillip Piper * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com. */ using System; using System.Collections.Generic; using System.Diagnostics; using System.Drawing; using System.ComponentModel; using System.Windows.Forms; namespace BrightIdeasSoftware { /// /// This enum tells the animation how it should behave when it reaches the end /// public enum Repeat { None = 0, Loop, Bounce, // Not yet implemented JPP 2010-02-23 Pause } /// /// An animation is the "canvas" upon which multiple sprites and sounds will be drawn. /// public class Animation { #region Life and death public Animation() { this.Timer = new System.Timers.Timer(); this.Stopwatch = new System.Diagnostics.Stopwatch(); this.Interval = 30; } #endregion #region Properties /// /// Gets or sets the outer bounds of the animation. All locations will be calculated /// with reference to these bounds. /// public Rectangle Bounds { get { return bounds; } set { bounds = value; } } private Rectangle bounds; /// /// Gets or sets the "tick" interval of the animation in milliseconds. /// public int Interval { get { return (int)this.Timer.Interval; } set { this.Timer.Interval = value; } } /// /// Gets or sets whether the sprites should be paused /// /// Sounds that are already in progress will not be paused. public bool Paused { get { return !this.Timer.Enabled; } set { if (value) this.Pause(); else this.Unpause(); } } /// /// Gets or sets if the animation is running. A animation is running /// when it has been started and not yet stopped. A paused animation /// is still running. /// public bool Running { get { return running; } protected set { running = value; } } private bool running; /// /// Gets or sets how the animation will behave when it reaches the /// end of the animation. /// public Repeat Repeat { get { return repeat; } set { repeat = value; } } private Repeat repeat; #endregion #region Events public event EventHandler Started; protected void OnStarted(StartAnimationEventArgs e) { if (this.Started != null) this.Started(this, e); } public event EventHandler Ticked; protected void OnTicked(TickEventArgs e) { if (this.Ticked != null) this.Ticked(this, e); } public event EventHandler Stopped; protected void OnStopped(StopAnimationEventArgs e) { if (this.Stopped != null) this.Stopped(this, e); } public event EventHandler Redraw; protected void OnRedraw(RedrawEventArgs e) { if (this.Redraw != null) this.Redraw(this, e); } #endregion #region Commands /// /// Force the animation to be redrawn /// public void Invalidate() { this.OnRedraw(new RedrawEventArgs()); } /// /// Force the animation to be redrawn /// public void Invalidate(Rectangle r) { this.OnRedraw(new RedrawEventArgs(r)); } /// /// Start the story /// public void Start() { this.Running = true; this.Timer.Elapsed += new System.Timers.ElapsedEventHandler(Timer_Elapsed); this.Stopwatch.Start(); this.Timer.Start(); this.OnStarted(new StartAnimationEventArgs()); } /// /// Pause the story /// /// Any sounds that are already playing will continue to play. public void Pause() { this.Stopwatch.Stop(); this.Timer.Stop(); } /// /// Unpause the story /// public void Unpause() { this.Stopwatch.Start(); this.Timer.Start(); } /// /// Stop the story. /// public void Stop() { this.Running = false; this.Timer.Elapsed -= new System.Timers.ElapsedEventHandler(Timer_Elapsed); this.Timer.Stop(); this.Stopwatch.Stop(); foreach (AnimateableControlBlock cb in this.ControlBlocks) { if (cb.Started && !cb.Stopped) { cb.Component.Stop(); cb.Stopped = true; } } // Tell the world we have stopped this.OnStopped(new StopAnimationEventArgs()); } /// /// Advance the animation one tick and then redraw. /// public void Tick() { this.TickOnce(); this.Invalidate(); } #endregion #region Sprite manipulation /// /// Add the given sprite to the animation so that it appears the given /// number of ticks after the animation starts. /// /// How many milliseconds after the animation starts should the sprite appear? /// The sprite that will appear public void Add(long startTick, ISprite sprite) { this.AddControlBlock(new AnimateableControlBlock(startTick, sprite)); } /// /// Add the given sound to the animation so that it begins to play the given /// number of ticks after the animation starts. /// /// When should the sound begin to play? /// The sprite that will appear public void Add(long startTick, Audio sound) { this.AddControlBlock(new AnimateableControlBlock(startTick, sound)); } #endregion #region Implementation /// /// Add the given control block to those used by the animation /// /// /// protected void AddControlBlock(AnimateableControlBlock cb) { cb.Component.Animation = this; this.ControlBlocks.Add(cb); } /// /// The timer has elapsed. Tickle the animation. /// /// /// void Timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) { TickEventArgs args = new TickEventArgs(); this.OnTicked(args); if (!args.Handled) this.Tick(); } /// /// Advance each sprite by one "tick" /// protected void TickOnce() { this.Timer.Enabled = false; long ms = this.Stopwatch.ElapsedMilliseconds; foreach (AnimateableControlBlock cb in this.ControlBlocks) { if (!cb.Stopped && cb.ScheduledStartTick <= ms) { if (!cb.Started) { cb.StartTick = ms; cb.Component.Start(); } if (!cb.Component.Tick(ms - cb.StartTick)) { cb.Stopped = true; cb.Component.Stop(); } } } // If any component is not stopped, restart the timer. Otherwise stop the animation. if (this.ControlBlocks.Exists(delegate(AnimateableControlBlock cb) { return !cb.Stopped; })) this.Timer.Enabled = true; else this.AnimationEnded(); } protected void AnimationEnded() { switch (this.Repeat) { case Repeat.None: this.Stop(); break; case Repeat.Loop: this.Restart(); break; case Repeat.Bounce: break; case Repeat.Pause: this.Pause(); break; } } /// /// The animation has stopped. Restart it from the beginning. /// protected void Restart() { this.Stop(); this.Stopwatch.Reset(); // Reset the components in reverse order so their effects are unwound for (int i = this.ControlBlocks.Count - 1; i >= 0; i--) { AnimateableControlBlock cb = this.ControlBlocks[i]; cb.Component.Reset(); cb.StartTick = 0; cb.Stopped = false; } this.Start(); } /// /// Draw the sprites for this animation onto the given context /// /// The graphic onto which the animation will draw itself /// It's normally a good idea for the given Graphics object to be /// double buffered to cut down on flicker. public void Draw(Graphics g) { // Draw each started sprite foreach (AnimateableControlBlock cb in this.ControlBlocks) { if (cb.Sprite != null && cb.Started) { cb.Sprite.Draw(g); } } } #endregion #region Implementation classes /// /// Instances of this class are used to control the animation of a component on a animation. /// protected class AnimateableControlBlock { public AnimateableControlBlock(long scheduledStartTick, IAnimateable component) { this.ScheduledStartTick = scheduledStartTick; this.Component = component; } public AnimateableControlBlock(long scheduledStartTick, ISprite sprite) : this(scheduledStartTick, (IAnimateable)sprite) { this.Sprite = sprite; } public IAnimateable Component; public long ScheduledStartTick; public long StartTick; public bool Stopped; public bool Started { get { return this.StartTick != 0; } } /// /// Gets or sets the sprite that is managed by this control block. /// /// Almost all components are sprites so we keep this property to /// prevent the use of casts. public ISprite Sprite; } #endregion #region Private variables private System.Timers.Timer Timer; private System.Diagnostics.Stopwatch Stopwatch; private List ControlBlocks = new List(); #endregion } }