From 8dade8c4b2ce04986a7c10ca00d59e0202d2e884 Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Wed, 20 May 2026 08:57:33 +0200 Subject: [PATCH] feat(themes): add ThemeAbgrCacheLerp pure-helper for crossfade Per-slot ABGR byte-lerp between two cache value-records, stack-allocated output, t clamped. Pattern anchor: imgui.cpp ImAlphaBlendColors. --- HellionChat/Themes/ThemeAbgrCacheLerp.cs | 61 ++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 HellionChat/Themes/ThemeAbgrCacheLerp.cs diff --git a/HellionChat/Themes/ThemeAbgrCacheLerp.cs b/HellionChat/Themes/ThemeAbgrCacheLerp.cs new file mode 100644 index 0000000..e2e750d --- /dev/null +++ b/HellionChat/Themes/ThemeAbgrCacheLerp.cs @@ -0,0 +1,61 @@ +namespace HellionChat.Themes; + +// Per-slot ABGR byte-lerp between two ThemeAbgrCache value-records. +// Pattern anchor: imgui.cpp:2820-2828 ImAlphaBlendColors -- decompose +// each byte, lerp via Math.Round, recompose. Stack-allocated output +// (readonly record struct), no heap pressure inside the crossfade +// window. t is clamped to [0, 1] so float drift cannot overshoot. +internal static class ThemeAbgrCacheLerp +{ + public static ThemeAbgrCache Lerp(ThemeAbgrCache from, ThemeAbgrCache to, float t) + { + t = Math.Clamp(t, 0f, 1f); + + return new ThemeAbgrCache( + PrimaryDark: LerpAbgr(from.PrimaryDark, to.PrimaryDark, t), + Primary: LerpAbgr(from.Primary, to.Primary, t), + PrimaryLight: LerpAbgr(from.PrimaryLight, to.PrimaryLight, t), + PrimaryGlow: LerpAbgr(from.PrimaryGlow, to.PrimaryGlow, t), + AccentDark: LerpAbgr(from.AccentDark, to.AccentDark, t), + Accent: LerpAbgr(from.Accent, to.Accent, t), + AccentLight: LerpAbgr(from.AccentLight, to.AccentLight, t), + Identity: LerpAbgr(from.Identity, to.Identity, t), + WindowBg: LerpAbgr(from.WindowBg, to.WindowBg, t), + ChildBg: LerpAbgr(from.ChildBg, to.ChildBg, t), + FrameBg: LerpAbgr(from.FrameBg, to.FrameBg, t), + Surface: LerpAbgr(from.Surface, to.Surface, t), + SurfaceHover: LerpAbgr(from.SurfaceHover, to.SurfaceHover, t), + Border: LerpAbgr(from.Border, to.Border, t), + TextPrimary: LerpAbgr(from.TextPrimary, to.TextPrimary, t), + TextMuted: LerpAbgr(from.TextMuted, to.TextMuted, t), + TextDim: LerpAbgr(from.TextDim, to.TextDim, t), + StatusSuccess: LerpAbgr(from.StatusSuccess, to.StatusSuccess, t), + StatusDanger: LerpAbgr(from.StatusDanger, to.StatusDanger, t), + StatusWarning: LerpAbgr(from.StatusWarning, to.StatusWarning, t), + StatusInfo: LerpAbgr(from.StatusInfo, to.StatusInfo, t) + ); + } + + private static uint LerpAbgr(uint from, uint to, float t) + { + var ra = (byte)(from & 0xFFu); + var ga = (byte)((from >> 8) & 0xFFu); + var ba = (byte)((from >> 16) & 0xFFu); + var aa = (byte)((from >> 24) & 0xFFu); + + var rb = (byte)(to & 0xFFu); + var gb = (byte)((to >> 8) & 0xFFu); + var bb = (byte)((to >> 16) & 0xFFu); + var ab = (byte)((to >> 24) & 0xFFu); + + // Math.Round (default ToEven) matches ColourUtil.ApplyAlpha so a + // crossfade-into-hover transition does not produce a one-byte + // jump at the midpoint between the two paths. + var r = (byte)Math.Round(ra + (rb - ra) * t); + var g = (byte)Math.Round(ga + (gb - ga) * t); + var b = (byte)Math.Round(ba + (bb - ba) * t); + var a = (byte)Math.Round(aa + (ab - aa) * t); + + return ((uint)a << 24) | ((uint)b << 16) | ((uint)g << 8) | r; + } +}