fix(style): let the backdrop follow the window's own opacity

Three problems from one screenshot, and the same root cause behind two of them.

The pane painted at full opacity regardless of the window it sat in. BgAlpha
only ever reaches WindowBg, so a draw-list fill ignores it entirely, and a
deliberately translucent settings window came out as a solid block beside a
translucent chat window. The backdrop now reads the alpha out of the resolved
WindowBg colour and carries it through the gradient, the accent wash and the
motes.

The banding is 8-bit quantisation, not a rendering fault: a gentle ramp across a
tall surface crosses so few distinct values that each one covers a visible band.
Halving the range leaves fewer and fainter steps, and the translucency now
underneath them breaks up what remains.

Motes over the chat log drop to 18%. A settings page is read in glances and can
carry motion behind it; a chat log is read line by line, where anything drifting
behind the text competes with it. Intensity is a parameter rather than a second
particle system, so the two surfaces share one implementation.
This commit is contained in:
2026-08-18 18:01:07 +02:00
parent 8ebed6846d
commit 8b96ffb2ec
3 changed files with 32 additions and 13 deletions
@@ -49,7 +49,13 @@ internal sealed class AmbientParticles
}
}
internal void Draw(ImDrawListPtr dl, Vector2 min, Vector2 max, uint accentAbgr)
internal void Draw(
ImDrawListPtr dl,
Vector2 min,
Vector2 max,
uint accentAbgr,
float intensity = 1f
)
{
if (Plugin.Config.ReduceMotion)
return;
@@ -98,13 +104,13 @@ internal sealed class AmbientParticles
dl.AddCircleFilled(
pos,
radius * 2.1f,
ColourUtil.ApplyAlpha(accentAbgr, 0.13f * edge * pulse),
ColourUtil.ApplyAlpha(accentAbgr, 0.13f * edge * pulse * intensity),
10
);
dl.AddCircleFilled(
pos,
radius,
ColourUtil.ApplyAlpha(accentAbgr, 0.42f * edge * pulse),
ColourUtil.ApplyAlpha(accentAbgr, 0.42f * edge * pulse * intensity),
10
);
}
+18 -6
View File
@@ -28,32 +28,44 @@ internal sealed class SurfaceBackdrop
// 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, bool motes = true)
internal void Draw(float accentWashHeight = 0.38f, float darken = 0f, float moteIntensity = 1f)
{
var dl = ImGui.GetWindowDrawList();
var min = ImGui.GetWindowPos();
var max = min + ImGui.GetWindowSize();
var colors = _themes.Active.Colors;
// Opacity taken from the window's own background colour rather than
// assumed. BgAlpha only reaches WindowBg, so a draw-list fill painted at
// full opacity turns a deliberately translucent window into a solid
// block -- which is exactly how the settings pane ended up looking like
// a foreign object next to the chat window.
var windowAlpha = ((ImGui.GetColorU32(ImGuiCol.WindowBg) >> 24) & 0xFFu) / 255f;
var surface = ColourUtil.RgbaToAbgr(_resolver.Resolve(Token.SurfaceBase, colors));
if (darken > 0f)
surface = ColourUtil.LerpTowardBlack(surface, darken);
surface = ColourUtil.ApplyAlpha(surface, windowAlpha);
var accent = ColourUtil.RgbaToAbgr(_resolver.Resolve(Token.AccentPrimary, colors));
dl.DrawVerticalGradient(min, max, surface, topLift: 0.20f, bottomDrop: 0.14f);
// Shallower than it was. The banding people see in a large gradient is
// 8-bit quantisation: a gentle ramp crosses few enough values that each
// one covers a visible band of pixels. Less range means fewer, fainter
// steps, and the window's translucency breaks up what is left.
dl.DrawVerticalGradient(min, max, surface, topLift: 0.10f, bottomDrop: 0.07f);
if (accentWashHeight > 0f)
dl.AddRectFilledMultiColor(
min,
new Vector2(max.X, min.Y + (max.Y - min.Y) * accentWashHeight),
ColourUtil.ApplyAlpha(accent, 0.10f),
ColourUtil.ApplyAlpha(accent, 0.05f),
ColourUtil.ApplyAlpha(accent, 0.09f * windowAlpha),
ColourUtil.ApplyAlpha(accent, 0.045f * windowAlpha),
accent & 0x00FFFFFFu,
accent & 0x00FFFFFFu
);
if (motes)
_motes.Draw(dl, min, max, accent);
if (moteIntensity > 0f)
_motes.Draw(dl, min, max, accent, moteIntensity * windowAlpha);
}
}
+5 -4
View File
@@ -385,10 +385,11 @@ internal sealed class MainWindow : Window, IFocusableChatWindow
{
if (messages.Success)
{
// No accent wash here. It works on a settings pane, where the
// top of the surface is a heading; over a chat log the first
// messages would sit in a tinted band and read as highlighted.
_backdrop.Draw(accentWashHeight: 0f);
// No accent wash, and the motes turned right down. Both work on
// a settings pane, which is read in glances; a chat log is read
// line by line, and anything drifting behind the text competes
// with it. What is left is barely a texture.
_backdrop.Draw(accentWashHeight: 0f, moteIntensity: 0.18f);
if (_activeTab is not null)
_messages.Draw(_activeTab);