Six behavior-preserving consolidations from the audit: - RPCClient::call() overloads share a private performCall() (payload dump + curl_easy_perform + http code) and a static parseRpcResult() (error->RpcError extraction). The timeout overload restores the prior timeout in both the success and catch paths, exactly as before. - The six UnifiedCallback methods delegate to one splitUnified() that builds the (Callback, ErrorCallback) pair once (null-check preserved). - One liteTrimCopy() in lite_connection_service replaces 5 file-local trim-copy helpers across the lite slice. - lite_wallet_controller's inline height JSON parse now routes through the tested parseLiteHeightResponse(). - theme_effects.cpp: unpackRGB()/scaledAlpha() replace 14 RGB-unpack + 5 alpha-scale copy-paste blocks. - settings.cpp load() uses loadScalar()/loadClamped() helpers for ~44 scalar fields. Besides removing boilerplate this HARDENS loading: ~44 fields that previously only checked contains() now also verify the JSON type, so a malformed value in settings.json falls back to the default instead of throwing/misreading. Custom cases (enum parses, legacy migrations, arrays) stay inline; save() is unchanged. Full-node + Lite build clean; ctest 1/1; hygiene clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1141 lines
47 KiB
C++
1141 lines
47 KiB
C++
// DragonX Wallet - ImGui Edition
|
||
// Copyright 2024-2026 The Hush Developers
|
||
// Released under the GPLv3
|
||
|
||
#include "theme_effects.h"
|
||
#include "low_spec.h"
|
||
#include "../schema/ui_schema.h"
|
||
#include <cmath>
|
||
#include <cstdlib>
|
||
#include <cstring>
|
||
#include <algorithm>
|
||
#include "../../util/logger.h"
|
||
|
||
namespace dragonx {
|
||
namespace ui {
|
||
namespace effects {
|
||
|
||
namespace {
|
||
|
||
// Colors are packed little-endian RGBA (R at bit 0, G at 8, B at 16, A at 24) — the
|
||
// same layout IM_COL32 produces. unpackRGB pulls out the three color channels (alpha is
|
||
// handled separately at each site).
|
||
struct RGB { int r, g, b; };
|
||
static inline RGB unpackRGB(ImU32 c) {
|
||
return RGB{ (int)(c & 0xFF), (int)((c >> 8) & 0xFF), (int)((c >> 16) & 0xFF) };
|
||
}
|
||
|
||
// Scales a 0..1 base alpha by the background opacity into a clamped 0..255 int.
|
||
static inline int scaledAlpha(float base, float bgOpacity) {
|
||
return std::clamp((int)(base * bgOpacity * 255.0f), 0, 255);
|
||
}
|
||
|
||
} // namespace
|
||
|
||
// ============================================================================
|
||
// Singleton
|
||
// ============================================================================
|
||
|
||
ThemeEffects& ThemeEffects::instance() {
|
||
static ThemeEffects s;
|
||
return s;
|
||
}
|
||
|
||
// ============================================================================
|
||
// Frame update
|
||
// ============================================================================
|
||
|
||
void ThemeEffects::beginFrame() {
|
||
time_ = (float)ImGui::GetTime();
|
||
ImGuiViewport* vp = ImGui::GetMainViewport();
|
||
vpMinY_ = vp->WorkPos.y;
|
||
vpMaxY_ = vp->WorkPos.y + vp->WorkSize.y;
|
||
}
|
||
|
||
// ============================================================================
|
||
// Load config from [effects] section via UISchema
|
||
// ============================================================================
|
||
|
||
void ThemeEffects::loadFromTheme() {
|
||
auto& S = schema::UI();
|
||
auto eff = [&](const char* name) {
|
||
return S.drawElement("effects", name);
|
||
};
|
||
|
||
// ---- Hue Cycle ----
|
||
hue_cycle_.enabled = eff("hue-cycle-enabled").sizeOr(0.0f) > 0.5f;
|
||
hue_cycle_.speed = eff("hue-cycle-speed").sizeOr(0.1f);
|
||
hue_cycle_.sat = eff("hue-cycle-saturation").sizeOr(0.6f);
|
||
hue_cycle_.val = eff("hue-cycle-value").sizeOr(0.85f);
|
||
hue_cycle_.range = eff("hue-cycle-range").sizeOr(1.0f);
|
||
hue_cycle_.offset = eff("hue-cycle-offset").sizeOr(0.0f);
|
||
|
||
// ---- Rainbow Border ----
|
||
rainbow_border_.enabled = eff("rainbow-border-enabled").sizeOr(0.0f) > 0.5f;
|
||
rainbow_border_.speed = eff("rainbow-border-speed").sizeOr(0.05f);
|
||
rainbow_border_.alpha = eff("rainbow-border-alpha").sizeOr(0.25f);
|
||
rainbow_border_.stops.clear();
|
||
|
||
// Read color stops from extraColors (stored as stop-0, stop-1, etc.)
|
||
for (int i = 0; i < 8; i++) {
|
||
char key[32];
|
||
snprintf(key, sizeof(key), "rainbow-border-stop-%d", i);
|
||
auto elem = eff(key);
|
||
if (!elem.color.empty()) {
|
||
rainbow_border_.stops.push_back(parseHexColor(elem.color));
|
||
} else if (elem.size > 0) {
|
||
break; // no more stops
|
||
}
|
||
}
|
||
// Fallback default stops
|
||
if (rainbow_border_.enabled && rainbow_border_.stops.empty()) {
|
||
rainbow_border_.stops.push_back(IM_COL32(255, 107, 157, 255));
|
||
rainbow_border_.stops.push_back(IM_COL32(192, 132, 252, 255));
|
||
rainbow_border_.stops.push_back(IM_COL32(103, 232, 249, 255));
|
||
rainbow_border_.stops.push_back(IM_COL32(252, 165, 165, 255));
|
||
}
|
||
|
||
// ---- Shimmer ----
|
||
shimmer_.enabled = eff("shimmer-enabled").sizeOr(0.0f) > 0.5f;
|
||
shimmer_.speed = eff("shimmer-speed").sizeOr(0.12f);
|
||
shimmer_.width = eff("shimmer-width").sizeOr(80.0f);
|
||
shimmer_.alpha = eff("shimmer-alpha").sizeOr(0.06f);
|
||
shimmer_.angle = eff("shimmer-angle").sizeOr(30.0f);
|
||
// Shimmer color: read from the schema's color resolver
|
||
auto shimmerColorElem = eff("shimmer-color");
|
||
if (!shimmerColorElem.color.empty()) {
|
||
shimmer_.color = S.resolveColor(shimmerColorElem.color, IM_COL32(255,255,255,255));
|
||
} else {
|
||
shimmer_.color = IM_COL32(255, 255, 255, 255);
|
||
}
|
||
|
||
// ---- Positional Hue ----
|
||
positional_hue_.enabled = eff("positional-hue-enabled").sizeOr(0.0f) > 0.5f;
|
||
positional_hue_.strength = eff("positional-hue-strength").sizeOr(0.3f);
|
||
auto topElem = eff("positional-hue-top");
|
||
auto botElem = eff("positional-hue-bottom");
|
||
if (!topElem.color.empty())
|
||
positional_hue_.topColor = parseHexColorVec4(topElem.color);
|
||
if (!botElem.color.empty())
|
||
positional_hue_.bottomColor = parseHexColorVec4(botElem.color);
|
||
|
||
// ---- Glow Pulse ----
|
||
glow_pulse_.enabled = eff("glow-pulse-enabled").sizeOr(0.0f) > 0.5f;
|
||
glow_pulse_.speed = eff("glow-pulse-speed").sizeOr(2.0f);
|
||
glow_pulse_.minAlpha = eff("glow-pulse-min-alpha").sizeOr(0.0f);
|
||
glow_pulse_.maxAlpha = eff("glow-pulse-max-alpha").sizeOr(0.15f);
|
||
glow_pulse_.radius = eff("glow-pulse-radius").sizeOr(4.0f);
|
||
auto glowColorElem = eff("glow-pulse-color");
|
||
if (!glowColorElem.color.empty()) {
|
||
glow_pulse_.color = S.resolveColor(glowColorElem.color, IM_COL32(255, 218, 0, 255));
|
||
} else {
|
||
glow_pulse_.color = IM_COL32(255, 218, 0, 255);
|
||
}
|
||
|
||
// ---- Edge Trace ----
|
||
edge_trace_.enabled = eff("edge-trace-enabled").sizeOr(0.0f) > 0.5f;
|
||
edge_trace_.speed = eff("edge-trace-speed").sizeOr(0.3f);
|
||
edge_trace_.length = eff("edge-trace-length").sizeOr(0.20f);
|
||
edge_trace_.thickness = eff("edge-trace-thickness").sizeOr(1.5f);
|
||
edge_trace_.alpha = eff("edge-trace-alpha").sizeOr(0.6f);
|
||
auto edgeColorElem = eff("edge-trace-color");
|
||
if (!edgeColorElem.color.empty()) {
|
||
edge_trace_.color = S.resolveColor(edgeColorElem.color, IM_COL32(255, 255, 255, 255));
|
||
} else {
|
||
edge_trace_.color = IM_COL32(255, 255, 255, 255);
|
||
}
|
||
|
||
// ---- Ember Rise ----
|
||
ember_rise_.enabled = eff("ember-rise-enabled").sizeOr(0.0f) > 0.5f;
|
||
ember_rise_.count = (int)eff("ember-rise-count").sizeOr(8.0f);
|
||
ember_rise_.speed = eff("ember-rise-speed").sizeOr(0.4f);
|
||
ember_rise_.particleSize = eff("ember-rise-particle-size").sizeOr(1.5f);
|
||
ember_rise_.alpha = eff("ember-rise-alpha").sizeOr(0.5f);
|
||
auto emberColorElem = eff("ember-rise-color");
|
||
if (!emberColorElem.color.empty()) {
|
||
ember_rise_.color = S.resolveColor(emberColorElem.color, IM_COL32(255, 120, 20, 255));
|
||
} else {
|
||
ember_rise_.color = IM_COL32(255, 120, 20, 255);
|
||
}
|
||
|
||
// ---- Gradient Border Shift ----
|
||
gradient_border_.enabled = eff("gradient-border-enabled").sizeOr(0.0f) > 0.5f;
|
||
gradient_border_.speed = eff("gradient-border-speed").sizeOr(0.15f);
|
||
gradient_border_.thickness = eff("gradient-border-thickness").sizeOr(1.5f);
|
||
gradient_border_.alpha = eff("gradient-border-alpha").sizeOr(0.6f);
|
||
auto gbColorA = eff("gradient-border-color-a");
|
||
if (!gbColorA.color.empty()) {
|
||
gradient_border_.colorA = S.resolveColor(gbColorA.color, IM_COL32(206, 147, 216, 255));
|
||
}
|
||
auto gbColorB = eff("gradient-border-color-b");
|
||
if (!gbColorB.color.empty()) {
|
||
gradient_border_.colorB = S.resolveColor(gbColorB.color, IM_COL32(26, 35, 126, 255));
|
||
}
|
||
|
||
// ---- Specular Glare ----
|
||
specular_glare_.enabled = eff("specular-glare-enabled").sizeOr(0.0f) > 0.5f;
|
||
specular_glare_.speed = eff("specular-glare-speed").sizeOr(0.02f);
|
||
specular_glare_.intensity = eff("specular-glare-intensity").sizeOr(0.06f);
|
||
specular_glare_.radius = eff("specular-glare-radius").sizeOr(0.5f);
|
||
specular_glare_.count = (int)eff("specular-glare-count").sizeOr(2.0f);
|
||
auto glareColorElem = eff("specular-glare-color");
|
||
if (!glareColorElem.color.empty()) {
|
||
specular_glare_.color = S.resolveColor(glareColorElem.color, IM_COL32(255, 255, 255, 255));
|
||
} else {
|
||
specular_glare_.color = IM_COL32(255, 255, 255, 255);
|
||
}
|
||
|
||
// ---- Sandstorm ----
|
||
sandstorm_.enabled = eff("sandstorm-enabled").sizeOr(0.0f) > 0.5f;
|
||
sandstorm_.count = (int)eff("sandstorm-count").sizeOr(80.0f);
|
||
sandstorm_.speed = eff("sandstorm-speed").sizeOr(0.35f);
|
||
sandstorm_.windAngle = eff("sandstorm-wind-angle").sizeOr(15.0f);
|
||
sandstorm_.particleSize = eff("sandstorm-particle-size").sizeOr(1.5f);
|
||
sandstorm_.alpha = eff("sandstorm-alpha").sizeOr(0.35f);
|
||
sandstorm_.gustSpeed = eff("sandstorm-gust-speed").sizeOr(0.07f);
|
||
sandstorm_.gustStrength = eff("sandstorm-gust-strength").sizeOr(0.4f);
|
||
sandstorm_.streakLength = eff("sandstorm-streak-length").sizeOr(3.0f);
|
||
auto sandColorElem = eff("sandstorm-color");
|
||
if (!sandColorElem.color.empty()) {
|
||
sandstorm_.color = S.resolveColor(sandColorElem.color, IM_COL32(200, 160, 96, 255));
|
||
} else {
|
||
sandstorm_.color = IM_COL32(200, 160, 96, 255);
|
||
}
|
||
|
||
// ---- Viewport Overlay (shader-like post-processing) ----
|
||
viewport_overlay_.colorWashEnabled = eff("viewport-wash-enabled").sizeOr(0.0f) > 0.5f;
|
||
viewport_overlay_.washAlpha = eff("viewport-wash-alpha").sizeOr(0.05f);
|
||
viewport_overlay_.washRotateSpeed = eff("viewport-wash-rotate").sizeOr(0.0f);
|
||
viewport_overlay_.washPulseSpeed = eff("viewport-wash-pulse").sizeOr(0.0f);
|
||
viewport_overlay_.washPulseDepth = eff("viewport-wash-pulse-depth").sizeOr(0.0f);
|
||
auto washTL = eff("viewport-wash-tl");
|
||
auto washTR = eff("viewport-wash-tr");
|
||
auto washBL = eff("viewport-wash-bl");
|
||
auto washBR = eff("viewport-wash-br");
|
||
if (!washTL.color.empty()) viewport_overlay_.cornerTL = S.resolveColor(washTL.color, IM_COL32(255,100,0,255));
|
||
if (!washTR.color.empty()) viewport_overlay_.cornerTR = S.resolveColor(washTR.color, IM_COL32(68,34,0,255));
|
||
if (!washBL.color.empty()) viewport_overlay_.cornerBL = S.resolveColor(washBL.color, IM_COL32(68,17,0,255));
|
||
if (!washBR.color.empty()) viewport_overlay_.cornerBR = S.resolveColor(washBR.color, IM_COL32(255,153,0,255));
|
||
|
||
viewport_overlay_.vignetteEnabled = eff("viewport-vignette-enabled").sizeOr(0.0f) > 0.5f;
|
||
viewport_overlay_.vignetteRadius = eff("viewport-vignette-radius").sizeOr(0.35f);
|
||
viewport_overlay_.vignetteAlpha = eff("viewport-vignette-alpha").sizeOr(0.30f);
|
||
auto vigColorElem = eff("viewport-vignette-color");
|
||
if (!vigColorElem.color.empty()) {
|
||
viewport_overlay_.vignetteColor = S.resolveColor(vigColorElem.color, IM_COL32(0,0,0,255));
|
||
} else {
|
||
viewport_overlay_.vignetteColor = IM_COL32(0,0,0,255);
|
||
}
|
||
|
||
DEBUG_LOGF("[ThemeEffects] Loaded — hue:%d rainbow:%d shimmer:%d pos_hue:%d glow:%d edge:%d ember:%d glare:%d sand:%d wash:%d vignette:%d\n",
|
||
hue_cycle_.enabled, rainbow_border_.enabled, shimmer_.enabled,
|
||
positional_hue_.enabled, glow_pulse_.enabled,
|
||
edge_trace_.enabled, ember_rise_.enabled, specular_glare_.enabled,
|
||
sandstorm_.enabled,
|
||
viewport_overlay_.colorWashEnabled, viewport_overlay_.vignetteEnabled);
|
||
}
|
||
|
||
// ============================================================================
|
||
// Hue-Cycling Accents
|
||
// ============================================================================
|
||
|
||
ImU32 ThemeEffects::getAccentColor(float phaseOffset) const {
|
||
if (!enabled_ || !hue_cycle_.enabled) return IM_COL32(255, 218, 0, 255);
|
||
|
||
float hue = std::fmod(hue_cycle_.offset + time_ * hue_cycle_.speed + phaseOffset, 1.0f);
|
||
if (hue < 0.0f) hue += 1.0f;
|
||
// Clamp to configured range
|
||
hue = hue_cycle_.offset + std::fmod(hue, std::max(0.01f, hue_cycle_.range));
|
||
hue = std::fmod(hue, 1.0f);
|
||
|
||
float r, g, b;
|
||
ImGui::ColorConvertHSVtoRGB(hue, hue_cycle_.sat, hue_cycle_.val, r, g, b);
|
||
return IM_COL32((int)(r * 255), (int)(g * 255), (int)(b * 255), 255);
|
||
}
|
||
|
||
// ============================================================================
|
||
// Rainbow Gradient Border
|
||
// ============================================================================
|
||
|
||
ImU32 ThemeEffects::sampleGradient(float t) const {
|
||
if (rainbow_border_.stops.empty()) return IM_COL32(255,255,255,128);
|
||
int n = (int)rainbow_border_.stops.size();
|
||
if (n == 1) return rainbow_border_.stops[0];
|
||
|
||
t = std::fmod(t, 1.0f);
|
||
if (t < 0.0f) t += 1.0f;
|
||
|
||
float segment = t * n;
|
||
int idx = (int)segment;
|
||
float frac = segment - idx;
|
||
int next = (idx + 1) % n;
|
||
idx = idx % n;
|
||
|
||
ImU32 c1 = rainbow_border_.stops[idx];
|
||
ImU32 c2 = rainbow_border_.stops[next];
|
||
|
||
// Lerp RGBA components
|
||
int r = (int)((c1 & 0xFF) + frac * (((int)(c2 & 0xFF)) - (int)(c1 & 0xFF)));
|
||
int g = (int)(((c1 >> 8) & 0xFF) + frac * (((int)((c2 >> 8) & 0xFF)) - (int)((c1 >> 8) & 0xFF)));
|
||
int b = (int)(((c1 >> 16) & 0xFF) + frac * (((int)((c2 >> 16) & 0xFF)) - (int)((c1 >> 16) & 0xFF)));
|
||
int a = (int)(rainbow_border_.alpha * bgOpacity_ * 255.0f);
|
||
|
||
return IM_COL32(r, g, b, a);
|
||
}
|
||
|
||
void ThemeEffects::drawRainbowBorder(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax,
|
||
float rounding, float thickness) const {
|
||
if (!enabled_ || !rainbow_border_.enabled || rainbow_border_.stops.empty()) return;
|
||
|
||
float t = time_ * rainbow_border_.speed;
|
||
|
||
// Draw the rainbow border as line segments around the rounded perimeter.
|
||
// Each segment gets a color sampled from the gradient at its position,
|
||
// giving a smoothly rotating hue-shift that follows rounded corners.
|
||
//
|
||
// We need enough segments so that the curved corners are smooth.
|
||
// Each corner arc ≈ π*r/2 pixels. We want ~1 segment per 2-3px of arc
|
||
// to look smooth, plus the straight edges.
|
||
float w = pMax.x - pMin.x;
|
||
float h = pMax.y - pMin.y;
|
||
float r = std::min(rounding, std::min(w, h) * 0.5f);
|
||
float cornerSegs = (r > 0.5f) ? std::ceil(r * 1.5f) : 0.0f; // segments per corner arc
|
||
int segments = std::max(48, (int)(4 * cornerSegs + 2 * (w + h) / 8.0f));
|
||
segments = std::min(segments, 256); // cap for perf
|
||
|
||
ImVec2 prev = perimeterPoint(pMin, pMax, 0.0f, rounding);
|
||
for (int i = 1; i <= segments; i++) {
|
||
float frac = (float)i / (float)segments;
|
||
ImVec2 pt = perimeterPoint(pMin, pMax, frac, rounding);
|
||
|
||
// Sample gradient at the midpoint of this segment for smooth color
|
||
float midFrac = (frac - 0.5f / segments);
|
||
ImU32 c = sampleGradient(t + midFrac);
|
||
|
||
dl->AddLine(prev, pt, c, thickness);
|
||
prev = pt;
|
||
}
|
||
}
|
||
|
||
// ============================================================================
|
||
// Shimmer Sweep
|
||
// ============================================================================
|
||
|
||
void ThemeEffects::drawShimmer(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax,
|
||
float rounding) const {
|
||
if (!enabled_ || !shimmer_.enabled) return;
|
||
|
||
float w = pMax.x - pMin.x;
|
||
float h = pMax.y - pMin.y;
|
||
if (w <= 0 || h <= 0) return;
|
||
|
||
// Sweep position: band moves left to right across the panel
|
||
float period = 1.0f / std::max(0.01f, shimmer_.speed);
|
||
float totalTravel = w + shimmer_.width * 2.0f; // band enters and exits
|
||
float pos = std::fmod(time_ / period, 1.0f) * totalTravel - shimmer_.width;
|
||
|
||
// Band bounds (horizontal, ignoring angle for simplicity in this version)
|
||
float bandLeft = pMin.x + pos;
|
||
float bandRight = bandLeft + shimmer_.width;
|
||
|
||
// Clip to panel
|
||
float clLeft = std::max(bandLeft, pMin.x);
|
||
float clRight = std::min(bandRight, pMax.x);
|
||
if (clLeft >= clRight) return;
|
||
|
||
float mid = (clLeft + clRight) * 0.5f;
|
||
int peakA = scaledAlpha(shimmer_.alpha, bgOpacity_);
|
||
|
||
// Extract shimmer color RGB (ignore alpha, use shimmer_.alpha)
|
||
RGB s = unpackRGB(shimmer_.color);
|
||
|
||
ImU32 clear = IM_COL32(s.r, s.g, s.b, 0);
|
||
ImU32 peak = IM_COL32(s.r, s.g, s.b, peakA);
|
||
|
||
// Inset clip rect by corner rounding to prevent shimmer bleeding into rounded corners
|
||
float cr = std::min(rounding, std::min(w, h) * 0.5f) * 0.3f;
|
||
dl->PushClipRect(ImVec2(pMin.x + cr, pMin.y + cr),
|
||
ImVec2(pMax.x - cr, pMax.y - cr), true);
|
||
|
||
// Left half: transparent → peak
|
||
if (clLeft < mid) {
|
||
dl->AddRectFilledMultiColor(
|
||
ImVec2(clLeft, pMin.y), ImVec2(mid, pMax.y),
|
||
clear, peak, peak, clear);
|
||
}
|
||
// Right half: peak → transparent
|
||
if (mid < clRight) {
|
||
dl->AddRectFilledMultiColor(
|
||
ImVec2(mid, pMin.y), ImVec2(clRight, pMax.y),
|
||
peak, clear, clear, peak);
|
||
}
|
||
|
||
dl->PopClipRect();
|
||
}
|
||
|
||
// ============================================================================
|
||
// Specular Glare — polished-surface highlights (obsidian / glass look)
|
||
// ============================================================================
|
||
|
||
void ThemeEffects::drawSpecularGlare(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax,
|
||
float rounding) const {
|
||
if (!enabled_ || !specular_glare_.enabled || reduced_transparency_) return;
|
||
|
||
float w = pMax.x - pMin.x;
|
||
float h = pMax.y - pMin.y;
|
||
if (w <= 20 || h <= 10) return; // skip tiny panels
|
||
|
||
float minDim = std::min(w, h);
|
||
float glareR = minDim * specular_glare_.radius;
|
||
|
||
RGB c = unpackRGB(specular_glare_.color);
|
||
|
||
// Per-panel seed from position — unique drift per panel
|
||
float seed = pMin.x * 0.0073f + pMin.y * 0.0137f;
|
||
|
||
// Clip to panel with rounding inset
|
||
float cr = std::min(rounding, minDim * 0.5f) * 0.3f;
|
||
dl->PushClipRect(ImVec2(pMin.x + cr, pMin.y + cr),
|
||
ImVec2(pMax.x - cr, pMax.y - cr), true);
|
||
|
||
for (int s = 0; s < specular_glare_.count; s++) {
|
||
float phaseSeed = seed + s * 3.14159265f;
|
||
|
||
// Slow Lissajous drift — each spot follows a unique orbit
|
||
float freqX = 2.0f * 3.14159265f * specular_glare_.speed;
|
||
float freqY = freqX * 1.618f; // golden ratio offset → non-repeating path
|
||
float driftX = std::sin(time_ * freqX + phaseSeed) * 0.3f + 0.5f;
|
||
float driftY = std::cos(time_ * freqY + phaseSeed * 1.3f) * 0.25f + 0.4f;
|
||
|
||
float cx = pMin.x + w * driftX;
|
||
float cy = pMin.y + h * driftY;
|
||
|
||
// Concentric circles with Gaussian falloff for a soft, blurred glow.
|
||
// More rings = smoother gradient, more segments = rounder circles.
|
||
const int rings = 20;
|
||
for (int r = rings; r >= 1; r--) {
|
||
float frac = (float)r / (float)rings; // 1.0 → outer, 0.05 → innermost
|
||
float ringRadius = glareR * frac;
|
||
// Gaussian falloff: exp(-frac² * 4) — soft blur-like appearance
|
||
float alphaMul = std::exp(-frac * frac * 4.0f);
|
||
int a = (int)(specular_glare_.intensity * alphaMul * bgOpacity_ * 255.0f);
|
||
if (a <= 0) continue;
|
||
|
||
dl->AddCircleFilled(ImVec2(cx, cy), ringRadius,
|
||
IM_COL32(c.r, c.g, c.b, a), 32);
|
||
}
|
||
}
|
||
|
||
dl->PopClipRect();
|
||
}
|
||
|
||
// ============================================================================
|
||
// Positional Hue Tinting
|
||
// ============================================================================
|
||
|
||
ImU32 ThemeEffects::getPositionalTint(float screenY) const {
|
||
if (!enabled_ || !positional_hue_.enabled) return IM_COL32(255,255,255,255);
|
||
|
||
float t = 0.5f;
|
||
float range = vpMaxY_ - vpMinY_;
|
||
if (range > 0) {
|
||
t = std::clamp((screenY - vpMinY_) / range, 0.0f, 1.0f);
|
||
}
|
||
|
||
// Lerp between top and bottom colors
|
||
float r = positional_hue_.topColor.x + t * (positional_hue_.bottomColor.x - positional_hue_.topColor.x);
|
||
float g = positional_hue_.topColor.y + t * (positional_hue_.bottomColor.y - positional_hue_.topColor.y);
|
||
float b = positional_hue_.topColor.z + t * (positional_hue_.bottomColor.z - positional_hue_.topColor.z);
|
||
|
||
return IM_COL32((int)(r * 255), (int)(g * 255), (int)(b * 255), 255);
|
||
}
|
||
|
||
ImU32 ThemeEffects::tintByPosition(ImU32 baseColor, float screenY) const {
|
||
if (!enabled_ || !positional_hue_.enabled || positional_hue_.strength <= 0.0f) return baseColor;
|
||
|
||
ImU32 tint = getPositionalTint(screenY);
|
||
float s = positional_hue_.strength;
|
||
float inv = 1.0f - s;
|
||
|
||
RGB base = unpackRGB(baseColor);
|
||
int bA = (baseColor >> 24) & 0xFF;
|
||
|
||
RGB t = unpackRGB(tint);
|
||
|
||
int r = (int)(base.r * inv + t.r * s);
|
||
int g = (int)(base.g * inv + t.g * s);
|
||
int b = (int)(base.b * inv + t.b * s);
|
||
|
||
return IM_COL32(r, g, b, bA);
|
||
}
|
||
|
||
// ============================================================================
|
||
// Glow Pulse
|
||
// ============================================================================
|
||
|
||
void ThemeEffects::drawGlowPulse(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax,
|
||
float rounding) const {
|
||
if (!enabled_ || !glow_pulse_.enabled) return;
|
||
|
||
// Sinusoidal oscillation
|
||
float phase = std::sin(time_ * glow_pulse_.speed * 2.0f * 3.14159265f) * 0.5f + 0.5f;
|
||
float alpha = glow_pulse_.minAlpha + phase * (glow_pulse_.maxAlpha - glow_pulse_.minAlpha);
|
||
if (alpha <= 0.001f) return;
|
||
|
||
int a = scaledAlpha(alpha, bgOpacity_);
|
||
RGB c = unpackRGB(glow_pulse_.color);
|
||
|
||
// Border-only glow: concentric outlines with smooth Gaussian falloff.
|
||
// Use many fine sub-rings (4× the pixel radius) to avoid visible
|
||
// stepping as the overall alpha pulses.
|
||
float maxExpand = glow_pulse_.radius;
|
||
int rings = std::max(4, (int)(maxExpand * 4.0f));
|
||
for (int i = 1; i <= rings; i++) {
|
||
float expand = maxExpand * (float)i / (float)rings;
|
||
float frac = (float)i / (float)rings; // 0→1 from inner to outer
|
||
// Gaussian-like falloff: smooth and continuous
|
||
float falloff = std::exp(-frac * frac * 3.0f);
|
||
int ringAlpha = (int)(a * falloff);
|
||
if (ringAlpha <= 0) continue;
|
||
|
||
float thickness = std::max(0.5f, 1.5f * (1.0f - frac));
|
||
dl->AddRect(
|
||
ImVec2(pMin.x - expand, pMin.y - expand),
|
||
ImVec2(pMax.x + expand, pMax.y + expand),
|
||
IM_COL32(c.r, c.g, c.b, ringAlpha),
|
||
rounding + expand * 0.5f, 0, thickness);
|
||
}
|
||
}
|
||
|
||
// ============================================================================
|
||
// Gradient Border Shift — border color oscillates between two gem colors
|
||
// ============================================================================
|
||
|
||
void ThemeEffects::drawGradientBorderShift(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax,
|
||
float rounding) const {
|
||
if (!enabled_ || !gradient_border_.enabled) return;
|
||
|
||
// Smooth sinusoidal oscillation between color A and color B
|
||
float phase = std::sin(time_ * gradient_border_.speed * 2.0f * 3.14159265f) * 0.5f + 0.5f;
|
||
|
||
// Extract RGBA from both colors and lerp
|
||
RGB ca = unpackRGB(gradient_border_.colorA);
|
||
RGB cb = unpackRGB(gradient_border_.colorB);
|
||
|
||
int r = ca.r + (int)((cb.r - ca.r) * phase);
|
||
int g = ca.g + (int)((cb.g - ca.g) * phase);
|
||
int b = ca.b + (int)((cb.b - ca.b) * phase);
|
||
int a = scaledAlpha(gradient_border_.alpha, bgOpacity_);
|
||
|
||
// Draw the shifting border
|
||
dl->AddRect(pMin, pMax, IM_COL32(r, g, b, a),
|
||
rounding, 0, gradient_border_.thickness);
|
||
|
||
// Subtle outer glow ring at lower alpha for depth
|
||
float expand = gradient_border_.thickness;
|
||
int glowA = a / 3;
|
||
if (glowA > 0) {
|
||
dl->AddRect(
|
||
ImVec2(pMin.x - expand, pMin.y - expand),
|
||
ImVec2(pMax.x + expand, pMax.y + expand),
|
||
IM_COL32(r, g, b, glowA),
|
||
rounding + expand * 0.5f, 0, 1.0f);
|
||
}
|
||
}
|
||
|
||
// ============================================================================
|
||
// Edge Trace — a bright segment that traces the border perimeter
|
||
// ============================================================================
|
||
|
||
ImVec2 ThemeEffects::perimeterPoint(ImVec2 pMin, ImVec2 pMax, float t, float rounding) const {
|
||
float w = pMax.x - pMin.x;
|
||
float h = pMax.y - pMin.y;
|
||
if (w <= 0 || h <= 0) return pMin;
|
||
|
||
// Clamp rounding to half of the smaller dimension
|
||
float r = std::min(rounding, std::min(w, h) * 0.5f);
|
||
if (r < 0.5f) r = 0.0f; // treat tiny rounding as none
|
||
|
||
t = std::fmod(t, 1.0f);
|
||
if (t < 0.0f) t += 1.0f;
|
||
|
||
if (r <= 0.0f) {
|
||
// Original rectangular path
|
||
float perimeter = 2.0f * (w + h);
|
||
float d = t * perimeter;
|
||
if (d < w) return ImVec2(pMin.x + d, pMin.y);
|
||
else if (d < w + h) return ImVec2(pMax.x, pMin.y + (d - w));
|
||
else if (d < 2*w+h) return ImVec2(pMax.x - (d - w - h), pMax.y);
|
||
else return ImVec2(pMin.x, pMax.y - (d - 2*w - h));
|
||
}
|
||
|
||
// Rounded rect perimeter:
|
||
// Segments (clockwise starting from top-left of top edge):
|
||
// 1. Top edge: (x+r, y) → (x+w-r, y) len = w - 2r
|
||
// 2. TR arc: center (x+w-r, y+r) -90° → 0° len = πr/2
|
||
// 3. Right edge: (x+w, y+r) → (x+w, y+h-r) len = h - 2r
|
||
// 4. BR arc: center (x+w-r, y+h-r) 0° → 90° len = πr/2
|
||
// 5. Bottom edge: (x+w-r, y+h) → (x+r, y+h) len = w - 2r
|
||
// 6. BL arc: center (x+r, y+h-r) 90° → 180° len = πr/2
|
||
// 7. Left edge: (x, y+h-r) → (x, y+r) len = h - 2r
|
||
// 8. TL arc: center (x+r, y+r) 180° → 270° len = πr/2
|
||
const float PI = 3.14159265f;
|
||
float arcLen = PI * r * 0.5f;
|
||
float edgeW = w - 2.0f * r;
|
||
float edgeH = h - 2.0f * r;
|
||
float perimeter = 2.0f * edgeW + 2.0f * edgeH + 4.0f * arcLen;
|
||
float d = t * perimeter;
|
||
|
||
// Segment boundaries (cumulative)
|
||
float s1 = edgeW; // top edge
|
||
float s2 = s1 + arcLen; // TR arc
|
||
float s3 = s2 + edgeH; // right edge
|
||
float s4 = s3 + arcLen; // BR arc
|
||
float s5 = s4 + edgeW; // bottom edge
|
||
float s6 = s5 + arcLen; // BL arc
|
||
float s7 = s6 + edgeH; // left edge
|
||
// s8 = s7 + arcLen = perimeter // TL arc
|
||
|
||
if (d < s1) {
|
||
// Top edge
|
||
return ImVec2(pMin.x + r + d, pMin.y);
|
||
} else if (d < s2) {
|
||
// TR arc: center (pMax.x - r, pMin.y + r), angle -90° to 0°
|
||
float frac = (d - s1) / arcLen;
|
||
float angle = -PI * 0.5f + frac * PI * 0.5f;
|
||
return ImVec2(pMax.x - r + r * std::cos(angle),
|
||
pMin.y + r + r * std::sin(angle));
|
||
} else if (d < s3) {
|
||
// Right edge
|
||
return ImVec2(pMax.x, pMin.y + r + (d - s2));
|
||
} else if (d < s4) {
|
||
// BR arc: center (pMax.x - r, pMax.y - r), angle 0° to 90°
|
||
float frac = (d - s3) / arcLen;
|
||
float angle = frac * PI * 0.5f;
|
||
return ImVec2(pMax.x - r + r * std::cos(angle),
|
||
pMax.y - r + r * std::sin(angle));
|
||
} else if (d < s5) {
|
||
// Bottom edge (right to left)
|
||
return ImVec2(pMax.x - r - (d - s4), pMax.y);
|
||
} else if (d < s6) {
|
||
// BL arc: center (pMin.x + r, pMax.y - r), angle 90° to 180°
|
||
float frac = (d - s5) / arcLen;
|
||
float angle = PI * 0.5f + frac * PI * 0.5f;
|
||
return ImVec2(pMin.x + r + r * std::cos(angle),
|
||
pMax.y - r + r * std::sin(angle));
|
||
} else if (d < s7) {
|
||
// Left edge (bottom to top)
|
||
return ImVec2(pMin.x, pMax.y - r - (d - s6));
|
||
} else {
|
||
// TL arc: center (pMin.x + r, pMin.y + r), angle 180° to 270°
|
||
float frac = (d - s7) / arcLen;
|
||
float angle = PI + frac * PI * 0.5f;
|
||
return ImVec2(pMin.x + r + r * std::cos(angle),
|
||
pMin.y + r + r * std::sin(angle));
|
||
}
|
||
}
|
||
|
||
void ThemeEffects::drawEdgeTrace(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax,
|
||
float rounding) const {
|
||
if (!enabled_ || !edge_trace_.enabled) return;
|
||
|
||
float headPos = std::fmod(time_ * edge_trace_.speed, 1.0f);
|
||
float traceLen = edge_trace_.length;
|
||
|
||
RGB c = unpackRGB(edge_trace_.color);
|
||
|
||
// Draw the trace as connected line segments with fading alpha.
|
||
// Use enough segments to make curved corners smooth.
|
||
float w = pMax.x - pMin.x;
|
||
float h = pMax.y - pMin.y;
|
||
float r = std::min(rounding, std::min(w, h) * 0.5f);
|
||
float cornerSegs = (r > 0.5f) ? std::ceil(r * 1.5f) : 0.0f;
|
||
// Scale by trace length — we only draw a fraction of the perimeter
|
||
int segments = std::max(24, (int)((4 * cornerSegs + 2 * (w + h) / 8.0f) * traceLen + 8));
|
||
segments = std::min(segments, 128);
|
||
|
||
ImVec2 prev = perimeterPoint(pMin, pMax, headPos, rounding);
|
||
for (int i = 1; i <= segments; i++) {
|
||
float frac = (float)i / (float)segments; // 0→1 from head to tail
|
||
float pos = headPos - frac * traceLen;
|
||
if (pos < 0.0f) pos += 1.0f;
|
||
|
||
ImVec2 pt = perimeterPoint(pMin, pMax, pos, rounding);
|
||
|
||
// Alpha fades from head (1.0) to tail (0.0), with a cubic falloff
|
||
float aFrac = 1.0f - frac;
|
||
aFrac = aFrac * aFrac; // quadratic falloff for natural fade
|
||
int a = (int)(edge_trace_.alpha * aFrac * bgOpacity_ * 255.0f);
|
||
if (a > 0) {
|
||
dl->AddLine(prev, pt, IM_COL32(c.r, c.g, c.b, a), edge_trace_.thickness);
|
||
}
|
||
prev = pt;
|
||
}
|
||
|
||
// Bright dot at the head
|
||
int headA = scaledAlpha(edge_trace_.alpha, bgOpacity_);
|
||
ImVec2 headPt = perimeterPoint(pMin, pMax, headPos, rounding);
|
||
dl->AddCircleFilled(headPt, edge_trace_.thickness * 1.5f,
|
||
IM_COL32(c.r, c.g, c.b, headA), 8);
|
||
}
|
||
|
||
// ============================================================================
|
||
// Ember Rise — fire particles that drift upward from the element
|
||
// ============================================================================
|
||
|
||
void ThemeEffects::drawEmberRise(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax) const {
|
||
if (!enabled_ || !ember_rise_.enabled) return;
|
||
|
||
float w = pMax.x - pMin.x;
|
||
float h = pMax.y - pMin.y;
|
||
if (w <= 0 || h <= 0) return;
|
||
|
||
RGB c = unpackRGB(ember_rise_.color);
|
||
|
||
for (int i = 0; i < ember_rise_.count; i++) {
|
||
// Golden-ratio spacing gives evenly distributed phases
|
||
float phase = std::fmod(time_ * ember_rise_.speed + i * 0.618033988f, 1.0f);
|
||
|
||
// Deterministic pseudo-random x position per particle
|
||
// Simple hash: sin of large prime multiples
|
||
float xHash = std::sin((float)(i + 1) * 127.1f) * 0.5f + 0.5f;
|
||
float xDrift = std::sin(time_ * 0.7f + i * 2.4f) * 4.0f; // gentle sway
|
||
|
||
float x = pMin.x + w * xHash + xDrift;
|
||
float y = pMax.y - phase * (h + 8.0f); // rise from bottom past top
|
||
|
||
// Fade in at bottom (phase 0→0.2), full in middle, fade out at top (0.7→1.0)
|
||
float aFrac;
|
||
if (phase < 0.15f) {
|
||
aFrac = phase / 0.15f;
|
||
} else if (phase > 0.7f) {
|
||
aFrac = (1.0f - phase) / 0.3f;
|
||
} else {
|
||
aFrac = 1.0f;
|
||
}
|
||
|
||
// Size decreases as particle rises (embers shrink)
|
||
float size = ember_rise_.particleSize * (1.0f - phase * 0.4f);
|
||
|
||
int a = (int)(ember_rise_.alpha * aFrac * bgOpacity_ * 255.0f);
|
||
|
||
// Warm core
|
||
dl->AddCircleFilled(ImVec2(x, y), size,
|
||
IM_COL32(c.r, c.g, c.b, a), 6);
|
||
// Softer outer glow
|
||
if (a > 30) {
|
||
dl->AddCircleFilled(ImVec2(x, y), size * 2.0f,
|
||
IM_COL32(c.r, c.g, c.b, a / 4), 6);
|
||
}
|
||
}
|
||
}
|
||
|
||
// ============================================================================
|
||
// Viewport-wide ambient ember particles (fire theme atmosphere)
|
||
// ============================================================================
|
||
|
||
void ThemeEffects::drawViewportEmbers(ImDrawList* dl) const {
|
||
if (!enabled_ || !ember_rise_.enabled) return;
|
||
|
||
ImGuiViewport* vp = ImGui::GetMainViewport();
|
||
float vpW = vp->WorkSize.x;
|
||
float vpH = vp->WorkSize.y;
|
||
float vpX = vp->WorkPos.x;
|
||
float vpY = vp->WorkPos.y;
|
||
if (vpW <= 0 || vpH <= 0) return;
|
||
|
||
RGB c = unpackRGB(ember_rise_.color);
|
||
|
||
// Viewport embers: more particles, spread across the whole screen
|
||
int vpCount = ember_rise_.count * 3;
|
||
float vpAlpha = ember_rise_.alpha * 0.4f * bgOpacity_; // softer than panel embers
|
||
|
||
for (int i = 0; i < vpCount; i++) {
|
||
float phase = std::fmod(time_ * ember_rise_.speed * 0.6f + i * 0.618033988f, 1.0f);
|
||
|
||
// Deterministic x position spread across viewport
|
||
float xHash = std::sin((float)(i + 1) * 127.1f) * 43758.5453f;
|
||
xHash = xHash - (int)xHash; // fractional part
|
||
if (xHash < 0) xHash += 1.0f;
|
||
float xDrift = std::sin(time_ * 0.5f + i * 1.7f) * 8.0f;
|
||
|
||
float x = vpX + vpW * xHash + xDrift;
|
||
float y = vpY + vpH * (1.0f - phase); // rise from bottom to top
|
||
|
||
// Fade in/out
|
||
float aFrac;
|
||
if (phase < 0.1f) aFrac = phase / 0.1f;
|
||
else if (phase > 0.75f) aFrac = (1.0f - phase) / 0.25f;
|
||
else aFrac = 1.0f;
|
||
|
||
float size = ember_rise_.particleSize * 0.8f * (1.0f - phase * 0.3f);
|
||
int a = (int)(vpAlpha * aFrac * 255.0f);
|
||
if (a <= 0) continue;
|
||
|
||
dl->AddCircleFilled(ImVec2(x, y), size,
|
||
IM_COL32(c.r, c.g, c.b, a), 6);
|
||
if (a > 20) {
|
||
dl->AddCircleFilled(ImVec2(x, y), size * 2.5f,
|
||
IM_COL32(c.r, c.g, c.b, a / 5), 6);
|
||
}
|
||
}
|
||
}
|
||
|
||
// ============================================================================
|
||
// Sandstorm — wind-driven sand/dust particles blowing across the viewport
|
||
// ============================================================================
|
||
|
||
void ThemeEffects::drawSandstorm(ImDrawList* dl) const {
|
||
if (!enabled_ || !sandstorm_.enabled || effects::isLowSpecMode()) return;
|
||
|
||
ImGuiViewport* vp = ImGui::GetMainViewport();
|
||
float vpW = vp->WorkSize.x;
|
||
float vpH = vp->WorkSize.y;
|
||
float vpX = vp->WorkPos.x;
|
||
float vpY = vp->WorkPos.y;
|
||
if (vpW <= 0 || vpH <= 0) return;
|
||
|
||
RGB c = unpackRGB(sandstorm_.color);
|
||
|
||
// Wind direction vector from angle (degrees from horizontal)
|
||
float windRad = sandstorm_.windAngle * 3.14159265f / 180.0f;
|
||
float windDx = std::cos(windRad); // horizontal component
|
||
float windDy = std::sin(windRad); // vertical component (positive = downward)
|
||
|
||
// Global gust modulation: smoothly varies wind speed
|
||
float gust = 1.0f + sandstorm_.gustStrength *
|
||
std::sin(time_ * sandstorm_.gustSpeed * 2.0f * 3.14159265f);
|
||
|
||
// Diagonal traversal distance (how far a particle travels across viewport)
|
||
float traversal = vpW + vpH * std::abs(windDy / std::max(0.01f, windDx));
|
||
|
||
for (int i = 0; i < sandstorm_.count; i++) {
|
||
// Deterministic pseudo-random properties per particle
|
||
float hash1 = std::sin((float)(i + 1) * 127.1f) * 43758.5453f;
|
||
hash1 = hash1 - (int)hash1;
|
||
if (hash1 < 0) hash1 += 1.0f;
|
||
|
||
float hash2 = std::sin((float)(i + 1) * 269.5f) * 27183.3291f;
|
||
hash2 = hash2 - (int)hash2;
|
||
if (hash2 < 0) hash2 += 1.0f;
|
||
|
||
float hash3 = std::sin((float)(i + 1) * 419.3f) * 15731.7927f;
|
||
hash3 = hash3 - (int)hash3;
|
||
if (hash3 < 0) hash3 += 1.0f;
|
||
|
||
// Per-particle speed variation (0.5x to 1.5x base speed)
|
||
float speedMul = 0.5f + hash1;
|
||
// Per-particle size variation (0.4x to 1.6x base)
|
||
float size = sandstorm_.particleSize * (0.4f + hash2 * 1.2f);
|
||
// Per-particle alpha variation (0.5x to 1.0x)
|
||
float alphaMul = 0.5f + hash3 * 0.5f;
|
||
|
||
// Phase: horizontal progress across viewport (0→1)
|
||
// Different particles have staggered starts via golden ratio
|
||
float phase = std::fmod(
|
||
time_ * sandstorm_.speed * speedMul * gust + i * 0.618033988f, 1.0f);
|
||
|
||
// Y position: distributed across viewport with turbulent drift
|
||
float yBase = vpY + vpH * hash2;
|
||
float yTurb = std::sin(time_ * 0.8f + i * 3.7f) * vpH * 0.04f;
|
||
// Additional gust-correlated vertical shift
|
||
float yGust = std::sin(time_ * sandstorm_.gustSpeed * 6.28318f + i * 1.3f) * vpH * 0.02f;
|
||
|
||
// Position: particle blows right-to-left with downward drift
|
||
float x = vpX + vpW * (1.0f - phase) + phase * traversal * (1.0f - windDx) * 0.1f;
|
||
float y = yBase + yTurb + yGust + phase * vpH * windDy * 0.3f;
|
||
|
||
// Wrap Y if it goes out of bounds
|
||
if (y < vpY) y += vpH;
|
||
if (y > vpY + vpH) y -= vpH;
|
||
|
||
// Fade in at entry edge (phase 0→0.1), fade out at exit (0.85→1.0)
|
||
float aFrac;
|
||
if (phase < 0.1f) aFrac = phase / 0.1f;
|
||
else if (phase > 0.85f) aFrac = (1.0f - phase) / 0.15f;
|
||
else aFrac = 1.0f;
|
||
|
||
int a = (int)(sandstorm_.alpha * alphaMul * aFrac * bgOpacity_ * 255.0f);
|
||
if (a <= 0) continue;
|
||
|
||
// Fast particles draw as motion-blurred streaks
|
||
float particleSpeed = sandstorm_.speed * speedMul * gust;
|
||
float streakLen = sandstorm_.streakLength * particleSpeed * size;
|
||
|
||
if (streakLen > size * 1.5f) {
|
||
// Draw as a short line (motion streak)
|
||
float sx = -windDx * streakLen;
|
||
float sy = -windDy * streakLen * 0.3f;
|
||
dl->AddLine(
|
||
ImVec2(x, y),
|
||
ImVec2(x + sx, y + sy),
|
||
IM_COL32(c.r, c.g, c.b, a),
|
||
size * 0.7f);
|
||
// Bright head dot
|
||
dl->AddCircleFilled(ImVec2(x, y), size * 0.5f,
|
||
IM_COL32(c.r, c.g, c.b, std::min(255, a * 3 / 2)), 5);
|
||
} else {
|
||
// Small/slow particles: simple circle
|
||
dl->AddCircleFilled(ImVec2(x, y), size,
|
||
IM_COL32(c.r, c.g, c.b, a), 6);
|
||
}
|
||
|
||
// Occasional larger dust puff (every ~8th particle, double size, half alpha)
|
||
if (i % 8 == 0 && a > 15) {
|
||
dl->AddCircleFilled(ImVec2(x, y), size * 2.5f,
|
||
IM_COL32(c.r, c.g, c.b, a / 4), 8);
|
||
}
|
||
}
|
||
}
|
||
|
||
// ============================================================================
|
||
// Panel-level effects (applied to every glass panel via DrawGlassPanel)
|
||
// ============================================================================
|
||
|
||
void ThemeEffects::drawPanelEffects(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax,
|
||
float rounding) const {
|
||
if (!enabled_ || effects::isLowSpecMode()) return;
|
||
|
||
// Edge trace on panels — use position-based phase offset so each
|
||
// panel's tracer is at a different position around the border
|
||
if (edge_trace_.enabled) {
|
||
float w = pMax.x - pMin.x;
|
||
float h = pMax.y - pMin.y;
|
||
if (w > 80 && h > 40) { // skip small panels
|
||
// Generate a consistent phase offset from panel position
|
||
float posKey = (pMin.x * 0.0073f + pMin.y * 0.0137f);
|
||
posKey = posKey - (int)posKey; // fractional 0..1
|
||
if (posKey < 0) posKey += 1.0f;
|
||
|
||
float headPos = std::fmod(time_ * edge_trace_.speed + posKey, 1.0f);
|
||
float traceLen = edge_trace_.length;
|
||
|
||
RGB c = unpackRGB(edge_trace_.color);
|
||
|
||
// Panel edge trace is subtler than sidebar — 60% alpha
|
||
float panelAlpha = edge_trace_.alpha * 0.6f * bgOpacity_;
|
||
|
||
const int segments = 10;
|
||
ImVec2 prev = perimeterPoint(pMin, pMax, headPos, rounding);
|
||
for (int i = 1; i <= segments; i++) {
|
||
float frac = (float)i / (float)segments;
|
||
float pos = headPos - frac * traceLen;
|
||
if (pos < 0.0f) pos += 1.0f;
|
||
|
||
ImVec2 pt = perimeterPoint(pMin, pMax, pos, rounding);
|
||
float aFrac = 1.0f - frac;
|
||
aFrac = aFrac * aFrac;
|
||
int a = (int)(panelAlpha * aFrac * 255.0f);
|
||
if (a > 0) {
|
||
dl->AddLine(prev, pt, IM_COL32(c.r, c.g, c.b, a),
|
||
edge_trace_.thickness * 0.8f);
|
||
}
|
||
prev = pt;
|
||
}
|
||
|
||
// Head dot
|
||
int headA = (int)(panelAlpha * 255.0f);
|
||
ImVec2 headPt = perimeterPoint(pMin, pMax, headPos, rounding);
|
||
dl->AddCircleFilled(headPt, edge_trace_.thickness * 1.2f,
|
||
IM_COL32(c.r, c.g, c.b, headA), 6);
|
||
}
|
||
}
|
||
|
||
// Ember rise on panels — sparse embers from larger panels only
|
||
if (ember_rise_.enabled) {
|
||
float w = pMax.x - pMin.x;
|
||
float h = pMax.y - pMin.y;
|
||
if (w > 120 && h > 60) { // only on large panels
|
||
// Fewer particles per panel (scale by area relative to viewport)
|
||
int panelCount = std::max(2, ember_rise_.count / 3);
|
||
float panelAlpha = ember_rise_.alpha * 0.5f * bgOpacity_;
|
||
|
||
RGB c = unpackRGB(ember_rise_.color);
|
||
|
||
// Use panel position as seed for unique particle distribution
|
||
float seed = pMin.x * 0.013f + pMin.y * 0.031f;
|
||
|
||
dl->PushClipRect(
|
||
ImVec2(pMin.x - 2, pMin.y - 4),
|
||
ImVec2(pMax.x + 2, pMax.y + 2), true);
|
||
|
||
for (int i = 0; i < panelCount; i++) {
|
||
float phase = std::fmod(time_ * ember_rise_.speed + i * 0.618033988f + seed, 1.0f);
|
||
float xHash = std::sin((float)(i + 1) * 127.1f + seed * 100.0f) * 0.5f + 0.5f;
|
||
float xDrift = std::sin(time_ * 0.7f + i * 2.4f + seed) * 3.0f;
|
||
|
||
float x = pMin.x + w * xHash + xDrift;
|
||
float y = pMax.y - phase * (h + 6.0f);
|
||
|
||
float aFrac;
|
||
if (phase < 0.15f) aFrac = phase / 0.15f;
|
||
else if (phase > 0.7f) aFrac = (1.0f - phase) / 0.3f;
|
||
else aFrac = 1.0f;
|
||
|
||
float size = ember_rise_.particleSize * 0.8f * (1.0f - phase * 0.4f);
|
||
int a = (int)(panelAlpha * aFrac * 255.0f);
|
||
if (a <= 0) continue;
|
||
|
||
dl->AddCircleFilled(ImVec2(x, y), size,
|
||
IM_COL32(c.r, c.g, c.b, a), 6);
|
||
}
|
||
|
||
dl->PopClipRect();
|
||
}
|
||
}
|
||
}
|
||
|
||
// ============================================================================
|
||
// Viewport Overlay — shader-like full-screen color wash + vignette
|
||
// ============================================================================
|
||
|
||
void ThemeEffects::drawViewportOverlay(ImDrawList* dl) const {
|
||
if (!enabled_ || effects::isLowSpecMode()) return;
|
||
|
||
ImGuiViewport* vp = ImGui::GetMainViewport();
|
||
ImVec2 vpPos = vp->WorkPos;
|
||
ImVec2 vpSize = vp->WorkSize;
|
||
if (vpSize.x <= 0 || vpSize.y <= 0) return;
|
||
|
||
ImVec2 vpMax = ImVec2(vpPos.x + vpSize.x, vpPos.y + vpSize.y);
|
||
|
||
// === Color Wash: full-screen 4-corner animated gradient ===
|
||
if (viewport_overlay_.colorWashEnabled) {
|
||
ImU32 corners[4] = {
|
||
viewport_overlay_.cornerTL,
|
||
viewport_overlay_.cornerTR,
|
||
viewport_overlay_.cornerBR,
|
||
viewport_overlay_.cornerBL
|
||
};
|
||
|
||
// Rotation: smoothly shift which color appears at which corner
|
||
if (viewport_overlay_.washRotateSpeed > 0.001f) {
|
||
float shift = std::fmod(time_ * viewport_overlay_.washRotateSpeed, 1.0f);
|
||
ImU32 originals[4] = { corners[0], corners[1], corners[2], corners[3] };
|
||
|
||
for (int c = 0; c < 4; c++) {
|
||
float pos = std::fmod(shift + c * 0.25f, 1.0f);
|
||
float segment = pos * 4.0f;
|
||
int idx = ((int)segment) % 4;
|
||
float frac = segment - (int)segment;
|
||
int next = (idx + 1) % 4;
|
||
|
||
int r1 = originals[idx] & 0xFF, g1 = (originals[idx]>>8)&0xFF, b1 = (originals[idx]>>16)&0xFF;
|
||
int r2 = originals[next] & 0xFF, g2 = (originals[next]>>8)&0xFF, b2 = (originals[next]>>16)&0xFF;
|
||
corners[c] = IM_COL32(
|
||
r1 + (int)(frac * (r2 - r1)),
|
||
g1 + (int)(frac * (g2 - g1)),
|
||
b1 + (int)(frac * (b2 - b1)),
|
||
255);
|
||
}
|
||
}
|
||
|
||
// Calculate final alpha — keep it constant to avoid integer
|
||
// quantisation stepping at low values (0.04–0.12 → only 10–30
|
||
// integer levels). Breathing is achieved by modulating the RGB
|
||
// colour intensity instead, which has far more resolution.
|
||
float alpha = viewport_overlay_.washAlpha * bgOpacity_;
|
||
int a = std::clamp((int)(alpha * 255.0f + 0.5f), 0, 255);
|
||
if (a > 0) {
|
||
// Smooth breathing via colour modulation (not alpha)
|
||
float colorMul = 1.0f;
|
||
if (viewport_overlay_.washPulseSpeed > 0.001f) {
|
||
float pulse = std::sin(time_ * viewport_overlay_.washPulseSpeed * 2.0f * 3.14159265f) * 0.5f + 0.5f;
|
||
colorMul = 1.0f - viewport_overlay_.washPulseDepth * (1.0f - pulse);
|
||
}
|
||
|
||
// Apply colour modulation to each corner while keeping alpha constant
|
||
auto modCorner = [&](ImU32 c) -> ImU32 {
|
||
int r = std::clamp((int)((c & 0xFF) * colorMul + 0.5f), 0, 255);
|
||
int g = std::clamp((int)(((c >> 8) & 0xFF) * colorMul + 0.5f), 0, 255);
|
||
int b = std::clamp((int)(((c >> 16) & 0xFF) * colorMul + 0.5f), 0, 255);
|
||
return IM_COL32(r, g, b, a);
|
||
};
|
||
|
||
ImU32 c0 = modCorner(corners[0]);
|
||
ImU32 c1 = modCorner(corners[1]);
|
||
ImU32 c2 = modCorner(corners[2]);
|
||
ImU32 c3 = modCorner(corners[3]);
|
||
|
||
dl->AddRectFilledMultiColor(vpPos, vpMax, c0, c1, c2, c3);
|
||
}
|
||
}
|
||
|
||
// === Vignette: edge darkening/tinting (4 gradient strips) ===
|
||
// Overlapping strips naturally darken corners more than edges — cinematic look
|
||
if (viewport_overlay_.vignetteEnabled) {
|
||
RGB c = unpackRGB(viewport_overlay_.vignetteColor);
|
||
int maxA = scaledAlpha(viewport_overlay_.vignetteAlpha, bgOpacity_);
|
||
|
||
if (maxA > 0) {
|
||
ImU32 edgeCol = IM_COL32(c.r, c.g, c.b, maxA);
|
||
ImU32 clearCol = IM_COL32(c.r, c.g, c.b, 0);
|
||
|
||
float fadeW = vpSize.x * viewport_overlay_.vignetteRadius;
|
||
float fadeH = vpSize.y * viewport_overlay_.vignetteRadius;
|
||
|
||
// Top strip: dark at top edge, transparent at fade boundary
|
||
dl->AddRectFilledMultiColor(
|
||
vpPos,
|
||
ImVec2(vpMax.x, vpPos.y + fadeH),
|
||
edgeCol, edgeCol, clearCol, clearCol);
|
||
|
||
// Bottom strip
|
||
dl->AddRectFilledMultiColor(
|
||
ImVec2(vpPos.x, vpMax.y - fadeH),
|
||
vpMax,
|
||
clearCol, clearCol, edgeCol, edgeCol);
|
||
|
||
// Left strip
|
||
dl->AddRectFilledMultiColor(
|
||
vpPos,
|
||
ImVec2(vpPos.x + fadeW, vpMax.y),
|
||
edgeCol, clearCol, clearCol, edgeCol);
|
||
|
||
// Right strip
|
||
dl->AddRectFilledMultiColor(
|
||
ImVec2(vpMax.x - fadeW, vpPos.y),
|
||
vpMax,
|
||
clearCol, edgeCol, edgeCol, clearCol);
|
||
}
|
||
}
|
||
}
|
||
|
||
// ============================================================================
|
||
// Hex color parsing helpers
|
||
// ============================================================================
|
||
|
||
ImU32 ThemeEffects::parseHexColor(const std::string& hex, ImU32 fallback) {
|
||
if (hex.empty() || hex[0] != '#') return fallback;
|
||
unsigned int val = 0;
|
||
if (hex.size() == 7) { // #RRGGBB
|
||
val = (unsigned int)strtoul(hex.c_str() + 1, nullptr, 16);
|
||
int r = (val >> 16) & 0xFF;
|
||
int g = (val >> 8) & 0xFF;
|
||
int b = val & 0xFF;
|
||
return IM_COL32(r, g, b, 255);
|
||
} else if (hex.size() == 9) { // #RRGGBBAA
|
||
val = (unsigned int)strtoul(hex.c_str() + 1, nullptr, 16);
|
||
int r = (val >> 24) & 0xFF;
|
||
int g = (val >> 16) & 0xFF;
|
||
int b = (val >> 8) & 0xFF;
|
||
int a = val & 0xFF;
|
||
return IM_COL32(r, g, b, a);
|
||
}
|
||
return fallback;
|
||
}
|
||
|
||
ImVec4 ThemeEffects::parseHexColorVec4(const std::string& hex, ImVec4 fallback) {
|
||
ImU32 c = parseHexColor(hex, 0);
|
||
if (c == 0 && hex != "#000000") return fallback;
|
||
float r = (c & 0xFF) / 255.0f;
|
||
float g = ((c >> 8) & 0xFF) / 255.0f;
|
||
float b = ((c >> 16) & 0xFF) / 255.0f;
|
||
float a = ((c >> 24) & 0xFF) / 255.0f;
|
||
return ImVec4(r, g, b, a);
|
||
}
|
||
|
||
} // namespace effects
|
||
} // namespace ui
|
||
} // namespace dragonx
|