// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Caching; namespace osu.Framework.Graphics.Animations { /// /// Represents a generic, frame-based animation. Inherit this class if you need custom animations. /// /// The type of content in the frames of the animation. public abstract class Animation : AnimationClockComposite, IFramedAnimation { /// /// The duration in milliseconds of a newly added frame, if no duration is explicitly specified when adding the frame. /// Defaults to 60fps. /// public double DefaultFrameLength = 1000.0 / 60.0; private readonly List> frameData; /// /// The number of frames this animation has. /// public int FrameCount => frameData.Count; public int CurrentFrameIndex { get; private set; } public T CurrentFrame => frameData[CurrentFrameIndex].Content; private readonly Cached currentFrameCache = new Cached(); /// /// Construct a new animation which loops by default. /// /// Whether the current clock time should be assumed as the 0th animation frame. protected Animation(bool startAtCurrentTime = true) : base(startAtCurrentTime) { frameData = new List>(); Loop = true; } /// /// Displays the frame with the given zero-based frame index. /// /// The zero-based index of the frame to display. public void GotoFrame(int frameIndex) { Seek(frameData[Math.Clamp(frameIndex, 0, frameData.Count)].DisplayStartTime); } /// /// Adds a new frame with the given content and display duration (in milliseconds) to this animation. /// /// The content of the new frame. /// The duration the new frame should be displayed for. public void AddFrame(T content, double? displayDuration = null) { AddFrame(new FrameData { Duration = displayDuration ?? DefaultFrameLength, // 60 fps by default Content = content, }); } public void AddFrame(FrameData frame) { var lastFrame = frameData.LastOrDefault(); frame.DisplayStartTime = lastFrame.DisplayEndTime; Duration += frame.Duration; frameData.Add(frame); OnFrameAdded(frame.Content, frame.Duration); } /// /// Adds a new frame for each element in the given enumerable. Every frame will be displayed for 1/60th of a second. /// /// The contents to use for creating new frames. public void AddFrames(IEnumerable contents) { foreach (var t in contents) AddFrame(t); } /// /// Adds a new frame for each element in the given enumerable. Every frame will be displayed for the given number of milliseconds. /// /// The contents and display durations to use for creating new frames. public void AddFrames(IEnumerable> frames) { foreach (var t in frames) AddFrame(t.Content, t.Duration); } /// /// Removes all frames from this animation. /// public void ClearFrames() { frameData.Clear(); Duration = 0; CurrentFrameIndex = 0; } /// /// Displays the given contents. /// /// This method will only be called after has been called at least once. /// The content that will be displayed. protected abstract void DisplayFrame(T content); /// /// Called whenever a new frame was added to this animation. /// /// The content of the new frame. /// The display duration of the new frame. protected virtual void OnFrameAdded(T content, double displayDuration) { } protected override void Update() { base.Update(); if (frameData.Count == 0) return; updateFrameIndex(); if (!currentFrameCache.IsValid) updateCurrentFrame(); } private void updateFrameIndex() { switch (PlaybackPosition.CompareTo(frameData[CurrentFrameIndex].DisplayStartTime)) { case -1: while (CurrentFrameIndex > 0 && PlaybackPosition < frameData[CurrentFrameIndex].DisplayStartTime) { CurrentFrameIndex--; currentFrameCache.Invalidate(); } break; case 1: while (CurrentFrameIndex < frameData.Count - 1 && PlaybackPosition >= frameData[CurrentFrameIndex].DisplayEndTime) { CurrentFrameIndex++; currentFrameCache.Invalidate(); } break; } } private void updateCurrentFrame() { DisplayFrame(CurrentFrame); UpdateSizing(); currentFrameCache.Validate(); } } }