mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-09-21 09:05:10 -07:00
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.
326 lines
11 KiB
C++
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);
|
|
}
|
|
}
|