Code/Demos/AnimationShowcase/Sections/DampersSection.cs
using System;
using Goo;
using Goo.Animation;
using Sandbox.UI;
using static Sandbox.DemoTokens;

namespace Sandbox.AnimationShowcase;

public class DampersSection
{
    const float TrackW    = 480f;
    const float TrackH    = 96f;
    const float DotSize   = 22f;
    const float DotTravel = TrackW - DotSize;

    SmoothFloat _smooth = new( initial: 0f, smoothTime: 0.35f );
    DecayFloat  _decay  = new( initial: 0f, halflife:   0.18f );
    SpringFloat _spring = new( initial: 0f, frequency:  9f, damping: 0.35f );

    public void Update( float dt )
    {
        _smooth.Update( dt );
        _decay.Update( dt );
        _spring.Update( dt );
    }

    public Container Build() => Section.Build(
        "dampers - click track to retarget",
        new Container
        {
            FlexDirection = FlexDirection.Row,
            Gap           = Space4,
            AlignItems    = Align.FlexStart,
            Children =
            {
                Track(),
                Legend.Build(
                    ( "SmoothFloat", "velocity tracked",            Accent ),
                    ( "DecayFloat",  "exponential, no overshoot",   AccentGreen ),
                    ( "SpringFloat", "oscillates",                  AccentOrange )
                ),
            },
        } );

    Container Track()
    {
        var track = new Container
        {
            Width           = TrackW,
            Height          = TrackH,
            BackgroundColor = BgCard,
            BorderRadius    = Radius2,
            Position        = PositionMode.Relative,
            PointerEvents   = PointerEvents.All,
            // LocalPosition is rendered-pixel; normalize via Box.Rect into authored coords.
            OnClick = e =>
            {
                float rectW = e.Target?.Box.Rect.Width ?? 0f;
                if ( rectW <= 0f ) return;
                float frac = Math.Clamp( e.LocalPosition.x / rectW, 0f, 1f );
                Retarget( frac * DotTravel );
            },
        };
        track.Children.Add( Dot.AtCorner( _smooth.Current, LaneTop( 0 ), DotSize, Accent ) );
        track.Children.Add( Dot.AtCorner( _decay.Current,  LaneTop( 1 ), DotSize, AccentGreen ) );
        track.Children.Add( Dot.AtCorner( _spring.Current, LaneTop( 2 ), DotSize, AccentOrange ) );
        return track;
    }

    static float LaneTop( int i ) => 14f + i * 24f;

    void Retarget( float left )
    {
        _smooth.Target = left;
        _decay.Target  = left;
        _spring.Target = left;
    }
}