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;
6using System.Linq;
7using NUnit.Framework;
8using osu.Framework.Allocation;
9using osu.Framework.Graphics;
10using osu.Framework.Graphics.Containers;
11using osu.Framework.Graphics.Shapes;
12using osu.Framework.Graphics.Sprites;
13using osu.Framework.Graphics.Transforms;
14using osu.Framework.Graphics.Visualisation;
15using osu.Framework.Utils;
16using osu.Framework.Timing;
17using osuTK;
18using osuTK.Graphics;
19
20namespace osu.Framework.Tests.Visual.Drawables
21{
22 public class TestSceneTransformRewinding : FrameworkTestScene
23 {
24 private const double interval = 250;
25 private const int interval_count = 4;
26
27 private static double intervalAt(int sequence) => interval * sequence;
28
29 private ManualClock manualClock;
30 private FramedClock manualFramedClock;
31
32 [SetUp]
33 public void SetUp() => Schedule(() =>
34 {
35 Clear();
36 manualClock = new ManualClock();
37 manualFramedClock = new FramedClock(manualClock);
38 });
39
40 [Test]
41 public void BasicScale()
42 {
43 boxTest(box =>
44 {
45 box.Scale = Vector2.One;
46 box.ScaleTo(0, interval * 4);
47 });
48
49 checkAtTime(250, box => Precision.AlmostEquals(box.Scale.X, 0.75f));
50 checkAtTime(500, box => Precision.AlmostEquals(box.Scale.X, 0.5f));
51 checkAtTime(750, box => Precision.AlmostEquals(box.Scale.X, 0.25f));
52 checkAtTime(1000, box => Precision.AlmostEquals(box.Scale.X, 0f));
53
54 checkAtTime(500, box => Precision.AlmostEquals(box.Scale.X, 0.5f));
55 checkAtTime(250, box => Precision.AlmostEquals(box.Scale.X, 0.75f));
56
57 AddAssert("check transform count", () => box.Transforms.Count() == 1);
58 }
59
60 [Test]
61 public void ScaleSequence()
62 {
63 boxTest(box =>
64 {
65 box.Scale = Vector2.One;
66
67 box.ScaleTo(0.75f, interval).Then()
68 .ScaleTo(0.5f, interval).Then()
69 .ScaleTo(0.25f, interval).Then()
70 .ScaleTo(0, interval);
71 });
72
73 int i = 0;
74 checkAtTime(interval * ++i, box => Precision.AlmostEquals(box.Scale.X, 0.75f));
75 checkAtTime(interval * ++i, box => Precision.AlmostEquals(box.Scale.X, 0.5f));
76 checkAtTime(interval * ++i, box => Precision.AlmostEquals(box.Scale.X, 0.25f));
77 checkAtTime(interval * ++i, box => Precision.AlmostEquals(box.Scale.X, 0f));
78
79 checkAtTime(interval * (i -= 2), box => Precision.AlmostEquals(box.Scale.X, 0.5f));
80 checkAtTime(interval * --i, box => Precision.AlmostEquals(box.Scale.X, 0.75f));
81
82 AddAssert("check transform count", () => box.Transforms.Count() == 4);
83 }
84
85 [Test]
86 public void BasicMovement()
87 {
88 boxTest(box =>
89 {
90 box.Scale = new Vector2(0.25f);
91 box.Anchor = Anchor.TopLeft;
92 box.Origin = Anchor.TopLeft;
93
94 box.MoveTo(new Vector2(0.75f, 0), interval).Then()
95 .MoveTo(new Vector2(0.75f, 0.75f), interval).Then()
96 .MoveTo(new Vector2(0, 0.75f), interval).Then()
97 .MoveTo(new Vector2(0), interval);
98 });
99
100 int i = 0;
101 checkAtTime(interval * ++i, box => Precision.AlmostEquals(box.X, 0.75f));
102 checkAtTime(interval * ++i, box => Precision.AlmostEquals(box.Y, 0.75f));
103 checkAtTime(interval * ++i, box => Precision.AlmostEquals(box.X, 0f));
104 checkAtTime(interval * ++i, box => Precision.AlmostEquals(box.Y, 0f));
105
106 checkAtTime(interval * (i -= 2), box => Precision.AlmostEquals(box.Y, 0.75f));
107 checkAtTime(interval * --i, box => Precision.AlmostEquals(box.X, 0.75f));
108
109 AddAssert("check transform count", () => box.Transforms.Count() == 4);
110 }
111
112 [Test]
113 public void MoveSequence()
114 {
115 boxTest(box =>
116 {
117 box.Scale = new Vector2(0.25f);
118 box.Anchor = Anchor.TopLeft;
119 box.Origin = Anchor.TopLeft;
120
121 box.ScaleTo(0.5f, interval).MoveTo(new Vector2(0.5f), interval).Then()
122 .ScaleTo(0.1f, interval).MoveTo(new Vector2(0, 0.75f), interval).Then()
123 .ScaleTo(1f, interval).MoveTo(new Vector2(0, 0), interval).Then()
124 .FadeTo(0, interval);
125 });
126
127 int i = 0;
128 checkAtTime(interval * ++i, box => Precision.AlmostEquals(box.X, 0.5f) && Precision.AlmostEquals(box.Scale.X, 0.5f));
129 checkAtTime(interval * ++i, box => Precision.AlmostEquals(box.Y, 0.75f) && Precision.AlmostEquals(box.Scale.X, 0.1f));
130 checkAtTime(interval * ++i, box => Precision.AlmostEquals(box.X, 0f));
131 checkAtTime(interval * (i += 2), box => Precision.AlmostEquals(box.Alpha, 0f));
132
133 checkAtTime(interval * (i - 2), box => Precision.AlmostEquals(box.Alpha, 1f));
134
135 AddAssert("check transform count", () => box.Transforms.Count() == 7);
136 }
137
138 [Test]
139 public void MoveCancelSequence()
140 {
141 boxTest(box =>
142 {
143 box.Scale = new Vector2(0.25f);
144 box.Anchor = Anchor.TopLeft;
145 box.Origin = Anchor.TopLeft;
146
147 box.ScaleTo(0.5f, interval).Then().ScaleTo(1, interval);
148
149 Scheduler.AddDelayed(() => { box.ScaleTo(new Vector2(0.1f), interval); }, interval / 2);
150 });
151
152 int i = 0;
153 checkAtTime(interval * i, box => Precision.AlmostEquals(box.Scale.X, 0.25f));
154 checkAtTime(interval * ++i, box => !Precision.AlmostEquals(box.Scale.X, 0.5f));
155
156 checkAtTime(interval * ++i, box => Precision.AlmostEquals(box.Scale.X, 0.1f));
157
158 AddAssert("check transform count", () => box.Transforms.Count() == 2);
159 }
160
161 [Test]
162 public void SameTypeInType()
163 {
164 boxTest(box =>
165 {
166 box.ScaleTo(0.5f, interval * 4);
167 box.Delay(interval * 2).ScaleTo(1, interval);
168 });
169
170 int i = 0;
171 checkAtTime(interval * i, box => Precision.AlmostEquals(box.Scale.X, 0.25f));
172 checkAtTime(interval * ++i, box => Precision.AlmostEquals(box.Scale.X, 0.3125f));
173 checkAtTime(interval * ++i, box => Precision.AlmostEquals(box.Scale.X, 0.375f));
174 checkAtTime(interval * ++i, box => Precision.AlmostEquals(box.Scale.X, 1));
175
176 checkAtTime(interval * --i, box => Precision.AlmostEquals(box.Scale.X, 0.375f));
177 checkAtTime(interval * --i, box => Precision.AlmostEquals(box.Scale.X, 0.3125f));
178 checkAtTime(interval * --i, box => Precision.AlmostEquals(box.Scale.X, 0.25f));
179
180 AddAssert("check transform count", () => box.Transforms.Count() == 2);
181 }
182
183 [Test]
184 public void SameTypeInPartialOverlap()
185 {
186 boxTest(box =>
187 {
188 box.ScaleTo(0.5f, interval * 2);
189 box.Delay(interval).ScaleTo(1, interval * 2);
190 });
191
192 int i = 0;
193 checkAtTime(interval * i, box => Precision.AlmostEquals(box.Scale.X, 0.25f));
194 checkAtTime(interval * ++i, box => Precision.AlmostEquals(box.Scale.X, 0.375f));
195 checkAtTime(interval * ++i, box => Precision.AlmostEquals(box.Scale.X, 0.6875f));
196 checkAtTime(interval * ++i, box => Precision.AlmostEquals(box.Scale.X, 1));
197 checkAtTime(interval * ++i, box => Precision.AlmostEquals(box.Scale.X, 1));
198
199 checkAtTime(interval * --i, box => Precision.AlmostEquals(box.Scale.X, 1));
200 checkAtTime(interval * --i, box => Precision.AlmostEquals(box.Scale.X, 0.6875f));
201 checkAtTime(interval * --i, box => Precision.AlmostEquals(box.Scale.X, 0.375f));
202
203 AddAssert("check transform count", () => box.Transforms.Count() == 2);
204 }
205
206 [Test]
207 public void StartInMiddleOfSequence()
208 {
209 boxTest(box =>
210 {
211 box.Alpha = 0;
212 box.Delay(interval * 2).FadeInFromZero(interval);
213 box.ScaleTo(0.9f, interval * 4);
214 }, 750);
215
216 checkAtTime(interval * 3, box => Precision.AlmostEquals(box.Alpha, 1));
217 checkAtTime(interval * 4, box => Precision.AlmostEquals(box.Alpha, 1) && Precision.AlmostEquals(box.Scale.X, 0.9f));
218 checkAtTime(interval * 2, box => Precision.AlmostEquals(box.Alpha, 0) && Precision.AlmostEquals(box.Scale.X, 0.575f));
219
220 AddAssert("check transform count", () => box.Transforms.Count() == 3);
221 }
222
223 [Test]
224 public void RewindBetweenDisparateValues()
225 {
226 boxTest(box =>
227 {
228 box.Alpha = 0;
229 });
230
231 // move forward to future point in time before adding transforms.
232 checkAtTime(interval * 4, _ => true);
233
234 AddStep("add transforms", () =>
235 {
236 using (box.BeginAbsoluteSequence(0))
237 {
238 box.FadeOutFromOne(interval);
239 box.Delay(interval * 3).FadeOutFromOne(interval);
240
241 // FadeOutFromOne adds extra transforms which disallow testing this scenario, so we remove them.
242 box.RemoveTransform(box.Transforms.ElementAt(2));
243 box.RemoveTransform(box.Transforms.ElementAt(0));
244 }
245 });
246
247 checkAtTime(0, box => Precision.AlmostEquals(box.Alpha, 1));
248 checkAtTime(interval * 1, box => Precision.AlmostEquals(box.Alpha, 0));
249 checkAtTime(interval * 2, box => Precision.AlmostEquals(box.Alpha, 0));
250 checkAtTime(interval * 3, box => Precision.AlmostEquals(box.Alpha, 1));
251 checkAtTime(interval * 4, box => Precision.AlmostEquals(box.Alpha, 0));
252
253 // importantly, this should be 0 not 1, reading from the EndValue of the first FadeOutFromOne transform.
254 checkAtTime(interval * 2, box => Precision.AlmostEquals(box.Alpha, 0));
255 }
256
257 [Test]
258 public void AddPastTransformFromFutureWhenNotInHierarchy()
259 {
260 AddStep("seek clock to 1000", () => manualClock.CurrentTime = interval * 4);
261
262 AddStep("create box", () =>
263 {
264 box = createBox();
265 box.Clock = manualFramedClock;
266 box.RemoveCompletedTransforms = false;
267
268 manualFramedClock.ProcessFrame();
269 using (box.BeginAbsoluteSequence(0))
270 box.Delay(interval * 2).FadeOut(interval);
271 });
272
273 AddStep("seek clock to 0", () => manualClock.CurrentTime = 0);
274
275 AddStep("add box", () =>
276 {
277 Add(new AnimationContainer
278 {
279 Child = box,
280 ExaminableDrawable = box,
281 });
282 });
283
284 checkAtTime(interval * 2, box => Precision.AlmostEquals(box.Alpha, 1));
285 checkAtTime(interval * 3, box => Precision.AlmostEquals(box.Alpha, 0));
286 }
287
288 [Test]
289 public void AddPastTransformFromFuture()
290 {
291 boxTest(box =>
292 {
293 box.Alpha = 0;
294 });
295
296 // move forward to future point in time before adding transforms.
297 checkAtTime(interval * 4, _ => true);
298
299 AddStep("add transforms", () =>
300 {
301 using (box.BeginAbsoluteSequence(0))
302 {
303 box.FadeOutFromOne(interval);
304 box.Delay(interval * 3).FadeInFromZero(interval);
305
306 // FadeOutFromOne adds extra transforms which disallow testing this scenario, so we remove them.
307 box.RemoveTransform(box.Transforms.ElementAt(2));
308 box.RemoveTransform(box.Transforms.ElementAt(0));
309 }
310 });
311
312 AddStep("add one more transform in the middle", () =>
313 {
314 using (box.BeginAbsoluteSequence(interval * 2))
315 box.FadeIn(interval * 0.5);
316 });
317
318 checkAtTime(interval * 2, box => Precision.AlmostEquals(box.Alpha, 0));
319 checkAtTime(interval * 2.5, box => Precision.AlmostEquals(box.Alpha, 1));
320 }
321
322 [Test]
323 public void LoopSequence()
324 {
325 boxTest(box => { box.RotateTo(0).RotateTo(90, interval).Loop(); });
326
327 const int count = 4;
328
329 for (int i = 0; i <= count; i++)
330 {
331 if (i > 0) checkAtTime(interval * i - 1, box => Precision.AlmostEquals(box.Rotation, 90f, 1));
332 checkAtTime(interval * i, box => Precision.AlmostEquals(box.Rotation, 0));
333 }
334
335 AddAssert("check transform count", () => box.Transforms.Count() == 10);
336
337 for (int i = count; i >= 0; i--)
338 {
339 if (i > 0) checkAtTime(interval * i - 1, box => Precision.AlmostEquals(box.Rotation, 90f, 1));
340 checkAtTime(interval * i, box => Precision.AlmostEquals(box.Rotation, 0));
341 }
342 }
343
344 [Test]
345 public void StartInMiddleOfLoopSequence()
346 {
347 boxTest(box => { box.RotateTo(0).RotateTo(90, interval).Loop(); }, 750);
348
349 checkAtTime(750, box => Precision.AlmostEquals(box.Rotation, 0f));
350
351 AddAssert("check transform count", () => box.Transforms.Count() == 8);
352
353 const int count = 4;
354
355 for (int i = 0; i <= count; i++)
356 {
357 if (i > 0) checkAtTime(interval * i - 1, box => Precision.AlmostEquals(box.Rotation, 90f, 1));
358 checkAtTime(interval * i, box => Precision.AlmostEquals(box.Rotation, 0));
359 }
360
361 AddAssert("check transform count", () => box.Transforms.Count() == 10);
362
363 for (int i = count; i >= 0; i--)
364 {
365 if (i > 0) checkAtTime(interval * i - 1, box => Precision.AlmostEquals(box.Rotation, 90f, 1));
366 checkAtTime(interval * i, box => Precision.AlmostEquals(box.Rotation, 0));
367 }
368 }
369
370 [Test]
371 public void TestSimultaneousTransformsOutOfOrder()
372 {
373 boxTest(box =>
374 {
375 using (box.BeginAbsoluteSequence(0))
376 {
377 box.MoveToX(0.5f, 4 * interval);
378 box.Delay(interval).MoveToY(0.5f, 2 * interval);
379 }
380 });
381
382 checkAtTime(0, box => Precision.AlmostEquals(box.Position, new Vector2(0)));
383 checkAtTime(interval, box => Precision.AlmostEquals(box.Position, new Vector2(0.125f, 0)));
384 checkAtTime(2 * interval, box => Precision.AlmostEquals(box.Position, new Vector2(0.25f, 0.25f)));
385 checkAtTime(3 * interval, box => Precision.AlmostEquals(box.Position, new Vector2(0.375f, 0.5f)));
386 checkAtTime(4 * interval, box => Precision.AlmostEquals(box.Position, new Vector2(0.5f)));
387 checkAtTime(3 * interval, box => Precision.AlmostEquals(box.Position, new Vector2(0.375f, 0.5f)));
388 checkAtTime(2 * interval, box => Precision.AlmostEquals(box.Position, new Vector2(0.25f, 0.25f)));
389 checkAtTime(interval, box => Precision.AlmostEquals(box.Position, new Vector2(0.125f, 0)));
390 checkAtTime(0, box => Precision.AlmostEquals(box.Position, new Vector2(0)));
391 }
392
393 [Test]
394 public void TestMultipleTransformTargets()
395 {
396 boxTest(box =>
397 {
398 box.Delay(500).MoveTo(new Vector2(0, 0.25f), 500);
399 box.MoveToY(0.5f, 250);
400 });
401
402 checkAtTime(double.MinValue, box => box.Y == 0);
403 checkAtTime(0, box => box.Y == 0);
404 checkAtTime(250, box => box.Y == 0.5f);
405 checkAtTime(750, box => box.Y == 0.375f);
406 checkAtTime(1000, box => box.Y == 0.25f);
407 checkAtTime(1500, box => box.Y == 0.25f);
408 checkAtTime(1250, box => box.Y == 0.25f);
409 checkAtTime(750, box => box.Y == 0.375f);
410 }
411
412 [Test]
413 public void TestMoveToOffsetRespectsRelevantTransforms()
414 {
415 boxTest(box =>
416 {
417 box.MoveToY(0.25f, 250);
418 box.Delay(500).MoveToOffset(new Vector2(0, 0.25f), 250);
419 });
420
421 checkAtTime(0, box => box.Y == 0);
422 checkAtTime(250, box => box.Y == 0.25f);
423 checkAtTime(500, box => box.Y == 0.25f);
424 checkAtTime(750, box => box.Y == 0.5f);
425 }
426
427 [Test]
428 public void TestMoveToOffsetRespectsTransformsOrder()
429 {
430 boxTest(box =>
431 {
432 box.Delay(500).MoveToOffset(new Vector2(0, 0.25f), 250);
433 box.MoveToY(0.25f, 250);
434 });
435
436 checkAtTime(0, box => box.Y == 0);
437 checkAtTime(250, box => box.Y == 0.25f);
438 checkAtTime(500, box => box.Y == 0.25f);
439 checkAtTime(750, box => box.Y == 0.5f);
440 }
441
442 private Box box;
443
444 private void checkAtTime(double time, Func<Box, bool> assert)
445 {
446 AddAssert($"check at time {time}", () =>
447 {
448 manualClock.CurrentTime = time;
449
450 box.Clock = manualFramedClock;
451 box.UpdateSubTree();
452
453 return assert(box);
454 });
455 }
456
457 private void boxTest(Action<Box> action, int startTime = 0)
458 {
459 AddStep("add box", () =>
460 {
461 Add(new AnimationContainer(startTime)
462 {
463 Child = box = createBox(),
464 ExaminableDrawable = box,
465 });
466
467 action(box);
468 });
469 }
470
471 private static Box createBox()
472 {
473 return new Box
474 {
475 Anchor = Anchor.Centre,
476 Origin = Anchor.Centre,
477 RelativeSizeAxes = Axes.Both,
478 RelativePositionAxes = Axes.Both,
479 Scale = new Vector2(0.25f),
480 };
481 }
482
483 private class AnimationContainer : Container
484 {
485 public override bool RemoveCompletedTransforms => false;
486 protected override Container<Drawable> Content => content;
487 private readonly Container content;
488 private readonly SpriteText minTimeText;
489 private readonly SpriteText currentTimeText;
490 private readonly SpriteText maxTimeText;
491 private readonly Tick seekingTick;
492 private readonly WrappingTimeContainer wrapping;
493 public Box ExaminableDrawable;
494 private readonly FlowContainer<DrawableTransform> transforms;
495
496 public AnimationContainer(int startTime = 0)
497 {
498 Anchor = Anchor.Centre;
499 Origin = Anchor.Centre;
500 RelativeSizeAxes = Axes.Both;
501 InternalChild = wrapping = new WrappingTimeContainer(startTime)
502 {
503 RelativeSizeAxes = Axes.Both,
504 Children = new Drawable[]
505 {
506 new Container
507 {
508 FillMode = FillMode.Fit,
509 RelativeSizeAxes = Axes.Both,
510 Anchor = Anchor.Centre,
511 Origin = Anchor.Centre,
512 Size = new Vector2(0.6f),
513 Children = new Drawable[]
514 {
515 new Box
516 {
517 RelativeSizeAxes = Axes.Both,
518 Colour = Color4.DarkGray,
519 },
520 content = new Container
521 {
522 RelativeSizeAxes = Axes.Both,
523 Masking = true,
524 },
525 }
526 },
527 transforms = new FillFlowContainer<DrawableTransform>
528 {
529 Anchor = Anchor.CentreLeft,
530 Origin = Anchor.CentreLeft,
531 Spacing = Vector2.One,
532 RelativeSizeAxes = Axes.X,
533 AutoSizeAxes = Axes.Y,
534 Width = 0.2f,
535 },
536 new Container
537 {
538 Anchor = Anchor.TopCentre,
539 Origin = Anchor.TopCentre,
540 RelativeSizeAxes = Axes.Both,
541 Size = new Vector2(0.8f, 0.1f),
542 Children = new Drawable[]
543 {
544 minTimeText = new SpriteText
545 {
546 Anchor = Anchor.BottomLeft,
547 Origin = Anchor.TopLeft,
548 },
549 currentTimeText = new SpriteText
550 {
551 RelativePositionAxes = Axes.X,
552 Anchor = Anchor.BottomLeft,
553 Origin = Anchor.BottomCentre,
554 Y = -10,
555 },
556 maxTimeText = new SpriteText
557 {
558 Anchor = Anchor.BottomRight,
559 Origin = Anchor.TopRight,
560 },
561 seekingTick = new Tick(0, false),
562 new Tick(0),
563 new Tick(1),
564 new Tick(2),
565 new Tick(3),
566 new Tick(4),
567 }
568 }
569 }
570 };
571 }
572
573 private List<Transform> displayedTransforms;
574
575 protected override void Update()
576 {
577 base.Update();
578 double time = wrapping.Time.Current;
579 minTimeText.Text = wrapping.MinTime.ToString("n0");
580 currentTimeText.Text = time.ToString("n0");
581 seekingTick.X = currentTimeText.X = (float)(time / (wrapping.MaxTime - wrapping.MinTime));
582 maxTimeText.Text = wrapping.MaxTime.ToString("n0");
583 maxTimeText.Colour = time > wrapping.MaxTime ? Color4.Gray : wrapping.Time.Elapsed > 0 ? Color4.Blue : Color4.Red;
584 minTimeText.Colour = time < wrapping.MinTime ? Color4.Gray : content.Time.Elapsed > 0 ? Color4.Blue : Color4.Red;
585
586 if (displayedTransforms == null || !ExaminableDrawable.Transforms.SequenceEqual(displayedTransforms))
587 {
588 transforms.Clear();
589 foreach (var t in ExaminableDrawable.Transforms)
590 transforms.Add(new DrawableTransform(t, 15));
591 displayedTransforms = new List<Transform>(ExaminableDrawable.Transforms);
592 }
593 }
594
595 private class Tick : Box
596 {
597 private readonly int tick;
598 private readonly bool colouring;
599
600 public Tick(int tick, bool colouring = true)
601 {
602 this.tick = tick;
603 this.colouring = colouring;
604 Anchor = Anchor.BottomLeft;
605 Origin = Anchor.BottomCentre;
606 Size = new Vector2(1, 10);
607 Colour = Color4.White;
608 RelativePositionAxes = Axes.X;
609 X = (float)tick / interval_count;
610 }
611
612 protected override void Update()
613 {
614 base.Update();
615 if (colouring)
616 Colour = Time.Current > tick * interval ? Color4.Yellow : Color4.White;
617 }
618 }
619 }
620
621 private class WrappingTimeContainer : Container
622 {
623 // Padding, in milliseconds, at each end of maxima of the clock time
624 private const double time_padding = 50;
625 public double MinTime => clock.MinTime + time_padding;
626 public double MaxTime => clock.MaxTime - time_padding;
627 private readonly ReversibleClock clock;
628
629 public WrappingTimeContainer(double startTime)
630 {
631 clock = new ReversibleClock(startTime);
632 }
633
634 [BackgroundDependencyLoader]
635 private void load()
636 {
637 // Replace the game clock, but keep it as a reference
638 clock.SetSource(Clock);
639 Clock = clock;
640 }
641
642 protected override void LoadComplete()
643 {
644 base.LoadComplete();
645 clock.MinTime = -time_padding;
646 clock.MaxTime = intervalAt(interval_count) + time_padding;
647 }
648
649 private class ReversibleClock : IFrameBasedClock
650 {
651 private readonly double startTime;
652 public double MinTime;
653 public double MaxTime = 1000;
654 private OffsetClock offsetClock;
655 private IFrameBasedClock trackingClock;
656 private bool reversed;
657
658 public ReversibleClock(double startTime)
659 {
660 this.startTime = startTime;
661 }
662
663 public void SetSource(IFrameBasedClock trackingClock)
664 {
665 this.trackingClock = trackingClock;
666
667 offsetClock = new OffsetClock(trackingClock) { Offset = -trackingClock.CurrentTime + startTime };
668 }
669
670 public double CurrentTime { get; private set; }
671 public double Rate => offsetClock.Rate;
672 public bool IsRunning => offsetClock.IsRunning;
673 public double ElapsedFrameTime => (reversed ? -1 : 1) * trackingClock.ElapsedFrameTime;
674 public double FramesPerSecond => trackingClock.FramesPerSecond;
675 public FrameTimeInfo TimeInfo => new FrameTimeInfo { Current = CurrentTime, Elapsed = ElapsedFrameTime };
676
677 public void ProcessFrame()
678 {
679 // There are two iterations, when iteration % 2 == 0 : not reversed
680 int iteration = (int)(offsetClock.CurrentTime / (MaxTime - MinTime));
681 reversed = iteration % 2 == 1;
682 double iterationTime = offsetClock.CurrentTime % (MaxTime - MinTime);
683 if (reversed)
684 CurrentTime = MaxTime - iterationTime;
685 else
686 CurrentTime = MinTime + iterationTime;
687 }
688 }
689 }
690 }
691}