diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index 63ccc4e9c..06ec15294 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -403,6 +403,7 @@ set(cockatrice_SOURCES src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_widget.cpp src/interface/widgets/tabs/api/commander_spellbook/handle_commander_brackets.cpp src/interface/widgets/onboarding/banner_shader_config.h + src/interface/widgets/onboarding/brand_colors.h src/interface/widgets/onboarding/first_run_wizard.cpp src/interface/widgets/onboarding/first_run_wizard.h src/interface/widgets/onboarding/first_run_wizard_page.cpp diff --git a/cockatrice/cockatrice.qrc b/cockatrice/cockatrice.qrc index 49c3f27b5..79e71bf84 100644 --- a/cockatrice/cockatrice.qrc +++ b/cockatrice/cockatrice.qrc @@ -2,6 +2,7 @@ resources/cardback.svg resources/cockatrice.svg + resources/cockatrice-logo-black.svg resources/cockatrice-logo-white.svg resources/hand.svg resources/hr.jpg diff --git a/cockatrice/resources/cockatrice-logo-black.svg b/cockatrice/resources/cockatrice-logo-black.svg new file mode 100644 index 000000000..c1a30ec62 --- /dev/null +++ b/cockatrice/resources/cockatrice-logo-black.svg @@ -0,0 +1,21 @@ + + + + + + + + + diff --git a/cockatrice/src/interface/widgets/onboarding/banner_shader_config.h b/cockatrice/src/interface/widgets/onboarding/banner_shader_config.h index 32f3e89c0..88666abcb 100644 --- a/cockatrice/src/interface/widgets/onboarding/banner_shader_config.h +++ b/cockatrice/src/interface/widgets/onboarding/banner_shader_config.h @@ -38,8 +38,11 @@ class BannerShaderConfig : public QObject Q_PROPERTY(QColor colorA READ colorA WRITE setColorA NOTIFY colorAChanged) Q_PROPERTY(QColor colorB READ colorB WRITE setColorB NOTIFY colorBChanged) Q_PROPERTY(QColor accent READ accent WRITE setAccent NOTIFY accentChanged) + Q_PROPERTY(QColor glowColor READ glowColor WRITE setGlowColor NOTIFY glowColorChanged) + Q_PROPERTY(qreal vignetteMin READ vignetteMin WRITE setVignetteMin NOTIFY vignetteMinChanged) Q_PROPERTY(bool logoVisible READ logoVisible WRITE setLogoVisible NOTIFY logoVisibleChanged) + Q_PROPERTY(bool logoDark READ logoDark WRITE setLogoDark NOTIFY logoDarkChanged) Q_PROPERTY(qreal logoGlow READ logoGlow WRITE setLogoGlow NOTIFY logoGlowChanged) public: @@ -185,6 +188,30 @@ public: } } + QColor glowColor() const + { + return m_glowColor; + } + void setGlowColor(const QColor &c) + { + if (c != m_glowColor) { + m_glowColor = c; + emit glowColorChanged(); + } + } + + qreal vignetteMin() const + { + return m_vignetteMin; + } + void setVignetteMin(qreal v) + { + if (v != m_vignetteMin) { + m_vignetteMin = v; + emit vignetteMinChanged(); + } + } + bool logoVisible() const { return m_logoVisible; @@ -197,6 +224,18 @@ public: } } + bool logoDark() const + { + return m_logoDark; + } + void setLogoDark(bool v) + { + if (v != m_logoDark) { + m_logoDark = v; + emit logoDarkChanged(); + } + } + qreal logoGlow() const { return m_logoGlow; @@ -222,7 +261,10 @@ signals: void colorAChanged(); void colorBChanged(); void accentChanged(); + void glowColorChanged(); + void vignetteMinChanged(); void logoVisibleChanged(); + void logoDarkChanged(); void logoGlowChanged(); private: @@ -239,11 +281,17 @@ private: bool m_frontIsA = true; + // Curated fallback seed values -- BannerHost overwrites these with + // palette-derived colours (see shader_banner_widget.cpp) before the first + // paint, so they only matter as a safe pre-first-apply default. QColor m_colorA{0x1A, 0x1A, 0x20}; QColor m_colorB{0x0E, 0x0E, 0x12}; QColor m_accent{0x8B, 0xDD, 0x6B}; + QColor m_glowColor{Qt::white}; + qreal m_vignetteMin = 0.62; bool m_logoVisible = false; + bool m_logoDark = false; qreal m_logoGlow = 1.0; }; diff --git a/cockatrice/src/interface/widgets/onboarding/brand_colors.h b/cockatrice/src/interface/widgets/onboarding/brand_colors.h new file mode 100644 index 000000000..bf173270b --- /dev/null +++ b/cockatrice/src/interface/widgets/onboarding/brand_colors.h @@ -0,0 +1,15 @@ +#ifndef BRAND_COLORS_H +#define BRAND_COLORS_H + +#include + +/** @brief Cockatrice brand green. + * + * Single source of truth for the onboarding brand accent: it backs the + * banner's shader-accent uniform as the curated fallback when the active + * palette resolves no usable Highlight, and it preseads the wizard's + * QuickSetupPanel so a freshly generated palette keeps the brand identity + * until the user picks their own look. */ +inline const QColor kCockatriceBrandGreen(0x8B, 0xDD, 0x6B); + +#endif // BRAND_COLORS_H \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/onboarding/pages/theme_setup_page.cpp b/cockatrice/src/interface/widgets/onboarding/pages/theme_setup_page.cpp index 3293b19ac..733274588 100644 --- a/cockatrice/src/interface/widgets/onboarding/pages/theme_setup_page.cpp +++ b/cockatrice/src/interface/widgets/onboarding/pages/theme_setup_page.cpp @@ -5,6 +5,7 @@ #include "../../interface/palette_editor/quick_setup_panel.h" #include "../../interface/theme_manager.h" #include "../../interface/widgets/general/background_sources.h" +#include "../brand_colors.h" #include "libcockatrice/settings/appearance_settings.h" #include @@ -30,6 +31,12 @@ ThemeSetupPage::ThemeSetupPage(QWidget *parent) : FirstRunWizardPage(parent) quickSetupPanel = new QuickSetupPanel(this); + // Preseed with the brand green so a fresh install's generated palette -- + // and therefore the banner accent, which follows QPalette::Highlight -- + // keeps the Cockatrice identity until the user picks their own look. + // setAccentColor blocks signals, so this never triggers a generation. + quickSetupPanel->setAccentColor(kCockatriceBrandGreen); + connect(themeCombo, QOverload::of(&QComboBox::currentIndexChanged), this, &ThemeSetupPage::onThemeChanged); connect(schemeCombo, QOverload::of(&QComboBox::currentIndexChanged), this, &ThemeSetupPage::onSchemeChanged); connect(quickSetupPanel, &QuickSetupPanel::valueChanged, this, &ThemeSetupPage::onGenerateFromAccent); diff --git a/cockatrice/src/interface/widgets/onboarding/qml/BrandBanner.qml b/cockatrice/src/interface/widgets/onboarding/qml/BrandBanner.qml index f1a385cad..c14bb2ca4 100644 --- a/cockatrice/src/interface/widgets/onboarding/qml/BrandBanner.qml +++ b/cockatrice/src/interface/widgets/onboarding/qml/BrandBanner.qml @@ -16,6 +16,8 @@ Item { property vector4d uColorA: Qt.vector4d(bannerConfig.colorA.r, bannerConfig.colorA.g, bannerConfig.colorA.b, 1.0) property vector4d uColorB: Qt.vector4d(bannerConfig.colorB.r, bannerConfig.colorB.g, bannerConfig.colorB.b, 1.0) property vector4d uAccent: Qt.vector4d(bannerConfig.accent.r, bannerConfig.accent.g, bannerConfig.accent.b, 1.0) + property vector4d uGlowColor: Qt.vector4d(bannerConfig.glowColor.r, bannerConfig.glowColor.g, bannerConfig.glowColor.b, 1.0) + property real uVignetteMin: bannerConfig.vignetteMin property real uLogoGlow: bannerConfig.logoGlow fragmentShader: "qrc:/onboarding/shaders/brand_banner.frag.qsb" } @@ -33,16 +35,21 @@ Item { property vector4d uColorA: Qt.vector4d(bannerConfig.colorA.r, bannerConfig.colorA.g, bannerConfig.colorA.b, 1.0) property vector4d uColorB: Qt.vector4d(bannerConfig.colorB.r, bannerConfig.colorB.g, bannerConfig.colorB.b, 1.0) property vector4d uAccent: Qt.vector4d(bannerConfig.accent.r, bannerConfig.accent.g, bannerConfig.accent.b, 1.0) + property vector4d uGlowColor: Qt.vector4d(bannerConfig.glowColor.r, bannerConfig.glowColor.g, bannerConfig.glowColor.b, 1.0) + property real uVignetteMin: bannerConfig.vignetteMin property real uLogoGlow: bannerConfig.logoGlow fragmentShader: "qrc:/onboarding/shaders/brand_banner.frag.qsb" } - // The hero logo itself — breathes cleanly over a 0.5–1.0 opacity range + // The hero logo itself — breathes cleanly over a 0.5–1.0 opacity range. + // White silhouette on dark stages, black on light ones, so it stays + // readable in both modes. Image { id: logo anchors.centerIn: parent visible: bannerConfig.logoVisible - source: "qrc:/resources/cockatrice-logo-white.svg" + source: bannerConfig.logoDark ? "qrc:/resources/cockatrice-logo-black.svg" + : "qrc:/resources/cockatrice-logo-white.svg" width: root.height * 0.6 height: width * (sourceSize.height > 0 ? sourceSize.height / Math.max(sourceSize.width, 1) : 1) fillMode: Image.PreserveAspectFit diff --git a/cockatrice/src/interface/widgets/onboarding/shader_banner_widget.cpp b/cockatrice/src/interface/widgets/onboarding/shader_banner_widget.cpp index fd1fb2a98..7db651c45 100644 --- a/cockatrice/src/interface/widgets/onboarding/shader_banner_widget.cpp +++ b/cockatrice/src/interface/widgets/onboarding/shader_banner_widget.cpp @@ -1,7 +1,10 @@ #include "shader_banner_widget.h" +#include "../../theme_manager.h" #include "banner_shader_config.h" +#include "brand_colors.h" +#include #include #include #include @@ -11,11 +14,68 @@ namespace { -// Near-black base palette -- the background is dark and quiet so the green -// accent stands out. -constexpr QRgb kColorA = 0x1A1A20; -constexpr QRgb kColorB = 0x0E0E12; -constexpr QRgb kAccent = 0x8BDD6B; +// 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; + qreal vignetteMin = 0.62; + bool lightStage = false; +}; + +SuggestedColors suggestedBannerColors() +{ + const QPalette &pal = qApp->palette(); + const QColor window = pal.color(QPalette::Active, QPalette::Window); + const QColor highlight = pal.color(QPalette::Active, QPalette::Highlight); + if (!window.isValid() || !highlight.isValid()) { + return { + QColor(kFallbackColorA), QColor(kFallbackColorB), kCockatriceBrandGreen, QColor(Qt::white), 0.62, false}; + } + + // 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 Highlight hue instead of a plain near-white copy: a gentle mint + // 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 = highlight.hslHue(); + // Achromatic accents (grey) get a neutral near-white stage instead. + const int stageSat = hue < 0 ? 0 : 35; + const int hueSafe = hue < 0 ? 0 : hue; + auto pastel = [hueSafe, stageSat](int lightness) { return QColor::fromHsl(hueSafe, stageSat, lightness); }; + // Brightness-lifted accent for additive glows: Highlight on a light + // stage must be mid-bright to read (the shipped light Highlight is a + // deep green that washes out additively against white). + const int accentLightness = qBound(120, highlight.lightness() + 70, 165); + const int accentSaturation = hue < 0 ? 0 : qMax(highlight.hslSaturation(), 140); + const QColor liftedAccent = hue < 0 ? highlight : QColor::fromHsl(hueSafe, accentSaturation, accentLightness); + // The centre glow uses the deep Highlight itself -- a coloured halo + // behind the dark logo instead of a white blowout. + return {pastel(247), pastel(231), liftedAccent, highlight, 0.88, true}; + } + + // Dark stage: force the window hue down to the banner's curated darkness, + // scaling saturation away so chromatic palettes tint it without going + // muddy. White glow and the original strong vignette stay untouched. + 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); + }; + return {stage(29), stage(16), QColor(highlight), QColor(Qt::white), 0.62, false}; +} } // namespace class GradientFallbackWidget : public QWidget @@ -23,15 +83,27 @@ 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, QColor(kColorA)); - gradient.setColorAt(1.0, QColor(kColorB)); + 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) @@ -62,6 +134,9 @@ BannerHost::BannerHost(QWidget *parent) : QWidget(parent) 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(); } @@ -123,9 +198,9 @@ void BannerHost::applyMotifPreset(Motif motif) const Preset p = presetFor(motif); - config->setColorA(QColor(kColorA)); - config->setColorB(QColor(kColorB)); - config->setAccent(QColor(kAccent)); + config->setColorA(bannerColorA); + config->setColorB(bannerColorB); + config->setAccent(bannerAccent); config->setLogoVisible(motif == Motif::Welcome); if (isFirstApply) { @@ -183,8 +258,33 @@ void BannerHost::hideEvent(QHideEvent *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->setVignetteMin(colors.vignetteMin); + config->setLogoDark(colors.lightStage); + } +} + 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); diff --git a/cockatrice/src/interface/widgets/onboarding/shader_banner_widget.h b/cockatrice/src/interface/widgets/onboarding/shader_banner_widget.h index 2e230ad7f..ac47b1141 100644 --- a/cockatrice/src/interface/widgets/onboarding/shader_banner_widget.h +++ b/cockatrice/src/interface/widgets/onboarding/shader_banner_widget.h @@ -1,6 +1,7 @@ #ifndef SHADER_BANNER_WIDGET_H #define SHADER_BANNER_WIDGET_H +#include #include #include #include @@ -53,6 +54,7 @@ protected: private slots: void tick(); void onSceneGraphFailed(); + void applyThemeColors(); private: struct Preset @@ -73,6 +75,12 @@ private: BannerShaderConfig *config = nullptr; GradientFallbackWidget *fallback = nullptr; + // Palette-derived banner colours -- the theme's window hue forced down to + // the banner's curated darkness, plus the theme's Highlight as accent. + QColor bannerColorA; + QColor bannerColorB; + QColor bannerAccent; + QTimer clock; QElapsedTimer elapsed; Motif currentMotif = Motif::Welcome; diff --git a/cockatrice/src/interface/widgets/onboarding/shaders/brand_banner.frag b/cockatrice/src/interface/widgets/onboarding/shaders/brand_banner.frag index 508bd4bc4..cea9101a8 100644 --- a/cockatrice/src/interface/widgets/onboarding/shaders/brand_banner.frag +++ b/cockatrice/src/interface/widgets/onboarding/shaders/brand_banner.frag @@ -28,6 +28,8 @@ layout(std140, binding = 0) uniform buf vec4 uColorA; vec4 uColorB; vec4 uAccent; + vec4 uGlowColor; + float uVignetteMin; float uLogoGlow; }; @@ -136,14 +138,17 @@ vec3 motifWelcome(vec2 uv, vec3 bg, float t) vec2 center = vec2(asp * 0.5, 0.5); float cDist = length(ac - center); - // Centre bloom at logo position; intensity scales with uLogoGlow + // Centre bloom at logo position; intensity scales with uLogoGlow. It + // uses the scheme-driven uGlowColor rather than plain white so light + // stages don't blow out (white halo on a near-white field) -- BannerHost + // feeds white on dark stages and the deep accent tone on light ones. float centreLight = bloom(cDist, 0.08 * asp, 0.40 * asp); - col += centreLight * 0.20 * uLogoGlow; + col += uGlowColor.rgb * centreLight * 0.20 * uLogoGlow; // Flow-noise shimmer gated by Gaussian mask at centre float shimmer = flowNoise(ac * 0.8 + vec2(55.0, 33.0), t * 0.05) * 0.5 + 0.5; float shimmerMask = exp(-(cDist * cDist) / (0.18 * asp * 0.18 * asp)); - col += shimmer * shimmerMask * 0.04 * uLogoGlow; + col += uGlowColor.rgb * shimmer * shimmerMask * 0.04 * uLogoGlow; // 48 ember particles: hash-seeded position, speed, size, brightness. // Embers within a distance threshold of centre are deflected into an @@ -456,6 +461,8 @@ void main() else if (uMode < 4.5) col = motifPreferences(uv, bg, t); else col = motifFinish(uv, bg, t); - col *= mix(0.62, 1.0, vignette(uv)); + // Corner vignette; uVignetteMin is scheme-driven (0.62 on dark stages, + // gentler on light ones so near-white corners don't go muddy grey). + col *= mix(uVignetteMin, 1.0, vignette(uv)); fragColor = vec4(col, 1.0) * qt_Opacity; }