// 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; namespace osu.Framework.Graphics.Transforms { /// /// A sequence of s all operating upon the same /// of type . /// Exposes various operations to extend the sequence by additional such as /// delays, loops, continuations, and events. /// /// /// The type of the the s in this sequence operate upon. /// public class TransformSequence : ITransformSequence where T : class, ITransformable { /// /// A delegate that generates a new on a given . /// /// The origin to generate a for. /// The generated . public delegate TransformSequence Generator(T origin); private readonly T origin; private readonly List transforms = new List(1); // the most common usage of transforms sees one transform being added. private bool hasCompleted = true; private readonly double startTime; private double currentTime; private double endTime => Math.Max(currentTime, lastEndTime); private Transform last; private double lastEndTime; private bool hasEnd => lastEndTime != double.PositiveInfinity; /// /// Creates a new empty attached to a given . /// /// The to attach the new to. public TransformSequence(T origin) { if (origin == null) throw new ArgumentNullException(nameof(origin), $"May not create a {nameof(TransformSequence)} with a null {nameof(origin)}."); this.origin = origin; startTime = currentTime = lastEndTime = origin.TransformStartTime; } private void onLoopingTransform() { // As soon as we have an infinitely looping transform, // completion no longer makes sense. if (last != null) last.CompletionTargetSequence = null; last = null; lastEndTime = double.PositiveInfinity; hasCompleted = false; } public TransformSequence TransformTo(string propertyOrFieldName, TValue newValue, double duration = 0, Easing easing = Easing.None) => Append(o => o.TransformTo(propertyOrFieldName, newValue, duration, easing)); /// /// Adds an existing operating on to this . /// /// The to add. internal void Add(Transform transform) { if (!ReferenceEquals(transform.TargetTransformable, origin)) { throw new InvalidOperationException( $"{nameof(transform)} must operate upon {nameof(origin)}={origin}, but operates upon {transform.TargetTransformable}."); } transforms.Add(transform); transform.CompletionTargetSequence = null; transform.AbortTargetSequence = this; if (transform.IsLooping) onLoopingTransform(); // Update last transform for completion callback if (last == null || transform.EndTime > lastEndTime) { if (last != null) last.CompletionTargetSequence = null; last = transform; last.CompletionTargetSequence = this; lastEndTime = last.EndTime; hasCompleted = false; } } /// /// Appends multiple s to this . /// /// The s to be appended. /// This . public TransformSequence Append(IEnumerable childGenerators) { foreach (var p in childGenerators) Append(p); return this; } /// /// Appends a s to this . /// The is invoked within a /// such that the generated starts at the correct point in time. /// Its s are then merged into this . /// /// The to be appended. /// This . public TransformSequence Append(Generator childGenerator) { TransformSequence child; using (origin.BeginAbsoluteSequence(currentTime, false)) child = childGenerator(origin); if (!ReferenceEquals(child.origin, origin)) throw new InvalidOperationException($"May not append {nameof(TransformSequence)} with different origin."); var oldLast = last; foreach (var t in child.transforms) Add(t); // If we flatten a child into ourselves that already completed, then // we need to make sure to update the hasCompleted value, too, since // the already completed final transform will no longer fire any events. if (oldLast != last) hasCompleted = child.hasCompleted; return this; } /// /// Invokes inside a /// such that is the current time of this . /// It is the responsibility of to make appropriate use of . /// /// The return type of . /// The function to be invoked. /// The resulting value of the invocation of . /// This . public TransformSequence Append(Func originFunc, out TResult result) { using (origin.BeginAbsoluteSequence(currentTime, false)) result = originFunc(origin); return this; } /// /// Invokes inside a /// such that is the current time of this . /// It is the responsibility of to make appropriate use of . /// /// The function to be invoked. /// This . public TransformSequence Append(Action originAction) { using (origin.BeginAbsoluteSequence(currentTime, false)) originAction(origin); return this; } private void subscribeComplete(Action func) { if (onComplete != null) { throw new InvalidOperationException( "May not subscribe completion multiple times." + $"This exception is also caused by calling {nameof(Then)} or {nameof(Finally)} on an infinitely looping {nameof(TransformSequence)}."); } onComplete = func; // Completion can be immediately triggered by instant transforms, // and therefore when subscribing we need to take into account // potential previous completions. if (hasCompleted) func(); } private void subscribeAbort(Action func) { if (onAbort != null) throw new InvalidOperationException("May not subscribe abort multiple times."); // No need to worry about new transforms immediately aborting, so // we can just subscribe here and be sure abort couldn't have been // triggered already. onAbort = func; } private Action onComplete; private Action onAbort; /// /// Append a looping to this . /// All s generated by are appended to /// this and then repeated times /// with milliseconds between iterations. /// /// The pause between iterations in milliseconds. /// The number of iterations. /// The functions to generate the s to be looped. /// This . public TransformSequence Loop(double pause, int numIters, params Generator[] childGenerators) { Append(o => { var childSequence = new TransformSequence(o); childSequence.Append(childGenerators); childSequence.Loop(pause, numIters); return childSequence; }); return this; } /// /// Repeats all s within this /// times with milliseconds between iterations. /// /// The pause between iterations in milliseconds. /// The number of iterations. /// This . public TransformSequence Loop(double pause, int numIters) { if (numIters < 1) throw new InvalidOperationException($"May not {nameof(Loop)} for fewer than 1 iteration ({numIters} attempted)."); if (!hasEnd) throw new InvalidOperationException($"Can not perform {nameof(Loop)} on an endless {nameof(TransformSequence)}."); var iterDuration = endTime - startTime + pause; var toLoop = transforms.ToArray(); // Duplicate existing transforms numIters times for (int i = 1; i < numIters; ++i) { foreach (var t in toLoop) { var clone = t.Clone(); clone.StartTime += i * iterDuration; clone.EndTime += i * iterDuration; clone.AppliedToEnd = false; clone.Applied = false; Add(clone); t.TargetTransformable.AddTransform(clone); } } return this; } /// /// Append a looping to this . /// All s generated by are appended to /// this and then repeated indefinitely. /// /// The functions to generate the s to be looped. /// This . public TransformSequence Loop(params Generator[] childGenerators) => Loop(0, childGenerators); /// /// Append a looping to this . /// All s generated by are appended to /// this and then repeated indefinitely with /// milliseconds between iterations. /// /// The pause between iterations in milliseconds. /// The functions to generate the s to be looped. /// This . public TransformSequence Loop(double pause, params Generator[] childGenerators) { Append(o => { var childSequence = new TransformSequence(o); childSequence.Append(childGenerators); childSequence.Loop(pause); return childSequence; }); return this; } /// /// Repeats all s within this indefinitely. /// /// The pause between iterations in milliseconds. /// This . public TransformSequence Loop(double pause = 0) { if (!hasEnd) throw new InvalidOperationException($"Can not perform {nameof(Loop)} on an endless {nameof(TransformSequence)}."); var iterDuration = endTime - startTime + pause; foreach (var t in transforms) { var tmpOnAbort = t.AbortTargetSequence; t.AbortTargetSequence = null; t.TargetTransformable.RemoveTransform(t); t.AbortTargetSequence = tmpOnAbort; // Update start and end times such that no transformations need to be instantly // looped right after they're added. This is required so that transforms can be // inserted in the correct order such that none of them trigger abortions on // each other due to instant re-sorting upon adding. double currentTransformTime = t.TargetTransformable.Time.Current; while (t.EndTime <= currentTransformTime) { t.StartTime += iterDuration; t.EndTime += iterDuration; } } // This sort is required such that no abortions happen. var sortedTransforms = new List(transforms); sortedTransforms.Sort(Transform.COMPARER); foreach (var t in sortedTransforms) { t.IsLooping = true; t.LoopDelay = iterDuration; t.Applied = false; t.AppliedToEnd = false; // we want to force a reprocess of this transform. it may have been applied-to-end in the Add, but not correctly looped as a result. t.TargetTransformable.AddTransform(t, t.TransformID); } onLoopingTransform(); return this; } /// /// Advances the start time of future appended s to the latest end time of all /// s in this . /// Then, are appended via . /// /// The optional s for s to be appended. /// This . public TransformSequence Then(params Generator[] childGenerators) => Then(0, childGenerators); /// /// Advances the start time of future appended s to the latest end time of all /// s in this plus milliseconds. /// Then, are appended via . /// /// The delay after the latest end time of all s. /// The optional s for s to be appended. /// This . public TransformSequence Then(double delay, params Generator[] childGenerators) { if (!hasEnd) throw new InvalidOperationException($"Can not perform {nameof(Then)} on an endless {nameof(TransformSequence)}."); // "Then" simply sets the currentTime to endTime to continue where the last transform left off, // followed by a subsequent delay call. currentTime = endTime; return Delay(delay, childGenerators); } /// /// Advances the start time of future appended s by milliseconds. /// Then, are appended via . /// /// The delay to advance the start time by. /// The optional s for s to be appended. /// This . public TransformSequence Delay(double delay, params Generator[] childGenerators) { // After a delay statement, future transforms are appended after a currentTime which got offset by a delay. currentTime += delay; return Append(childGenerators); } /// /// Registers a callback which is triggered once all s in this /// complete successfully. /// If all s already completed successfully at the point of this call, then /// is triggered immediately. /// Only a single callback function may be registered. /// /// The callback function. public void OnComplete(Action function) { if (!hasEnd) throw new InvalidOperationException($"Can not perform {nameof(Then)} on an endless {nameof(TransformSequence)}."); subscribeComplete(() => function(origin)); } /// /// Registers a callback which is triggered once any in this /// is aborted (e.g. by another overriding it). /// Only a single callback function may be registered. /// /// The callback function. public void OnAbort(Action function) => subscribeAbort(() => function(origin)); /// /// Registers a callback which is triggered once any in this /// is aborted or when all s complete successfully. /// This is equivalent with calling both and . /// Only a single callback function may be registered. /// /// The callback function. public void Finally(Action function) { if (hasEnd) OnComplete(function); OnAbort(function); } void ITransformSequence.TransformAborted() { if (transforms.Count == 0) return; // No need for OnAbort events to trigger anymore, since // we are already aware of the abortion. foreach (var t in transforms) { t.AbortTargetSequence = null; t.CompletionTargetSequence = null; if (!t.HasStartValue) t.TargetTransformable.RemoveTransform(t); } transforms.Clear(); last = null; onAbort?.Invoke(); } void ITransformSequence.TransformCompleted() { hasCompleted = true; onComplete?.Invoke(); } } }