using System.Collections.Immutable; using System.Runtime.CompilerServices; using System.Text.Json.Serialization; namespace Sandbox; /// /// Describes a gradient between multiple colors /// public struct Gradient { /// /// The blend mode /// [JsonPropertyName( "blend" )] public BlendMode Blending { readonly get; set; } /// /// A list of color stops, which should be ordered by time /// [JsonPropertyName( "color" )] public ImmutableList Colors { readonly get; set; } /// /// A list of color stops, which should be ordered by time /// [JsonPropertyName( "alpha" )] public ImmutableList Alphas { readonly get; set; } public Gradient( params ColorFrame[] frames ) { Colors = ImmutableList.Empty.AddRange( frames ); } public Gradient() { Colors = ImmutableList.Empty; Alphas = ImmutableList.Empty; } /// /// A single float creates a flat color /// static public implicit operator Gradient( Color value ) { var c = new Gradient(); c.AddColor( 0.5f, value ); return c; } /// /// Make a copy of this with changed keyframes /// public readonly Gradient WithFrames( ImmutableList frames ) { var c = this; c.Colors = frames; return c; } /// /// Keyframes times and values should range between 0 and 1 /// public struct ColorFrame { [JsonPropertyName( "t" )] public float Time { readonly get; set; } = 0.0f; [JsonPropertyName( "c" )] public Color Value { readonly get; set; } = Color.White; public ColorFrame( float timedelta, Color color ) { Time = timedelta; Value = color; } } /// /// Keyframes times and values should range between 0 and 1 /// public struct AlphaFrame { [JsonPropertyName( "t" )] public float Time { readonly get; set; } = 0.0f; [JsonPropertyName( "a" )] public float Value { readonly get; set; } = 1.0f; public AlphaFrame( float timedelta, float alpha ) { Time = timedelta; Value = alpha; } } public ColorFrame this[int index] { get => Colors[index]; set { Colors = Colors.SetItem( index, value ); } } /// /// Add a color position /// public int AddColor( float x, in Color color ) => AddColor( new ColorFrame( Math.Clamp( x, 0, 1.0f ), color ) ); /// /// Add an alpha position /// public int AddAlpha( float x, float alpha ) => AddAlpha( new AlphaFrame( Math.Clamp( x, 0, 1.0f ), alpha ) ); /// /// If the lists aren't in time order for some reason, this will fix them. This should really /// just be called when serializing, and in every other situation we should assume they're /// okay. /// public void FixOrder() { if ( !IsOrderedIncorrectly() ) return; if ( Colors is not null ) Colors = Colors.Sort( ( x, y ) => x.Time.CompareTo( y.Time ) ); if ( Alphas is not null ) Alphas = Alphas.Sort( ( x, y ) => x.Time.CompareTo( y.Time ) ); } /// /// Returns true if the lists are not in time order /// private readonly bool IsOrderedIncorrectly() { if ( Colors is not null ) { float time = float.MinValue; foreach ( var f in Colors ) { if ( f.Time < time ) return true; time = f.Time; } } if ( Alphas is not null ) { float time = float.MinValue; foreach ( var f in Alphas ) { if ( f.Time < time ) return true; time = f.Time; } } return false; } /// /// Add given keyframe to this curve. /// /// The keyframe to add. /// The position of newly added keyframe in the list. public int AddColor( in ColorFrame keyframe ) { Colors ??= ImmutableList.Empty; for ( int i = 0; i < Colors.Count; i++ ) { if ( Colors[i].Time > keyframe.Time ) { Colors = Colors.Insert( i, keyframe ); return i; } } Colors = Colors.Add( keyframe ); return Colors.Count - 1; } public int AddAlpha( in AlphaFrame keyframe ) { Alphas ??= ImmutableList.Empty; for ( int i = 0; i < Alphas.Count; i++ ) { if ( Alphas[i].Time > keyframe.Time ) { Alphas = Alphas.Insert( i, keyframe ); return i; } } Alphas = Alphas.Add( keyframe ); return Alphas.Count - 1; } /// /// Given a time, get the keyframes on either side of it, along with the delta of where we are between /// readonly (ColorFrame previous, ColorFrame next, float delta) GetSurroundingColors( float time ) { var length = Colors?.Count ?? 0; if ( length == 0 ) return (default, default, 0); var firstFrame = Colors[0]; if ( length == 1 || time <= firstFrame.Time ) return (firstFrame, firstFrame, 0); var lastFrame = Colors[length - 1]; // handle looping here? time = time % lastFrame.Time; if ( time > lastFrame.Time ) // clamp to end { return (lastFrame, lastFrame, 1); } int prev = 0; for ( int i = 0; i < length; i++ ) { if ( Colors[i].Time < time ) { prev = i; continue; } var delta = time.LerpInverse( Colors[prev].Time, Colors[i].Time ); if ( float.IsNaN( delta ) ) delta = 1f; return (Colors[prev], Colors[i], delta); } // should never happen but okay return (lastFrame, lastFrame, 1); } /// /// Given a time, get the keyframes on either side of it, along with the delta of where we are between /// readonly (AlphaFrame previous, AlphaFrame next, float delta) GetSurroundingAlphas( float time ) { var length = Alphas?.Count ?? 0; if ( length == 0 ) return (new AlphaFrame( 0, 1.0f ), new AlphaFrame( 0, 1.0f ), 0); var firstFrame = Alphas[0]; if ( length == 1 || time <= firstFrame.Time ) return (firstFrame, firstFrame, 0); var lastFrame = Alphas[length - 1]; // handle looping here? time = time % lastFrame.Time; if ( time > lastFrame.Time ) // clamp to end { return (lastFrame, lastFrame, 1); } int prev = 0; for ( int i = 0; i < length; i++ ) { if ( Alphas[i].Time < time ) { prev = i; continue; } var delta = time.LerpInverse( Alphas[prev].Time, Alphas[i].Time ); if ( float.IsNaN( delta ) ) delta = 1f; return (Alphas[prev], Alphas[i], delta); } // should never happen but okay return (lastFrame, lastFrame, 1); } /// /// Describes how the line should behave when entering/leaving a frame /// public enum BlendMode { /// /// Linear interoplation between /// [Icon( "show_chart" )] Linear, /// /// No interpolation use last raw value /// [Icon( "turn_sharp_right" )] Stepped, } /// /// Evaluate the blend using the time, which is generally between 0 and 1 /// [MethodImpl( MethodImplOptions.AggressiveInlining )] public readonly Color Evaluate( float time ) { if ( Colors?.Count == 0 && Alphas?.Count == 0 ) return Color.White; if ( Colors?.Count == 1 && Alphas?.Count == 0 ) return Colors[0].Value; if ( Colors?.Count == 1 && Alphas?.Count == 1 ) return Colors[0].Value.WithAlphaMultiplied( Alphas[0].Value ); if ( Colors?.Count == 0 && Alphas?.Count == 1 ) return Color.White.WithAlphaMultiplied( Alphas[0].Value ); var col = GetSurroundingColors( time ); var alp = GetSurroundingAlphas( time ); if ( Blending == BlendMode.Linear ) { var alpha = MathX.Lerp( alp.previous.Value, alp.next.Value, alp.delta, true ); return Color.Lerp( col.previous.Value, col.next.Value, col.delta, true ).WithAlphaMultiplied( alpha ); } // BlendMode.Stepped var stepAlpha = alp.next.Time <= time ? alp.next.Value : alp.previous.Value; if ( col.next.Time <= time ) return col.next.Value.WithAlphaMultiplied( stepAlpha ); return col.previous.Value.WithAlphaMultiplied( stepAlpha ); } /// /// Create a gradient from colors spaced out evenly /// public static Gradient FromColors( params Color[] colors ) { var g = new Gradient(); if ( colors.Length == 0 ) return g; if ( colors.Length == 1 ) return colors[0]; var step = 1.0f / (colors.Length - 1); for ( int i = 0; i < colors.Length; i++ ) { g.AddColor( i * step, colors[i] ); } return g; } }