mirror of
https://github.com/ppy/osu.git
synced 2024-12-13 08:32:57 +08:00
Implement StoryboardElementWithDuration
This commit is contained in:
parent
4a7635e488
commit
8b03acd27b
@ -17,7 +17,7 @@ namespace osu.Game.Beatmaps.Formats
|
|||||||
{
|
{
|
||||||
public class LegacyStoryboardDecoder : LegacyDecoder<Storyboard>
|
public class LegacyStoryboardDecoder : LegacyDecoder<Storyboard>
|
||||||
{
|
{
|
||||||
private StoryboardSprite? storyboardSprite;
|
private StoryboardElementWithDuration? storyboardSprite;
|
||||||
private CommandTimelineGroup? timelineGroup;
|
private CommandTimelineGroup? timelineGroup;
|
||||||
|
|
||||||
private Storyboard storyboard = null!;
|
private Storyboard storyboard = null!;
|
||||||
|
@ -50,6 +50,7 @@ namespace osu.Game.Storyboards
|
|||||||
StartValue = command.StartValue,
|
StartValue = command.StartValue,
|
||||||
EndValue = command.EndValue,
|
EndValue = command.EndValue,
|
||||||
PropertyName = command.PropertyName,
|
PropertyName = command.PropertyName,
|
||||||
|
IsParameterCommand = command.IsParameterCommand,
|
||||||
LoopCount = TotalIterations,
|
LoopCount = TotalIterations,
|
||||||
Delay = fullLoopDuration - command.EndTime + command.StartTime
|
Delay = fullLoopDuration - command.EndTime + command.StartTime
|
||||||
};
|
};
|
||||||
|
@ -25,6 +25,7 @@ namespace osu.Game.Storyboards
|
|||||||
public T EndValue { get; private set; }
|
public T EndValue { get; private set; }
|
||||||
|
|
||||||
public string PropertyName { get; }
|
public string PropertyName { get; }
|
||||||
|
public bool IsParameterTimeline { get; set; }
|
||||||
|
|
||||||
public CommandTimeline(string propertyName)
|
public CommandTimeline(string propertyName)
|
||||||
{
|
{
|
||||||
@ -38,7 +39,7 @@ namespace osu.Game.Storyboards
|
|||||||
endTime = startTime;
|
endTime = startTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
commands.Add(new TypedCommand { Easing = easing, StartTime = startTime, EndTime = endTime, StartValue = startValue, EndValue = endValue, PropertyName = PropertyName });
|
commands.Add(new TypedCommand { Easing = easing, StartTime = startTime, EndTime = endTime, StartValue = startValue, EndValue = endValue, PropertyName = PropertyName, IsParameterCommand = IsParameterTimeline });
|
||||||
|
|
||||||
if (startTime < StartTime)
|
if (startTime < StartTime)
|
||||||
{
|
{
|
||||||
@ -65,6 +66,7 @@ namespace osu.Game.Storyboards
|
|||||||
public string PropertyName { get; set; }
|
public string PropertyName { get; set; }
|
||||||
public int LoopCount { get; set; }
|
public int LoopCount { get; set; }
|
||||||
public double Delay { get; set; }
|
public double Delay { get; set; }
|
||||||
|
public bool IsParameterCommand { get; set; }
|
||||||
|
|
||||||
public T StartValue;
|
public T StartValue;
|
||||||
public T EndValue;
|
public T EndValue;
|
||||||
|
@ -22,9 +22,9 @@ namespace osu.Game.Storyboards
|
|||||||
public CommandTimeline<float> Rotation = new CommandTimeline<float>("Rotation");
|
public CommandTimeline<float> Rotation = new CommandTimeline<float>("Rotation");
|
||||||
public CommandTimeline<ColourInfo> Colour = new CommandTimeline<ColourInfo>("Colour");
|
public CommandTimeline<ColourInfo> Colour = new CommandTimeline<ColourInfo>("Colour");
|
||||||
public CommandTimeline<float> Alpha = new CommandTimeline<float>("Alpha");
|
public CommandTimeline<float> Alpha = new CommandTimeline<float>("Alpha");
|
||||||
public CommandTimeline<BlendingParameters> BlendingParameters = new CommandTimeline<BlendingParameters>("Blending");
|
public CommandTimeline<BlendingParameters> BlendingParameters = new CommandTimeline<BlendingParameters>("Blending") { IsParameterTimeline = true };
|
||||||
public CommandTimeline<bool> FlipH = new CommandTimeline<bool>("FlipH");
|
public CommandTimeline<bool> FlipH = new CommandTimeline<bool>("FlipH") { IsParameterTimeline = true };
|
||||||
public CommandTimeline<bool> FlipV = new CommandTimeline<bool>("FlipV");
|
public CommandTimeline<bool> FlipV = new CommandTimeline<bool>("FlipV") { IsParameterTimeline = true };
|
||||||
|
|
||||||
private readonly ICommandTimeline[] timelines;
|
private readonly ICommandTimeline[] timelines;
|
||||||
|
|
||||||
@ -109,7 +109,8 @@ namespace osu.Game.Storyboards
|
|||||||
EndTime = offset + command.EndTime,
|
EndTime = offset + command.EndTime,
|
||||||
StartValue = command.StartValue,
|
StartValue = command.StartValue,
|
||||||
EndValue = command.EndValue,
|
EndValue = command.EndValue,
|
||||||
PropertyName = command.PropertyName
|
PropertyName = command.PropertyName,
|
||||||
|
IsParameterCommand = command.IsParameterCommand
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@ using osuTK;
|
|||||||
|
|
||||||
namespace osu.Game.Storyboards.Drawables
|
namespace osu.Game.Storyboards.Drawables
|
||||||
{
|
{
|
||||||
public partial class DrawableStoryboardAnimation : TextureAnimation, IFlippable, IVectorScalable
|
public partial class DrawableStoryboardAnimation : TextureAnimation, IDrawableStoryboardElement
|
||||||
{
|
{
|
||||||
public StoryboardAnimation Animation { get; }
|
public StoryboardAnimation Animation { get; }
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@ using osuTK;
|
|||||||
|
|
||||||
namespace osu.Game.Storyboards.Drawables
|
namespace osu.Game.Storyboards.Drawables
|
||||||
{
|
{
|
||||||
public partial class DrawableStoryboardSprite : Sprite, IFlippable, IVectorScalable
|
public partial class DrawableStoryboardSprite : Sprite, IDrawableStoryboardElement
|
||||||
{
|
{
|
||||||
public StoryboardSprite Sprite { get; }
|
public StoryboardSprite Sprite { get; }
|
||||||
|
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using osu.Framework.Graphics.Transforms;
|
using osu.Framework.Graphics.Transforms;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Storyboards.Drawables
|
namespace osu.Game.Storyboards.Drawables
|
||||||
{
|
{
|
||||||
internal interface IFlippable : ITransformable
|
public interface IDrawableStoryboardElement : ITransformable
|
||||||
{
|
{
|
||||||
bool FlipH { get; set; }
|
bool FlipH { get; set; }
|
||||||
bool FlipV { get; set; }
|
bool FlipV { get; set; }
|
||||||
|
Vector2 VectorScale { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,13 +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 osu.Framework.Graphics.Transforms;
|
|
||||||
using osuTK;
|
|
||||||
|
|
||||||
namespace osu.Game.Storyboards.Drawables
|
|
||||||
{
|
|
||||||
internal interface IVectorScalable : ITransformable
|
|
||||||
{
|
|
||||||
Vector2 VectorScale { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
@ -7,7 +7,7 @@ using osu.Game.Storyboards.Drawables;
|
|||||||
|
|
||||||
namespace osu.Game.Storyboards
|
namespace osu.Game.Storyboards
|
||||||
{
|
{
|
||||||
public class StoryboardAnimation : StoryboardSprite
|
public class StoryboardAnimation : StoryboardElementWithDuration<DrawableStoryboardAnimation>
|
||||||
{
|
{
|
||||||
public int FrameCount;
|
public int FrameCount;
|
||||||
public double FrameDelay;
|
public double FrameDelay;
|
||||||
@ -21,8 +21,7 @@ namespace osu.Game.Storyboards
|
|||||||
LoopType = loopType;
|
LoopType = loopType;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Drawable CreateDrawable()
|
public override DrawableStoryboardAnimation CreateStoryboardDrawable() => new DrawableStoryboardAnimation(this);
|
||||||
=> new DrawableStoryboardAnimation(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum AnimationLoopType
|
public enum AnimationLoopType
|
||||||
|
261
osu.Game/Storyboards/StoryboardElementWithDuration.cs
Normal file
261
osu.Game/Storyboards/StoryboardElementWithDuration.cs
Normal file
@ -0,0 +1,261 @@
|
|||||||
|
// 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))
|
||||||
|
transform(drawable);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void transform(TDrawable drawable)
|
||||||
|
{
|
||||||
|
if (command.IsParameterCommand)
|
||||||
|
{
|
||||||
|
if (command.LoopCount == 0)
|
||||||
|
{
|
||||||
|
drawable.TransformTo(command.PropertyName, command.StartValue).Delay(command.Duration)
|
||||||
|
.TransformTo(command.PropertyName, command.EndValue);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
drawable.TransformTo(command.PropertyName, command.StartValue).Delay(command.Duration)
|
||||||
|
.TransformTo(command.PropertyName, command.EndValue)
|
||||||
|
.Loop(command.Delay, command.LoopCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (command.LoopCount == 0)
|
||||||
|
{
|
||||||
|
drawable.TransformTo(command.PropertyName, command.StartValue).Then()
|
||||||
|
.TransformTo(command.PropertyName, command.EndValue, command.Duration, command.Easing);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
drawable.TransformTo(command.PropertyName, command.StartValue).Then()
|
||||||
|
.TransformTo(command.PropertyName, command.EndValue, command.Duration, command.Easing)
|
||||||
|
.Loop(command.Delay, command.LoopCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,308 +1,21 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// 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.Framework.Graphics;
|
||||||
using osu.Game.Storyboards.Drawables;
|
using osu.Game.Storyboards.Drawables;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Storyboards
|
namespace osu.Game.Storyboards
|
||||||
{
|
{
|
||||||
public class StoryboardSprite : IStoryboardElementWithDuration
|
public class StoryboardSprite : StoryboardElementWithDuration<DrawableStoryboardSprite>
|
||||||
{
|
{
|
||||||
private 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);
|
|
||||||
|
|
||||||
private delegate void DrawablePropertyInitializer<in T>(Drawable drawable, T value);
|
|
||||||
|
|
||||||
public StoryboardSprite(string path, Anchor origin, Vector2 initialPosition)
|
public StoryboardSprite(string path, Anchor origin, Vector2 initialPosition)
|
||||||
|
: base(path, origin, initialPosition)
|
||||||
{
|
{
|
||||||
Path = path;
|
|
||||||
Origin = origin;
|
|
||||||
InitialPosition = initialPosition;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public CommandLoop AddLoop(double startTime, int repeatCount)
|
public override DrawableStoryboardSprite CreateStoryboardDrawable() => new DrawableStoryboardSprite(this);
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
public virtual Drawable CreateDrawable()
|
|
||||||
=> new DrawableStoryboardSprite(this);
|
|
||||||
|
|
||||||
public void ApplyTransforms(Drawable 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> generated = new List<IGeneratedCommand>();
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
if (drawable is IVectorScalable vectorScalable)
|
|
||||||
{
|
|
||||||
generateCommands(generated, getCommands(g => g.VectorScale, triggeredGroups), (_, value) => vectorScalable.VectorScale = value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (drawable is IFlippable flippable)
|
|
||||||
{
|
|
||||||
generateCommands(generated, getCommands(g => g.FlipH, triggeredGroups), (_, value) => flippable.FlipH = value, false);
|
|
||||||
generateCommands(generated, getCommands(g => g.FlipV, triggeredGroups), (_, value) => flippable.FlipV = value, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var command in generated.OrderBy(g => g.StartTime))
|
|
||||||
command.ApplyTo(drawable);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void generateCommands<T>(List<IGeneratedCommand> 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>(command, initFunc));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private 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}";
|
|
||||||
|
|
||||||
private interface IGeneratedCommand
|
|
||||||
{
|
|
||||||
double StartTime { get; }
|
|
||||||
|
|
||||||
void ApplyTo(Drawable drawable);
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly struct GeneratedCommand<T> : IGeneratedCommand
|
|
||||||
{
|
|
||||||
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(Drawable drawable)
|
|
||||||
{
|
|
||||||
initializeProperty?.Invoke(drawable, command.StartValue);
|
|
||||||
|
|
||||||
switch (command.PropertyName)
|
|
||||||
{
|
|
||||||
case "VectorScale":
|
|
||||||
if (command.LoopCount == 0)
|
|
||||||
{
|
|
||||||
using (drawable.BeginAbsoluteSequence(command.StartTime))
|
|
||||||
{
|
|
||||||
((IVectorScalable)drawable).TransformTo(command.PropertyName, command.StartValue).Then()
|
|
||||||
.TransformTo(command.PropertyName, command.EndValue, command.Duration, command.Easing);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
using (drawable.BeginAbsoluteSequence(command.StartTime))
|
|
||||||
{
|
|
||||||
((IVectorScalable)drawable).TransformTo(command.PropertyName, command.StartValue).Then()
|
|
||||||
.TransformTo(command.PropertyName, command.EndValue, command.Duration, command.Easing)
|
|
||||||
.Loop(command.Delay, command.LoopCount);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "FlipH":
|
|
||||||
case "FlipV":
|
|
||||||
if (command.LoopCount == 0)
|
|
||||||
{
|
|
||||||
using (drawable.BeginAbsoluteSequence(command.StartTime))
|
|
||||||
{
|
|
||||||
((IFlippable)drawable).TransformTo(command.PropertyName, command.StartValue).Delay(command.Duration)
|
|
||||||
.TransformTo(command.PropertyName, command.EndValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
using (drawable.BeginAbsoluteSequence(command.StartTime))
|
|
||||||
{
|
|
||||||
((IFlippable)drawable).TransformTo(command.PropertyName, command.StartValue).Delay(command.Duration)
|
|
||||||
.TransformTo(command.PropertyName, command.EndValue)
|
|
||||||
.Loop(command.Delay, command.LoopCount);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "Blending":
|
|
||||||
if (command.LoopCount == 0)
|
|
||||||
{
|
|
||||||
using (drawable.BeginAbsoluteSequence(command.StartTime))
|
|
||||||
{
|
|
||||||
drawable.TransformTo(command.PropertyName, command.StartValue).Delay(command.Duration)
|
|
||||||
.TransformTo(command.PropertyName, command.EndValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
using (drawable.BeginAbsoluteSequence(command.StartTime))
|
|
||||||
{
|
|
||||||
drawable.TransformTo(command.PropertyName, command.StartValue).Delay(command.Duration)
|
|
||||||
.TransformTo(command.PropertyName, command.EndValue)
|
|
||||||
.Loop(command.Delay, command.LoopCount);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
if (command.LoopCount == 0)
|
|
||||||
{
|
|
||||||
using (drawable.BeginAbsoluteSequence(command.StartTime))
|
|
||||||
{
|
|
||||||
drawable.TransformTo(command.PropertyName, command.StartValue).Then()
|
|
||||||
.TransformTo(command.PropertyName, command.EndValue, command.Duration, command.Easing);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
using (drawable.BeginAbsoluteSequence(command.StartTime))
|
|
||||||
{
|
|
||||||
drawable.TransformTo(command.PropertyName, command.StartValue).Then()
|
|
||||||
.TransformTo(command.PropertyName, command.EndValue, command.Duration, command.Easing)
|
|
||||||
.Loop(command.Delay, command.LoopCount);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user