perf(effects): cull off-screen card effects and sample border by arc
Glass cards scrolled out of view still ran their full effect path, and the rainbow border always used a segment count derived from a fixed per-pixel budget, so wide cards drew far more line segments than their slowly changing gradient needed. - A glass panel whose rectangle lies wholly outside the draw list's clip rect now returns early, so a scrolled-out card draws nothing (its border and ember passes are skipped along with the fill). - The rainbow border is split into segments by perimeter arc length: straight edges get one segment per ~32 dp, corner arcs get a few chords each to stay round, and a fully transparent border is skipped outright. A wide card now draws tens of segments instead of a couple hundred, with no visible change. Tests: the segment fractions are ascending and bounded, each corner arc keeps at least three chords, and the rect-cull margin test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
#pragma once
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace dragonx { namespace ui { namespace effects {
|
||||
|
||||
/// Splits a rounded-rect border into line segments and writes the ascending perimeter fractions
|
||||
/// out[0]=0 .. out[n]=1 (same parametrisation and piece order as ThemeEffects::perimeterPoint:
|
||||
/// top, TR arc, right, BR arc, bottom, BL arc, left, TL arc). Straight edges get one segment per
|
||||
/// `edgeStepPx` (<= 24 per edge); each corner arc gets clamp(ceil(r*0.4),3,8) chords, i.e. a
|
||||
/// worst-case sag below 0.2 px. Returns n (>=1); needs maxSegs >= 128 + 1 entries in `out`.
|
||||
inline int BorderSampleFractions(float w, float h, float r, float edgeStepPx,
|
||||
float* out, int maxOut /* capacity of out, >= 129 */) {
|
||||
constexpr float kPi = 3.14159265f;
|
||||
out[0] = 0.0f;
|
||||
if (w <= 0.0f || h <= 0.0f || maxOut < 129) { out[1] = 1.0f; return 1; }
|
||||
r = std::min(r, std::min(w, h) * 0.5f);
|
||||
if (r < 0.5f) r = 0.0f; // perimeterPoint treats tiny rounding as none
|
||||
const float step = std::max(1.0f, edgeStepPx);
|
||||
float len[8]; int segs[8]; int np;
|
||||
auto edge = [&](float l) { return l <= 0.0f ? 0 : std::clamp((int)std::ceil(l / step), 1, 24); };
|
||||
if (r <= 0.0f) {
|
||||
np = 4;
|
||||
len[0] = w; len[1] = h; len[2] = w; len[3] = h;
|
||||
for (int i = 0; i < 4; ++i) segs[i] = edge(len[i]);
|
||||
} else {
|
||||
np = 8;
|
||||
const float arc = kPi * r * 0.5f, ew = w - 2.0f * r, eh = h - 2.0f * r;
|
||||
const int arcSegs = std::clamp((int)std::ceil(r * 0.4f), 3, 8);
|
||||
const float L[8] = { ew, arc, eh, arc, ew, arc, eh, arc };
|
||||
for (int i = 0; i < 8; ++i) { len[i] = L[i]; segs[i] = (i & 1) ? arcSegs : edge(L[i]); }
|
||||
}
|
||||
float perim = 0.0f; for (int i = 0; i < np; ++i) perim += len[i];
|
||||
int n = 0; float d0 = 0.0f;
|
||||
for (int i = 0; i < np; ++i) {
|
||||
for (int s = 1; s <= segs[i]; ++s) {
|
||||
out[++n] = (d0 + len[i] * (float)s / (float)segs[i]) / perim;
|
||||
}
|
||||
d0 += len[i];
|
||||
}
|
||||
if (n < 1) { out[1] = 1.0f; return 1; }
|
||||
out[n] = 1.0f;
|
||||
return n; // worst case 4*24 + 4*8 = 128
|
||||
}
|
||||
|
||||
}}} // namespace
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include "theme_effects.h"
|
||||
#include "low_spec.h"
|
||||
#include "border_sampling.h"
|
||||
#include "effect_clock.h"
|
||||
#include "../schema/ui_schema.h"
|
||||
#include "../layout.h"
|
||||
@@ -294,31 +295,25 @@ void ThemeEffects::drawRainbowBorder(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax,
|
||||
float rounding, float thickness) const {
|
||||
if (!enabled_ || !rainbow_border_.enabled || rainbow_border_.stops.empty()) return;
|
||||
|
||||
// Final line alpha is rainbow_border_.alpha * bgOpacity_ (sampleGradient); under 1/255 nothing shows.
|
||||
if (rainbow_border_.alpha * bgOpacity_ * 255.0f < 1.0f) return;
|
||||
|
||||
float t = cyclePhase(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.
|
||||
// The border is drawn as line segments whose colour is sampled from the gradient at the segment
|
||||
// midpoint. Straight edges change colour slowly (stops span a third of the perimeter at alpha
|
||||
// ~0.08), so they get one segment per ~32 dp; corner arcs get a few chords each so they stay round.
|
||||
const float dp = Layout::dpiScale();
|
||||
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
|
||||
float fr[129];
|
||||
const int segments = BorderSampleFractions(w, h, std::min(rounding, std::min(w, h) * 0.5f),
|
||||
32.0f * dp, fr, 129);
|
||||
|
||||
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);
|
||||
|
||||
ImVec2 pt = perimeterPoint(pMin, pMax, fr[i], rounding);
|
||||
ImU32 c = sampleGradient(t + 0.5f * (fr[i - 1] + fr[i])); // colour at the segment midpoint
|
||||
dl->AddLine(prev, pt, c, thickness);
|
||||
prev = pt;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "type.h"
|
||||
#include "tooltip_style.h"
|
||||
#include "scroll_edge_fade.h"
|
||||
#include "rect_cull.h"
|
||||
#include "../layout.h"
|
||||
#include "../schema/element_styles.h"
|
||||
#include "../schema/color_var_resolver.h"
|
||||
@@ -694,10 +695,22 @@ inline void DrawCardDropShadow(ImDrawList* dl, const ImVec2& pMin, const ImVec2&
|
||||
dl->PopClipRect();
|
||||
}
|
||||
|
||||
// True when a card lies wholly outside the draw list's current clip rect (a scrolled-out panel):
|
||||
// everything it would draw is scissored away, except the theme effects, which go to the unclipped
|
||||
// foreground list. The margin covers the light-theme drop shadow (reach 6 dp) and the border.
|
||||
inline bool CardOutsideClip(const ImDrawList* dl, const ImVec2& pMin, const ImVec2& pMax)
|
||||
{
|
||||
if (dl->_ClipRectStack.Size == 0) return false;
|
||||
return RectOutsideRect(pMin, pMax, dl->GetClipRectMin(), dl->GetClipRectMax(),
|
||||
16.0f * Layout::dpiScale());
|
||||
}
|
||||
|
||||
inline void DrawGlassPanel(ImDrawList* dl, const ImVec2& pMin,
|
||||
const ImVec2& pMax,
|
||||
const GlassPanelSpec& spec = GlassPanelSpec())
|
||||
{
|
||||
if (CardOutsideClip(dl, pMin, pMax)) return; // scrolled out of view: nothing visible to draw
|
||||
|
||||
// Subtle drop shadow behind the card (light themes only). Drawn first so the card fill below
|
||||
// covers the interior, leaving only the outer soft blur — the card body is never contaminated.
|
||||
if (CardDropShadowWanted())
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
#include "imgui.h"
|
||||
namespace dragonx { namespace ui { namespace material {
|
||||
/// True when [aMin,aMax] grown by `margin` does not intersect [bMin,bMax].
|
||||
inline bool RectOutsideRect(const ImVec2& aMin, const ImVec2& aMax,
|
||||
const ImVec2& bMin, const ImVec2& bMax, float margin) {
|
||||
return aMax.x + margin < bMin.x || aMin.x - margin > bMax.x ||
|
||||
aMax.y + margin < bMin.y || aMin.y - margin > bMax.y;
|
||||
}
|
||||
}}}
|
||||
@@ -70,6 +70,8 @@
|
||||
#include "ui/node_status_banner.h"
|
||||
#include "ui/staleness_badge.h"
|
||||
#include "ui/effects/effect_clock.h"
|
||||
#include "ui/effects/border_sampling.h"
|
||||
#include "ui/material/rect_cull.h"
|
||||
#include "ui/notifications.h"
|
||||
#include "data/seed_migration_resume.h"
|
||||
#include "data/pin_vault_policy.h"
|
||||
@@ -13513,6 +13515,30 @@ void testEffectClock() {
|
||||
EXPECT_NEAR(radSin(1e6, 0.0, 1.5707963267948966), 1.0f, 1e-6f);
|
||||
}
|
||||
|
||||
void testBorderSampling() {
|
||||
using namespace dragonx::ui::effects; float fr[129];
|
||||
int n = BorderSampleFractions(500, 150, 8, 32, fr, 129);
|
||||
EXPECT_TRUE(n >= 40 && n <= 80); // old formula: ~210
|
||||
EXPECT_EQ(fr[0], 0.0f); EXPECT_EQ(fr[n], 1.0f);
|
||||
for (int i = 1; i <= n; ++i) EXPECT_TRUE(fr[i] > fr[i - 1]);
|
||||
n = BorderSampleFractions(3000, 800, 12, 32, fr, 129); EXPECT_TRUE(n <= 128); // old: 256
|
||||
n = BorderSampleFractions(300, 100, 0, 32, fr, 129); EXPECT_TRUE(n >= 4 && n <= 40); // r=0 rectangle
|
||||
// each corner arc keeps >= 3 chords: top-right arc spans [s1/P, s2/P]
|
||||
{ float r = 12, w = 400, h = 120, P = 2*(w-2*r) + 2*(h-2*r) + 4*(3.14159265f*r*0.5f);
|
||||
n = BorderSampleFractions(w, h, r, 32, fr, 129);
|
||||
float lo = (w-2*r)/P, hi = lo + 3.14159265f*r*0.5f/P; int c = 0;
|
||||
for (int i = 1; i <= n; ++i) if (fr[i] > lo + 1e-5f && fr[i] <= hi + 1e-5f) ++c;
|
||||
EXPECT_TRUE(c >= 3); }
|
||||
n = BorderSampleFractions(0, 50, 4, 32, fr, 129); EXPECT_EQ(n, 1); // degenerate
|
||||
}
|
||||
void testRectCull() {
|
||||
using namespace dragonx::ui::material; ImVec2 a(0,0), b(100,100);
|
||||
EXPECT_TRUE(!RectOutsideRect(ImVec2(10,10), ImVec2(50,50), a, b, 0));
|
||||
EXPECT_TRUE(RectOutsideRect(ImVec2(0,150), ImVec2(80,250), a, b, 16)); // below
|
||||
EXPECT_TRUE(!RectOutsideRect(ImVec2(0,110), ImVec2(80,250), a, b, 16)); // inside margin
|
||||
EXPECT_TRUE(RectOutsideRect(ImVec2(-90,0), ImVec2(-20,50), a, b, 16)); // left
|
||||
}
|
||||
|
||||
// The effective pool list = official pools ∪ saved favorites ∪ current custom pool,
|
||||
// endpoint-deduped, with synthetic user rows flagged official=false.
|
||||
void testEffectivePools()
|
||||
@@ -18249,6 +18275,8 @@ int main()
|
||||
testPoolFeeParsing();
|
||||
testEffectivePools();
|
||||
testEffectClock();
|
||||
testBorderSampling();
|
||||
testRectCull();
|
||||
testFormatFeePercent();
|
||||
testPoolWeightedSelection();
|
||||
testCoalescingWriter();
|
||||
|
||||
Reference in New Issue
Block a user