perf(ui): replace animated-sidebar drop shadows with pre-baked 9-patch overlay

QGraphicsDropShadowEffect forces the host widget through an off-screen
software Gaussian blur on every repaint. On the floating sidebar (HomeView,
blur 25) and the player right sidebar (PlayerView, blur 35), the blur was
re-rasterized on every frame of the 350ms slide animation, the main source
of UI jank.

Bake the blur once into a 9-patch PNG (cached to CacheLocation/shadows via
QGraphicsBlurEffect) and render it at runtime as a cheap 9-slice stretch with
zero per-frame blur cost:

- utils/shadowpixmap: off-screen bake + on-disk cache, dpr-aware.
- components/shadowoverlay: mouse-transparent sibling widget, lazy-baked,
  paints the 9-patch; optional body cutout for semi-transparent targets so
  the shadow does not bleed through the body.
- homeview/playerview: drop the effect; the overlay tracks the sidebar
  geometry via the existing eventFilter (Move/Resize/Show/Hide), so every
  setGeometry/move site and the slide animation drive it for free.
This commit is contained in:
dela
2026-06-18 17:03:57 +08:00
parent a608910148
commit 907de1feeb
8 changed files with 394 additions and 36 deletions
+108
View File
@@ -0,0 +1,108 @@
#include "shadowpixmap.h"
#include <QDir>
#include <QGraphicsBlurEffect>
#include <QGraphicsPixmapItem>
#include <QGraphicsScene>
#include <QImage>
#include <QPainter>
#include <QStandardPaths>
namespace
{
// Width of the uniform, stretchable interior between the rounded corners.
// Kept small (the centre is a flat solid colour, so 4 logical px is plenty).
constexpr int kCenter = 4;
int borderFor(int blurRadius, int cornerRadius)
{
// Falloff reach beyond the object edge, plus the corner radius which lives
// inside the fixed (non-stretched) border so corners never distort.
return (blurRadius + 1) + cornerRadius;
}
int totalFor(int blurRadius, int cornerRadius)
{
return 2 * borderFor(blurRadius, cornerRadius) + kCenter;
}
QString cacheFilePath(int blurRadius, int cornerRadius, const QColor &color, qreal dpr)
{
const QString dir =
QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/shadows";
QDir().mkpath(dir);
// dpr folded in at x100 so e.g. 1.5 -> 150; colour as rrggbbaa.
const QString name = QString("shadow_b%1_c%2_%3_d%4.png")
.arg(blurRadius)
.arg(cornerRadius)
.arg(static_cast<uint>(color.rgba()), 8, 16, QLatin1Char('0'))
.arg(qRound(dpr * 100));
return dir + '/' + name;
}
// Render the rounded rect, then blur it off-screen via QGraphicsBlurEffect.
// Done once per cache key; the cost here is irrelevant to runtime frame rate.
QImage bake(int blurRadius, int cornerRadius, const QColor &color, qreal dpr)
{
const int total = totalFor(blurRadius, cornerRadius);
const int border = borderFor(blurRadius, cornerRadius);
const qreal s = dpr;
const int physTotal = qMax(1, qRound(total * s));
QImage shape(physTotal, physTotal, QImage::Format_ARGB32_Premultiplied);
shape.fill(Qt::transparent);
{
QPainter p(&shape);
p.setRenderHint(QPainter::Antialiasing, true);
p.setPen(Qt::NoPen);
p.setBrush(color);
// Object footprint: inset by the falloff reach (border - corner) so the
// blur has room to fade on every side.
const qreal reach = (border - cornerRadius) * s;
const qreal objSide = (total - 2 * (border - cornerRadius)) * s;
p.drawRoundedRect(QRectF(reach, reach, objSide, objSide),
cornerRadius * s, cornerRadius * s);
}
QGraphicsScene scene;
auto *item = scene.addPixmap(QPixmap::fromImage(shape));
auto *blur = new QGraphicsBlurEffect;
blur->setBlurRadius(blurRadius * s);
blur->setBlurHints(QGraphicsBlurEffect::QualityHint);
item->setGraphicsEffect(blur);
QImage out(physTotal, physTotal, QImage::Format_ARGB32_Premultiplied);
out.fill(Qt::transparent);
{
QPainter op(&out);
scene.render(&op, QRectF(0, 0, physTotal, physTotal),
QRectF(0, 0, physTotal, physTotal));
}
return out;
}
} // namespace
namespace ShadowPixmap
{
NinePatchShadow make(int blurRadius, int cornerRadius, const QColor &color, qreal dpr)
{
NinePatchShadow result;
result.border = borderFor(blurRadius, cornerRadius);
result.total = totalFor(blurRadius, cornerRadius);
result.dpr = dpr;
const QString path = cacheFilePath(blurRadius, cornerRadius, color, dpr);
QPixmap cached;
if (cached.load(path, "PNG") && !cached.isNull())
{
result.pixmap = cached;
return result;
}
const QImage baked = bake(blurRadius, cornerRadius, color, dpr);
result.pixmap = QPixmap::fromImage(baked);
baked.save(path, "PNG"); // best-effort cache; failure just re-bakes next time
return result;
}
} // namespace ShadowPixmap