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
2017-09-09 00:00:17 +08:00
using System ;
2017-09-08 05:55:05 +08:00
using System.Collections.Generic ;
2017-09-08 18:11:57 +08:00
using System.Linq ;
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
2017-09-08 05:55:05 +08:00
namespace osu.Game.Storyboards
{
2021-04-18 12:45:24 +08:00
public class StoryboardSprite : IStoryboardElementWithDuration
2017-09-08 05:55:05 +08:00
{
2017-09-11 03:25:23 +08:00
private readonly List < CommandLoop > loops = new List < CommandLoop > ( ) ;
private readonly List < CommandTrigger > triggers = new List < CommandTrigger > ( ) ;
2018-04-13 17:19:50 +08:00
2020-01-20 22:59:21 +08:00
public string Path { get ; }
2017-09-15 17:23:37 +08:00
public bool IsDrawable = > HasCommands ;
2018-04-13 17:19:50 +08:00
2017-09-08 05:55:05 +08:00
public Anchor Origin ;
public Vector2 InitialPosition ;
2018-04-13 17:19:50 +08:00
2017-09-11 03:25:23 +08:00
public readonly CommandTimelineGroup TimelineGroup = new CommandTimelineGroup ( ) ;
2018-04-13 17:19:50 +08:00
2022-09-12 13:20:48 +08:00
public double StartTime
2021-03-09 14:55:05 +08:00
{
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-12-19 15:42:21 +08:00
var firstAlpha = alphaCommands . MinBy ( t = > t . startTime ) ;
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-12 13:05:16 +08:00
return EarliestTransformTime ;
}
}
public double EarliestTransformTime
{
get
{
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
2023-04-25 14:22:11 +08:00
public double EndTimeForDisplay
{
get
{
2024-03-01 04:23:57 +08:00
double latestEndTime = double . MaxValue ;
// Ignore the whole setup if there are loops. In theory they can be handled here too, however the logic will be overly complex.
if ( loops . Count = = 0 )
{
// Take the minimum time of all the potential "death" reasons.
2024-03-06 19:23:29 +08:00
latestEndTime = calculateOptimisedEndTime ( TimelineGroup ) ;
2024-03-01 04:23:57 +08:00
}
// If the logic above fails to find anything or discarded by the fact that there are loops present, latestEndTime will be double.MaxValue
// and thus conservativeEndTime will be used.
double conservativeEndTime = TimelineGroup . EndTime ;
2023-04-25 14:22:11 +08:00
foreach ( var l in loops )
2024-03-01 04:23:57 +08:00
conservativeEndTime = Math . Max ( conservativeEndTime , l . StartTime + l . CommandsDuration * l . TotalIterations ) ;
2023-04-25 14:22:11 +08:00
2024-03-01 04:23:57 +08:00
return Math . Min ( latestEndTime , conservativeEndTime ) ;
2023-04-25 14:22:11 +08:00
}
}
2017-09-11 03:25:23 +08:00
public bool HasCommands = > TimelineGroup . HasCommands | | loops . Any ( l = > l . HasCommands ) ;
2018-04-13 17:19:50 +08:00
2017-09-09 03:23:24 +08:00
private delegate void DrawablePropertyInitializer < in T > ( Drawable drawable , T value ) ;
2019-02-28 12:31:40 +08:00
2017-09-09 03:23:24 +08:00
private delegate void DrawableTransformer < in T > ( Drawable drawable , T value , double duration , Easing easing ) ;
2018-04-13 17:19:50 +08:00
2017-09-13 17:22:24 +08:00
public StoryboardSprite ( string path , Anchor origin , Vector2 initialPosition )
2017-09-08 05:55:05 +08:00
{
Path = path ;
Origin = origin ;
InitialPosition = initialPosition ;
}
2018-04-13 17:19:50 +08:00
2021-10-01 17:24:46 +08:00
public CommandLoop AddLoop ( double startTime , int repeatCount )
2017-09-08 05:55:05 +08:00
{
2021-10-01 17:24:46 +08:00
var loop = new CommandLoop ( startTime , repeatCount ) ;
2017-09-08 05:55:05 +08:00
loops . Add ( loop ) ;
return loop ;
}
2018-04-13 17:19:50 +08:00
2017-09-08 05:55:05 +08:00
public CommandTrigger AddTrigger ( string triggerName , double startTime , double endTime , int groupNumber )
{
var trigger = new CommandTrigger ( triggerName , startTime , endTime , groupNumber ) ;
triggers . Add ( trigger ) ;
return trigger ;
}
2018-04-13 17:19:50 +08:00
2017-09-08 05:55:05 +08:00
public virtual Drawable CreateDrawable ( )
2017-09-13 17:22:24 +08:00
= > new DrawableStoryboardSprite ( this ) ;
2018-04-13 17:19:50 +08:00
2023-04-25 13:28:56 +08:00
public void ApplyTransforms ( Drawable drawable , IEnumerable < Tuple < CommandTimelineGroup , double > > ? triggeredGroups = null )
2017-09-09 00:00:17 +08:00
{
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 )
2017-09-09 00:00:17 +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 ) ;
2017-09-09 00:00:17 +08:00
}
2019-12-18 23:52:50 +08:00
foreach ( var command in generated . OrderBy ( g = > g . StartTime ) )
command . ApplyTo ( drawable ) ;
2017-09-09 00:00:17 +08:00
}
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 )
2017-09-09 00:00:17 +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 )
2017-09-09 00:00:17 +08:00
{
2023-04-25 13:28:56 +08:00
DrawablePropertyInitializer < T > ? initFunc = null ;
2019-12-18 23:52:50 +08:00
2017-09-09 00:00:17 +08:00
if ( ! initialized )
{
2017-09-09 21:34:26 +08:00
if ( alwaysInitialize | | command . StartTime = = command . EndTime )
2019-12-18 23:52:50 +08:00
initFunc = initializeProperty ;
2017-09-09 00:00:17 +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 ) ) ;
2017-09-09 00:00:17 +08:00
}
}
2018-04-13 17:19:50 +08:00
2023-04-25 13:28:56 +08:00
private IEnumerable < CommandTimeline < T > . TypedCommand > getCommands < T > ( CommandTimelineSelector < T > timelineSelector , IEnumerable < Tuple < CommandTimelineGroup , double > > ? triggeredGroups )
2017-09-08 05:55:05 +08:00
{
2017-09-11 03:25:23 +08:00
var commands = TimelineGroup . GetCommands ( timelineSelector ) ;
foreach ( var loop in loops )
commands = commands . Concat ( loop . GetCommands ( timelineSelector ) ) ;
2019-11-11 20:05:36 +08:00
2017-09-09 00:00:17 +08:00
if ( triggeredGroups ! = null )
2019-11-11 19:53:22 +08:00
{
2017-09-09 00:00:17 +08:00
foreach ( var pair in triggeredGroups )
commands = commands . Concat ( pair . Item1 . GetCommands ( timelineSelector , pair . Item2 ) ) ;
2019-11-11 19:53:22 +08:00
}
2017-09-09 17:00:58 +08:00
return commands ;
2017-09-08 05:55:05 +08:00
}
2018-04-13 17:19:50 +08:00
2024-03-06 19:23:29 +08:00
private static double calculateOptimisedEndTime ( CommandTimelineGroup timelineGroup )
{
// Here we are starting from maximum value and trying to minimise the end time on each step.
// There are few solid guesses we can make using which sprite's end time can be minimised: alpha = 0, scale = 0, colour.a = 0.
double [ ] deathTimes =
{
double . MaxValue , // alpha
double . MaxValue , // colour alpha
double . MaxValue , // scale
double . MaxValue , // scale x
double . MaxValue , // scale y
} ;
// The loops below are following the same pattern.
// We could be using TimelineGroup.EndValue here, however it's possible to have multiple commands with 0 value in a row
// so we are saving the earliest of them.
foreach ( var alphaCommand in timelineGroup . Alpha . Commands )
{
if ( alphaCommand . EndValue = = 0 )
// commands are ordered by the start time, however end time may vary. Save the earliest.
deathTimes [ 0 ] = Math . Min ( alphaCommand . EndTime , deathTimes [ 0 ] ) ;
else
// If value isn't 0 (sprite becomes visible again), revert the saved state.
deathTimes [ 0 ] = double . MaxValue ;
}
foreach ( var colourCommand in timelineGroup . Colour . Commands )
deathTimes [ 1 ] = colourCommand . EndValue . A = = 0 ? Math . Min ( colourCommand . EndTime , deathTimes [ 1 ] ) : double . MaxValue ;
foreach ( var scaleCommand in timelineGroup . Scale . Commands )
deathTimes [ 2 ] = scaleCommand . EndValue = = 0 ? Math . Min ( scaleCommand . EndTime , deathTimes [ 2 ] ) : double . MaxValue ;
foreach ( var scaleCommand in timelineGroup . VectorScale . Commands )
{
deathTimes [ 3 ] = scaleCommand . EndValue . X = = 0 ? Math . Min ( scaleCommand . EndTime , deathTimes [ 3 ] ) : double . MaxValue ;
deathTimes [ 4 ] = scaleCommand . EndValue . Y = = 0 ? Math . Min ( scaleCommand . EndTime , deathTimes [ 4 ] ) : double . MaxValue ;
}
return deathTimes . Min ( ) ;
}
2017-09-08 05:55:05 +08:00
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 ;
2023-04-25 13:28:56 +08:00
private readonly DrawablePropertyInitializer < T > ? initializeProperty ;
2019-12-18 23:52:50 +08:00
private readonly DrawableTransformer < T > transform ;
private readonly CommandTimeline < T > . TypedCommand command ;
2023-04-25 13:28:56 +08:00
public GeneratedCommand ( CommandTimeline < T > . TypedCommand command , DrawablePropertyInitializer < T > ? initializeProperty , DrawableTransformer < T > transform )
2019-12-18 23:52:50 +08:00
{
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 ) ;
}
}
}
2017-09-08 05:55:05 +08:00
}
}