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 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.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, SeluneGradientMode mode) { switch (mode) { case SeluneGradientMode.Vertical: DrawVerticalBackground(drawList, gradientLeft, gradientRight, clampedTopY, clampedBottomY, topColorVec, midColorVec, bottomColorVec); break; case SeluneGradientMode.Horizontal: DrawHorizontalBackground(drawList, gradientLeft, gradientRight, clampedTopY, clampedBottomY, topColorVec, midColorVec, bottomColorVec); break; case SeluneGradientMode.Both: DrawVerticalBackground(drawList, gradientLeft, gradientRight, clampedTopY, clampedBottomY, topColorVec, midColorVec, bottomColorVec); DrawHorizontalBackground(drawList, gradientLeft, gradientRight, clampedTopY, clampedBottomY, topColorVec, midColorVec, bottomColorVec); break; } } private static void DrawVerticalBackground( ImDrawListPtr drawList, float gradientLeft, float gradientRight, float clampedTopY, float clampedBottomY, Vector4 topColorVec, Vector4 midColorVec, Vector4 bottomColorVec) { var topColor = ImGui.ColorConvertFloat4ToU32(topColorVec); var midColor = ImGui.ColorConvertFloat4ToU32(midColorVec); var bottomColor = ImGui.ColorConvertFloat4ToU32(bottomColorVec); var midY = clampedTopY + (clampedBottomY - clampedTopY) * 0.035f; 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) { var leftColor = ImGui.ColorConvertFloat4ToU32(leftColorVec); var midColor = ImGui.ColorConvertFloat4ToU32(midColorVec); var rightColor = ImGui.ColorConvertFloat4ToU32(rightColorVec); var midX = gradientLeft + (gradientRight - gradientLeft) * 0.035f; 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); } }