/* * Sprite - A graphic item that can be animated on a animation * * Author: Phillip Piper * Date: 23/10/2009 10:29 PM * * Change log: * 2010-03-01 JPP - Added FixedLocation and FixedBounds properties * 2010-02-08 JPP - Handle multithreaded access to ImageSprites * 2009-10-23 JPP - Initial version * * To do: * 2010-02-08 Given TextSprite more formatting options * 2010-01-18 Change ShapeSprite so it can use arbitrary Pens and Brushes * * Copyright (C) 2009-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.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Imaging; using System.Windows.Forms; namespace BrightIdeasSoftware { /// /// A Sprite is an animated graphic. /// /// /// A sprite is animated by adding effects, which change its properties between frames. /// public class Sprite : Animateable, ISprite { #region Life and death /// /// Create new do nothing sprite. /// public Sprite() { this.Init(); this.ReferenceBoundsLocator = new AnimationBoundsLocator(); this.ReferenceBoundsLocator.Sprite = this; } #endregion #region Configuration properties /// /// Gets or sets where the sprite is located /// public virtual Point Location { get { return location; } set { location = value; } } private Point location; /// /// Gets or sets how transparent the sprite is. /// 0.0 is completely transparent, 1.0 is completely opaque. /// public virtual float Opacity { get { return opacity; } set { opacity = value; } } private float opacity; /// /// Gets or sets the scaling that is applied to the extent of the sprite. /// The location of the sprite is not scaled. /// public virtual float Scale { get { return scale; } set { scale = value; } } private float scale; /// /// Gets or sets the size of the sprite /// public virtual Size Size { get { return size; } set { size = value; } } private Size size; /// /// Gets or sets the angle in degrees of the sprite. /// 0 means no angle, 90 means right edge lifted vertical. /// public virtual float Spin { get { return spin; } set { spin = value; } } private float spin; /// /// Gets or set if the spinning should be done around the /// top left of the sprite. If this is false, the sprite /// will spin around the center of the sprite. /// public bool SpinAroundOrigin { get { return spinAroundOrigin; } set { spinAroundOrigin = value; } } private bool spinAroundOrigin; /// /// Gets or sets the point at which this sprite will always be placed. /// /// /// Most sprites play with their location as part of their animation. /// But other just want to stay in the same place. /// Do not set this if you use Move or Goto effects on the sprite. /// public IPointLocator FixedLocation { get { return fixedLocation; } set { fixedLocation = value; if (fixedLocation != null) fixedLocation.Sprite = this; } } private IPointLocator fixedLocation; /// /// Gets or sets the bounds at which this sprite will always be placed. /// /// See remarks on FixedLocation public IRectangleLocator FixedBounds { get { return fixedBounds; } set { fixedBounds = value; if (fixedBounds != null) fixedBounds.Sprite = this; } } private IRectangleLocator fixedBounds; #endregion #region Reference properties /// /// Gets or sets the bounds of the sprite. This is boundary within which /// the sprite will be drawn. /// public virtual Rectangle Bounds { get { return new Rectangle(this.Location, this.Size); } set { this.Location = value.Location; this.Size = value.Size; } } /// /// Gets the outer bounds of this sprite, which is normally the /// bounds of the control that is hosting the story board. /// Nothing outside of this rectangle will be drawn. /// public virtual Rectangle OuterBounds { get { return this.Animation.Bounds; } } /// /// Gets or sets the reference rectangle in relation to which /// the sprite will be drawn. This is normal the ClientArea of /// the control that is hosting the story board, though it /// could be a subarea of that control (e.g. a particular /// cell within a ListView). /// /// This value is controlled by ReferenceBoundsLocator property. public virtual Rectangle ReferenceBounds { get { return this.ReferenceBoundsLocator.GetRectangle(); } set { this.ReferenceBoundsLocator = new FixedRectangleLocator(value); } } /// /// Gets or sets the locator that will calculate the reference rectangle /// for the sprite. /// public virtual IRectangleLocator ReferenceBoundsLocator { get { return referenceBoundsLocator; } set { referenceBoundsLocator = value; } } private IRectangleLocator referenceBoundsLocator; #endregion #region Animation methods /// /// Draw the sprite in its current state /// /// public virtual void Draw(Graphics g) { } /// /// Set the sprite to its initial state /// public virtual void Init() { this.Location = Point.Empty; this.Opacity = 1.0f; this.Scale = 1.0f; this.Spin = 0.0f; } /// /// The sprite should advance its state. /// /// Milliseconds since Start() was called /// True if Tick() should be called again public override bool Tick(long elapsed) { float elapsedAsFloat = (float)elapsed; foreach (EffectControlBlock cb in this.ControlBlocks) { if (!cb.Stopped && cb.ScheduledStartTick <= elapsed) { if (!cb.Started) { cb.StartTick = elapsed; cb.Effect.Start(); cb.Started = true; } if (cb.Duration > 0 && elapsed <= cb.ScheduledEndTick) { float fractionDone = (elapsedAsFloat - cb.StartTick) / cb.Duration; cb.Effect.Apply(fractionDone); } else { cb.Effect.Apply(1.0f); cb.Effect.Stop(); cb.Stopped = true; } } } this.ApplyFixedLocations(); return (this.ControlBlocks.Exists(delegate(EffectControlBlock cb) { return !cb.Stopped; })); } /// /// Apply any FixedLocation or FixedBounds properties that have been set /// protected void ApplyFixedLocations() { if (this.FixedBounds != null) this.Bounds = this.FixedBounds.GetRectangle(); else if (this.FixedLocation != null) this.Location = this.FixedLocation.GetPoint(); } /// /// Reset the sprite to its neutral state /// public override void Reset() { this.Init(); // Reset the effects in reverse order so their side-effects are unwound List sortedBlocks = new List(this.ControlBlocks); sortedBlocks.Sort(delegate(EffectControlBlock b1, EffectControlBlock b2) { return b2.ScheduledStartTick.CompareTo(b1.ScheduledStartTick); }); foreach (EffectControlBlock cb in sortedBlocks) { cb.Effect.Reset(); cb.StartTick = 0; cb.Started = false; cb.Stopped = false; } } /// /// Stop this sprite /// public override void Stop() { foreach (EffectControlBlock cb in this.ControlBlocks) { if (cb.Started && !cb.Stopped) { cb.Effect.Stop(); cb.Stopped = true; } } } #endregion #region Effect methods /// /// Add a run-once effect which starts with the sprite /// /// public void Add(IEffect effect) { this.Add(0, 0, effect); } /// /// Add a run-once effect /// /// /// public void Add(long startTick, IEffect effect) { this.Add(startTick, 0, effect); } /// /// Add an effect to this sprite /// /// When should the effect begin in ms since Start() /// For how many milliseconds should the effect continue. /// 0 means the effect will be applied only once. /// The effect to be applied public void Add(long startTick, long duration, IEffect effect) { EffectControlBlock cb = new EffectControlBlock(startTick, duration, effect); effect.Sprite = this; this.ControlBlocks.Add(cb); } private List ControlBlocks = new List(); #endregion #region Drawing utility methods /// /// Apply any graphic state (translation, rotation, scale) to the given graphic context /// /// Once the state is applied, the co-ordinates will be translated so that /// Location is at (0,0). This is necessary for spinning to work. So when the sprite /// draws itself, all its coordinattes should be based on 0,0, not on this.Location. /// This means you cannot use this.Bounds when drawing. /// g.DrawRectangle(this.Bounds, Pens.Black); will not draw your rectangle where you want. /// g.DrawRectangle(new Rectangle(Point.Empty, this.Size), Pens.Black); will work. /// The graphic to be configured protected virtual void ApplyState(Graphics g) { Matrix m = new Matrix(); if (this.Spin != 0) { Rectangle r = this.Bounds; // TODO: Make a SpinCentre property Point spinCentre = r.Location; if (!this.SpinAroundOrigin) spinCentre = new Point(r.X + r.Width / 2, r.Y + r.Height / 2); m.RotateAt(this.Spin, spinCentre); } m.Translate((float)this.Location.X, (float)this.Location.Y); g.Transform = m; } /// /// Remove any graphic state applied by ApplyState(). /// /// The graphic to be configured protected virtual void UnapplyState(Graphics g) { g.ResetTransform(); } #endregion /// /// Instances of this class hold the state of an effect as it progresses /// protected class EffectControlBlock { public EffectControlBlock(long start, long duration, IEffect effect) { this.ScheduledStartTick = start; this.Duration = duration; this.Effect = effect; } public long ScheduledStartTick; public long Duration ; public bool Started ; public bool Stopped ; public long StartTick ; public long ScheduledEndTick { get { return this.StartTick + this.Duration; } } public IEffect Effect ; } } }