diff --git a/HellionChat/Ui/StyleEngine/HoverState.cs b/HellionChat/Ui/StyleEngine/HoverState.cs index 15775ad..0f1246c 100644 --- a/HellionChat/Ui/StyleEngine/HoverState.cs +++ b/HellionChat/Ui/StyleEngine/HoverState.cs @@ -61,7 +61,10 @@ internal static class HoverState // OR, never assign: a second caller in the same frame that is not hovered // must not cancel the first one that is. entry.Hovered |= hovered; - return entry.Value; + + // Callers get the eased value; the entry keeps the linear one, which is + // what the evict rule and the advance step are written against. + return HoverMath.Ease(entry.Value); } // Never re-entrant: BeginFrame runs once from the draw thread, AdvanceForTest diff --git a/HellionChat/Util/HoverMath.cs b/HellionChat/Util/HoverMath.cs index 7a2d624..6fd8263 100644 --- a/HellionChat/Util/HoverMath.cs +++ b/HellionChat/Util/HoverMath.cs @@ -1,12 +1,17 @@ namespace HellionChat.Util; // State rules for the held hover value, split from HoverState so the build suite -// can pin them without an ImGui frame. Rates follow Lightless (Selune.cs:36-37): -// slower out than in is what makes a fade read as deliberate rather than laggy. +// can pin them without an ImGui frame. Slower out than in is what makes a fade +// read as deliberate rather than laggy (the asymmetry follows Lightless, +// Selune.cs:36-37). +// +// The rates started at 14/s in and 8/s out -- a full fade-in in seventy +// milliseconds, which tester feedback called light speed. Half that now: rise in +// about 150ms, settle in about 250ms, which is where UI fades usually live. internal static class HoverMath { - internal const float FadeInPerSecond = 14f; - internal const float FadeOutPerSecond = 8f; + internal const float FadeInPerSecond = 6.5f; + internal const float FadeOutPerSecond = 4f; // Below this an entry is indistinguishable from zero and can be dropped. internal const float EvictBelow = 0.001f; @@ -20,4 +25,9 @@ internal static class HoverMath ); internal static bool ShouldEvict(float value, bool hovered) => !hovered && value <= EvictBelow; + + // Smoothstep over the linear state. The state itself stays linear -- evict + // and threshold logic depend on it -- but what callers paint with starts + // gently and lands gently instead of moving at one speed and stopping dead. + internal static float Ease(float t) => t * t * (3f - 2f * t); }