1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-14 15:17:27 +08:00

Apply major refactor to the storyboard commands flow structrure

This commit is contained in:
Salman Ahmed 2024-03-06 19:39:28 +03:00
parent 07392a4d3e
commit 585ab59768
28 changed files with 713 additions and 561 deletions

View File

@ -175,7 +175,8 @@ namespace osu.Game.Tests.Visual.Gameplay
var layer = storyboard.GetLayer("Background");
var sprite = new StoryboardSprite(lookupName, origin, initialPosition);
sprite.AddLoop(Time.Current, 100).Alpha.Add(Easing.None, 0, 10000, 1, 1);
var loop = sprite.AddLoopingGroup(Time.Current, 100);
loop.AddAlpha(0, 10000, 1, 1, Easing.None);
layer.Elements.Clear();
layer.Add(sprite);

View File

@ -47,7 +47,7 @@ namespace osu.Game.Tests.Visual.Gameplay
var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero);
sprite.TimelineGroup.Alpha.Add(Easing.None, firstStoryboardEvent, firstStoryboardEvent + 500, 0, 1);
sprite.Group.AddAlpha(firstStoryboardEvent, firstStoryboardEvent + 500, 0, 1, Easing.None);
storyboard.GetLayer("Background").Add(sprite);
@ -73,17 +73,17 @@ namespace osu.Game.Tests.Visual.Gameplay
var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero);
// these should be ignored as we have an alpha visibility blocker proceeding this command.
sprite.TimelineGroup.Scale.Add(Easing.None, loop_start_time, -18000, Vector2.Zero, Vector2.One);
var loopGroup = sprite.AddLoop(loop_start_time, 50);
loopGroup.Scale.Add(Easing.None, loop_start_time, -18000, Vector2.Zero, Vector2.One);
sprite.Group.AddScale(loop_start_time, -18000, 0, 1, Easing.None);
var loopGroup = sprite.AddLoopingGroup(loop_start_time, 50);
loopGroup.AddScale(loop_start_time, -18000, 0, 1, Easing.None);
var target = addEventToLoop ? loopGroup : sprite.TimelineGroup;
var target = addEventToLoop ? loopGroup : sprite.Group;
double loopRelativeOffset = addEventToLoop ? -loop_start_time : 0;
target.Alpha.Add(Easing.None, loopRelativeOffset + firstStoryboardEvent, loopRelativeOffset + firstStoryboardEvent + 500, 0, 1);
target.AddAlpha(loopRelativeOffset + firstStoryboardEvent, loopRelativeOffset + firstStoryboardEvent + 500, 0, 1, Easing.None);
// these should be ignored due to being in the future.
sprite.TimelineGroup.Alpha.Add(Easing.None, 18000, 20000, 0, 1);
loopGroup.Alpha.Add(Easing.None, 38000, 40000, 0, 1);
sprite.Group.AddAlpha(18000, 20000, 0, 1, Easing.None);
loopGroup.AddAlpha(38000, 40000, 0, 1, Easing.None);
storyboard.GetLayer("Background").Add(sprite);

View File

@ -216,7 +216,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
var storyboard = new Storyboard();
var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero);
sprite.TimelineGroup.Alpha.Add(Easing.None, 0, duration, 1, 0);
sprite.Group.AddAlpha(0, duration, 1, 0, Easing.None);
storyboard.GetLayer("Background").Add(sprite);
return storyboard;
}

View File

@ -424,7 +424,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
public void TestIntroStoryboardElement() => testLeadIn(b =>
{
var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero);
sprite.TimelineGroup.Alpha.Add(Easing.None, -2000, 0, 0, 1);
sprite.Group.AddAlpha(-2000, 0, 0, 1, Easing.None);
b.Storyboard.GetLayer("Background").Add(sprite);
});

View File

@ -10,6 +10,7 @@ using osu.Framework.Utils;
using osu.Game.Beatmaps.Legacy;
using osu.Game.IO;
using osu.Game.Storyboards;
using osu.Game.Storyboards.Commands;
using osuTK;
using osuTK.Graphics;
@ -17,8 +18,8 @@ namespace osu.Game.Beatmaps.Formats
{
public class LegacyStoryboardDecoder : LegacyDecoder<Storyboard>
{
private StoryboardElementWithDuration? storyboardSprite;
private CommandTimelineGroup? timelineGroup;
private StoryboardSprite? storyboardSprite;
private StoryboardCommandGroup? currentGroup;
private Storyboard storyboard = null!;
@ -165,7 +166,7 @@ namespace osu.Game.Beatmaps.Formats
else
{
if (depth < 2)
timelineGroup = storyboardSprite?.TimelineGroup;
currentGroup = storyboardSprite?.Group;
string commandType = split[0];
@ -177,7 +178,7 @@ namespace osu.Game.Beatmaps.Formats
double startTime = split.Length > 2 ? Parsing.ParseDouble(split[2]) : double.MinValue;
double endTime = split.Length > 3 ? Parsing.ParseDouble(split[3]) : double.MaxValue;
int groupNumber = split.Length > 4 ? Parsing.ParseInt(split[4]) : 0;
timelineGroup = storyboardSprite?.AddTrigger(triggerName, startTime, endTime, groupNumber);
currentGroup = storyboardSprite?.AddTriggerGroup(triggerName, startTime, endTime, groupNumber);
break;
}
@ -185,7 +186,7 @@ namespace osu.Game.Beatmaps.Formats
{
double startTime = Parsing.ParseDouble(split[1]);
int repeatCount = Parsing.ParseInt(split[2]);
timelineGroup = storyboardSprite?.AddLoop(startTime, Math.Max(0, repeatCount - 1));
currentGroup = storyboardSprite?.AddLoopingGroup(startTime, Math.Max(0, repeatCount - 1));
break;
}
@ -204,7 +205,7 @@ namespace osu.Game.Beatmaps.Formats
{
float startValue = Parsing.ParseFloat(split[4]);
float endValue = split.Length > 5 ? Parsing.ParseFloat(split[5]) : startValue;
timelineGroup?.Alpha.Add(easing, startTime, endTime, startValue, endValue);
currentGroup?.AddAlpha(startTime, endTime, startValue, endValue, easing);
break;
}
@ -212,7 +213,7 @@ namespace osu.Game.Beatmaps.Formats
{
float startValue = Parsing.ParseFloat(split[4]);
float endValue = split.Length > 5 ? Parsing.ParseFloat(split[5]) : startValue;
timelineGroup?.Scale.Add(easing, startTime, endTime, new Vector2(startValue), new Vector2(endValue));
currentGroup?.AddScale(startTime, endTime, startValue, endValue, easing);
break;
}
@ -222,7 +223,7 @@ namespace osu.Game.Beatmaps.Formats
float startY = Parsing.ParseFloat(split[5]);
float endX = split.Length > 6 ? Parsing.ParseFloat(split[6]) : startX;
float endY = split.Length > 7 ? Parsing.ParseFloat(split[7]) : startY;
timelineGroup?.VectorScale.Add(easing, startTime, endTime, new Vector2(startX, startY), new Vector2(endX, endY));
currentGroup?.AddVectorScale(startTime, endTime, new Vector2(startX, startY), new Vector2(endX, endY), easing);
break;
}
@ -230,7 +231,7 @@ namespace osu.Game.Beatmaps.Formats
{
float startValue = Parsing.ParseFloat(split[4]);
float endValue = split.Length > 5 ? Parsing.ParseFloat(split[5]) : startValue;
timelineGroup?.Rotation.Add(easing, startTime, endTime, MathUtils.RadiansToDegrees(startValue), MathUtils.RadiansToDegrees(endValue));
currentGroup?.AddRotation(startTime, endTime, MathUtils.RadiansToDegrees(startValue), MathUtils.RadiansToDegrees(endValue), easing);
break;
}
@ -240,8 +241,8 @@ namespace osu.Game.Beatmaps.Formats
float startY = Parsing.ParseFloat(split[5]);
float endX = split.Length > 6 ? Parsing.ParseFloat(split[6]) : startX;
float endY = split.Length > 7 ? Parsing.ParseFloat(split[7]) : startY;
timelineGroup?.X.Add(easing, startTime, endTime, startX, endX);
timelineGroup?.Y.Add(easing, startTime, endTime, startY, endY);
currentGroup?.AddX(startTime, endTime, startX, endX, easing);
currentGroup?.AddY(startTime, endTime, startY, endY, easing);
break;
}
@ -249,7 +250,7 @@ namespace osu.Game.Beatmaps.Formats
{
float startValue = Parsing.ParseFloat(split[4]);
float endValue = split.Length > 5 ? Parsing.ParseFloat(split[5]) : startValue;
timelineGroup?.X.Add(easing, startTime, endTime, startValue, endValue);
currentGroup?.AddX(startTime, endTime, startValue, endValue, easing);
break;
}
@ -257,7 +258,7 @@ namespace osu.Game.Beatmaps.Formats
{
float startValue = Parsing.ParseFloat(split[4]);
float endValue = split.Length > 5 ? Parsing.ParseFloat(split[5]) : startValue;
timelineGroup?.Y.Add(easing, startTime, endTime, startValue, endValue);
currentGroup?.AddY(startTime, endTime, startValue, endValue, easing);
break;
}
@ -269,9 +270,9 @@ namespace osu.Game.Beatmaps.Formats
float endRed = split.Length > 7 ? Parsing.ParseFloat(split[7]) : startRed;
float endGreen = split.Length > 8 ? Parsing.ParseFloat(split[8]) : startGreen;
float endBlue = split.Length > 9 ? Parsing.ParseFloat(split[9]) : startBlue;
timelineGroup?.Colour.Add(easing, startTime, endTime,
currentGroup?.AddColour(startTime, endTime,
new Color4(startRed / 255f, startGreen / 255f, startBlue / 255f, 1),
new Color4(endRed / 255f, endGreen / 255f, endBlue / 255f, 1));
new Color4(endRed / 255f, endGreen / 255f, endBlue / 255f, 1), easing);
break;
}
@ -282,16 +283,16 @@ namespace osu.Game.Beatmaps.Formats
switch (type)
{
case "A":
timelineGroup?.BlendingParameters.Add(easing, startTime, endTime, BlendingParameters.Additive,
startTime == endTime ? BlendingParameters.Additive : BlendingParameters.Inherit);
currentGroup?.AddBlendingParameters(startTime, endTime, BlendingParameters.Additive,
startTime == endTime ? BlendingParameters.Additive : BlendingParameters.Inherit, easing);
break;
case "H":
timelineGroup?.FlipH.Add(easing, startTime, endTime, true, startTime == endTime);
currentGroup?.AddFlipH(startTime, endTime, true, startTime == endTime, easing);
break;
case "V":
timelineGroup?.FlipV.Add(easing, startTime, endTime, true, startTime == endTime);
currentGroup?.AddFlipV(startTime, endTime, true, startTime == endTime, easing);
break;
}

View File

@ -1,63 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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.Game.Storyboards
{
public class CommandLoop : CommandTimelineGroup
{
public double LoopStartTime;
/// <summary>
/// The total number of times this loop is played back. Always greater than zero.
/// </summary>
public readonly int TotalIterations;
public override double StartTime => LoopStartTime + CommandsStartTime;
public override double EndTime =>
// In an ideal world, we would multiply the command duration by TotalIterations here.
// Unfortunately this would clash with how stable handled end times, and results in some storyboards playing outro
// sequences for minutes or hours.
StartTime + CommandsDuration;
/// <summary>
/// Construct a new command loop.
/// </summary>
/// <param name="startTime">The start time of the loop.</param>
/// <param name="repeatCount">The number of times the loop should repeat. Should be greater than zero. Zero means a single playback.</param>
public CommandLoop(double startTime, int repeatCount)
{
if (repeatCount < 0) throw new ArgumentException("Repeat count must be zero or above.", nameof(repeatCount));
LoopStartTime = startTime;
TotalIterations = repeatCount + 1;
}
public override IEnumerable<CommandTimeline<T>.TypedCommand> GetCommands<T>(CommandTimelineSelector<T> timelineSelector, double offset = 0)
{
double fullLoopDuration = CommandsEndTime - CommandsStartTime;
foreach (var command in timelineSelector(this).Commands)
{
yield return new CommandTimeline<T>.TypedCommand
{
Easing = command.Easing,
StartTime = offset + LoopStartTime + command.StartTime,
EndTime = offset + LoopStartTime + command.EndTime,
StartValue = command.StartValue,
EndValue = command.EndValue,
PropertyName = command.PropertyName,
IsParameterCommand = command.IsParameterCommand,
LoopCount = TotalIterations,
Delay = fullLoopDuration - command.EndTime + command.StartTime
};
}
}
public override string ToString()
=> $"{LoopStartTime} x{TotalIterations}";
}
}

View File

@ -1,101 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Framework.Graphics;
using System;
using System.Collections.Generic;
using System.Linq;
namespace osu.Game.Storyboards
{
public class CommandTimeline<T> : ICommandTimeline
{
private readonly List<TypedCommand> commands = new List<TypedCommand>();
public IEnumerable<TypedCommand> Commands => commands.OrderBy(c => c.StartTime);
public bool HasCommands => commands.Count > 0;
public double StartTime { get; private set; } = double.MaxValue;
public double EndTime { get; private set; } = double.MinValue;
public T StartValue { get; private set; }
public T EndValue { get; private set; }
public string PropertyName { get; }
public bool IsParameterTimeline { get; set; }
public CommandTimeline(string propertyName)
{
PropertyName = propertyName;
}
public void Add(Easing easing, double startTime, double endTime, T startValue, T endValue)
{
if (endTime < startTime)
{
endTime = startTime;
}
commands.Add(new TypedCommand { Easing = easing, StartTime = startTime, EndTime = endTime, StartValue = startValue, EndValue = endValue, PropertyName = PropertyName, IsParameterCommand = IsParameterTimeline });
if (startTime < StartTime)
{
StartValue = startValue;
StartTime = startTime;
}
if (endTime > EndTime)
{
EndValue = endValue;
EndTime = endTime;
}
}
public override string ToString()
=> $"{commands.Count} command(s)";
public class TypedCommand : ICommand
{
public Easing Easing { get; set; }
public double StartTime { get; set; }
public double EndTime { get; set; }
public double Duration => EndTime - StartTime;
public string PropertyName { get; set; }
public int LoopCount { get; set; }
public double Delay { get; set; }
public bool IsParameterCommand { get; set; }
public T StartValue;
public T EndValue;
public int CompareTo(ICommand other)
{
int result = StartTime.CompareTo(other.StartTime);
if (result != 0) return result;
return EndTime.CompareTo(other.EndTime);
}
public override string ToString()
=> $"{StartTime} -> {EndTime}, {StartValue} -> {EndValue} {Easing}";
}
}
public interface ICommandTimeline
{
double StartTime { get; }
double EndTime { get; }
bool HasCommands { get; }
}
public interface ICommand : IComparable<ICommand>
{
Easing Easing { get; set; }
double StartTime { get; set; }
double EndTime { get; set; }
double Duration { get; }
}
}

View File

@ -1,120 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osuTK;
using osu.Framework.Graphics;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using osu.Framework.Graphics.Colour;
namespace osu.Game.Storyboards
{
public delegate CommandTimeline<T> CommandTimelineSelector<T>(CommandTimelineGroup commandTimelineGroup);
public class CommandTimelineGroup
{
public CommandTimeline<float> X = new CommandTimeline<float>("X");
public CommandTimeline<float> Y = new CommandTimeline<float>("Y");
public CommandTimeline<Vector2> Scale = new CommandTimeline<Vector2>("Scale");
public CommandTimeline<Vector2> VectorScale = new CommandTimeline<Vector2>("VectorScale");
public CommandTimeline<float> Rotation = new CommandTimeline<float>("Rotation");
public CommandTimeline<ColourInfo> Colour = new CommandTimeline<ColourInfo>("Colour");
public CommandTimeline<float> Alpha = new CommandTimeline<float>("Alpha");
public CommandTimeline<BlendingParameters> BlendingParameters = new CommandTimeline<BlendingParameters>("Blending") { IsParameterTimeline = true };
public CommandTimeline<bool> FlipH = new CommandTimeline<bool>("FlipH") { IsParameterTimeline = true };
public CommandTimeline<bool> FlipV = new CommandTimeline<bool>("FlipV") { IsParameterTimeline = true };
private readonly ICommandTimeline[] timelines;
public CommandTimelineGroup()
{
timelines = new ICommandTimeline[]
{
X,
Y,
Scale,
VectorScale,
Rotation,
Colour,
Alpha,
BlendingParameters,
FlipH,
FlipV
};
}
[JsonIgnore]
public double CommandsStartTime
{
get
{
double min = double.MaxValue;
for (int i = 0; i < timelines.Length; i++)
min = Math.Min(min, timelines[i].StartTime);
return min;
}
}
[JsonIgnore]
public double CommandsEndTime
{
get
{
double max = double.MinValue;
for (int i = 0; i < timelines.Length; i++)
max = Math.Max(max, timelines[i].EndTime);
return max;
}
}
[JsonIgnore]
public double CommandsDuration => CommandsEndTime - CommandsStartTime;
[JsonIgnore]
public virtual double StartTime => CommandsStartTime;
[JsonIgnore]
public virtual double EndTime => CommandsEndTime;
[JsonIgnore]
public bool HasCommands
{
get
{
for (int i = 0; i < timelines.Length; i++)
{
if (timelines[i].HasCommands)
return true;
}
return false;
}
}
public virtual IEnumerable<CommandTimeline<T>.TypedCommand> GetCommands<T>(CommandTimelineSelector<T> timelineSelector, double offset = 0)
{
if (offset != 0)
{
return timelineSelector(this).Commands.Select(command =>
new CommandTimeline<T>.TypedCommand
{
Easing = command.Easing,
StartTime = offset + command.StartTime,
EndTime = offset + command.EndTime,
StartValue = command.StartValue,
EndValue = command.EndValue,
PropertyName = command.PropertyName,
IsParameterCommand = command.IsParameterCommand
});
}
return timelineSelector(this).Commands;
}
}
}

View File

@ -0,0 +1,28 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Transforms;
namespace osu.Game.Storyboards.Commands
{
public interface IStoryboardCommand
{
/// <summary>
/// The start time of the storyboard command.
/// </summary>
double StartTime { get; }
/// <summary>
/// The end time of the storyboard command.
/// </summary>
double EndTime { get; }
/// <summary>
/// Applies the transforms described by this storyboard command to the target drawable.
/// </summary>
/// <param name="d">The target drawable.</param>
/// <returns>The sequence of transforms applied to the target drawable.</returns>
TransformSequence<Drawable> ApplyTransform(Drawable d);
}
}

View File

@ -0,0 +1,19 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Transforms;
namespace osu.Game.Storyboards.Commands
{
public class StoryboardAlphaCommand : StoryboardCommand<float>
{
public StoryboardAlphaCommand(double startTime, double endTime, float startValue, float endValue, Easing easing)
: base(startTime, endTime, startValue, endValue, easing)
{
}
public override void SetInitialValue(Drawable d) => d.Alpha = StartValue;
public override TransformSequence<Drawable> ApplyTransform(Drawable d) => d.FadeTo(StartValue).Then().FadeTo(EndValue, Duration, Easing);
}
}

View File

@ -0,0 +1,21 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Transforms;
namespace osu.Game.Storyboards.Commands
{
public class StoryboardBlendingParametersCommand : StoryboardCommand<BlendingParameters>
{
public StoryboardBlendingParametersCommand(double startTime, double endTime, BlendingParameters startValue, BlendingParameters endValue, Easing easing)
: base(startTime, endTime, startValue, endValue, easing)
{
}
public override void SetInitialValue(Drawable d) => d.Blending = StartValue;
public override TransformSequence<Drawable> ApplyTransform(Drawable d)
=> d.TransformTo(nameof(d.Blending), StartValue).Delay(Duration).TransformTo(nameof(d.Blending), EndValue);
}
}

View File

@ -0,0 +1,20 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Transforms;
using osuTK.Graphics;
namespace osu.Game.Storyboards.Commands
{
public class StoryboardColourCommand : StoryboardCommand<Color4>
{
public StoryboardColourCommand(double startTime, double endTime, Color4 startValue, Color4 endValue, Easing easing)
: base(startTime, endTime, startValue, endValue, easing)
{
}
public override void SetInitialValue(Drawable d) => d.Colour = StartValue;
public override TransformSequence<Drawable> ApplyTransform(Drawable d) => d.FadeColour(StartValue).Then().FadeColour(EndValue, Duration, Easing);
}
}

View File

@ -0,0 +1,54 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Transforms;
namespace osu.Game.Storyboards.Commands
{
public abstract class StoryboardCommand<T> : IStoryboardCommand
{
public double StartTime { get; }
public double EndTime { get; }
public double Duration => EndTime - StartTime;
protected StoryboardCommand(double startTime, double endTime, T startValue, T endValue, Easing easing)
{
if (endTime < startTime)
endTime = startTime;
StartTime = startTime;
StartValue = startValue;
EndTime = endTime;
EndValue = endValue;
Easing = easing;
}
public Easing Easing { get; set; }
public int LoopCount { get; set; }
public double Delay { get; set; }
public T StartValue;
public T EndValue;
/// <summary>
/// Sets the value of the corresponding property in <see cref="Drawable"/> to the start value of this command.
/// </summary>
public abstract void SetInitialValue(Drawable d);
/// <summary>
/// Transforms a corresponding property in <see cref="Drawable"/> that corresponds to this command group with the specified parameters.
/// </summary>
public abstract TransformSequence<Drawable> ApplyTransform(Drawable d);
public int CompareTo(IStoryboardCommand other)
{
int result = StartTime.CompareTo(other.StartTime);
if (result != 0) return result;
return EndTime.CompareTo(other.EndTime);
}
public override string ToString() => $"{StartTime} -> {EndTime}, {StartValue} -> {EndValue} {Easing}";
}
}

View File

@ -0,0 +1,115 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using osu.Framework.Graphics;
using osu.Framework.Lists;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Storyboards.Commands
{
public class StoryboardCommandGroup
{
public SortedList<StoryboardCommand<float>> X;
public SortedList<StoryboardCommand<float>> Y;
public SortedList<StoryboardCommand<float>> Scale;
public SortedList<StoryboardCommand<Vector2>> VectorScale;
public SortedList<StoryboardCommand<float>> Rotation;
public SortedList<StoryboardCommand<Color4>> Colour;
public SortedList<StoryboardCommand<float>> Alpha;
public SortedList<StoryboardCommand<BlendingParameters>> BlendingParameters;
public SortedList<StoryboardCommand<bool>> FlipH;
public SortedList<StoryboardCommand<bool>> FlipV;
private readonly IReadOnlyList<IStoryboardCommand>[] lists;
/// <summary>
/// Returns the earliest start time of the commands added to this group.
/// </summary>
[JsonIgnore]
public double StartTime { get; private set; }
/// <summary>
/// Returns the latest end time of the commands added to this group.
/// </summary>
[JsonIgnore]
public double EndTime { get; private set; }
[JsonIgnore]
public double Duration => EndTime - StartTime;
[JsonIgnore]
public bool HasCommands { get; private set; }
public StoryboardCommandGroup()
{
lists = new IReadOnlyList<IStoryboardCommand>[]
{
X = new SortedList<StoryboardCommand<float>>(),
Y = new SortedList<StoryboardCommand<float>>(),
Scale = new SortedList<StoryboardCommand<float>>(),
VectorScale = new SortedList<StoryboardCommand<Vector2>>(),
Rotation = new SortedList<StoryboardCommand<float>>(),
Colour = new SortedList<StoryboardCommand<Color4>>(),
Alpha = new SortedList<StoryboardCommand<float>>(),
BlendingParameters = new SortedList<StoryboardCommand<BlendingParameters>>(),
FlipH = new SortedList<StoryboardCommand<bool>>(),
FlipV = new SortedList<StoryboardCommand<bool>>()
};
}
/// <summary>
/// Returns all commands contained by this group unsorted.
/// </summary>
public virtual IEnumerable<IStoryboardCommand> GetAllCommands() => lists.SelectMany(l => l);
public void AddX(double startTime, double endTime, float startValue, float endValue, Easing easing)
=> AddCommand(X, new StoryboardXCommand(startTime, endTime, startValue, endValue, easing));
public void AddY(double startTime, double endTime, float startValue, float endValue, Easing easing)
=> AddCommand(Y, new StoryboardYCommand(startTime, endTime, startValue, endValue, easing));
public void AddScale(double startTime, double endTime, float startValue, float endValue, Easing easing)
=> AddCommand(Scale, new StoryboardScaleCommand(startTime, endTime, startValue, endValue, easing));
public void AddVectorScale(double startTime, double endTime, Vector2 startValue, Vector2 endValue, Easing easing)
=> AddCommand(VectorScale, new StoryboardVectorScaleCommand(startTime, endTime, startValue, endValue, easing));
public void AddRotation(double startTime, double endTime, float startValue, float endValue, Easing easing)
=> AddCommand(Rotation, new StoryboardRotationCommand(startTime, endTime, startValue, endValue, easing));
public void AddColour(double startTime, double endTime, Color4 startValue, Color4 endValue, Easing easing)
=> AddCommand(Colour, new StoryboardColourCommand(startTime, endTime, startValue, endValue, easing));
public void AddAlpha(double startTime, double endTime, float startValue, float endValue, Easing easing)
=> AddCommand(Alpha, new StoryboardAlphaCommand(startTime, endTime, startValue, endValue, easing));
public void AddBlendingParameters(double startTime, double endTime, BlendingParameters startValue, BlendingParameters endValue, Easing easing)
=> AddCommand(BlendingParameters, new StoryboardBlendingParametersCommand(startTime, endTime, startValue, endValue, easing));
public void AddFlipH(double startTime, double endTime, bool startValue, bool endValue, Easing easing)
=> AddCommand(FlipH, new StoryboardFlipHCommand(startTime, endTime, startValue, endValue, easing));
public void AddFlipV(double startTime, double endTime, bool startValue, bool endValue, Easing easing)
=> AddCommand(FlipV, new StoryboardFlipVCommand(startTime, endTime, startValue, endValue, easing));
/// <summary>
/// Adds the given storyboard <paramref name="command"/> to the target <paramref name="list"/>.
/// Can be overriden to apply custom effects to the given command before adding it to the list (e.g. looping or time offsets).
/// </summary>
/// <typeparam name="T">The value type of the target property affected by this storyboard command.</typeparam>
protected virtual void AddCommand<T>(ICollection<StoryboardCommand<T>> list, StoryboardCommand<T> command)
{
list.Add(command);
if (command.StartTime < StartTime)
StartTime = command.StartTime;
if (command.EndTime > EndTime)
EndTime = command.EndTime;
}
}
}

View File

@ -0,0 +1,41 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
namespace osu.Game.Storyboards.Commands
{
// public class StoryboardCommandList<T> : IStoryboardCommandList
// {
// // todo: change to sorted list and avoid enumerable type on exposed properties?
// private readonly List<StoryboardCommand<T>> commands = new List<StoryboardCommand<T>>();
//
// public IEnumerable<StoryboardCommand<T>> Commands => commands.OrderBy(c => c.StartTime);
//
// IEnumerable<IStoryboardCommand> IStoryboardCommandList.Commands => Commands;
// public bool HasCommands => commands.Count > 0;
//
// public double StartTime { get; private set; } = double.MaxValue;
// public double EndTime { get; private set; } = double.MinValue;
//
// public T? StartValue { get; private set; }
// public T? EndValue { get; private set; }
//
// public void Add(StoryboardCommand<T> command)
// {
// commands.Add(command);
//
// if (command.StartTime < StartTime)
// {
// StartValue = command.StartValue;
// StartTime = command.StartTime;
// }
//
// if (command.EndTime > EndTime)
// {
// EndValue = command.EndValue;
// EndTime = command.EndTime;
// }
// }
//
// public override string ToString() => $"{commands.Count} command(s)";
// }
}

View File

@ -0,0 +1,23 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Transforms;
using osu.Game.Storyboards.Drawables;
namespace osu.Game.Storyboards.Commands
{
public class StoryboardFlipHCommand : StoryboardCommand<bool>
{
public StoryboardFlipHCommand(double startTime, double endTime, bool startValue, bool endValue, Easing easing)
: base(startTime, endTime, startValue, endValue, easing)
{
}
public override void SetInitialValue(Drawable d) => ((IDrawableStoryboardElement)d).FlipH = StartValue;
public override TransformSequence<Drawable> ApplyTransform(Drawable d)
=> d.TransformTo(nameof(IDrawableStoryboardElement.FlipH), StartValue).Delay(Duration)
.TransformTo(nameof(IDrawableStoryboardElement.FlipH), EndValue);
}
}

View File

@ -0,0 +1,23 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Transforms;
using osu.Game.Storyboards.Drawables;
namespace osu.Game.Storyboards.Commands
{
public class StoryboardFlipVCommand : StoryboardCommand<bool>
{
public StoryboardFlipVCommand(double startTime, double endTime, bool startValue, bool endValue, Easing easing)
: base(startTime, endTime, startValue, endValue, easing)
{
}
public override void SetInitialValue(Drawable d) => ((IDrawableStoryboardElement)d).FlipV = StartValue;
public override TransformSequence<Drawable> ApplyTransform(Drawable d)
=> d.TransformTo(nameof(IDrawableStoryboardElement.FlipV), StartValue).Delay(Duration)
.TransformTo(nameof(IDrawableStoryboardElement.FlipV), EndValue);
}
}

View File

@ -0,0 +1,71 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Transforms;
namespace osu.Game.Storyboards.Commands
{
public class StoryboardLoopingGroup : StoryboardCommandGroup
{
public double LoopStartTime;
/// <summary>
/// The total number of times this loop is played back. Always greater than zero.
/// </summary>
public readonly int TotalIterations;
/// <summary>
/// Construct a new command loop.
/// </summary>
/// <param name="startTime">The start time of the loop.</param>
/// <param name="repeatCount">The number of times the loop should repeat. Should be greater than zero. Zero means a single playback.</param>
public StoryboardLoopingGroup(double startTime, int repeatCount)
{
if (repeatCount < 0) throw new ArgumentException("Repeat count must be zero or above.", nameof(repeatCount));
LoopStartTime = startTime;
TotalIterations = repeatCount + 1;
}
protected override void AddCommand<T>(ICollection<StoryboardCommand<T>> list, StoryboardCommand<T> command)
{
// todo: this is broke!
double fullLoopDuration = EndTime - StartTime;
double loopDelay = fullLoopDuration - command.EndTime + command.StartTime;
base.AddCommand(list, new StoryboardLoopingCommand<T>(command, LoopStartTime, TotalIterations, loopDelay));
}
public override string ToString() => $"{LoopStartTime} x{TotalIterations}";
private class StoryboardLoopingCommand<T> : StoryboardCommand<T>
{
private readonly StoryboardCommand<T> command;
private readonly int loopCount;
private readonly double loopDelay;
public StoryboardLoopingCommand(StoryboardCommand<T> command, double loopStartTime, int loopCount, double loopDelay)
// In an ideal world, we would multiply the command duration by TotalIterations in command end time.
// Unfortunately this would clash with how stable handled end times, and results in some storyboards playing outro
// sequences for minutes or hours.
: base(loopStartTime + command.StartTime, loopStartTime + command.EndTime, command.StartValue, command.EndValue, command.Easing)
{
this.command = command;
this.loopCount = loopCount;
this.loopDelay = loopDelay;
}
public override void SetInitialValue(Drawable d) => command.SetInitialValue(d);
public override TransformSequence<Drawable> ApplyTransform(Drawable d)
{
if (loopCount == 0)
return command.ApplyTransform(d);
return command.ApplyTransform(d).Loop(loopDelay, loopCount);
}
}
}
}

View File

@ -0,0 +1,21 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Transforms;
namespace osu.Game.Storyboards.Commands
{
public class StoryboardRotationCommand : StoryboardCommand<float>
{
public StoryboardRotationCommand(double startTime, double endTime, float startValue, float endValue, Easing easing)
: base(startTime, endTime, startValue, endValue, easing)
{
}
public override void SetInitialValue(Drawable d) => d.Rotation = StartValue;
public override TransformSequence<Drawable> ApplyTransform(Drawable d)
=> d.RotateTo(StartValue).Then().RotateTo(EndValue, Duration, Easing);
}
}

View File

@ -0,0 +1,22 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Transforms;
using osuTK;
namespace osu.Game.Storyboards.Commands
{
public class StoryboardScaleCommand : StoryboardCommand<float>
{
public StoryboardScaleCommand(double startTime, double endTime, float startValue, float endValue, Easing easing)
: base(startTime, endTime, startValue, endValue, easing)
{
}
public override void SetInitialValue(Drawable d) => d.Scale = new Vector2(StartValue);
public override TransformSequence<Drawable> ApplyTransform(Drawable d)
=> d.ScaleTo(StartValue).Then().ScaleTo(EndValue, Duration, Easing);
}
}

View File

@ -1,16 +1,17 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
namespace osu.Game.Storyboards
namespace osu.Game.Storyboards.Commands
{
public class CommandTrigger : CommandTimelineGroup
// todo: this is not implemented and has never been, keep that in mind.
public class StoryboardTriggerGroup : StoryboardCommandGroup
{
public string TriggerName;
public double TriggerStartTime;
public double TriggerEndTime;
public int GroupNumber;
public CommandTrigger(string triggerName, double startTime, double endTime, int groupNumber)
public StoryboardTriggerGroup(string triggerName, double startTime, double endTime, int groupNumber)
{
TriggerName = triggerName;
TriggerStartTime = startTime;

View File

@ -0,0 +1,24 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Transforms;
using osu.Game.Storyboards.Drawables;
using osuTK;
namespace osu.Game.Storyboards.Commands
{
public class StoryboardVectorScaleCommand : StoryboardCommand<Vector2>
{
public StoryboardVectorScaleCommand(double startTime, double endTime, Vector2 startValue, Vector2 endValue, Easing easing)
: base(startTime, endTime, startValue, endValue, easing)
{
}
public override void SetInitialValue(Drawable d) => ((IDrawableStoryboardElement)d).VectorScale = StartValue;
public override TransformSequence<Drawable> ApplyTransform(Drawable d)
=> d.TransformTo(nameof(IDrawableStoryboardElement.VectorScale), StartValue).Then()
.TransformTo(nameof(IDrawableStoryboardElement.VectorScale), EndValue, Duration, Easing);
}
}

View File

@ -0,0 +1,21 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Transforms;
namespace osu.Game.Storyboards.Commands
{
public class StoryboardXCommand : StoryboardCommand<float>
{
public StoryboardXCommand(double startTime, double endTime, float startValue, float endValue, Easing easing)
: base(startTime, endTime, startValue, endValue, easing)
{
}
public override void SetInitialValue(Drawable d) => d.X = StartValue;
public override TransformSequence<Drawable> ApplyTransform(Drawable d)
=> d.MoveToX(StartValue).Then().MoveToX(EndValue, Duration, Easing);
}
}

View File

@ -0,0 +1,21 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Transforms;
namespace osu.Game.Storyboards.Commands
{
public class StoryboardYCommand : StoryboardCommand<float>
{
public StoryboardYCommand(double startTime, double endTime, float startValue, float endValue, Easing easing)
: base(startTime, endTime, startValue, endValue, easing)
{
}
public override void SetInitialValue(Drawable d) => d.Y = StartValue;
public override TransformSequence<Drawable> ApplyTransform(Drawable d)
=> d.MoveToY(StartValue).Then().MoveToY(EndValue, Duration, Easing);
}
}

View File

@ -1,12 +1,12 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics.Transforms;
using osu.Framework.Graphics;
using osuTK;
namespace osu.Game.Storyboards.Drawables
{
public interface IDrawableStoryboardElement : ITransformable
public interface IDrawableStoryboardElement : IDrawable
{
bool FlipH { get; set; }
bool FlipV { get; set; }

View File

@ -7,7 +7,7 @@ using osu.Game.Storyboards.Drawables;
namespace osu.Game.Storyboards
{
public class StoryboardAnimation : StoryboardElementWithDuration<DrawableStoryboardAnimation>
public class StoryboardAnimation : StoryboardSprite
{
public int FrameCount;
public double FrameDelay;
@ -21,7 +21,7 @@ namespace osu.Game.Storyboards
LoopType = loopType;
}
public override DrawableStoryboardAnimation CreateStoryboardDrawable() => new DrawableStoryboardAnimation(this);
public override Drawable CreateDrawable() => new DrawableStoryboardAnimation(this);
}
public enum AnimationLoopType

View File

@ -1,236 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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.Graphics;
using osu.Game.Storyboards.Drawables;
using osuTK;
namespace osu.Game.Storyboards
{
public abstract class StoryboardElementWithDuration : IStoryboardElementWithDuration
{
protected readonly List<CommandLoop> Loops = new List<CommandLoop>();
private readonly List<CommandTrigger> triggers = new List<CommandTrigger>();
public string Path { get; }
public bool IsDrawable => HasCommands;
public Anchor Origin;
public Vector2 InitialPosition;
public readonly CommandTimelineGroup TimelineGroup = new CommandTimelineGroup();
public double StartTime
{
get
{
// To get the initial start time, we need to check whether the first alpha command to exist (across all loops) has a StartValue of zero.
// A StartValue of zero governs, above all else, the first valid display time of a sprite.
//
// You can imagine that the first command of each type decides that type's start value, so if the initial alpha is zero,
// anything before that point can be ignored (the sprite is not visible after all).
var alphaCommands = new List<(double startTime, bool isZeroStartValue)>();
var command = TimelineGroup.Alpha.Commands.FirstOrDefault();
if (command != null) alphaCommands.Add((command.StartTime, command.StartValue == 0));
foreach (var loop in Loops)
{
command = loop.Alpha.Commands.FirstOrDefault();
if (command != null) alphaCommands.Add((command.StartTime + loop.LoopStartTime, command.StartValue == 0));
}
if (alphaCommands.Count > 0)
{
var firstAlpha = alphaCommands.MinBy(t => t.startTime);
if (firstAlpha.isZeroStartValue)
return firstAlpha.startTime;
}
return EarliestTransformTime;
}
}
public double EarliestTransformTime
{
get
{
// If we got to this point, either no alpha commands were present, or the earliest had a non-zero start value.
// The sprite's StartTime will be determined by the earliest command, regardless of type.
double earliestStartTime = TimelineGroup.StartTime;
foreach (var l in Loops)
earliestStartTime = Math.Min(earliestStartTime, l.StartTime);
return earliestStartTime;
}
}
public double EndTime
{
get
{
double latestEndTime = TimelineGroup.EndTime;
foreach (var l in Loops)
latestEndTime = Math.Max(latestEndTime, l.EndTime);
return latestEndTime;
}
}
public double EndTimeForDisplay
{
get
{
double latestEndTime = TimelineGroup.EndTime;
foreach (var l in Loops)
latestEndTime = Math.Max(latestEndTime, l.StartTime + l.CommandsDuration * l.TotalIterations);
return latestEndTime;
}
}
public bool HasCommands => TimelineGroup.HasCommands || Loops.Any(l => l.HasCommands);
protected StoryboardElementWithDuration(string path, Anchor origin, Vector2 initialPosition)
{
Path = path;
Origin = origin;
InitialPosition = initialPosition;
}
public abstract Drawable CreateDrawable();
public CommandLoop AddLoop(double startTime, int repeatCount)
{
var loop = new CommandLoop(startTime, repeatCount);
Loops.Add(loop);
return loop;
}
public CommandTrigger AddTrigger(string triggerName, double startTime, double endTime, int groupNumber)
{
var trigger = new CommandTrigger(triggerName, startTime, endTime, groupNumber);
triggers.Add(trigger);
return trigger;
}
protected IEnumerable<CommandTimeline<T>.TypedCommand> GetCommands<T>(CommandTimelineSelector<T> timelineSelector, IEnumerable<Tuple<CommandTimelineGroup, double>>? triggeredGroups)
{
var commands = TimelineGroup.GetCommands(timelineSelector);
foreach (var loop in Loops)
commands = commands.Concat(loop.GetCommands(timelineSelector));
if (triggeredGroups != null)
{
foreach (var pair in triggeredGroups)
commands = commands.Concat(pair.Item1.GetCommands(timelineSelector, pair.Item2));
}
return commands;
}
public override string ToString()
=> $"{Path}, {Origin}, {InitialPosition}";
}
public abstract class StoryboardElementWithDuration<U> : StoryboardElementWithDuration
where U : Drawable, IDrawableStoryboardElement
{
private delegate void DrawablePropertyInitializer<in T>(U drawable, T value);
protected StoryboardElementWithDuration(string path, Anchor origin, Vector2 initialPosition)
: base(path, origin, initialPosition)
{
}
public override Drawable CreateDrawable() => CreateStoryboardDrawable();
public abstract U CreateStoryboardDrawable();
public void ApplyTransforms(U drawable, IEnumerable<Tuple<CommandTimelineGroup, double>>? triggeredGroups = null)
{
// For performance reasons, we need to apply the commands in order by start time. Not doing so will cause many functions to be interleaved, resulting in O(n^2) complexity.
// To achieve this, commands are "generated" as pairs of (command, initFunc, transformFunc) and batched into a contiguous list
// The list is then stably-sorted (to preserve command order), and applied to the drawable sequentially.
List<IGeneratedCommand<U>> generated = new List<IGeneratedCommand<U>>();
generateCommands(generated, GetCommands(g => g.X, triggeredGroups), (d, value) => d.X = value);
generateCommands(generated, GetCommands(g => g.Y, triggeredGroups), (d, value) => d.Y = value);
generateCommands(generated, GetCommands(g => g.Scale, triggeredGroups), (d, value) => d.Scale = value);
generateCommands(generated, GetCommands(g => g.Rotation, triggeredGroups), (d, value) => d.Rotation = value);
generateCommands(generated, GetCommands(g => g.Colour, triggeredGroups), (d, value) => d.Colour = value);
generateCommands(generated, GetCommands(g => g.Alpha, triggeredGroups), (d, value) => d.Alpha = value);
generateCommands(generated, GetCommands(g => g.BlendingParameters, triggeredGroups), (d, value) => d.Blending = value, false);
generateCommands(generated, GetCommands(g => g.VectorScale, triggeredGroups), (d, value) => d.VectorScale = value);
generateCommands(generated, GetCommands(g => g.FlipH, triggeredGroups), (d, value) => d.FlipH = value, false);
generateCommands(generated, GetCommands(g => g.FlipV, triggeredGroups), (d, value) => d.FlipV = value, false);
foreach (var command in generated.OrderBy(g => g.StartTime))
command.ApplyTo(drawable);
}
private void generateCommands<T>(List<IGeneratedCommand<U>> resultList, IEnumerable<CommandTimeline<T>.TypedCommand> commands,
DrawablePropertyInitializer<T> initializeProperty, bool alwaysInitialize = true)
{
bool initialized = false;
foreach (var command in commands)
{
DrawablePropertyInitializer<T>? initFunc = null;
if (!initialized)
{
if (alwaysInitialize || command.StartTime == command.EndTime)
initFunc = initializeProperty;
initialized = true;
}
resultList.Add(new GeneratedCommand<T, U>(command, initFunc));
}
}
private interface IGeneratedCommand<in TDrawable>
where TDrawable : U
{
double StartTime { get; }
void ApplyTo(TDrawable drawable);
}
private readonly struct GeneratedCommand<T, TDrawable> : IGeneratedCommand<TDrawable>
where TDrawable : U
{
public double StartTime => command.StartTime;
private readonly DrawablePropertyInitializer<T>? initializeProperty;
private readonly CommandTimeline<T>.TypedCommand command;
public GeneratedCommand(CommandTimeline<T>.TypedCommand command, DrawablePropertyInitializer<T>? initializeProperty)
{
this.command = command;
this.initializeProperty = initializeProperty;
}
public void ApplyTo(TDrawable drawable)
{
initializeProperty?.Invoke(drawable, command.StartValue);
using (drawable.BeginAbsoluteSequence(command.StartTime))
{
var sequence = command.IsParameterCommand
? drawable.TransformTo(command.PropertyName, command.StartValue).Delay(command.Duration).TransformTo(command.PropertyName, command.EndValue)
: drawable.TransformTo(command.PropertyName, command.StartValue).Then().TransformTo(command.PropertyName, command.EndValue, command.Duration, command.Easing);
if (command.LoopCount > 0)
sequence.Loop(command.Delay, command.LoopCount);
}
}
}
}
}

View File

@ -1,19 +1,164 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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.Graphics;
using osu.Game.Storyboards.Commands;
using osu.Game.Storyboards.Drawables;
using osuTK;
namespace osu.Game.Storyboards
{
public class StoryboardSprite : StoryboardElementWithDuration<DrawableStoryboardSprite>
public class StoryboardSprite : IStoryboardElementWithDuration
{
public StoryboardSprite(string path, Anchor origin, Vector2 initialPosition)
: base(path, origin, initialPosition)
private readonly List<StoryboardLoopingGroup> loopGroups = new List<StoryboardLoopingGroup>();
private readonly List<StoryboardTriggerGroup> triggerGroups = new List<StoryboardTriggerGroup>();
public string Path { get; }
public bool IsDrawable => HasCommands;
public Anchor Origin;
public Vector2 InitialPosition;
public readonly StoryboardCommandGroup Group = new StoryboardCommandGroup();
public double StartTime
{
get
{
// To get the initial start time, we need to check whether the first alpha command to exist (across all loops) has a StartValue of zero.
// A StartValue of zero governs, above all else, the first valid display time of a sprite.
//
// You can imagine that the first command of each type decides that type's start value, so if the initial alpha is zero,
// anything before that point can be ignored (the sprite is not visible after all).
var alphaCommands = new List<(double startTime, bool isZeroStartValue)>();
var command = Group.Alpha.FirstOrDefault();
if (command != null) alphaCommands.Add((command.StartTime, command.StartValue == 0));
foreach (var loop in loopGroups)
{
command = loop.Alpha.FirstOrDefault();
if (command != null) alphaCommands.Add((command.StartTime + loop.LoopStartTime, command.StartValue == 0));
}
if (alphaCommands.Count > 0)
{
var firstAlpha = alphaCommands.MinBy(t => t.startTime);
if (firstAlpha.isZeroStartValue)
return firstAlpha.startTime;
}
return EarliestTransformTime;
}
}
public override DrawableStoryboardSprite CreateStoryboardDrawable() => new DrawableStoryboardSprite(this);
public double EarliestTransformTime
{
get
{
// If we got to this point, either no alpha commands were present, or the earliest had a non-zero start value.
// The sprite's StartTime will be determined by the earliest command, regardless of type.
double earliestStartTime = Group.StartTime;
foreach (var l in loopGroups)
earliestStartTime = Math.Min(earliestStartTime, l.StartTime);
return earliestStartTime;
}
}
public double EndTime
{
get
{
double latestEndTime = Group.EndTime;
foreach (var l in loopGroups)
latestEndTime = Math.Max(latestEndTime, l.EndTime);
return latestEndTime;
}
}
public double EndTimeForDisplay
{
get
{
double latestEndTime = Group.StartTime;
foreach (var l in loopGroups)
latestEndTime = Math.Max(latestEndTime, l.StartTime + l.Duration * l.TotalIterations);
return latestEndTime;
}
}
public bool HasCommands => Group.HasCommands || loopGroups.Any(l => l.HasCommands);
public StoryboardSprite(string path, Anchor origin, Vector2 initialPosition)
{
Path = path;
Origin = origin;
InitialPosition = initialPosition;
}
public virtual Drawable CreateDrawable() => new DrawableStoryboardSprite(this);
public StoryboardLoopingGroup AddLoopingGroup(double loopStartTime, int repeatCount)
{
var loop = new StoryboardLoopingGroup(loopStartTime, repeatCount);
loopGroups.Add(loop);
return loop;
}
public StoryboardTriggerGroup AddTriggerGroup(string triggerName, double startTime, double endTime, int groupNumber)
{
var trigger = new StoryboardTriggerGroup(triggerName, startTime, endTime, groupNumber);
triggerGroups.Add(trigger);
return trigger;
}
public override string ToString() => $"{Path}, {Origin}, {InitialPosition}";
public void ApplyTransforms(Drawable drawable, IEnumerable<Tuple<StoryboardCommandGroup, double>>? triggeredGroups = null)
{
// For performance reasons, we need to apply the commands in order by start time. Not doing so will cause many functions to be interleaved, resulting in O(n^2) complexity.
var commands = Group.GetAllCommands();
commands = commands.Concat(loopGroups.SelectMany(l => l.GetAllCommands()));
// todo: triggers are not implemented yet.
// if (triggeredGroups != null)
// commands = commands.Concat(triggeredGroups.SelectMany(tuple => tuple.Item1.GetAllCommands(tuple.Item2)));
foreach (var command in commands.OrderBy(c => c.StartTime))
{
using (drawable.BeginAbsoluteSequence(command.StartTime))
command.ApplyTransform(drawable);
}
}
// todo: need to revisit property initialisation. apparently it has to be done per first command of every affected property (transforms are supposed to do that already?).
// private void generateCommands<T>(List<IGeneratedCommand> resultList, IEnumerable<StoryboardCommandList<T>.TypedCommand> commands,
// DrawablePropertyInitializer<T> initializeProperty, DrawableTransform<T> transform, bool alwaysInitialize = true)
// {
// bool initialized = false;
//
// foreach (var command in commands)
// {
// DrawablePropertyInitializer<T>? initFunc = null;
//
// if (!initialized)
// {
// if (alwaysInitialize || command.StartTime == command.EndTime)
// initFunc = initializeProperty;
// initialized = true;
// }
//
// resultList.Add(new GeneratedCommand<T>(command, initFunc, transform));
// }
// }
}
}