Files
HellionChat/HellionChat/Ui/StyleEngine/SurfaceBackdrop.cs
T
JonKazama-Hellion 7df18bd552 fix(style): shorten the ramp instead of layering it
The dithering attempt made it worse and the reasoning was backwards. Four layers
at a quarter alpha each carry a quarter of the steps each, so every layer bands
more coarsely than the single ramp did, and blending four coarse ramps adds
interference on top. Reverted.

The real variable is distance, not layer count. A ramp has as many steps as it
has distinct alpha values, so stretching it over the full height of a pane gives
each step a stripe twenty-odd pixels tall, which the eye sharpens into
scanlines. Running the same ramp over 190 pixels puts those steps a few pixels
apart, where they read as a falloff.

So: light fading down from the top edge, shadow gathering at the bottom, and a
flat surface in between. Constant alpha cannot band at all, which leaves most of
the pane immune by construction.
2026-08-18 18:14:02 +02:00

82 lines
3.4 KiB
C#

using System.Numerics;
using Dalamud.Bindings.ImGui;
using HellionChat.Themes;
using HellionChat.Util;
namespace HellionChat.Ui.StyleEngine;
// The ground under a scrolling surface: gradient, accent wash, drifting motes.
// Pulled out of the settings pane so the chat log can stand on the same floor --
// two windows in one plugin looking like two different products was half of why
// the settings window read as untouched.
//
// Each instance owns its motes, so two surfaces on screen at once do not share
// one drift pattern.
internal sealed class SurfaceBackdrop
{
private readonly ThemeRegistry _themes;
private readonly TokenResolver _resolver;
private readonly AmbientParticles _motes;
internal SurfaceBackdrop(ThemeRegistry themes, TokenResolver resolver, int moteCount = 70)
{
_themes = themes;
_resolver = resolver;
_motes = new AmbientParticles(moteCount);
}
// Call right after entering a child, before its content. GetWindowDrawList
// is then that child's own list, so everything lands behind the content
// without a channel split.
internal void Draw(
float accentWashHeight = 0.38f,
float darken = 0f,
float moteIntensity = 1f,
float strength = 1f
)
{
var dl = ImGui.GetWindowDrawList();
var min = ImGui.GetWindowPos();
var max = min + ImGui.GetWindowSize();
var colors = _themes.Active.Colors;
// Modulation, not a second ground. The window has already painted its
// own background; filling the same area again stacked two layers and
// made a deliberately translucent window read as solid. Worse, an opaque
// fill had to carry the whole gradient by itself, which is what produced
// the banding -- a shallow ramp across an opaque surface crosses few
// enough 8-bit values that each covers a visible stripe.
//
// Near-transparent white over black instead: the window colour stays
// visible underneath, the ramp only tints it, and the game showing
// through breaks up any step that is left.
var opacity = ((ImGui.GetColorU32(ImGuiCol.WindowBg) >> 24) & 0xFFu) / 255f;
// Two short fades rather than one tall ramp: light from the top edge,
// shadow gathering at the bottom, flat in between. A ramp stretched over
// the full height is exactly the case that bands, because its handful of
// alpha steps each cover twenty-odd pixels.
var fade = MathF.Min(190f * Metrics.Scale, (max.Y - min.Y) * 0.45f);
dl.DrawEdgeTint(min, max, 0x00FFFFFFu | ((uint)(0x22 * strength * opacity) << 24), fade);
dl.DrawEdgeTint(min, max, (uint)(0x30 * strength * opacity) << 24, fade, fromBottom: true);
if (darken > 0f)
dl.AddRectFilled(min, max, (uint)(0xFF * darken * opacity) << 24);
var accent = ColourUtil.RgbaToAbgr(_resolver.Resolve(Token.AccentPrimary, colors));
if (accentWashHeight > 0f)
dl.AddRectFilledMultiColor(
min,
new Vector2(max.X, min.Y + (max.Y - min.Y) * accentWashHeight),
ColourUtil.ApplyAlpha(accent, 0.07f * opacity),
ColourUtil.ApplyAlpha(accent, 0.035f * opacity),
accent & 0x00FFFFFFu,
accent & 0x00FFFFFFu
);
if (moteIntensity > 0f)
_motes.Draw(dl, min, max, accent, moteIntensity * opacity);
}
}