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:
@@ -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
|
||||
Reference in New Issue
Block a user