// 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 #include #include #include #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