Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 131 additions & 0 deletions src/KappaDuck.Quack/Graphics/Drawing/AnimatedSprite.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// Copyright (c) KappaDuck.
// Licensed under the MIT license.

using KappaDuck.Quack.Geometry;
using KappaDuck.Quack.Graphics.Rendering;

namespace KappaDuck.Quack.Graphics.Drawing;

/// <summary>
/// A <see cref="Sprite"/> that plays a <see cref="SpriteAnimation"/> from a <see cref="SpriteSheet"/>, cycling its
/// displayed frame over time.
/// </summary>
/// <remarks>
/// Choose a clip with <see cref="Play(SpriteAnimation)"/>, advance it with <see cref="Update(TimeSpan)"/>
/// once per frame, then draw it like any drawable with <see cref="IRenderTarget.Draw(IDrawable)"/>. Flip the sprite
/// with a negative <see cref="Transformable.Scale"/>. <see cref="Play(SpriteAnimation)"/> is idempotent, so calling it
/// every frame while a key is held keeps the animation running rather than restarting it.
/// </remarks>
/// <remarks>
/// Creates an animated sprite that draws frames from the given sheet.
/// </remarks>
/// <param name="sheet">The sheet the animation frames are taken from.</param>
public sealed class AnimatedSprite(SpriteSheet sheet)
: Sprite(sheet.Texture, sheet.Count > 0 ? sheet[0] : new RectI(0, 0, sheet.Texture.Width, sheet.Texture.Height))
{
private TimeSpan _elapsed;
private int _frameIndex;

/// <summary>
/// Gets or sets the sheet the animation frames are taken from.
/// </summary>
public SpriteSheet Sheet
{
get;
set
{
field = value;
Texture = value.Texture;

UpdateRegion();
}
} = sheet;

/// <summary>
/// Gets the animation currently playing, or <see langword="null"/> if none has been played yet.
/// </summary>
public SpriteAnimation? Animation { get; private set; }

/// <summary>
/// Gets a value indicating whether an animation is currently playing.
/// </summary>
public bool IsPlaying { get; private set; }

/// <summary>
/// Plays an animation from its first frame, or does nothing if it is already the animation playing.
/// </summary>
/// <param name="animation">The animation to play.</param>
public void Play(SpriteAnimation animation)
{
if (ReferenceEquals(Animation, animation) && IsPlaying)
return;

Animation = animation;
_elapsed = TimeSpan.Zero;
_frameIndex = 0;
IsPlaying = true;

UpdateRegion();
}

/// <summary>
/// Stops the animation and rewinds it to its first frame.
/// </summary>
public void Stop()
{
IsPlaying = false;
_elapsed = TimeSpan.Zero;
_frameIndex = 0;

UpdateRegion();
}

/// <summary>
/// Pauses the animation on its current frame.
/// </summary>
public void Pause() => IsPlaying = false;

/// <summary>
/// Resumes a paused animation from its current frame.
/// </summary>
public void Resume() => IsPlaying = Animation is not null;

/// <summary>
/// Advances the current animation by the elapsed time.
/// </summary>
/// <param name="deltaTime">The time since the last update, such as the frame delta.</param>
public void Update(TimeSpan deltaTime)
{
if (!IsPlaying || Animation is null)
return;

_elapsed += deltaTime;

while (_elapsed >= Animation.DurationAt(_frameIndex))
{
_elapsed -= Animation.DurationAt(_frameIndex);
_frameIndex++;

if (_frameIndex < Animation.FrameCount)
continue;

if (Animation.Loop)
{
_frameIndex = 0;
continue;
}

_frameIndex = Animation.FrameCount - 1;
IsPlaying = false;
break;
}

UpdateRegion();
}

private void UpdateRegion()
{
if (Animation is not null)
Region = Sheet[Animation.FrameAt(_frameIndex)];
}
}
82 changes: 82 additions & 0 deletions src/KappaDuck.Quack/Graphics/Drawing/SpriteAnimation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright (c) KappaDuck.
// Licensed under the MIT license.

namespace KappaDuck.Quack.Graphics.Drawing;

/// <summary>
/// A reusable animation clip: a sequence of <see cref="SpriteSheet"/> frame indices, each shown for a duration, with
/// an optional loop.
/// </summary>
/// <remarks>
/// A clip is independent of any particular sprite; the same clip can be played by several <see cref="AnimatedSprite"/>
/// instances. Frame indices refer to the sheet the animation is played on.
/// </remarks>
public sealed class SpriteAnimation
{
private readonly int[] _frames;
private readonly TimeSpan[] _durations;

/// <summary>
/// Creates an animation whose frames are all shown for the same duration.
/// </summary>
/// <param name="frames">The sheet frame indices, in playback order.</param>
/// <param name="frameDuration">How long each frame is shown.</param>
/// <param name="loop"><see langword="true"/> to restart from the first frame after the last; otherwise <see langword="false"/>.</param>
public SpriteAnimation(ReadOnlySpan<int> frames, TimeSpan frameDuration, bool loop = true)
{
_frames = frames.ToArray();
_durations = new TimeSpan[_frames.Length];

Array.Fill(_durations, frameDuration);

Loop = loop;
Duration = frameDuration * _frames.Length;
}

/// <summary>
/// Creates an animation with a separate duration for each frame.
/// </summary>
/// <param name="frames">The sheet frame indices, in playback order.</param>
/// <param name="durations">How long each corresponding frame is shown. Must have the same length as <paramref name="frames"/>.</param>
/// <param name="loop"><see langword="true"/> to restart from the first frame after the last; otherwise <see langword="false"/>.</param>
/// <exception cref="ArgumentException"><paramref name="frames"/> and <paramref name="durations"/> have different lengths.</exception>
public SpriteAnimation(ReadOnlySpan<int> frames, ReadOnlySpan<TimeSpan> durations, bool loop = true)
{
ArgumentOutOfRangeException.ThrowIfNotEqual(frames.Length, durations.Length);

_frames = frames.ToArray();
_durations = durations.ToArray();

Loop = loop;
Duration = Sum(durations);
}

/// <summary>
/// Gets or sets a value indicating whether the animation restarts after its last frame.
/// </summary>
public bool Loop { get; set; }

/// <summary>
/// Gets the number of frames in the animation.
/// </summary>
public int FrameCount => _frames.Length;

/// <summary>
/// Gets the total time for one pass through every frame.
/// </summary>
public TimeSpan Duration { get; }

internal int FrameAt(int index) => _frames[index];

internal TimeSpan DurationAt(int index) => _durations[index];

private static TimeSpan Sum(ReadOnlySpan<TimeSpan> durations)
{
TimeSpan total = TimeSpan.Zero;

foreach (TimeSpan duration in durations)
total += duration;

return total;
}
}
Loading