Files
LightlessClient/LightlessSync/UI/Style/Selune.cs
defnotken 72a62b7449
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m9s
2.1.0 (#123)
# Patchnotes 2.1.0
The changes in this update are more than just "patches". With a new UI, a new feature, and a bunch of bug fixes, improvements and a new member on the dev team, we thought this was more of a minor update.

We would like to introduce @tsubasahane of MareCN to the team! We’re happy to work with them to bring Lightless and its features to the CN client as well as having another talented dev bring features and ideas to us. Speaking of which:

# Location Sharing (Big shout out to @tsubasahane for bringing this feature)

- Are you TIRED of scrambling to find the address of the venue you're in to share with your friends? We are introducing Location Sharing! An optional feature where you can share your location with direct pairs temporarily [30 minutes, 1 hour, 3 hours] minutes or until you turn it off for them. That's up to you! [#125](<#125>)  [#49](<Lightless-Sync/LightlessServer#49>)
- To share your location with a pair, click the three dots beside the pair and choose a duration to share with them. [#125](<#125>)  [#49](<Lightless-Sync/LightlessServer#49>)
- To view the location of someone who's shared with you, simply hover over the globe icon! [#125](<#125>)  [#49](<Lightless-Sync/LightlessServer#49>)

[1]

# Model Optimization (Mesh Decimating)
 - This new option can automatically “simplify” incoming character meshes to help performance by reducing triangle counts. You choose how strong the reduction is (default/recommended is 80%). [#131](<#131>)
 - Decimation only kicks in when a mesh is above a certain triangle threshold, and only for the items that qualify for it and you selected for. [#131](<#131>)
 - Hair meshes is always excluded, since simplifying hair meshes is very prone to breaking.
 - You can find everything under Settings → Performance → Model Optimization. [#131](<#131>)
+ ** IF YOU HAVE USED DECIMATION IN TESTING, PLEASE CLEAR YOUR CACHE  **

[2]

# Animation (PAP) Validation (Safer animations)
 - Lightless now checks your currently animations to see if they work with your local skeleton/bone mod. If an animation matches, it’s included in what gets sent to other players. If it doesn’t, Lightless will skip it and write a warning to your log showing how many were skipped due to skeleton changes. Its defaulted to Unsafe (off). turn it on if you experience crashes from others users. [#131](<#131>)
 - Lightless also does the same kind of check for incoming animation files, to make sure they match the body/skeleton they were sent with. [#131](<#131>)
 - Because these checks can sometimes be a little picky, you can adjust how strict they are in Settings -> General -> Animation & Bones to reduce false positives. [#131](<#131>)

# UI Changes (Thanks to @kyuwu for UI Changes)
- The top part of the main screen has gotten a makeover. You can adjust the colors of the gradiant in the Color settings of Lightless. [#127](<#127>)

[3]

- Settings have gotten some changes as well to make this change more universal, and will use the same color settings. [#127](<#127>)
- The particle effects of the gradient are toggleable in 'Settings -> UI -> Behavior' [#127](<#127>)
- Instead of showing download/upload on bottom of Main UI, it will show VRAM usage and triangles with their optimization options next to it [#138](<#138>)

# LightFinder / ShellFinder
- UI Changes that follow our new design follow the color codes for the Gradient top as the main screen does.  [#127](<#127>)

[4]

Co-authored-by: defnotken <itsdefnotken@gmail.com>
Co-authored-by: azyges <aaaaaa@aaa.aaa>
Co-authored-by: cake <admin@cakeandbanana.nl>
Co-authored-by: Tsubasa <tsubasa@noreply.git.lightless-sync.org>
Co-authored-by: choco <choco@patat.nl>
Co-authored-by: celine <aaa@aaa.aaa>
Co-authored-by: celine <celine@noreply.git.lightless-sync.org>
Co-authored-by: Tsubasahane <wozaiha@gmail.com>
Co-authored-by: cake <cake@noreply.git.lightless-sync.org>
Reviewed-on: #123
2026-01-20 19:43:00 +00:00

1013 lines
34 KiB
C#

using System;
using System.Numerics;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.Utility;
using LightlessSync.UI;
// imagine putting this goober name here
namespace LightlessSync.UI.Style;
public enum SeluneGradientMode
{
Vertical,
Horizontal,
Both,
}
public enum SeluneHighlightMode
{
Horizontal,
Vertical,
Both,
Point,
}
public sealed class SeluneGradientSettings
{
public Vector4 GradientColor { get; init; } = UIColors.Get("LightlessPurple");
public Vector4? HighlightColor { get; init; }
public float GradientPeakOpacity { get; init; } = 0.07f;
public float GradientPeakPosition { get; init; } = 0.035f;
public float HighlightPeakAlpha { get; init; } = 0.13f;
public float HighlightEdgeAlpha { get; init; } = 0f;
public float HighlightMidpoint { get; init; } = 0.45f;
public float MinimumHighlightHalfHeight { get; init; } = 25f;
public float MinimumHighlightHalfWidth { get; init; } = 25f;
public float HighlightFadeInSpeed { get; init; } = 14f;
public float HighlightFadeOutSpeed { get; init; } = 8f;
public float HighlightBorderThickness { get; init; } = 10f;
public float HighlightBorderRounding { get; init; } = 8f;
public SeluneGradientMode BackgroundMode { get; init; } = SeluneGradientMode.Vertical;
public SeluneHighlightMode HighlightMode { get; init; } = SeluneHighlightMode.Horizontal;
public static SeluneGradientSettings Default
=> new()
{
GradientColor = UIColors.Get("LightlessPurple"),
HighlightColor = UIColors.Get("LightlessPurple"),
};
}
public static class Selune
{
[ThreadStatic] private static SeluneCanvas? _activeCanvas;
public static SeluneCanvas Begin(SeluneBrush brush, ImDrawListPtr drawList, Vector2 windowPos, Vector2 windowSize, SeluneGradientSettings? settings = null)
{
var canvas = new SeluneCanvas(brush, drawList, windowPos, windowSize, settings ?? SeluneGradientSettings.Default, _activeCanvas);
_activeCanvas = canvas;
return canvas;
}
internal static void Release(SeluneCanvas canvas)
{
if (_activeCanvas == canvas)
_activeCanvas = canvas.Previous;
}
public static void RegisterHighlight(
Vector2 rectMin,
Vector2 rectMax,
SeluneHighlightMode? modeOverride = null,
bool borderOnly = false,
float? borderThicknessOverride = null,
bool exactSize = false,
bool clipToElement = false,
Vector2? clipPadding = null,
float? roundingOverride = null,
bool spanFullWidth = false,
Vector4? highlightColorOverride = null,
float? highlightAlphaOverride = null)
=> _activeCanvas?.RegisterHighlight(
rectMin,
rectMax,
modeOverride,
borderOnly,
borderThicknessOverride,
exactSize,
clipToElement,
clipPadding,
roundingOverride,
spanFullWidth,
highlightColorOverride,
highlightAlphaOverride);
}
public sealed class SeluneBrush
{
private Vector2? _highlightCenter;
private Vector2 _highlightHalfSize;
private SeluneHighlightMode _highlightMode;
private bool _highlightBorderOnly;
private float _borderThickness;
private bool _useClipRect;
private Vector2 _clipMin;
private Vector2 _clipMax;
private float _highlightRounding;
private bool _highlightUsedThisFrame;
private float _highlightIntensity;
private Vector4? _highlightColorOverride;
private float? _highlightAlphaOverride;
internal void BeginFrame()
{
_highlightUsedThisFrame = false;
}
internal void RegisterHighlight(Vector2 center, Vector2 halfSize, SeluneHighlightMode mode, bool borderOnly, float borderThickness, bool useClipRect, Vector2 clipMin, Vector2 clipMax, float rounding, Vector4? highlightColorOverride, float? highlightAlphaOverride)
{
if (halfSize.X <= 0f || halfSize.Y <= 0f)
return;
_highlightUsedThisFrame = true;
_highlightCenter = center;
_highlightHalfSize = halfSize;
_highlightMode = mode;
_highlightBorderOnly = borderOnly;
_borderThickness = borderOnly ? Math.Max(borderThickness, 0f) : 0f;
_useClipRect = useClipRect;
_clipMin = clipMin;
_clipMax = clipMax;
_highlightRounding = rounding;
_highlightColorOverride = highlightColorOverride;
_highlightAlphaOverride = highlightAlphaOverride;
}
internal void UpdateFade(float deltaTime, SeluneGradientSettings settings)
{
if (deltaTime <= 0f)
return;
if (_highlightUsedThisFrame)
{
_highlightIntensity = MathF.Min(1f, _highlightIntensity + deltaTime * settings.HighlightFadeInSpeed);
}
else
{
_highlightIntensity = MathF.Max(0f, _highlightIntensity - deltaTime * settings.HighlightFadeOutSpeed);
if (_highlightIntensity <= 0.001f)
{
ResetHighlightState();
}
}
}
internal SeluneHighlightRenderState GetRenderState()
=> new(
_highlightCenter,
_highlightHalfSize,
_highlightMode,
_highlightBorderOnly,
_borderThickness,
_useClipRect,
_clipMin,
_clipMax,
_highlightRounding,
_highlightIntensity,
_highlightColorOverride,
_highlightAlphaOverride);
private void ResetHighlightState()
{
_highlightCenter = null;
_highlightHalfSize = Vector2.Zero;
_highlightBorderOnly = false;
_borderThickness = 0f;
_useClipRect = false;
_highlightRounding = 0f;
_highlightColorOverride = null;
_highlightAlphaOverride = null;
}
}
internal readonly struct SeluneHighlightRenderState
{
public SeluneHighlightRenderState(
Vector2? center,
Vector2 halfSize,
SeluneHighlightMode mode,
bool borderOnly,
float borderThickness,
bool useClipRect,
Vector2 clipMin,
Vector2 clipMax,
float rounding,
float intensity,
Vector4? colorOverride,
float? alphaOverride)
{
Center = center;
HalfSize = halfSize;
Mode = mode;
BorderOnly = borderOnly;
BorderThickness = borderThickness;
UseClipRect = useClipRect;
ClipMin = clipMin;
ClipMax = clipMax;
Rounding = rounding;
Intensity = intensity;
ColorOverride = colorOverride;
AlphaOverride = alphaOverride;
}
public Vector2? Center { get; }
public Vector2 HalfSize { get; }
public SeluneHighlightMode Mode { get; }
public bool BorderOnly { get; }
public float BorderThickness { get; }
public bool UseClipRect { get; }
public Vector2 ClipMin { get; }
public Vector2 ClipMax { get; }
public float Rounding { get; }
public float Intensity { get; }
public Vector4? ColorOverride { get; }
public float? AlphaOverride { get; }
public bool HasHighlight => Center.HasValue && HalfSize.X > 0f && HalfSize.Y > 0f && Intensity > 0.001f;
}
public sealed class SeluneCanvas : IDisposable
{
private readonly SeluneBrush _brush;
private readonly ImDrawListPtr _drawList;
private readonly Vector2 _windowPos;
private readonly Vector2 _windowSize;
private readonly SeluneGradientSettings _settings;
private bool _fadeUpdatedThisFrame;
internal SeluneCanvas? Previous { get; }
internal SeluneCanvas(SeluneBrush brush, ImDrawListPtr drawList, Vector2 windowPos, Vector2 windowSize, SeluneGradientSettings settings, SeluneCanvas? previous)
{
_brush = brush;
_drawList = drawList;
_windowPos = windowPos;
_windowSize = windowSize;
_settings = settings;
Previous = previous;
_fadeUpdatedThisFrame = false;
_brush.BeginFrame();
}
public void DrawGradient(float gradientTopY, float gradientBottomY, float deltaTime)
{
DrawInternal(gradientTopY, gradientBottomY, deltaTime, true, true);
}
public void DrawHighlightOnly(float gradientTopY, float gradientBottomY, float deltaTime)
{
DrawInternal(gradientTopY, gradientBottomY, deltaTime, false, true);
}
public void DrawHighlightOnly(float deltaTime)
=> DrawHighlightOnly(_windowPos.Y, _windowPos.Y + _windowSize.Y, deltaTime);
public void Animate(float deltaTime)
{
UpdateFadeOnce(deltaTime);
}
private void UpdateFadeOnce(float deltaTime)
{
if (_fadeUpdatedThisFrame)
return;
_brush.UpdateFade(deltaTime, _settings);
_fadeUpdatedThisFrame = true;
}
private void DrawInternal(float gradientTopY, float gradientBottomY, float deltaTime, bool drawBackground, bool drawHighlight)
{
UpdateFadeOnce(deltaTime);
SeluneRenderer.DrawGradient(
_drawList,
_windowPos,
_windowSize,
gradientTopY,
gradientBottomY,
_brush.GetRenderState(),
_settings,
drawBackground,
drawHighlight);
}
internal void RegisterHighlight(
Vector2 rectMin,
Vector2 rectMax,
SeluneHighlightMode? modeOverride,
bool borderOnly,
float? borderThicknessOverride,
bool exactSize,
bool clipToElement,
Vector2? clipPadding,
float? roundingOverride,
bool spanFullWidth,
Vector4? highlightColorOverride,
float? highlightAlphaOverride)
{
if (spanFullWidth)
{
rectMin.X = _windowPos.X;
rectMax.X = _windowPos.X + _windowSize.X;
}
var size = rectMax - rectMin;
if (size.X <= 0f || size.Y <= 0f)
return;
var center = rectMin + size * 0.5f;
var halfWidth = exactSize ? size.X * 0.5f : Math.Max(size.X * 0.5f, _settings.MinimumHighlightHalfWidth * ImGuiHelpers.GlobalScale);
var halfHeight = exactSize ? size.Y * 0.5f : Math.Max(size.Y * 0.5f, _settings.MinimumHighlightHalfHeight * ImGuiHelpers.GlobalScale);
var mode = modeOverride ?? _settings.HighlightMode;
var thickness = borderOnly ? (borderThicknessOverride ?? _settings.HighlightBorderThickness) * ImGuiHelpers.GlobalScale : 0f;
var useClip = clipToElement;
var padding = clipPadding ?? (borderOnly && thickness > 0f
? new Vector2(thickness)
: Vector2.Zero);
var clipMin = rectMin - padding;
var clipMax = rectMax + padding;
var rounding = (roundingOverride ?? _settings.HighlightBorderRounding) * ImGuiHelpers.GlobalScale;
_brush.RegisterHighlight(center, new Vector2(halfWidth, halfHeight), mode, borderOnly, thickness, useClip, clipMin, clipMax, rounding, highlightColorOverride, highlightAlphaOverride);
_fadeUpdatedThisFrame = false;
}
public void Dispose()
=> Selune.Release(this);
}
// i wonder which sync will copy this shitty code now
internal static class SeluneRenderer
{
public static void DrawGradient(
ImDrawListPtr drawList,
Vector2 windowPos,
Vector2 windowSize,
float gradientTopY,
float gradientBottomY,
SeluneHighlightRenderState highlightState,
SeluneGradientSettings settings,
bool drawBackground = true,
bool drawHighlight = true)
{
var gradientLeft = windowPos.X;
var gradientRight = windowPos.X + windowSize.X;
var windowBottomY = windowPos.Y + windowSize.Y;
var clampedTopY = MathF.Max(gradientTopY, windowPos.Y);
var clampedBottomY = MathF.Min(gradientBottomY, windowBottomY);
if (clampedBottomY <= clampedTopY)
return;
var color = settings.GradientColor;
var topColorVec = new Vector4(color.X, color.Y, color.Z, 0f);
var bottomColorVec = new Vector4(color.X, color.Y, color.Z, 0f);
var midColorVec = new Vector4(color.X, color.Y, color.Z, settings.GradientPeakOpacity);
if (drawBackground)
{
DrawBackground(
drawList,
gradientLeft,
gradientRight,
clampedTopY,
clampedBottomY,
topColorVec,
midColorVec,
bottomColorVec,
settings,
settings.BackgroundMode);
}
if (!drawHighlight)
return;
DrawHighlight(
drawList,
gradientLeft,
gradientRight,
clampedTopY,
clampedBottomY,
highlightState,
settings);
}
private static void DrawBackground(
ImDrawListPtr drawList,
float gradientLeft,
float gradientRight,
float clampedTopY,
float clampedBottomY,
Vector4 topColorVec,
Vector4 midColorVec,
Vector4 bottomColorVec,
SeluneGradientSettings settings,
SeluneGradientMode mode)
{
var peakPosition = Math.Clamp(settings.GradientPeakPosition, 0.01f, 0.99f);
switch (mode)
{
case SeluneGradientMode.Vertical:
DrawVerticalBackground(drawList, gradientLeft, gradientRight, clampedTopY, clampedBottomY, topColorVec, midColorVec, bottomColorVec, peakPosition);
break;
case SeluneGradientMode.Horizontal:
DrawHorizontalBackground(drawList, gradientLeft, gradientRight, clampedTopY, clampedBottomY, topColorVec, midColorVec, bottomColorVec, peakPosition);
break;
case SeluneGradientMode.Both:
DrawVerticalBackground(drawList, gradientLeft, gradientRight, clampedTopY, clampedBottomY, topColorVec, midColorVec, bottomColorVec, peakPosition);
DrawHorizontalBackground(drawList, gradientLeft, gradientRight, clampedTopY, clampedBottomY, topColorVec, midColorVec, bottomColorVec, peakPosition);
break;
}
}
private static void DrawVerticalBackground(
ImDrawListPtr drawList,
float gradientLeft,
float gradientRight,
float clampedTopY,
float clampedBottomY,
Vector4 topColorVec,
Vector4 midColorVec,
Vector4 bottomColorVec,
float peakPosition)
{
var topColor = ImGui.ColorConvertFloat4ToU32(topColorVec);
var midColor = ImGui.ColorConvertFloat4ToU32(midColorVec);
var bottomColor = ImGui.ColorConvertFloat4ToU32(bottomColorVec);
var midY = clampedTopY + (clampedBottomY - clampedTopY) * peakPosition;
drawList.AddRectFilledMultiColor(
new Vector2(gradientLeft, clampedTopY),
new Vector2(gradientRight, midY),
topColor,
topColor,
midColor,
midColor);
drawList.AddRectFilledMultiColor(
new Vector2(gradientLeft, midY),
new Vector2(gradientRight, clampedBottomY),
midColor,
midColor,
bottomColor,
bottomColor);
}
private static void DrawHorizontalBackground(
ImDrawListPtr drawList,
float gradientLeft,
float gradientRight,
float clampedTopY,
float clampedBottomY,
Vector4 leftColorVec,
Vector4 midColorVec,
Vector4 rightColorVec,
float peakPosition)
{
var leftColor = ImGui.ColorConvertFloat4ToU32(leftColorVec);
var midColor = ImGui.ColorConvertFloat4ToU32(midColorVec);
var rightColor = ImGui.ColorConvertFloat4ToU32(rightColorVec);
var midX = gradientLeft + (gradientRight - gradientLeft) * peakPosition;
drawList.AddRectFilledMultiColor(
new Vector2(gradientLeft, clampedTopY),
new Vector2(midX, clampedBottomY),
leftColor,
midColor,
midColor,
leftColor);
drawList.AddRectFilledMultiColor(
new Vector2(midX, clampedTopY),
new Vector2(gradientRight, clampedBottomY),
midColor,
rightColor,
rightColor,
midColor);
}
private static void DrawHighlight(
ImDrawListPtr drawList,
float gradientLeft,
float gradientRight,
float clampedTopY,
float clampedBottomY,
SeluneHighlightRenderState highlightState,
SeluneGradientSettings settings)
{
if (!highlightState.HasHighlight)
return;
var highlightColor = highlightState.ColorOverride ?? settings.HighlightColor ?? settings.GradientColor;
var clampedIntensity = Math.Clamp(highlightState.Intensity, 0f, 1f);
var alphaScale = Math.Clamp(highlightState.AlphaOverride ?? 1f, 0f, 1f);
var peakAlpha = settings.HighlightPeakAlpha * clampedIntensity * alphaScale;
var edgeAlpha = settings.HighlightEdgeAlpha * clampedIntensity * alphaScale;
if (peakAlpha <= 0f && edgeAlpha <= 0f)
return;
var highlightEdgeVec = new Vector4(highlightColor.X, highlightColor.Y, highlightColor.Z, edgeAlpha);
var highlightPeakVec = new Vector4(highlightColor.X, highlightColor.Y, highlightColor.Z, peakAlpha);
var center = highlightState.Center!.Value;
var halfSize = highlightState.HalfSize;
if (highlightState.UseClipRect)
drawList.PushClipRect(highlightState.ClipMin, highlightState.ClipMax, true);
switch (highlightState.Mode)
{
case SeluneHighlightMode.Horizontal:
DrawHorizontalHighlight(
drawList,
gradientLeft,
gradientRight,
clampedTopY,
clampedBottomY,
center,
halfSize,
highlightEdgeVec,
highlightPeakVec,
settings.HighlightMidpoint,
highlightState.BorderOnly,
highlightState.BorderThickness,
highlightState.Rounding);
break;
case SeluneHighlightMode.Vertical:
DrawVerticalHighlight(
drawList,
gradientLeft,
gradientRight,
clampedTopY,
clampedBottomY,
center,
halfSize,
highlightEdgeVec,
highlightPeakVec,
settings.HighlightMidpoint,
highlightState.BorderOnly,
highlightState.BorderThickness,
highlightState.Rounding);
break;
case SeluneHighlightMode.Both:
DrawCombinedHighlight(
drawList,
gradientLeft,
gradientRight,
clampedTopY,
clampedBottomY,
center,
halfSize,
highlightEdgeVec,
highlightPeakVec,
highlightState.BorderOnly,
highlightState.BorderThickness,
highlightState.Rounding);
break;
case SeluneHighlightMode.Point:
DrawPointHighlight(
drawList,
center,
halfSize,
highlightEdgeVec,
highlightPeakVec,
highlightState.BorderOnly,
highlightState.BorderThickness);
break;
}
if (highlightState.UseClipRect)
drawList.PopClipRect();
}
private static void DrawHorizontalHighlight(
ImDrawListPtr drawList,
float gradientLeft,
float gradientRight,
float clampedTopY,
float clampedBottomY,
Vector2 center,
Vector2 halfSize,
Vector4 edgeColor,
Vector4 peakColor,
float midpoint,
bool borderOnly,
float borderThickness,
float rounding)
{
var highlightTop = MathF.Max(clampedTopY, center.Y - halfSize.Y);
var highlightBottom = MathF.Min(clampedBottomY, center.Y + halfSize.Y);
if (highlightBottom <= highlightTop)
return;
var highlightLeft = MathF.Max(gradientLeft, center.X - halfSize.X);
var highlightRight = MathF.Min(gradientRight, center.X + halfSize.X);
if (highlightRight <= highlightLeft || highlightBottom <= highlightTop)
return;
if (!borderOnly || borderThickness <= 0f)
{
DrawHorizontalHighlightRect(
drawList,
highlightLeft,
highlightRight,
highlightTop,
highlightBottom,
edgeColor,
peakColor,
midpoint,
1f);
return;
}
var innerTop = MathF.Min(highlightBottom, MathF.Max(highlightTop, center.Y - MathF.Max(halfSize.Y - borderThickness, 0f)));
var innerBottom = MathF.Max(highlightTop, MathF.Min(highlightBottom, center.Y + MathF.Max(halfSize.Y - borderThickness, 0f)));
var edgeU32 = ImGui.ColorConvertFloat4ToU32(edgeColor);
var peakU32 = ImGui.ColorConvertFloat4ToU32(peakColor);
if (innerTop > highlightTop)
{
drawList.AddRectFilledMultiColor(
new Vector2(highlightLeft, highlightTop),
new Vector2(highlightRight, innerTop),
edgeU32,
edgeU32,
peakU32,
peakU32);
}
if (innerBottom < highlightBottom)
{
drawList.AddRectFilledMultiColor(
new Vector2(highlightLeft, innerBottom),
new Vector2(highlightRight, highlightBottom),
peakU32,
peakU32,
edgeU32,
edgeU32);
}
}
private static void DrawVerticalHighlight(
ImDrawListPtr drawList,
float gradientLeft,
float gradientRight,
float clampedTopY,
float clampedBottomY,
Vector2 center,
Vector2 halfSize,
Vector4 edgeColor,
Vector4 peakColor,
float midpoint,
bool borderOnly,
float borderThickness,
float rounding)
{
var highlightTop = MathF.Max(clampedTopY, center.Y - halfSize.Y);
var highlightBottom = MathF.Min(clampedBottomY, center.Y + halfSize.Y);
var highlightLeft = MathF.Max(gradientLeft, center.X - halfSize.X);
var highlightRight = MathF.Min(gradientRight, center.X + halfSize.X);
if (highlightRight <= highlightLeft || highlightBottom <= highlightTop)
return;
if (!borderOnly || borderThickness <= 0f)
{
DrawVerticalHighlightRect(
drawList,
highlightLeft,
highlightRight,
highlightTop,
highlightBottom,
edgeColor,
peakColor,
midpoint,
1f);
return;
}
var innerLeft = MathF.Min(highlightRight, MathF.Max(highlightLeft, center.X - MathF.Max(halfSize.X - borderThickness, 0f)));
var innerRight = MathF.Max(highlightLeft, MathF.Min(highlightRight, center.X + MathF.Max(halfSize.X - borderThickness, 0f)));
var edgeU32 = ImGui.ColorConvertFloat4ToU32(edgeColor);
var peakU32 = ImGui.ColorConvertFloat4ToU32(peakColor);
if (innerLeft > highlightLeft)
{
drawList.AddRectFilledMultiColor(
new Vector2(highlightLeft, highlightTop),
new Vector2(innerLeft, highlightBottom),
edgeU32,
peakU32,
peakU32,
edgeU32);
}
if (innerRight < highlightRight)
{
drawList.AddRectFilledMultiColor(
new Vector2(innerRight, highlightTop),
new Vector2(highlightRight, highlightBottom),
peakU32,
edgeU32,
edgeU32,
peakU32);
}
}
private static void DrawCombinedHighlight(
ImDrawListPtr drawList,
float gradientLeft,
float gradientRight,
float clampedTopY,
float clampedBottomY,
Vector2 center,
Vector2 halfSize,
Vector4 edgeColor,
Vector4 peakColor,
bool borderOnly,
float borderThickness,
float rounding)
{
var highlightLeft = MathF.Max(gradientLeft, center.X - halfSize.X);
var highlightRight = MathF.Min(gradientRight, center.X + halfSize.X);
var highlightTop = MathF.Max(clampedTopY, center.Y - halfSize.Y);
var highlightBottom = MathF.Min(clampedBottomY, center.Y + halfSize.Y);
if (highlightRight <= highlightLeft || highlightBottom <= highlightTop)
return;
if (borderOnly && borderThickness > 0f)
{
DrawRoundedBorderGlow(drawList, center, halfSize, borderThickness, edgeColor, peakColor, rounding);
return;
}
if (!borderOnly || borderThickness <= 0f)
{
const float combinedScale = 0.85f;
DrawHorizontalHighlightRect(
drawList,
highlightLeft,
highlightRight,
highlightTop,
highlightBottom,
edgeColor,
peakColor,
0.5f,
combinedScale);
DrawVerticalHighlightRect(
drawList,
highlightLeft,
highlightRight,
highlightTop,
highlightBottom,
edgeColor,
peakColor,
0.5f,
combinedScale);
return;
}
var outerLeft = MathF.Max(gradientLeft, highlightLeft - borderThickness);
var outerRight = MathF.Min(gradientRight, highlightRight + borderThickness);
var outerTop = MathF.Max(clampedTopY, highlightTop - borderThickness);
var outerBottom = MathF.Min(clampedBottomY, highlightBottom + borderThickness);
var edge = ImGui.ColorConvertFloat4ToU32(edgeColor);
var peak = ImGui.ColorConvertFloat4ToU32(peakColor);
if (outerTop < highlightTop)
{
drawList.AddRectFilledMultiColor(
new Vector2(outerLeft, outerTop),
new Vector2(outerRight, highlightTop),
edge,
edge,
peak,
peak);
}
if (outerBottom > highlightBottom)
{
drawList.AddRectFilledMultiColor(
new Vector2(outerLeft, highlightBottom),
new Vector2(outerRight, outerBottom),
peak,
peak,
edge,
edge);
}
if (outerLeft < highlightLeft)
{
drawList.AddRectFilledMultiColor(
new Vector2(outerLeft, highlightTop),
new Vector2(highlightLeft, highlightBottom),
edge,
peak,
peak,
edge);
}
if (outerRight > highlightRight)
{
drawList.AddRectFilledMultiColor(
new Vector2(highlightRight, highlightTop),
new Vector2(outerRight, highlightBottom),
peak,
edge,
edge,
peak);
}
}
private static void DrawPointHighlight(
ImDrawListPtr drawList,
Vector2 center,
Vector2 halfSize,
Vector4 edgeColor,
Vector4 peakColor,
bool borderOnly,
float borderThickness)
{
if (halfSize.X <= 0f || halfSize.Y <= 0f)
return;
if (borderOnly && borderThickness > 0f)
{
DrawPointBorderGlow(drawList, center, halfSize, borderThickness, edgeColor, peakColor);
return;
}
const int layers = 7;
for (int layer = 0; layer < layers; layer++)
{
float t = layers <= 1 ? 1f : layer / (layers - 1f);
float scale = 1f - 0.75f * t;
var scaledHalfSize = new Vector2(MathF.Max(1f, halfSize.X * scale), MathF.Max(1f, halfSize.Y * scale));
var color = Vector4.Lerp(edgeColor, peakColor, t);
DrawEllipseFilled(drawList, center, scaledHalfSize, ImGui.ColorConvertFloat4ToU32(color));
}
}
private static void DrawPointBorderGlow(
ImDrawListPtr drawList,
Vector2 center,
Vector2 halfSize,
float thickness,
Vector4 edgeColor,
Vector4 peakColor)
{
int layers = Math.Max(6, (int)MathF.Ceiling(thickness));
for (int i = 0; i < layers; i++)
{
float t = layers <= 1 ? 1f : i / (layers - 1f);
float offset = thickness * t;
var expandedHalfSize = new Vector2(MathF.Max(1f, halfSize.X + offset), MathF.Max(1f, halfSize.Y + offset));
var color = Vector4.Lerp(peakColor, edgeColor, t);
color.W = Math.Clamp((peakColor.W * 0.8f) + (edgeColor.W - peakColor.W) * t, 0f, 1f);
DrawEllipseStroke(drawList, center, expandedHalfSize, ImGui.ColorConvertFloat4ToU32(color), 2f);
}
}
private static void DrawEllipseFilled(ImDrawListPtr drawList, Vector2 center, Vector2 halfSize, uint color, int segments = 48)
{
if (halfSize.X <= 0f || halfSize.Y <= 0f)
return;
BuildEllipsePath(drawList, center, halfSize, segments);
drawList.PathFillConvex(color);
}
private static void DrawEllipseStroke(ImDrawListPtr drawList, Vector2 center, Vector2 halfSize, uint color, float thickness, int segments = 48)
{
if (halfSize.X <= 0f || halfSize.Y <= 0f)
return;
BuildEllipsePath(drawList, center, halfSize, segments);
drawList.PathStroke(color, ImDrawFlags.None, MathF.Max(1f, thickness));
}
private static void BuildEllipsePath(ImDrawListPtr drawList, Vector2 center, Vector2 halfSize, int segments)
{
const float twoPi = MathF.PI * 2f;
segments = Math.Clamp(segments, 12, 96);
drawList.PathClear();
for (int i = 0; i < segments; i++)
{
float angle = twoPi * (i / (float)segments);
var point = new Vector2(
center.X + MathF.Cos(angle) * halfSize.X,
center.Y + MathF.Sin(angle) * halfSize.Y);
drawList.PathLineTo(point);
}
}
private static void DrawRoundedBorderGlow(
ImDrawListPtr drawList,
Vector2 center,
Vector2 halfSize,
float thickness,
Vector4 edgeColor,
Vector4 peakColor,
float rounding)
{
int layers = Math.Max(6, (int)MathF.Ceiling(thickness));
for (int i = 0; i < layers; i++)
{
float t = layers <= 1 ? 0f : i / (layers - 1f);
float offset = thickness * t;
var min = new Vector2(center.X - halfSize.X - offset, center.Y - halfSize.Y - offset);
var max = new Vector2(center.X + halfSize.X + offset, center.Y + halfSize.Y + offset);
var color = Vector4.Lerp(peakColor, edgeColor, t);
color.W = Math.Clamp((peakColor.W * 0.8f) + (edgeColor.W - peakColor.W) * t, 0f, 1f);
drawList.AddRect(min, max, ImGui.ColorConvertFloat4ToU32(color), MathF.Max(0f, rounding + offset), ImDrawFlags.RoundCornersAll, 2f);
}
}
private static void DrawHorizontalHighlightRect(
ImDrawListPtr drawList,
float left,
float right,
float top,
float bottom,
Vector4 edgeColor,
Vector4 peakColor,
float midpoint,
float alphaScale)
{
if (right <= left || bottom <= top)
return;
edgeColor.W *= alphaScale;
peakColor.W *= alphaScale;
var edge = ImGui.ColorConvertFloat4ToU32(edgeColor);
var peak = ImGui.ColorConvertFloat4ToU32(peakColor);
var highlightMid = top + (bottom - top) * midpoint;
drawList.AddRectFilledMultiColor(
new Vector2(left, top),
new Vector2(right, highlightMid),
edge,
edge,
peak,
peak);
drawList.AddRectFilledMultiColor(
new Vector2(left, highlightMid),
new Vector2(right, bottom),
peak,
peak,
edge,
edge);
}
private static void DrawVerticalHighlightRect(
ImDrawListPtr drawList,
float left,
float right,
float top,
float bottom,
Vector4 edgeColor,
Vector4 peakColor,
float midpoint,
float alphaScale)
{
if (right <= left || bottom <= top)
return;
edgeColor.W *= alphaScale;
peakColor.W *= alphaScale;
var edge = ImGui.ColorConvertFloat4ToU32(edgeColor);
var peak = ImGui.ColorConvertFloat4ToU32(peakColor);
var highlightMid = left + (right - left) * midpoint;
drawList.AddRectFilledMultiColor(
new Vector2(left, top),
new Vector2(highlightMid, bottom),
edge,
peak,
peak,
edge);
drawList.AddRectFilledMultiColor(
new Vector2(highlightMid, top),
new Vector2(right, bottom),
peak,
edge,
edge,
peak);
}
}