A game framework written with osu! in mind.
1// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
2// See the LICENCE file in the repository root for full licence text.
3
4using System;
5using System.Collections.Generic;
6
7namespace osu.Framework.Graphics.Transforms
8{
9 /// <summary>
10 /// A sequence of <see cref="Transform"/>s all operating upon the same <see cref="ITransformable"/>
11 /// of type <typeparamref name="T"/>.
12 /// Exposes various operations to extend the sequence by additional <see cref="Transforms"/> such as
13 /// delays, loops, continuations, and events.
14 /// </summary>
15 /// <typeparam name="T">
16 /// The type of the <see cref="ITransformable"/> the <see cref="Transform"/>s in this sequence operate upon.
17 /// </typeparam>
18 public class TransformSequence<T> : ITransformSequence where T : class, ITransformable
19 {
20 /// <summary>
21 /// A delegate that generates a new <see cref="TransformSequence{T}"/> on a given <paramref name="origin"/>.
22 /// </summary>
23 /// <param name="origin">The origin to generate a <see cref="TransformSequence{T}"/> for.</param>
24 /// <returns>The generated <see cref="TransformSequence{T}"/>.</returns>
25 public delegate TransformSequence<T> Generator(T origin);
26
27 private readonly T origin;
28
29 private readonly List<Transform> transforms = new List<Transform>(1); // the most common usage of transforms sees one transform being added.
30
31 private bool hasCompleted = true;
32
33 private readonly double startTime;
34 private double currentTime;
35 private double endTime => Math.Max(currentTime, lastEndTime);
36
37 private Transform last;
38 private double lastEndTime;
39
40 private bool hasEnd => lastEndTime != double.PositiveInfinity;
41
42 /// <summary>
43 /// Creates a new empty <see cref="TransformSequence{T}"/> attached to a given <paramref name="origin"/>.
44 /// </summary>
45 /// <param name="origin">The <typeparamref name="T"/> to attach the new <see cref="TransformSequence{T}"/> to.</param>
46 public TransformSequence(T origin)
47 {
48 if (origin == null)
49 throw new ArgumentNullException(nameof(origin), $"May not create a {nameof(TransformSequence<T>)} with a null {nameof(origin)}.");
50
51 this.origin = origin;
52 startTime = currentTime = lastEndTime = origin.TransformStartTime;
53 }
54
55 private void onLoopingTransform()
56 {
57 // As soon as we have an infinitely looping transform,
58 // completion no longer makes sense.
59 if (last != null)
60 last.CompletionTargetSequence = null;
61
62 last = null;
63 lastEndTime = double.PositiveInfinity;
64 hasCompleted = false;
65 }
66
67 public TransformSequence<T> TransformTo<TValue>(string propertyOrFieldName, TValue newValue, double duration = 0, Easing easing = Easing.None) =>
68 Append(o => o.TransformTo(propertyOrFieldName, newValue, duration, easing));
69
70 /// <summary>
71 /// Adds an existing <see cref="Transform"/> operating on <see cref="origin"/> to this <see cref="TransformSequence{T}"/>.
72 /// </summary>
73 /// <param name="transform">The <see cref="Transform"/> to add.</param>
74 internal void Add(Transform transform)
75 {
76 if (!ReferenceEquals(transform.TargetTransformable, origin))
77 {
78 throw new InvalidOperationException(
79 $"{nameof(transform)} must operate upon {nameof(origin)}={origin}, but operates upon {transform.TargetTransformable}.");
80 }
81
82 transforms.Add(transform);
83
84 transform.CompletionTargetSequence = null;
85 transform.AbortTargetSequence = this;
86
87 if (transform.IsLooping)
88 onLoopingTransform();
89
90 // Update last transform for completion callback
91 if (last == null || transform.EndTime > lastEndTime)
92 {
93 if (last != null)
94 last.CompletionTargetSequence = null;
95
96 last = transform;
97 last.CompletionTargetSequence = this;
98 lastEndTime = last.EndTime;
99 hasCompleted = false;
100 }
101 }
102
103 /// <summary>
104 /// Appends multiple <see cref="Generator"/>s to this <see cref="TransformSequence{T}"/>.
105 /// </summary>
106 /// <param name="childGenerators">The <see cref="Generator"/>s to be appended.</param>
107 /// <returns>This <see cref="TransformSequence{T}"/>.</returns>
108 public TransformSequence<T> Append(IEnumerable<Generator> childGenerators)
109 {
110 foreach (var p in childGenerators)
111 Append(p);
112
113 return this;
114 }
115
116 /// <summary>
117 /// Appends a <see cref="Generator"/>s to this <see cref="TransformSequence{T}"/>.
118 /// The <see cref="Generator"/> is invoked within a <see cref="Transformable.BeginAbsoluteSequence(double, bool)"/>
119 /// such that the generated <see cref="TransformSequence{T}"/> starts at the correct point in time.
120 /// Its <see cref="Transform"/>s are then merged into this <see cref="TransformSequence{T}"/>.
121 /// </summary>
122 /// <param name="childGenerator">The <see cref="Generator"/> to be appended.</param>
123 /// <returns>This <see cref="TransformSequence{T}"/>.</returns>
124 public TransformSequence<T> Append(Generator childGenerator)
125 {
126 TransformSequence<T> child;
127 using (origin.BeginAbsoluteSequence(currentTime, false))
128 child = childGenerator(origin);
129
130 if (!ReferenceEquals(child.origin, origin))
131 throw new InvalidOperationException($"May not append {nameof(TransformSequence<T>)} with different origin.");
132
133 var oldLast = last;
134 foreach (var t in child.transforms)
135 Add(t);
136
137 // If we flatten a child into ourselves that already completed, then
138 // we need to make sure to update the hasCompleted value, too, since
139 // the already completed final transform will no longer fire any events.
140 if (oldLast != last)
141 hasCompleted = child.hasCompleted;
142
143 return this;
144 }
145
146 /// <summary>
147 /// Invokes <paramref name="originFunc"/> inside a <see cref="Transformable.BeginAbsoluteSequence(double, bool)"/>
148 /// such that <see cref="ITransformable.TransformStartTime"/> is the current time of this <see cref="TransformSequence{T}"/>.
149 /// It is the responsibility of <paramref name="originFunc"/> to make appropriate use of <see cref="ITransformable.TransformStartTime"/>.
150 /// </summary>
151 /// <typeparam name="TResult">The return type of <paramref name="originFunc"/>.</typeparam>
152 /// <param name="originFunc">The function to be invoked.</param>
153 /// <param name="result">The resulting value of the invocation of <paramref name="originFunc"/>.</param>
154 /// <returns>This <see cref="TransformSequence{T}"/>.</returns>
155 public TransformSequence<T> Append<TResult>(Func<T, TResult> originFunc, out TResult result)
156 {
157 using (origin.BeginAbsoluteSequence(currentTime, false))
158 result = originFunc(origin);
159
160 return this;
161 }
162
163 /// <summary>
164 /// Invokes <paramref name="originAction"/> inside a <see cref="Transformable.BeginAbsoluteSequence(double, bool)"/>
165 /// such that <see cref="ITransformable.TransformStartTime"/> is the current time of this <see cref="TransformSequence{T}"/>.
166 /// It is the responsibility of <paramref name="originAction"/> to make appropriate use of <see cref="ITransformable.TransformStartTime"/>.
167 /// </summary>
168 /// <param name="originAction">The function to be invoked.</param>
169 /// <returns>This <see cref="TransformSequence{T}"/>.</returns>
170 public TransformSequence<T> Append(Action<T> originAction)
171 {
172 using (origin.BeginAbsoluteSequence(currentTime, false))
173 originAction(origin);
174
175 return this;
176 }
177
178 private void subscribeComplete(Action func)
179 {
180 if (onComplete != null)
181 {
182 throw new InvalidOperationException(
183 "May not subscribe completion multiple times." +
184 $"This exception is also caused by calling {nameof(Then)} or {nameof(Finally)} on an infinitely looping {nameof(TransformSequence<T>)}.");
185 }
186
187 onComplete = func;
188
189 // Completion can be immediately triggered by instant transforms,
190 // and therefore when subscribing we need to take into account
191 // potential previous completions.
192 if (hasCompleted)
193 func();
194 }
195
196 private void subscribeAbort(Action func)
197 {
198 if (onAbort != null)
199 throw new InvalidOperationException("May not subscribe abort multiple times.");
200
201 // No need to worry about new transforms immediately aborting, so
202 // we can just subscribe here and be sure abort couldn't have been
203 // triggered already.
204 onAbort = func;
205 }
206
207 private Action onComplete;
208 private Action onAbort;
209
210 /// <summary>
211 /// Append a looping <see cref="TransformSequence{T}"/> to this <see cref="TransformSequence{T}"/>.
212 /// All <see cref="Transform"/>s generated by <paramref name="childGenerators"/> are appended to
213 /// this <see cref="TransformSequence{T}"/> and then repeated <paramref name="numIters"/> times
214 /// with <paramref name="pause"/> milliseconds between iterations.
215 /// </summary>
216 /// <param name="pause">The pause between iterations in milliseconds.</param>
217 /// <param name="numIters">The number of iterations.</param>
218 /// <param name="childGenerators">The functions to generate the <see cref="TransformSequence{T}"/>s to be looped.</param>
219 /// <returns>This <see cref="TransformSequence{T}"/>.</returns>
220 public TransformSequence<T> Loop(double pause, int numIters, params Generator[] childGenerators)
221 {
222 Append(o =>
223 {
224 var childSequence = new TransformSequence<T>(o);
225 childSequence.Append(childGenerators);
226 childSequence.Loop(pause, numIters);
227 return childSequence;
228 });
229
230 return this;
231 }
232
233 /// <summary>
234 /// Repeats all <see cref="Transform"/>s within this <see cref="TransformSequence{T}"/>
235 /// <paramref name="numIters"/> times with <paramref name="pause"/> milliseconds between iterations.
236 /// </summary>
237 /// <param name="pause">The pause between iterations in milliseconds.</param>
238 /// <param name="numIters">The number of iterations.</param>
239 /// <returns>This <see cref="TransformSequence{T}"/>.</returns>
240 public TransformSequence<T> Loop(double pause, int numIters)
241 {
242 if (numIters < 1)
243 throw new InvalidOperationException($"May not {nameof(Loop)} for fewer than 1 iteration ({numIters} attempted).");
244
245 if (!hasEnd)
246 throw new InvalidOperationException($"Can not perform {nameof(Loop)} on an endless {nameof(TransformSequence<T>)}.");
247
248 var iterDuration = endTime - startTime + pause;
249 var toLoop = transforms.ToArray();
250
251 // Duplicate existing transforms numIters times
252 for (int i = 1; i < numIters; ++i)
253 {
254 foreach (var t in toLoop)
255 {
256 var clone = t.Clone();
257
258 clone.StartTime += i * iterDuration;
259 clone.EndTime += i * iterDuration;
260
261 clone.AppliedToEnd = false;
262 clone.Applied = false;
263
264 Add(clone);
265 t.TargetTransformable.AddTransform(clone);
266 }
267 }
268
269 return this;
270 }
271
272 /// <summary>
273 /// Append a looping <see cref="TransformSequence{T}"/> to this <see cref="TransformSequence{T}"/>.
274 /// All <see cref="Transform"/>s generated by <paramref name="childGenerators"/> are appended to
275 /// this <see cref="TransformSequence{T}"/> and then repeated indefinitely.
276 /// </summary>
277 /// <param name="childGenerators">The functions to generate the <see cref="TransformSequence{T}"/>s to be looped.</param>
278 /// <returns>This <see cref="TransformSequence{T}"/>.</returns>
279 public TransformSequence<T> Loop(params Generator[] childGenerators) => Loop(0, childGenerators);
280
281 /// <summary>
282 /// Append a looping <see cref="TransformSequence{T}"/> to this <see cref="TransformSequence{T}"/>.
283 /// All <see cref="Transform"/>s generated by <paramref name="childGenerators"/> are appended to
284 /// this <see cref="TransformSequence{T}"/> and then repeated indefinitely with <paramref name="pause"/>
285 /// milliseconds between iterations.
286 /// </summary>
287 /// <param name="pause">The pause between iterations in milliseconds.</param>
288 /// <param name="childGenerators">The functions to generate the <see cref="TransformSequence{T}"/>s to be looped.</param>
289 /// <returns>This <see cref="TransformSequence{T}"/>.</returns>
290 public TransformSequence<T> Loop(double pause, params Generator[] childGenerators)
291 {
292 Append(o =>
293 {
294 var childSequence = new TransformSequence<T>(o);
295 childSequence.Append(childGenerators);
296 childSequence.Loop(pause);
297 return childSequence;
298 });
299
300 return this;
301 }
302
303 /// <summary>
304 /// Repeats all <see cref="Transform"/>s within this <see cref="TransformSequence{T}"/> indefinitely.
305 /// </summary>
306 /// <param name="pause">The pause between iterations in milliseconds.</param>
307 /// <returns>This <see cref="TransformSequence{T}"/>.</returns>
308 public TransformSequence<T> Loop(double pause = 0)
309 {
310 if (!hasEnd)
311 throw new InvalidOperationException($"Can not perform {nameof(Loop)} on an endless {nameof(TransformSequence<T>)}.");
312
313 var iterDuration = endTime - startTime + pause;
314
315 foreach (var t in transforms)
316 {
317 var tmpOnAbort = t.AbortTargetSequence;
318 t.AbortTargetSequence = null;
319 t.TargetTransformable.RemoveTransform(t);
320 t.AbortTargetSequence = tmpOnAbort;
321
322 // Update start and end times such that no transformations need to be instantly
323 // looped right after they're added. This is required so that transforms can be
324 // inserted in the correct order such that none of them trigger abortions on
325 // each other due to instant re-sorting upon adding.
326 double currentTransformTime = t.TargetTransformable.Time.Current;
327
328 while (t.EndTime <= currentTransformTime)
329 {
330 t.StartTime += iterDuration;
331 t.EndTime += iterDuration;
332 }
333 }
334
335 // This sort is required such that no abortions happen.
336 var sortedTransforms = new List<Transform>(transforms);
337 sortedTransforms.Sort(Transform.COMPARER);
338
339 foreach (var t in sortedTransforms)
340 {
341 t.IsLooping = true;
342 t.LoopDelay = iterDuration;
343
344 t.Applied = false;
345 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.
346
347 t.TargetTransformable.AddTransform(t, t.TransformID);
348 }
349
350 onLoopingTransform();
351 return this;
352 }
353
354 /// <summary>
355 /// Advances the start time of future appended <see cref="TransformSequence{T}"/>s to the latest end time of all
356 /// <see cref="Transform"/>s in this <see cref="TransformSequence{T}"/>.
357 /// Then, <paramref name="childGenerators"/> are appended via <see cref="Append(IEnumerable{Generator})"/>.
358 /// </summary>
359 /// <param name="childGenerators">The optional <see cref="Generator"/>s for <see cref="TransformSequence{T}"/>s to be appended.</param>
360 /// <returns>This <see cref="TransformSequence{T}"/>.</returns>
361 public TransformSequence<T> Then(params Generator[] childGenerators) => Then(0, childGenerators);
362
363 /// <summary>
364 /// Advances the start time of future appended <see cref="TransformSequence{T}"/>s to the latest end time of all
365 /// <see cref="Transform"/>s in this <see cref="TransformSequence{T}"/> plus <paramref name="delay"/> milliseconds.
366 /// Then, <paramref name="childGenerators"/> are appended via <see cref="Append(IEnumerable{Generator})"/>.
367 /// </summary>
368 /// <param name="delay">The delay after the latest end time of all <see cref="Transform"/>s.</param>
369 /// <param name="childGenerators">The optional <see cref="Generator"/>s for <see cref="TransformSequence{T}"/>s to be appended.</param>
370 /// <returns>This <see cref="TransformSequence{T}"/>.</returns>
371 public TransformSequence<T> Then(double delay, params Generator[] childGenerators)
372 {
373 if (!hasEnd)
374 throw new InvalidOperationException($"Can not perform {nameof(Then)} on an endless {nameof(TransformSequence<T>)}.");
375
376 // "Then" simply sets the currentTime to endTime to continue where the last transform left off,
377 // followed by a subsequent delay call.
378 currentTime = endTime;
379 return Delay(delay, childGenerators);
380 }
381
382 /// <summary>
383 /// Advances the start time of future appended <see cref="TransformSequence{T}"/>s by <paramref name="delay"/> milliseconds.
384 /// Then, <paramref name="childGenerators"/> are appended via <see cref="Append(IEnumerable{Generator})"/>.
385 /// </summary>
386 /// <param name="delay">The delay to advance the start time by.</param>
387 /// <param name="childGenerators">The optional <see cref="Generator"/>s for <see cref="TransformSequence{T}"/>s to be appended.</param>
388 /// <returns>This <see cref="TransformSequence{T}"/>.</returns>
389 public TransformSequence<T> Delay(double delay, params Generator[] childGenerators)
390 {
391 // After a delay statement, future transforms are appended after a currentTime which got offset by a delay.
392 currentTime += delay;
393 return Append(childGenerators);
394 }
395
396 /// <summary>
397 /// Registers a callback <paramref name="function"/> which is triggered once all <see cref="Transform"/>s in this
398 /// <see cref="TransformSequence{T}"/> complete successfully.
399 /// If all <see cref="Transform"/>s already completed successfully at the point of this call, then
400 /// <paramref name="function"/> is triggered immediately.
401 /// Only a single callback function may be registered.
402 /// </summary>
403 /// <param name="function">The callback function.</param>
404 public void OnComplete(Action<T> function)
405 {
406 if (!hasEnd)
407 throw new InvalidOperationException($"Can not perform {nameof(Then)} on an endless {nameof(TransformSequence<T>)}.");
408
409 subscribeComplete(() => function(origin));
410 }
411
412 /// <summary>
413 /// Registers a callback <paramref name="function"/> which is triggered once any <see cref="Transform"/> in this
414 /// <see cref="TransformSequence{T}"/> is aborted (e.g. by another <see cref="Transform"/> overriding it).
415 /// Only a single callback function may be registered.
416 /// </summary>
417 /// <param name="function">The callback function.</param>
418 public void OnAbort(Action<T> function) => subscribeAbort(() => function(origin));
419
420 /// <summary>
421 /// Registers a callback <paramref name="function"/> which is triggered once any <see cref="Transform"/> in this
422 /// <see cref="TransformSequence{T}"/> is aborted or when all <see cref="Transform"/>s complete successfully.
423 /// This is equivalent with calling both <see cref="OnComplete(Action{T})"/> and <see cref="OnAbort(Action{T})"/>.
424 /// Only a single callback function may be registered.
425 /// </summary>
426 /// <param name="function">The callback function.</param>
427 public void Finally(Action<T> function)
428 {
429 if (hasEnd)
430 OnComplete(function);
431 OnAbort(function);
432 }
433
434 void ITransformSequence.TransformAborted()
435 {
436 if (transforms.Count == 0)
437 return;
438
439 // No need for OnAbort events to trigger anymore, since
440 // we are already aware of the abortion.
441 foreach (var t in transforms)
442 {
443 t.AbortTargetSequence = null;
444 t.CompletionTargetSequence = null;
445
446 if (!t.HasStartValue)
447 t.TargetTransformable.RemoveTransform(t);
448 }
449
450 transforms.Clear();
451 last = null;
452
453 onAbort?.Invoke();
454 }
455
456 void ITransformSequence.TransformCompleted()
457 {
458 hasCompleted = true;
459 onComplete?.Invoke();
460 }
461 }
462}