2019-01-24 16:43:03 +08:00
// 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.
2018-04-13 17:19:50 +08:00
2022-06-17 15:37:17 +08:00
#nullable disable
2018-04-13 17:19:50 +08:00
using System ;
using System.Collections.Generic ;
using System.Linq ;
2019-12-18 23:52:50 +08:00
using JetBrains.Annotations ;
2021-10-01 17:24:46 +08:00
using osu.Framework.Graphics ;
using osu.Game.Storyboards.Drawables ;
using osuTK ;
2018-04-13 17:19:50 +08:00
namespace osu.Game.Storyboards
{
2021-04-18 12:45:24 +08:00
public class StoryboardSprite : IStoryboardElementWithDuration
2018-04-13 17:19:50 +08:00
{
private readonly List < CommandLoop > loops = new List < CommandLoop > ( ) ;
private readonly List < CommandTrigger > triggers = new List < CommandTrigger > ( ) ;
2020-01-20 22:59:21 +08:00
public string Path { get ; }
2018-04-13 17:19:50 +08:00
public bool IsDrawable = > HasCommands ;
public Anchor Origin ;
public Vector2 InitialPosition ;
public readonly CommandTimelineGroup TimelineGroup = new CommandTimelineGroup ( ) ;
2021-03-09 14:55:05 +08:00
public double StartTime
{
get
{
2022-09-06 15:58:51 +08:00
// 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 ) > ( ) ;
2022-09-06 15:09:16 +08:00
2022-09-06 15:58:51 +08:00
var command = TimelineGroup . Alpha . Commands . FirstOrDefault ( ) ;
if ( command ! = null ) alphaCommands . Add ( ( command . StartTime , command . StartValue = = 0 ) ) ;
2022-09-06 15:41:32 +08:00
2022-09-06 15:58:51 +08:00
foreach ( var loop in loops )
2022-09-06 15:41:32 +08:00
{
2022-09-06 15:58:51 +08:00
command = loop . Alpha . Commands . FirstOrDefault ( ) ;
2022-09-06 16:46:03 +08:00
if ( command ! = null ) alphaCommands . Add ( ( command . StartTime + loop . LoopStartTime , command . StartValue = = 0 ) ) ;
2022-09-06 15:41:32 +08:00
}
2022-09-06 15:09:16 +08:00
2022-09-06 15:58:51 +08:00
if ( alphaCommands . Count > 0 )
2021-03-09 14:55:05 +08:00
{
2022-09-06 15:58:51 +08:00
var firstAlpha = alphaCommands . OrderBy ( t = > t . startTime ) . First ( ) ;
2021-03-09 14:55:05 +08:00
2022-09-06 15:58:51 +08:00
if ( firstAlpha . isZeroStartValue )
return firstAlpha . startTime ;
2022-09-06 15:09:16 +08:00
}
2022-09-06 15:58:51 +08:00
// 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.
2022-09-06 15:09:16 +08:00
double earliestStartTime = TimelineGroup . StartTime ;
2021-03-09 14:55:05 +08:00
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 ;
}
}
2018-04-13 17:19:50 +08:00
public bool HasCommands = > TimelineGroup . HasCommands | | loops . Any ( l = > l . HasCommands ) ;
private delegate void DrawablePropertyInitializer < in T > ( Drawable drawable , T value ) ;
2019-02-28 12:31:40 +08:00
2018-04-13 17:19:50 +08:00
private delegate void DrawableTransformer < in T > ( Drawable drawable , T value , double duration , Easing easing ) ;
public StoryboardSprite ( string path , Anchor origin , Vector2 initialPosition )
{
Path = path ;
Origin = origin ;
InitialPosition = initialPosition ;
}
2021-10-01 17:24:46 +08:00
public CommandLoop AddLoop ( double startTime , int repeatCount )
2018-04-13 17:19:50 +08:00
{
2021-10-01 17:24:46 +08:00
var loop = new CommandLoop ( startTime , repeatCount ) ;
2018-04-13 17:19:50 +08:00
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 )
{
2019-12-18 23:52:50 +08:00
// 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 , ( d , value , duration , easing ) = > d . MoveToX ( value , duration , easing ) ) ;
generateCommands ( generated , getCommands ( g = > g . Y , triggeredGroups ) , ( d , value ) = > d . Y = value , ( d , value , duration , easing ) = > d . MoveToY ( value , duration , easing ) ) ;
generateCommands ( generated , getCommands ( g = > g . Scale , triggeredGroups ) , ( d , value ) = > d . Scale = new Vector2 ( value ) , ( d , value , duration , easing ) = > d . ScaleTo ( value , duration , easing ) ) ;
generateCommands ( generated , getCommands ( g = > g . Rotation , triggeredGroups ) , ( d , value ) = > d . Rotation = value , ( d , value , duration , easing ) = > d . RotateTo ( value , duration , easing ) ) ;
generateCommands ( generated , getCommands ( g = > g . Colour , triggeredGroups ) , ( d , value ) = > d . Colour = value , ( d , value , duration , easing ) = > d . FadeColour ( value , duration , easing ) ) ;
generateCommands ( generated , getCommands ( g = > g . Alpha , triggeredGroups ) , ( d , value ) = > d . Alpha = value , ( d , value , duration , easing ) = > d . FadeTo ( value , duration , easing ) ) ;
2022-06-24 20:25:23 +08:00
generateCommands ( generated , getCommands ( g = > g . BlendingParameters , triggeredGroups ) , ( d , value ) = > d . Blending = value , ( d , value , duration , _ ) = > d . TransformBlendingMode ( value , duration ) ,
2019-12-18 16:21:38 +08:00
false ) ;
if ( drawable is IVectorScalable vectorScalable )
{
2022-06-24 20:25:23 +08:00
generateCommands ( generated , getCommands ( g = > g . VectorScale , triggeredGroups ) , ( _ , value ) = > vectorScalable . VectorScale = value ,
( _ , value , duration , easing ) = > vectorScalable . VectorScaleTo ( value , duration , easing ) ) ;
2019-12-18 16:21:38 +08:00
}
2018-04-13 17:19:50 +08:00
2019-02-28 13:35:00 +08:00
if ( drawable is IFlippable flippable )
2018-04-13 17:19:50 +08:00
{
2022-06-24 20:25:23 +08:00
generateCommands ( generated , getCommands ( g = > g . FlipH , triggeredGroups ) , ( _ , value ) = > flippable . FlipH = value , ( _ , value , duration , _ ) = > flippable . TransformFlipH ( value , duration ) ,
2019-12-18 16:21:38 +08:00
false ) ;
2022-06-24 20:25:23 +08:00
generateCommands ( generated , getCommands ( g = > g . FlipV , triggeredGroups ) , ( _ , value ) = > flippable . FlipV = value , ( _ , value , duration , _ ) = > flippable . TransformFlipV ( value , duration ) ,
2019-12-18 16:21:38 +08:00
false ) ;
2018-04-13 17:19:50 +08:00
}
2019-12-18 23:52:50 +08:00
foreach ( var command in generated . OrderBy ( g = > g . StartTime ) )
command . ApplyTo ( drawable ) ;
2018-04-13 17:19:50 +08:00
}
2019-12-18 23:52:50 +08:00
private void generateCommands < T > ( List < IGeneratedCommand > resultList , IEnumerable < CommandTimeline < T > . TypedCommand > commands ,
DrawablePropertyInitializer < T > initializeProperty , DrawableTransformer < T > transform , bool alwaysInitialize = true )
2018-04-13 17:19:50 +08:00
{
2019-12-18 23:52:50 +08:00
bool initialized = false ;
2019-04-01 11:16:05 +08:00
2019-12-18 23:52:50 +08:00
foreach ( var command in commands )
2018-04-13 17:19:50 +08:00
{
2019-12-18 23:52:50 +08:00
DrawablePropertyInitializer < T > initFunc = null ;
2018-04-13 17:19:50 +08:00
if ( ! initialized )
{
if ( alwaysInitialize | | command . StartTime = = command . EndTime )
2019-12-18 23:52:50 +08:00
initFunc = initializeProperty ;
2018-04-13 17:19:50 +08:00
initialized = true ;
}
2019-02-28 12:31:40 +08:00
2019-12-18 23:52:50 +08:00
resultList . Add ( new GeneratedCommand < T > ( command , initFunc , transform ) ) ;
2018-04-13 17:19:50 +08:00
}
}
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 ) ) ;
2019-11-11 20:05:36 +08:00
2018-04-13 17:19:50 +08:00
if ( triggeredGroups ! = null )
2019-11-11 19:53:22 +08:00
{
2018-04-13 17:19:50 +08:00
foreach ( var pair in triggeredGroups )
commands = commands . Concat ( pair . Item1 . GetCommands ( timelineSelector , pair . Item2 ) ) ;
2019-11-11 19:53:22 +08:00
}
2018-04-13 17:19:50 +08:00
return commands ;
}
public override string ToString ( )
= > $"{Path}, {Origin}, {InitialPosition}" ;
2019-12-18 23:52:50 +08:00
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 DrawableTransformer < T > transform ;
private readonly CommandTimeline < T > . TypedCommand command ;
public GeneratedCommand ( [ NotNull ] CommandTimeline < T > . TypedCommand command , [ CanBeNull ] DrawablePropertyInitializer < T > initializeProperty , [ NotNull ] DrawableTransformer < T > transform )
{
this . command = command ;
this . initializeProperty = initializeProperty ;
this . transform = transform ;
}
public void ApplyTo ( Drawable drawable )
{
initializeProperty ? . Invoke ( drawable , command . StartValue ) ;
using ( drawable . BeginAbsoluteSequence ( command . StartTime ) )
{
transform ( drawable , command . StartValue , 0 , Easing . None ) ;
transform ( drawable , command . EndValue , command . Duration , command . Easing ) ;
}
}
}
2018-04-13 17:19:50 +08:00
}
}