diff --git a/CHANGELOG.md b/CHANGELOG.md index 03ad1af..fdd8d35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,7 @@ Removals ### MLEM.Ui Additions - Added AutoInlineCenter and AutoInlineBottom anchors +- Added UiAnimation system Improvements - Increased Element area calculation recursion limit to 64 diff --git a/Demos/UiDemo.cs b/Demos/UiDemo.cs index 5fb7503..a3381c0 100644 --- a/Demos/UiDemo.cs +++ b/Demos/UiDemo.cs @@ -164,21 +164,13 @@ namespace Demos { PositionOffset = new Vector2(0, 1) }); // Another button that shows animations! - var fancyHoverTimer = 0D; - var fancyButton = this.root.AddChild(new Button(Anchor.AutoCenter, new Vector2(0.5F, 10), "Fancy Hover") { + this.root.AddChild(new Button(Anchor.AutoCenter, new Vector2(0.5F, 10), "Fancy Hover") { PositionOffset = new Vector2(0, 1), - OnUpdated = (e, time) => { - if (e.IsMouseOver && fancyHoverTimer <= 0.5F) - return; - if (fancyHoverTimer > 0) { - fancyHoverTimer -= time.ElapsedGameTime.TotalSeconds * 3; - e.ScaleTransform(1 + (float) Math.Sin(fancyHoverTimer * MathHelper.Pi) * 0.05F); - } else { - e.Transform = Matrix.Identity; - } + MouseEnterAnimation = new UiAnimation(0.15, (a, e, p) => e.ScaleTransform(1 + Easings.OutSine(p) * 0.05F)), + MouseExitAnimation = new UiAnimation(0.15, (a, e, p) => e.ScaleTransform(1 + Easings.OutSine.ReverseOutput()(p) * 0.05F)) { + Finished = (a, e) => e.Transform = Matrix.Identity } }); - fancyButton.OnMouseEnter += e => fancyHoverTimer = 1; this.root.AddChild(new Button(Anchor.AutoCenter, new Vector2(0.5F, 10), "Transform Ui", "This button causes the entire ui to be transformed (both in positioning, rotation and scale)") { OnPressed = element => { if (element.Root.Transform == Matrix.Identity) { diff --git a/MLEM.Ui/Elements/Element.cs b/MLEM.Ui/Elements/Element.cs index 4b5192c..e97b1c7 100644 --- a/MLEM.Ui/Elements/Element.cs +++ b/MLEM.Ui/Elements/Element.cs @@ -379,6 +379,14 @@ namespace MLEM.Ui.Elements { this.SetAreaDirty(); } } + /// + /// A that is played when the mouse enters this element, in . + /// + public StyleProp MouseEnterAnimation; + /// + /// A that is played when the mouse exits this element, in . + /// + public StyleProp MouseExitAnimation; /// /// Event that is called after this element is drawn, but before its children are drawn @@ -490,6 +498,12 @@ namespace MLEM.Ui.Elements { /// Use or to manipulate this list while calling all of the necessary callbacks. /// protected readonly IList Children; + /// + /// A list of all of the instances that are currently playing. + /// You can modify this collection through and . + /// + protected readonly List PlayingAnimations = new List(); + /// /// A sorted version of . The children are sorted by their . /// @@ -541,8 +555,16 @@ namespace MLEM.Ui.Elements { this.size = size; this.Children = new ReadOnlyCollection(this.children); - this.GetTabNextElement = (backward, next) => next; - this.GetGamepadNextElement = (dir, next) => next; + this.GetTabNextElement += (backward, next) => next; + this.GetGamepadNextElement += (dir, next) => next; + this.OnMouseEnter += e => { + if (e.MouseEnterAnimation.HasValue()) + e.PlayAnimation(e.MouseEnterAnimation); + }; + this.OnMouseExit += e => { + if (e.MouseExitAnimation.HasValue()) + e.PlayAnimation(e.MouseExitAnimation); + }; this.SetAreaDirty(); this.SetSortedChildrenDirty(); @@ -1028,6 +1050,14 @@ namespace MLEM.Ui.Elements { public virtual void Update(GameTime time) { this.System.InvokeOnElementUpdated(this, time); + for (var i = this.PlayingAnimations.Count - 1; i >= 0; i--) { + var anim = this.PlayingAnimations[i]; + if (anim.Update(this, time)) { + anim.OnFinished(this); + this.PlayingAnimations.RemoveAt(i); + } + } + // update all sorted children, not just relevant ones, because they might become relevant or irrelevant through updates foreach (var child in this.SortedChildren) { if (child.System != null) @@ -1170,6 +1200,33 @@ namespace MLEM.Ui.Elements { return this.CanBeMoused && this.DisplayArea.Contains(position) ? this : null; } + /// + /// Plays the given on this element, causing it to be added to the and updated in . + /// If the given is already playing on this element, it will be restarted. + /// + /// The animation to play. + public virtual void PlayAnimation(UiAnimation animation) { + if (this.PlayingAnimations.Contains(animation)) { + // if we're already playing this animation, just restart it + animation.OnFinished(this); + } else { + this.PlayingAnimations.Add(animation); + } + } + + /// + /// Stops the given on this element, causing it to be removed from the and to be invoked. + /// + /// The animation to stop. + /// Whether the animation was present in this element's . + public virtual bool StopAnimation(UiAnimation animation) { + if (this.PlayingAnimations.Remove(animation)) { + animation.OnFinished(this); + return true; + } + return false; + } + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. [Obsolete("Dispose will be removed in a future update. To unregister custom event handlers, use OnRemovedFromUi instead.")] public virtual void Dispose() { diff --git a/MLEM.Ui/UiAnimation.cs b/MLEM.Ui/UiAnimation.cs new file mode 100644 index 0000000..61a9a3c --- /dev/null +++ b/MLEM.Ui/UiAnimation.cs @@ -0,0 +1,86 @@ +using System; +using Microsoft.Xna.Framework; +using MLEM.Misc; +using MLEM.Ui.Elements; + +namespace MLEM.Ui { + /// + /// A ui animation is a simple timed event that an in a can use to play a visual or other type of animation. + /// To use ui animations, you can use , or one of the built-in style properties like or . + /// + public class UiAnimation : GenericDataHolder { + + /// + /// The total time that this ui animation plays for. + /// + public readonly TimeSpan TotalTime; + /// + /// The that is invoked every . + /// + public readonly AnimationFunction Function; + + /// + /// An event that is raised when this ui animation is (re)started in . + /// + public Action Started; + /// + /// An event that is raised when this ui animation is stopped or finished through . + /// + public Action Finished; + /// + /// The current time that this ui animation has been playing for, out of the . + /// + public TimeSpan CurrentTime { get; private set; } + + /// + /// Creates a new ui animation with the given settings. + /// + /// The amount of seconds that this ui animation should play for. + /// The that is invoked every . + public UiAnimation(double seconds, AnimationFunction function) : this(TimeSpan.FromSeconds(seconds), function) {} + + /// + /// Creates a new ui animation with the given settings. + /// + /// The that this ui animation should play for. + /// The that is invoked every . + public UiAnimation(TimeSpan totalTime, AnimationFunction function) { + this.TotalTime = totalTime; + this.Function = function; + } + + /// + /// Updates this ui animation, invoking its event if necessary, increasing its and invoking its . + /// This method is called by an in . + /// + /// The element that this ui animation is attached to. + /// The game's current time. + /// Whether this animation is ready to finish, that is, if its is greater than or equal to its . + public virtual bool Update(Element element, GameTime time) { + if (this.CurrentTime <= TimeSpan.Zero) + this.Started?.Invoke(this, element); + + this.CurrentTime += time.ElapsedGameTime; + this.Function?.Invoke(this, element, this.CurrentTime.Ticks / (float) this.TotalTime.Ticks); + + return this.CurrentTime >= this.TotalTime; + } + + /// + /// Causes this ui animation's event to be raised, and sets the to . + /// This allows the animation to play from the start again. + /// This method is invoked automatically when returns in , as well as in . + /// + /// + public virtual void OnFinished(Element element) { + this.Finished?.Invoke(this, element); + this.CurrentTime = TimeSpan.Zero; + } + + /// + /// A delegate method used by . + /// + public delegate void AnimationFunction(UiAnimation animation, Element element, float timePercentage); + + } +}