diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index 06ec15294..4263fc6e2 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -531,6 +531,7 @@ qt6_add_shaders( "src/interface/widgets/onboarding/shaders" FILES src/interface/widgets/onboarding/shaders/brand_banner.frag + src/interface/widgets/onboarding/shaders/brand_plate.frag ) qt6_add_resources( diff --git a/cockatrice/src/interface/widgets/onboarding/banner_shader_config.h b/cockatrice/src/interface/widgets/onboarding/banner_shader_config.h index 88666abcb..6008044ff 100644 --- a/cockatrice/src/interface/widgets/onboarding/banner_shader_config.h +++ b/cockatrice/src/interface/widgets/onboarding/banner_shader_config.h @@ -39,10 +39,11 @@ class BannerShaderConfig : public QObject 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(QColor brandStrong READ brandStrong WRITE setBrandStrong NOTIFY brandStrongChanged) + Q_PROPERTY(QColor brandSoft READ brandSoft WRITE setBrandSoft NOTIFY brandSoftChanged) 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: @@ -200,6 +201,30 @@ public: } } + QColor brandStrong() const + { + return m_brandStrong; + } + void setBrandStrong(const QColor &c) + { + if (c != m_brandStrong) { + m_brandStrong = c; + emit brandStrongChanged(); + } + } + + QColor brandSoft() const + { + return m_brandSoft; + } + void setBrandSoft(const QColor &c) + { + if (c != m_brandSoft) { + m_brandSoft = c; + emit brandSoftChanged(); + } + } + qreal vignetteMin() const { return m_vignetteMin; @@ -224,18 +249,6 @@ 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; @@ -262,9 +275,10 @@ signals: void colorBChanged(); void accentChanged(); void glowColorChanged(); + void brandStrongChanged(); + void brandSoftChanged(); void vignetteMinChanged(); void logoVisibleChanged(); - void logoDarkChanged(); void logoGlowChanged(); private: @@ -288,10 +302,11 @@ private: QColor m_colorB{0x0E, 0x0E, 0x12}; QColor m_accent{0x8B, 0xDD, 0x6B}; QColor m_glowColor{Qt::white}; + QColor m_brandStrong{0x13, 0x97, 0x40}; + QColor m_brandSoft{0xC9, 0xFD, 0x62}; 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/qml/BrandBanner.qml b/cockatrice/src/interface/widgets/onboarding/qml/BrandBanner.qml index c14bb2ca4..d94e15280 100644 --- a/cockatrice/src/interface/widgets/onboarding/qml/BrandBanner.qml +++ b/cockatrice/src/interface/widgets/onboarding/qml/BrandBanner.qml @@ -41,29 +41,56 @@ Item { fragmentShader: "qrc:/onboarding/shaders/brand_banner.frag.qsb" } - // 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 + // The white logo sits at full opacity on top of the static gradient plate — + // no glow, no breathing. The plate matches home_widget's QPainter composite. + Item { + id: logoHost visible: bannerConfig.logoVisible - source: bannerConfig.logoDark ? "qrc:/resources/cockatrice-logo-black.svg" - : "qrc:/resources/cockatrice-logo-white.svg" + anchors.centerIn: parent width: root.height * 0.6 - height: width * (sourceSize.height > 0 ? sourceSize.height / Math.max(sourceSize.width, 1) : 1) - fillMode: Image.PreserveAspectFit - smooth: true - opacity: 0.5 + 0.5 * bannerConfig.logoGlow - sourceSize: Qt.size(256, 256) + height: width - Behavior on opacity { NumberAnimation { duration: 300; easing.type: Easing.InOutSine } } + // The full-color logo renders beneath the white mark and is consumed as + // a texture (layer.enabled) by the plate shader's silhouette mask, so + // the gradient is clipped to the bird exactly as the SVG's gradient + // paths are. It is never drawn to the screen itself. + Image { + id: silhouetteMask + anchors.fill: parent + source: "qrc:/resources/cockatrice.svg" + sourceSize: Qt.size(256, 256) + fillMode: Image.PreserveAspectFit + smooth: true + visible: false + layer.enabled: true + layer.smooth: true + } - transform: Scale { - origin.x: logo.width / 2 - origin.y: logo.height / 2 - xScale: 0.94 + 0.06 * bannerConfig.logoGlow - yScale: 0.94 + 0.06 * bannerConfig.logoGlow + // The logo's gradient plate, drawn behind the mark: a linear + // AccentSoft (light) -> AccentStrong (dark) sheet along the same + // top-left -> bottom-right userSpaceOnUse axis the baked-in SVG used, + // clipped to the bird silhouette via uSilhouette. The white highlight + // path above is theme independent. Sized to the logo itself — no + // rounded badge, matching home_widget's QPainter composite. Static. + // Small ShaderEffect, Qt 6.4-safe. + ShaderEffect { + id: brandPlate + anchors.fill: parent + property vector4d uStrong: Qt.vector4d(bannerConfig.brandStrong.r, bannerConfig.brandStrong.g, + bannerConfig.brandStrong.b, 1.0) + property vector4d uSoft: Qt.vector4d(bannerConfig.brandSoft.r, bannerConfig.brandSoft.g, + bannerConfig.brandSoft.b, 1.0) + property var uSilhouette: silhouetteMask + fragmentShader: "qrc:/onboarding/shaders/brand_plate.frag.qsb" + } + + Image { + id: logoImage + anchors.fill: parent + source: "qrc:/resources/cockatrice-logo-white.svg" + sourceSize: Qt.size(256, 256) + fillMode: Image.PreserveAspectFit + smooth: true } } -} +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/onboarding/shader_banner_widget.cpp b/cockatrice/src/interface/widgets/onboarding/shader_banner_widget.cpp index 7db651c45..0b9f65783 100644 --- a/cockatrice/src/interface/widgets/onboarding/shader_banner_widget.cpp +++ b/cockatrice/src/interface/widgets/onboarding/shader_banner_widget.cpp @@ -27,54 +27,84 @@ struct SuggestedColors QColor colorB; QColor accent; QColor glowColor; + QColor brandStrong; + QColor brandSoft; 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}; + // 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 Highlight hue instead of a plain near-white copy: a gentle mint - // wash that clearly belongs to the theme. + // 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 = highlight.hslHue(); + const int hue = accentStrong.hslHue(); // Achromatic accents (grey) get a neutral near-white stage instead. - const int stageSat = hue < 0 ? 0 : 35; + 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); }; - // 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}; + 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. White glow and the original strong vignette stay untouched. + // 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); }; - return {stage(29), stage(16), QColor(highlight), QColor(Qt::white), 0.62, false}; + 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 @@ -273,8 +303,9 @@ void BannerHost::applyThemeColors() config->setColorB(bannerColorB); config->setAccent(bannerAccent); config->setGlowColor(colors.glowColor); + config->setBrandStrong(colors.brandStrong); + config->setBrandSoft(colors.brandSoft); config->setVignetteMin(colors.vignetteMin); - config->setLogoDark(colors.lightStage); } } diff --git a/cockatrice/src/interface/widgets/onboarding/shaders/brand_banner.frag b/cockatrice/src/interface/widgets/onboarding/shaders/brand_banner.frag index cea9101a8..2bf6d0abf 100644 --- a/cockatrice/src/interface/widgets/onboarding/shaders/brand_banner.frag +++ b/cockatrice/src/interface/widgets/onboarding/shaders/brand_banner.frag @@ -121,7 +121,7 @@ vec3 backgroundField(vec2 uv, float time) // Accent-coloured fog layer: flowNoise peaks above 0.6 contribute accent float fog = flowNoise(uv * 0.8 + vec2(100.0, 50.0), time * 0.02); - col += uAccent.rgb * max(fog - 0.6, 0.0) * 0.10; + col += uAccent.rgb * max(fog - 0.6, 0.0) * 0.14; return col; } @@ -138,10 +138,10 @@ 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. 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. + // Centre bloom at logo position; intensity scales with uLogoGlow. The + // QML brandGlow halo now supplies the primary logo surround (the two + // brand appColors), so this shader bloom is deliberately kept as a subtle + // ambience rather than a competing glow. float centreLight = bloom(cDist, 0.08 * asp, 0.40 * asp); col += uGlowColor.rgb * centreLight * 0.20 * uLogoGlow; @@ -167,8 +167,8 @@ vec3 motifWelcome(vec2 uv, vec3 bg, float t) float pX = baseX * asp + sin(t * driftFreq + fi * 1.7) * driftAmp * asp; float pY = fract(baseY + t * riseSpeed); - float size = 0.006 + hash21(vec2(fi * 2.9, uSeed * 4.7)) * 0.012; - float bright = 0.15 + hash21(vec2(fi * 6.1, uSeed * 0.9)) * 0.30; + float size = 0.010 + hash21(vec2(fi * 2.9, uSeed * 4.7)) * 0.014; + float bright = 0.18 + hash21(vec2(fi * 6.1, uSeed * 0.9)) * 0.32; // Fade out near top/bottom edges float edgeFade = smoothstep(0.0, 0.12, pY) * smoothstep(1.0, 0.88, pY); @@ -237,7 +237,7 @@ vec3 motifCardDatabase(vec2 uv, vec3 bg, float t) // Semi-transparent dark fill float fill = smoothstep(0.015, -0.005, d); - col = mix(col, uColorB.rgb * 0.55, fill * 0.50); + col = mix(col, uColorB.rgb * 0.60, fill * 0.62); // Accent outline float edge = smoothstep(0.035, 0.0, abs(d)); @@ -314,7 +314,7 @@ vec3 motifAccount(vec2 uv, vec3 bg, float t) // Node glow via bloom; intensity modulated by pulse float dist = length(ac - pos); - col += uAccent.rgb * bloom(dist, 0.018, 0.08) * mix(0.20, 0.45, pulse); + col += uAccent.rgb * bloom(dist, 0.018, 0.08) * mix(0.30, 0.55, pulse); } // Edges: connect nodes within a radius threshold @@ -328,19 +328,19 @@ vec3 motifAccount(vec2 uv, vec3 bg, float t) vec2 ba = nodePos[j] - nodePos[i]; float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0); float lineDist = length(pa - ba * h); - col += uAccent.rgb * smoothstep(0.010, 0.0, lineDist) * strength * 0.10; + col += uAccent.rgb * smoothstep(0.010, 0.0, lineDist) * strength * 0.14; } } } // Central bloom at banner centre float cDist = length(ac - center); - col += uAccent.rgb * bloom(cDist, 0.04, 0.25) * 0.12; + col += uAccent.rgb * bloom(cDist, 0.04, 0.25) * 0.20; // Periodic expanding ring from centre float ripplePhase = t * 0.4; float rippleDist = abs(cDist - fract(ripplePhase) * asp * 0.7); - col += uAccent.rgb * smoothstep(0.02, 0.0, rippleDist) * 0.10; + col += uAccent.rgb * smoothstep(0.02, 0.0, rippleDist) * 0.14; return col; } diff --git a/cockatrice/src/interface/widgets/onboarding/shaders/brand_plate.frag b/cockatrice/src/interface/widgets/onboarding/shaders/brand_plate.frag new file mode 100644 index 000000000..43b798c9f --- /dev/null +++ b/cockatrice/src/interface/widgets/onboarding/shaders/brand_plate.frag @@ -0,0 +1,42 @@ +#version 440 + +// The logo's gradient plate, drawn by us rather than the baked-in SVG: a +// linear blend between the two brand appColors (light AccentSoft at the +// top-left grading to dark AccentStrong at the bottom-right, mirroring +// cockatrice.svg's linearGradient4265-7-8 userSpaceOnUse axis), clipped to +// the bird's full silhouette via the full-color logo's alpha (uSilhouette). +// The white highlight path (cockatrice-logo-white) is overlaid in QML on top, +// exactly as the SVG stacks its white path over the gradient paths. Fully +// static: no glow, no breathing — the plate just sits there like the home +// widget's QPainter composite. + +layout(location = 0) in vec2 qt_TexCoord0; +layout(location = 0) out vec4 fragColor; + +layout(std140, binding = 0) uniform buf +{ + mat4 qt_Matrix; + float qt_Opacity; + vec4 uStrong; + vec4 uSoft; +}; + +// The full-color logo's alpha channel acts as the silhouette mask: the +// gradient only appears inside the bird, exactly like the SVG's gradient paths. +layout(binding = 1) uniform sampler2D uSilhouette; + +void main() +{ + // Recreate cockatrice.svg's own gradient geometry (linearGradient4265-7-8, + // userSpaceOnUse): light AccentSoft at the start point S=(-8.097,-97.746), + // dark AccentStrong at the end E=(162.455,295.208), on the SVG's 300x300 + // canvas. Normalized to UV space, V=E-S=(0.5685,1.3098), so + // t = dot(uv - S_norm, V)/|V|^2 with S_norm=(-0.0270,-0.3258). + float t = clamp(dot(qt_TexCoord0 - vec2(-0.02699, -0.32582), vec2(0.56851, 1.30985)) / 2.03891, 0.0, 1.0); + vec3 color = mix(uSoft.rgb, uStrong.rgb, t); + + // Anti-aliased silhouette clip from the full-color logo's alpha. + float alpha = texture(uSilhouette, qt_TexCoord0).a; + + fragColor = vec4(color * alpha, alpha) * qt_Opacity; +} \ No newline at end of file