/*
* 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 ;
}
}
}