Cockatrice/cockatrice/src/interface/widgets/onboarding/shader_banner_widget.cpp
Lukas Brübach d7529e0c6a [Onboarding] Draw the banner logo as a static gradient plate
Replace the black/white logo tint switch with a ShaderEffect plate that
repaints the SVG's brand gradient (light AccentSoft -> dark AccentStrong
along the baked-in userSpaceOnUse axis) clipped to the full-color logo's
alpha silhouette, with the white highlight path overlaid on top — matching
the home widget's QPainter composite. The plate is static: no glow or
breathing. Brand colors flow from BannerShaderConfig's new brandStrong/
brandSoft pair instead of the removed logoDark flag, and the background
motifs get a touch more accent so the mark keeps its coloured surround.
2026-09-13 19:25:05 +02:00

326 lines
11 KiB
C++

#include "shader_banner_widget.h"
#include "../../theme_manager.h"
#include "banner_shader_config.h"
#include "brand_colors.h"
#include <QApplication>
#include <QPainter>
#include <QQmlContext>
#include <QQmlEngine>
#include <QQuickWidget>
#include <QResizeEvent>
#include <QStackedLayout>
namespace
{
// Curated near-black stage -- used only when the active palette resolves no
// usable window colour. Matches the banner's original design (dark and quiet
// so the accent stands out) and satisfies design-plans §2.1's "identity
// survives a bare palette".
constexpr QRgb kFallbackColorA = 0x1A1A20;
constexpr QRgb kFallbackColorB = 0x0E0E12;
struct SuggestedColors
{
QColor colorA;
QColor colorB;
QColor accent;
QColor glowColor;
QColor brandStrong;
QColor brandSoft;
qreal vignetteMin = 0.62;
};
SuggestedColors suggestedBannerColors()
{
const QPalette &pal = qApp->palette();
const QColor window = pal.color(QPalette::Active, QPalette::Window);
// Identity accent: the theme's [AppColors] AccentStrong, which appColor()
// resolves to QPalette::Highlight when a theme doesn't pin AccentStrong.
// Reading bare Highlight ignored curated accent tokens (Plasma's violet
// vs Default's green) whenever a palette didn't set the role itself.
const QColor accentStrong = themeManager->appColor(AppColor::AccentStrong);
if (!window.isValid() || !accentStrong.isValid()) {
return {QColor(kFallbackColorA),
QColor(kFallbackColorB),
kCockatriceBrandGreen,
QColor(Qt::white),
kCockatriceBrandGreen,
QColor(0xC9, 0xFD, 0x62),
0.62};
}
// The theme's brand pair: AccentStrong is the deep green, AccentSoft the
// lime. These two appColors form the logo's "surrounding gradient" (deep
// core grading out to the soft, brand-toned glow) on both the banner and
// the home screen.
const QColor brandStrong = accentStrong;
const QColor brandSoft = themeManager->appColor(AppColor::AccentSoft);
// Dress the stage for the scheme so the banner never fights the
// surrounding window in either mode. Dark palettes keep the original
// quiet near-black stage (lightness 29 → 16) with the theme's window
// hue; light palettes get a pastel "frosted accent" treatment built from
// the accent hue instead of a plain near-white copy: a coloured wash that
// clearly belongs to the theme.
const qreal luma = 0.299 * window.red() + 0.587 * window.green() + 0.114 * window.blue();
const bool lightStage = luma > 115.0;
if (lightStage) {
const int hue = accentStrong.hslHue();
// Achromatic accents (grey) get a neutral near-white stage instead.
const int stageSat = hue < 0 ? 0 : 64;
const int hueSafe = hue < 0 ? 0 : hue;
// Depth is what stops a light stage reading as a washed-out near-white
// copy of the page behind the banner: deepen the lower pastel band and
// raise saturation so the hue is clearly present while staying frosted.
auto pastel = [hueSafe, stageSat](int lightness) { return QColor::fromHsl(hueSafe, stageSat, lightness); };
auto pastelLower = [hueSafe](int lightness) { return QColor::fromHsl(hueSafe, 76, lightness); };
// Brightness-lifted accent for additive glows: the raw accent on a
// light stage must be mid-bright to read instead of washing out, so
// lift lightness and saturation together.
const int accentLightness = qBound(158, accentStrong.lightness() + 82, 198);
const int accentSaturation = hue < 0 ? 0 : qMax(accentStrong.hslSaturation(), 180);
const QColor liftedAccent =
hue < 0 ? accentStrong : QColor::fromHsl(hueSafe, accentSaturation, accentLightness);
// The centre glow (and logo tint in QML) uses the deep accent itself:
// a coloured halo/fill behind the logo instead of a white or black one.
return {pastel(214), pastelLower(186), liftedAccent, accentStrong, brandStrong, brandSoft, 0.80};
}
// Dark stage: force the window hue down to the banner's curated darkness,
// scaling saturation away so chromatic palettes tint it without going
// muddy. The accent is the bright, brand-driven tone (hue from the accent
// itself, never the -- often grey -- window), and it drives both the
// embers/fog and the logo glow so the mark tints like the light stage.
auto stage = [&window](int lightness) {
const int hue = window.hslHue();
const int saturation = hue < 0 ? 0 : qBound(0, qRound(window.hslSaturation() * (lightness / 40.0)), 255);
return QColor::fromHsl(hue, saturation, lightness);
};
const int accentHue = accentStrong.hslHue();
const int accentHueSafe = accentHue < 0 ? 0 : accentHue;
const int accentLightness = qBound(150, accentStrong.lightness() + 70, 185);
const int accentSaturation = accentHue < 0 ? 0 : qMax(accentStrong.hslSaturation(), 160);
const QColor accent =
accentHue < 0 ? accentStrong : QColor::fromHsl(accentHueSafe, accentSaturation, accentLightness);
return {stage(29), stage(16), accent, accent, brandStrong, brandSoft, 0.62};
}
} // namespace
class GradientFallbackWidget : public QWidget
{
public:
using QWidget::QWidget;
void setColors(const QColor &a, const QColor &b)
{
if (a != colorA || b != colorB) {
colorA = a;
colorB = b;
}
}
protected:
void paintEvent(QPaintEvent *) override
{
QPainter painter(this);
QLinearGradient gradient(0, 0, width(), height());
gradient.setColorAt(0.0, colorA);
gradient.setColorAt(1.0, colorB);
painter.fillRect(rect(), gradient);
}
private:
QColor colorA{QColor(kFallbackColorA)};
QColor colorB{QColor(kFallbackColorB)};
};
BannerHost::BannerHost(QWidget *parent) : QWidget(parent)
{
setFixedHeight(150);
stack = new QStackedLayout(this);
stack->setContentsMargins(0, 0, 0, 0);
fallback = new GradientFallbackWidget(this);
stack->addWidget(fallback);
quickWidget = new QQuickWidget(this);
quickWidget->setResizeMode(QQuickWidget::SizeRootObjectToView);
config = new BannerShaderConfig(quickWidget->engine());
quickWidget->rootContext()->setContextProperty("bannerConfig", config);
quickWidget->setSource(QUrl("qrc:/onboarding/qml/BrandBanner.qml"));
if (quickWidget->status() == QQuickWidget::Error) {
activateFallback();
} else {
connect(quickWidget, &QQuickWidget::sceneGraphError, this, &BannerHost::onSceneGraphFailed);
stack->addWidget(quickWidget);
stack->setCurrentWidget(quickWidget);
}
connect(&clock, &QTimer::timeout, this, &BannerHost::tick);
clock.setInterval(16); // ~60fps; the shader itself is cheap, this is just a wall clock
connect(themeManager, &ThemeManager::themeChanged, this, &BannerHost::applyThemeColors);
applyThemeColors();
applyMotifPreset(currentMotif);
updateAspect();
}
void BannerHost::activateFallback()
{
if (usingFallback) {
return;
}
usingFallback = true;
clock.stop();
stack->setCurrentWidget(fallback);
if (quickWidget) {
quickWidget->deleteLater(); // takes BannerShaderConfig (parented to its engine) with it
quickWidget = nullptr;
config = nullptr;
}
}
void BannerHost::onSceneGraphFailed()
{
activateFallback();
}
void BannerHost::setMotif(Motif motif)
{
currentMotif = motif;
applyMotifPreset(motif);
}
BannerHost::Preset BannerHost::presetFor(Motif motif)
{
// speed/seed tuned per motif so e.g. the network "pulse" (Account) reads
// at a deliberately calmer cadence than the data "scan" lines
// (Preferences), even though both come from the same shader.
switch (motif) {
case Motif::Welcome:
return {0.0, 0.6, 0.15};
case Motif::CardDatabase:
return {1.0, 1.3, 0.42};
case Motif::Theming:
return {2.0, 1.2, 0.73};
case Motif::Account:
return {3.0, 0.8, 0.28};
case Motif::Preferences:
return {4.0, 1.0, 0.61};
case Motif::Finish:
return {5.0, 1.0, 0.91};
}
return {0.0, 0.6, 0.15};
}
void BannerHost::applyMotifPreset(Motif motif)
{
if (usingFallback || !config) {
return;
}
const Preset p = presetFor(motif);
config->setColorA(bannerColorA);
config->setColorB(bannerColorB);
config->setAccent(bannerAccent);
config->setLogoVisible(motif == Motif::Welcome);
if (isFirstApply) {
// Nothing on screen yet -- write straight into the front bank, no
// crossfade needed for the very first paint.
config->setModeA(p.mode);
config->setSpeedA(p.speed);
config->setSeedA(p.seed);
config->setFrontIsA(true);
isFirstApply = false;
return;
}
// Write the new preset into whichever bank is currently hidden, then
// flip which one is front. QML's opacity Behavior does the actual
// crossfade -- BannerHost never animates anything itself.
if (config->frontIsA()) {
config->setModeB(p.mode);
config->setSpeedB(p.speed);
config->setSeedB(p.seed);
config->setFrontIsA(false);
} else {
config->setModeA(p.mode);
config->setSpeedA(p.speed);
config->setSeedA(p.seed);
config->setFrontIsA(true);
}
}
void BannerHost::updateAspect()
{
if (config && height() > 0) {
config->setAspect(qreal(width()) / qreal(height()));
}
}
void BannerHost::resizeEvent(QResizeEvent *event)
{
QWidget::resizeEvent(event);
updateAspect();
}
void BannerHost::showEvent(QShowEvent *event)
{
QWidget::showEvent(event);
if (!usingFallback) {
elapsed.restart();
clock.start();
}
}
void BannerHost::hideEvent(QHideEvent *event)
{
QWidget::hideEvent(event);
clock.stop();
}
void BannerHost::applyThemeColors()
{
const SuggestedColors colors = suggestedBannerColors();
bannerColorA = colors.colorA;
bannerColorB = colors.colorB;
bannerAccent = colors.accent;
if (usingFallback) {
fallback->setColors(bannerColorA, bannerColorB);
fallback->update();
} else if (config) {
config->setColorA(bannerColorA);
config->setColorB(bannerColorB);
config->setAccent(bannerAccent);
config->setGlowColor(colors.glowColor);
config->setBrandStrong(colors.brandStrong);
config->setBrandSoft(colors.brandSoft);
config->setVignetteMin(colors.vignetteMin);
}
}
void BannerHost::tick()
{
// Palette previews (e.g. accent drags in the wizard's QuickSetupPanel)
// apply qApp->palette() without firing themeChanged, so re-derive here;
// BannerShaderConfig's setters are equality-guarded, so this is a no-op
// unless the colours actually changed.
applyThemeColors();
if (config) {
qreal t = elapsed.elapsed() / 1000.0;
config->setTime(t);
// Visible breathing for the logo: oscillates between 0.0 and 1.0
qreal glow = 0.5 + 0.5 * qSin(t * 0.4);
config->setLogoGlow(glow);
}
}