From bf159672c2b72525923ce28711e94de6bfb79d2c Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Fri, 21 Aug 2026 08:11:30 +0300 Subject: [PATCH] Added particle effects for premium promo pages. --- .../Resources/icons/premium/adsbubble.svg | 1 + Telegram/Resources/icons/premium/bold.svg | 7 + Telegram/Resources/icons/premium/bubble.svg | 1 + Telegram/Resources/icons/premium/check.svg | 1 + Telegram/Resources/icons/premium/code.svg | 7 + Telegram/Resources/icons/premium/folder.svg | 1 + Telegram/Resources/icons/premium/like.svg | 1 + Telegram/Resources/icons/premium/list.svg | 9 + Telegram/Resources/icons/premium/math.svg | 7 + Telegram/Resources/icons/premium/noads.svg | 1 + Telegram/Resources/icons/premium/settings.svg | 1 + Telegram/Resources/icons/premium/smile1.svg | 1 + Telegram/Resources/icons/premium/smile2.svg | 1 + Telegram/Resources/icons/premium/star.svg | 1 + .../Resources/icons/premium/superscript.svg | 7 + Telegram/Resources/icons/premium/table.svg | 7 + Telegram/Resources/icons/premium/tag.svg | 1 + Telegram/Resources/icons/premium/user.svg | 1 + Telegram/Resources/icons/premium/video.svg | 1 + Telegram/Resources/icons/premium/video2.svg | 1 + Telegram/Resources/qrc/telegram/telegram.qrc | 20 + Telegram/SourceFiles/ui/effects/premium.style | 14 + .../ui/effects/premium_promo_particles.cpp | 832 ++++++++++++++++++ .../ui/effects/premium_promo_particles.h | 41 + Telegram/cmake/td_ui.cmake | 2 + 25 files changed, 967 insertions(+) create mode 100644 Telegram/Resources/icons/premium/adsbubble.svg create mode 100644 Telegram/Resources/icons/premium/bold.svg create mode 100644 Telegram/Resources/icons/premium/bubble.svg create mode 100644 Telegram/Resources/icons/premium/check.svg create mode 100644 Telegram/Resources/icons/premium/code.svg create mode 100644 Telegram/Resources/icons/premium/folder.svg create mode 100644 Telegram/Resources/icons/premium/like.svg create mode 100644 Telegram/Resources/icons/premium/list.svg create mode 100644 Telegram/Resources/icons/premium/math.svg create mode 100644 Telegram/Resources/icons/premium/noads.svg create mode 100644 Telegram/Resources/icons/premium/settings.svg create mode 100644 Telegram/Resources/icons/premium/smile1.svg create mode 100644 Telegram/Resources/icons/premium/smile2.svg create mode 100644 Telegram/Resources/icons/premium/star.svg create mode 100644 Telegram/Resources/icons/premium/superscript.svg create mode 100644 Telegram/Resources/icons/premium/table.svg create mode 100644 Telegram/Resources/icons/premium/tag.svg create mode 100644 Telegram/Resources/icons/premium/user.svg create mode 100644 Telegram/Resources/icons/premium/video.svg create mode 100644 Telegram/Resources/icons/premium/video2.svg create mode 100644 Telegram/SourceFiles/ui/effects/premium_promo_particles.cpp create mode 100644 Telegram/SourceFiles/ui/effects/premium_promo_particles.h diff --git a/Telegram/Resources/icons/premium/adsbubble.svg b/Telegram/Resources/icons/premium/adsbubble.svg new file mode 100644 index 0000000000..757e6e24c9 --- /dev/null +++ b/Telegram/Resources/icons/premium/adsbubble.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Telegram/Resources/icons/premium/bold.svg b/Telegram/Resources/icons/premium/bold.svg new file mode 100644 index 0000000000..71ac027fb2 --- /dev/null +++ b/Telegram/Resources/icons/premium/bold.svg @@ -0,0 +1,7 @@ + + + Filled / Page / page_bold + + + + \ No newline at end of file diff --git a/Telegram/Resources/icons/premium/bubble.svg b/Telegram/Resources/icons/premium/bubble.svg new file mode 100644 index 0000000000..c16b6f9ad5 --- /dev/null +++ b/Telegram/Resources/icons/premium/bubble.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Telegram/Resources/icons/premium/check.svg b/Telegram/Resources/icons/premium/check.svg new file mode 100644 index 0000000000..ca3605faa2 --- /dev/null +++ b/Telegram/Resources/icons/premium/check.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Telegram/Resources/icons/premium/code.svg b/Telegram/Resources/icons/premium/code.svg new file mode 100644 index 0000000000..026f4760bc --- /dev/null +++ b/Telegram/Resources/icons/premium/code.svg @@ -0,0 +1,7 @@ + + + Filled / Page / page_code + + + + \ No newline at end of file diff --git a/Telegram/Resources/icons/premium/folder.svg b/Telegram/Resources/icons/premium/folder.svg new file mode 100644 index 0000000000..c8f6cff694 --- /dev/null +++ b/Telegram/Resources/icons/premium/folder.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Telegram/Resources/icons/premium/like.svg b/Telegram/Resources/icons/premium/like.svg new file mode 100644 index 0000000000..89a4957937 --- /dev/null +++ b/Telegram/Resources/icons/premium/like.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Telegram/Resources/icons/premium/list.svg b/Telegram/Resources/icons/premium/list.svg new file mode 100644 index 0000000000..5a3dc72600 --- /dev/null +++ b/Telegram/Resources/icons/premium/list.svg @@ -0,0 +1,9 @@ + + + Filled / Page / page_numbered + + + + + + \ No newline at end of file diff --git a/Telegram/Resources/icons/premium/math.svg b/Telegram/Resources/icons/premium/math.svg new file mode 100644 index 0000000000..18bcf8a4cf --- /dev/null +++ b/Telegram/Resources/icons/premium/math.svg @@ -0,0 +1,7 @@ + + + Filled / Page / page_equation + + + + \ No newline at end of file diff --git a/Telegram/Resources/icons/premium/noads.svg b/Telegram/Resources/icons/premium/noads.svg new file mode 100644 index 0000000000..3800563d27 --- /dev/null +++ b/Telegram/Resources/icons/premium/noads.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Telegram/Resources/icons/premium/settings.svg b/Telegram/Resources/icons/premium/settings.svg new file mode 100644 index 0000000000..38d7940870 --- /dev/null +++ b/Telegram/Resources/icons/premium/settings.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Telegram/Resources/icons/premium/smile1.svg b/Telegram/Resources/icons/premium/smile1.svg new file mode 100644 index 0000000000..759600fcca --- /dev/null +++ b/Telegram/Resources/icons/premium/smile1.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Telegram/Resources/icons/premium/smile2.svg b/Telegram/Resources/icons/premium/smile2.svg new file mode 100644 index 0000000000..467d5e5c50 --- /dev/null +++ b/Telegram/Resources/icons/premium/smile2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Telegram/Resources/icons/premium/star.svg b/Telegram/Resources/icons/premium/star.svg new file mode 100644 index 0000000000..d643ed7ee9 --- /dev/null +++ b/Telegram/Resources/icons/premium/star.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Telegram/Resources/icons/premium/superscript.svg b/Telegram/Resources/icons/premium/superscript.svg new file mode 100644 index 0000000000..d0b1d977a5 --- /dev/null +++ b/Telegram/Resources/icons/premium/superscript.svg @@ -0,0 +1,7 @@ + + + Filled / Page / page_superscript + + + + \ No newline at end of file diff --git a/Telegram/Resources/icons/premium/table.svg b/Telegram/Resources/icons/premium/table.svg new file mode 100644 index 0000000000..63f8fa4a1c --- /dev/null +++ b/Telegram/Resources/icons/premium/table.svg @@ -0,0 +1,7 @@ + + + Filled / Page / page_table + + + + \ No newline at end of file diff --git a/Telegram/Resources/icons/premium/tag.svg b/Telegram/Resources/icons/premium/tag.svg new file mode 100644 index 0000000000..4791085cc3 --- /dev/null +++ b/Telegram/Resources/icons/premium/tag.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Telegram/Resources/icons/premium/user.svg b/Telegram/Resources/icons/premium/user.svg new file mode 100644 index 0000000000..35815b81f9 --- /dev/null +++ b/Telegram/Resources/icons/premium/user.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Telegram/Resources/icons/premium/video.svg b/Telegram/Resources/icons/premium/video.svg new file mode 100644 index 0000000000..02788670f8 --- /dev/null +++ b/Telegram/Resources/icons/premium/video.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Telegram/Resources/icons/premium/video2.svg b/Telegram/Resources/icons/premium/video2.svg new file mode 100644 index 0000000000..16366077c7 --- /dev/null +++ b/Telegram/Resources/icons/premium/video2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Telegram/Resources/qrc/telegram/telegram.qrc b/Telegram/Resources/qrc/telegram/telegram.qrc index 2aac736dbc..6c966a19f3 100644 --- a/Telegram/Resources/qrc/telegram/telegram.qrc +++ b/Telegram/Resources/qrc/telegram/telegram.qrc @@ -28,6 +28,26 @@ ../../icons/settings/star.svg ../../icons/settings/starmini.svg ../../icons/settings/premium_dollar.svg + ../../icons/premium/folder.svg + ../../icons/premium/bubble.svg + ../../icons/premium/settings.svg + ../../icons/premium/smile1.svg + ../../icons/premium/smile2.svg + ../../icons/premium/like.svg + ../../icons/premium/adsbubble.svg + ../../icons/premium/noads.svg + ../../icons/premium/video.svg + ../../icons/premium/video2.svg + ../../icons/premium/user.svg + ../../icons/premium/tag.svg + ../../icons/premium/check.svg + ../../icons/premium/star.svg + ../../icons/premium/list.svg + ../../icons/premium/math.svg + ../../icons/premium/table.svg + ../../icons/premium/superscript.svg + ../../icons/premium/bold.svg + ../../icons/premium/code.svg ../../icons/tray_monochrome.svg ../../icons/tray_monochrome_attention.svg ../../icons/tray_monochrome_mute.svg diff --git a/Telegram/SourceFiles/ui/effects/premium.style b/Telegram/SourceFiles/ui/effects/premium.style index c4427b56e4..53784f0e65 100644 --- a/Telegram/SourceFiles/ui/effects/premium.style +++ b/Telegram/SourceFiles/ui/effects/premium.style @@ -129,6 +129,20 @@ premiumDotPadding: margins(4px, 4px, 4px, 4px); premiumDotsMargin: margins(0px, 5px, 0px, 6px); premiumVideoWidth: 182px; +premiumPromoParticlesInset: 30px; +premiumPromoParticlesDeviceSkip: 10px; +premiumPromoIconSmall: 14px; +premiumPromoIconMedium: 15px; +premiumPromoIconLarge: 16px; +premiumPromoIconWide: 18px; +premiumPromoStarSmall: 4px; +premiumPromoStarMedium: 10px; +premiumPromoStarLarge: 12px; +premiumPromoMatrixCell: 16px; +premiumPromoSpeedInset: 100px; +premiumPromoSpeedLine: 30px; +premiumPromoHelloFont: font(24px bold); + // Graphics. defaultPremiumBubble: PremiumBubble { diff --git a/Telegram/SourceFiles/ui/effects/premium_promo_particles.cpp b/Telegram/SourceFiles/ui/effects/premium_promo_particles.cpp new file mode 100644 index 0000000000..bd6c34d78b --- /dev/null +++ b/Telegram/SourceFiles/ui/effects/premium_promo_particles.cpp @@ -0,0 +1,832 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "ui/effects/premium_promo_particles.h" + +#include "ui/effects/animation_value.h" +#include "ui/effects/drifting_particles.h" +#include "ui/painter.h" +#include "ui/rect.h" +#include "styles/style_premium.h" + +#include +#include + +namespace Ui::Premium { +namespace { + +constexpr auto kBrightAlpha = 90; +constexpr auto kIconAlpha = 30; +constexpr auto kBrightParticleAlpha = 255; +constexpr auto kIconParticleAlpha = 120; + +constexpr auto kMinLifeTime = crl::time(2000); +constexpr auto kRandLifeTime = 1000; +constexpr auto kSpeedDivider = 660.; +constexpr auto kSpeedBase = 4.; +constexpr auto kMinDelta = crl::time(4); +constexpr auto kMaxDelta = crl::time(50); +constexpr auto kDistributionTries = 10; + +constexpr auto kIconParticlesCount = 40; +constexpr auto kStarParticlesCount = 400; +constexpr auto kSpeedLinesCount = 200; +constexpr auto kHelloWordsCount = 25; + +constexpr auto kStarsRectFactor = 0.4; +constexpr auto kStarsCornerFactor = 0.98; +constexpr auto kStarsSpeedScale = 4.; +constexpr auto kStarsBlurRadius = 2; + +constexpr auto kMatrixGlyphsCount = 16; +constexpr auto kMatrixStepTime = crl::time(50); +constexpr auto kMatrixMinLength = 4; +constexpr auto kMatrixRandLength = 6; +constexpr auto kMatrixSpawnChance = 4; +constexpr auto kMatrixMinAlpha = 0.2; +constexpr auto kMatrixSwapTime = crl::time(150); +constexpr auto kMatrixRandSwapTime = 300; + +constexpr auto kSpeedLineAlpha = 80; +constexpr auto kSpeedLineScale = 150.; +constexpr auto kSpeedLineMinScale = 0.02; + +constexpr auto kHelloMinDuration = crl::time(2250); +constexpr auto kHelloRandDuration = 2250; +constexpr auto kHelloMinScale = 0.6; +constexpr auto kHelloRandScale = 0.45; +constexpr auto kHelloLongScale = 0.6; +constexpr auto kHelloMediumScale = 0.75; +constexpr auto kHelloLongLength = 7; +constexpr auto kHelloMediumLength = 5; +constexpr auto kVerticalFactor = 0.1; +constexpr auto kHelloOversample = 2; + +constexpr auto kSpeedTimestamps = std::array{ + kSpeedLineMinScale, 1., 1., 1., 1., 1., 1., 1., 1., kSpeedLineMinScale, +}; + +[[nodiscard]] const std::vector &HelloWords() { + static const auto result = std::vector{ + u"Hello"_q, u"Привіт"_q, u"Привет"_q, u"Bonjour"_q, u"Hola"_q, + u"Ciao"_q, u"Olá"_q, u"여보세요"_q, u"你好"_q, u"Salve"_q, + u"Sveiki"_q, u"Halo"_q, u"გამარჯობა"_q, u"Hallå"_q, u"Salam"_q, + u"Tere"_q, u"Dia dhuit"_q, u"こんにちは"_q, u"Сайн уу"_q, + u"Bongu"_q, u"Ahoj"_q, u"γεια"_q, u"Zdravo"_q, u"नमस्ते"_q, + u"Habari"_q, u"Hallo"_q, u"ជំរាបសួរ"_q, u"مرحبًا"_q, u"ನಮಸ್ಕಾರ"_q, + u"Салам"_q, u"Silav li wir"_q, u"سڵاو"_q, u"Kif inti"_q, + u"Talofa"_q, u"Thobela"_q, u"हॅलो"_q, u"ሰላም"_q, u"Здраво"_q, + u"ഹലോ"_q, u"ہیلو"_q, u"ꯍꯦꯜꯂꯣ"_q, u"Alô"_q, u"வணக்கம்"_q, + u"Mhoro"_q, u"Moni"_q, u"Alo"_q, u"สวัสดี"_q, u"Salom"_q, + u"Բարեւ"_q, + }; + return result; +} + +[[nodiscard]] QImage CreateFrame(int size) { + const auto ratio = style::DevicePixelRatio(); + auto result = QImage( + Size(size) * ratio, + QImage::Format_ARGB32_Premultiplied); + result.setDevicePixelRatio(ratio); + result.fill(Qt::transparent); + return result; +} + +[[nodiscard]] QImage RenderIconSprite( + const QString &path, + int size, + QColor color) { + auto result = CreateFrame(size); + const auto full = Rect(Size(float64(size))); + auto renderer = QSvgRenderer(path); + auto p = QPainter(&result); + renderer.render(&p, full); + p.setCompositionMode(QPainter::CompositionMode_SourceIn); + p.fillRect(full, color); + p.end(); + return result; +} + +struct SpriteConfig { + QString path; + int size = 0; + bool randomRotate = false; + bool bright = false; +}; + +[[nodiscard]] DriftingSprite MakeSprite( + const SpriteConfig &sprite, + float64 corner, + bool roundEffect, + bool blur) { + const auto color = anim::with_alpha( + Qt::white, + (sprite.bright ? kBrightAlpha : kIconAlpha) / 255.); + return { + .image = (sprite.path.isEmpty() + ? FourPointStarImage({ + .size = sprite.size, + .corner = corner, + .cornerRadius = roundEffect ? (sprite.size / 5.) : 0., + .blurRadius = blur ? kStarsBlurRadius : 0, + .color = color, + }) + : RenderIconSprite(sprite.path, sprite.size, color)), + .maxAlpha = (sprite.bright + ? kBrightParticleAlpha + : kIconParticleAlpha), + .randomRotate = sprite.randomRotate, + }; +} + +class StarParticles final : public PromoParticlesPainter { +public: + struct Config { + std::vector sprites; + int count = kIconParticlesCount; + float64 corner = kStarsCornerFactor; + float64 speedScale = kStarsSpeedScale; + bool orbit = false; + bool blur = false; + bool roundEffect = true; + bool checkBounds = false; + bool aroundDevice = true; + }; + + explicit StarParticles(Config config); + + void setGeometry(QRect outer, QRect device) override; + void paint(QPainter &p) override; + +private: + const bool _aroundDevice = false; + + DriftingParticles _particles; + +}; + +StarParticles::StarParticles(Config config) +: _aroundDevice(config.aroundDevice) +, _particles([&] { + auto sprites = std::vector(); + sprites.reserve(config.sprites.size()); + for (const auto &sprite : config.sprites) { + sprites.push_back(MakeSprite( + sprite, + config.corner, + config.roundEffect, + config.blur)); + } + return DriftingParticles::Config{ + .sprites = std::move(sprites), + .count = config.count, + .speed = style::ConvertScaleExact(kSpeedBase) * config.speedScale, + .orbit = config.orbit, + .checkBounds = config.checkBounds, + .spawnInCircle = config.aroundDevice, + .spread = !config.aroundDevice, + }; +}()) { +} + +void StarParticles::setGeometry(QRect outer, QRect device) { + const auto field = [&] { + if (!_aroundDevice) { + const auto inset = float64(st::premiumPromoParticlesInset); + return QRectF(outer).marginsRemoved(Margins(inset)); + } + const auto half = base::SafeRound(device.width() * kStarsRectFactor); + const auto center = rect::center(device); + return Rect(center.x() - half, center.y() - half, Size(half * 2.)); + }(); + const auto skip = float64(st::premiumPromoParticlesDeviceSkip); + _particles.setGeometry( + field, + outer, + QRectF(device).marginsRemoved(Margins(skip))); +} + +void StarParticles::paint(QPainter &p) { + _particles.paint(p); +} + + +class MatrixParticles final : public PromoParticlesPainter { +public: + MatrixParticles(); + + void setGeometry(QRect outer, QRect device) override; + void paint(QPainter &p) override; + +private: + struct Drop { + int y = 0; + int length = 0; + crl::time time = 0; + }; + struct Glyph { + int index = 0; + int nextIndex = 0; + crl::time nextTime = 0; + }; + + void resetDrop(Drop &drop, crl::time now); + void resetGlyph(Glyph &glyph, crl::time now); + void paintGlyph( + QPainter &p, + Glyph &glyph, + QPointF position, + crl::time now, + float64 alpha); + + const int _cell = 0; + + std::vector _glyphs; + std::vector> _columns; + std::vector> _cells; + + QRect _outer; + QRectF _exclude; + + ParticlesRandom _random; + +}; + +MatrixParticles::MatrixParticles() +: _cell(st::premiumPromoMatrixCell) { + const auto font = style::font( + _cell, + style::FontFlag::Bold, + st::normalFont->family()); + _glyphs.reserve(kMatrixGlyphsCount); + for (auto i = 0; i != kMatrixGlyphsCount; ++i) { + const auto text = QString( + QChar((i < 10) ? ('0' + i) : ('A' + (i - 10)))); + auto image = CreateFrame(_cell); + auto p = QPainter(&image); + p.setFont(font); + p.setPen(anim::with_alpha(Qt::white, kIconAlpha / 255.)); + p.drawText( + QPointF((_cell - font->width(text)) / 2., _cell), + text); + p.end(); + _glyphs.push_back(std::move(image)); + } +} + +void MatrixParticles::setGeometry(QRect outer, QRect device) { + _outer = outer; + const auto skip = float64(st::premiumPromoMatrixCell); + _exclude = QRectF(device).marginsRemoved(Margins(skip)); + + const auto now = crl::now(); + const auto nx = (_cell > 0) ? (outer.width() / _cell) : 0; + const auto ny = (_cell > 0) ? (outer.height() / _cell) : 0; + if (nx <= 0 || ny <= 0) { + _columns.clear(); + _cells.clear(); + return; + } + _columns.assign(nx + 1, std::vector()); + _cells.assign(nx + 1, std::vector(ny + 1)); + for (auto x = 0; x != nx + 1; ++x) { + auto drop = Drop(); + resetDrop(drop, now); + drop.y = _random.index(ny); + _columns[x].push_back(drop); + for (auto &glyph : _cells[x]) { + resetGlyph(glyph, now); + glyph.index = _random.index(kMatrixGlyphsCount); + } + } +} + +void MatrixParticles::resetDrop(Drop &drop, crl::time now) { + drop.y = 0; + drop.time = now; + drop.length = kMatrixMinLength + _random.index(kMatrixRandLength); +} + +void MatrixParticles::resetGlyph(Glyph &glyph, crl::time now) { + glyph.index = glyph.nextIndex; + glyph.nextIndex = _random.index(kMatrixGlyphsCount); + glyph.nextTime = now + kMatrixSwapTime + _random.index( + kMatrixRandSwapTime); +} + +void MatrixParticles::paintGlyph( + QPainter &p, + Glyph &glyph, + QPointF position, + crl::time now, + float64 alpha) { + const auto left = glyph.nextTime - now; + if (left >= kMatrixSwapTime) { + p.setOpacity(alpha); + p.drawImage(position, _glyphs[glyph.index]); + return; + } + const auto progress = std::clamp( + 1. - (left / float64(kMatrixSwapTime)), + 0., + 1.); + p.setOpacity(alpha * (1. - progress)); + p.drawImage(position, _glyphs[glyph.index]); + p.setOpacity(alpha * progress); + p.drawImage(position, _glyphs[glyph.nextIndex]); + if (progress >= 1.) { + resetGlyph(glyph, now); + } +} + +void MatrixParticles::paint(QPainter &p) { + if (_columns.empty()) { + return; + } + const auto now = crl::now(); + const auto nx = int(_columns.size()) - 1; + const auto ny = int(_cells.front().size()) - 1; + const auto opacity = p.opacity(); + for (auto x = 0; x != nx + 1; ++x) { + auto &drops = _columns[x]; + for (auto i = 0; i != int(drops.size()); ++i) { + auto &drop = drops[i]; + if (now - drop.time > kMatrixStepTime) { + ++drop.y; + drop.time = now; + if (drop.y - drop.length >= ny) { + if (drops.size() == 1) { + resetDrop(drop, now); + } else { + drops.erase(drops.begin() + i); + --i; + continue; + } + } + if (drop.y > drop.length + && i == int(drops.size()) - 1 + && _random.chance(kMatrixSpawnChance)) { + auto added = Drop(); + resetDrop(added, now); + + drops.push_back(added); + } + } + const auto bottom = drops[i].y; + const auto length = drops[i].length; + const auto till = std::min(bottom, ny + 1); + for (auto y = std::max(0, bottom - length); y < till; ++y) { + const auto position = QPointF(_cell * x, _cell * y); + if (_exclude.contains(position)) { + continue; + } + const auto alpha = std::clamp( + kMatrixMinAlpha + + (1. - kMatrixMinAlpha) + * (1. - (bottom - y) / float64(length - 1)), + 0., + 1.); + paintGlyph(p, _cells[x][y], position, now, opacity * alpha); + } + } + } + p.setOpacity(opacity); +} + +class SpeedLineParticles final : public PromoParticlesPainter { +public: + SpeedLineParticles() = default; + + void setGeometry(QRect outer, QRect device) override; + void setVideoProgress(float64 progress) override; + void paint(QPainter &p) override; + +private: + struct Particle { + crl::time lifeTime = 0; + float64 x = 0.; + float64 y = 0.; + float64 vecX = 0.; + float64 vecY = 0.; + }; + + void generate(Particle &particle, crl::time now, bool reset); + + std::vector _particles = std::vector( + kSpeedLinesCount); + std::vector _lines; + + QRectF _rect; + QRectF _screen; + crl::time _lastTime = 0; + float64 _speedScale = kSpeedLineScale; + + ParticlesRandom _random; + +}; + +void SpeedLineParticles::setGeometry(QRect outer, QRect device) { + const auto inset = float64(st::premiumPromoSpeedInset); + _screen = outer; + _rect = QRectF(outer).marginsRemoved( + Margins(inset) + ).translated(0, outer.height() * kVerticalFactor); + + const auto now = crl::now(); + for (auto &particle : _particles) { + generate(particle, now, true); + } +} + +void SpeedLineParticles::setVideoProgress(float64 progress) { + const auto count = int(kSpeedTimestamps.size()); + const auto step = 1. / (count - 1); + const auto clamped = std::clamp(progress, 0., 1.); + const auto from = std::min(int(clamped / step), count - 1); + const auto till = from + 1; + const auto inner = (clamped - from * step) / step; + const auto video = (till < count) + ? (kSpeedTimestamps[from] * (1. - inner) + + kSpeedTimestamps[till] * inner) + : kSpeedTimestamps[from]; + _speedScale = kSpeedLineScale * video; +} + +void SpeedLineParticles::generate( + Particle &particle, + crl::time now, + bool reset) { + particle.lifeTime = now + kMinLifeTime + _random.index(kRandLifeTime); + const auto &from = reset ? _screen : _rect; + particle.x = _random.value(from.left(), from.right()); + particle.y = _random.value(from.top(), from.bottom()); + + const auto center = rect::center(_rect); + const auto angle = std::atan2( + particle.x - center.x(), + particle.y - center.y()); + particle.vecX = std::sin(angle); + particle.vecY = std::cos(angle); +} + +void SpeedLineParticles::paint(QPainter &p) { + if (_rect.isEmpty()) { + return; + } + const auto now = crl::now(); + const auto delta = float64( + std::clamp(now - _lastTime, kMinDelta, kMaxDelta)); + _lastTime = now; + + const auto speed = style::ConvertScaleExact(kSpeedBase) + * (delta / kSpeedDivider) + * _speedScale; + const auto length = float64(st::premiumPromoSpeedLine); + _lines.clear(); + _lines.reserve(_particles.size()); + for (auto &particle : _particles) { + _lines.push_back(QLineF( + particle.x, + particle.y, + particle.x + length * particle.vecX, + particle.y + length * particle.vecY)); + particle.x += particle.vecX * speed; + particle.y += particle.vecY * speed; + if (now > particle.lifeTime + || !_screen.contains(particle.x, particle.y)) { + generate(particle, now, false); + } + } + auto hq = PainterHighQualityEnabler(p); + p.setPen(anim::with_alpha(Qt::white, kSpeedLineAlpha / 255.)); + p.drawLines(_lines.data(), int(_lines.size())); +} + +class HelloParticles final : public PromoParticlesPainter { +public: + HelloParticles() = default; + + void setGeometry(QRect outer, QRect device) override; + void paint(QPainter &p) override; + +private: + struct Particle { + QString word; + float64 x = 0.; + float64 y = 0.; + float64 scale = 1.; + crl::time bornTime = 0; + crl::time duration = 0; + int alpha = 0; + bool set = false; + }; + + void generate(Particle &particle, int index, crl::time now, bool reset); + [[nodiscard]] const QImage &lookupWord(const QString &word); + [[nodiscard]] QSizeF wordSize(const QString &word); + + std::vector _particles = std::vector( + kHelloWordsCount); + base::flat_map _words; + + QRectF _rect; + + ParticlesRandom _random; + +}; + +const QImage &HelloParticles::lookupWord(const QString &word) { + const auto i = _words.find(word); + if (i != end(_words)) { + return i->second; + } + const auto &base = st::premiumPromoHelloFont; + const auto font = style::font( + base->size() * kHelloOversample, + base->flags(), + base->family()); + const auto width = std::max(font->width(word), 1); + const auto height = std::max(font->height, 1); + auto image = QImage( + QSize(width, height) * style::DevicePixelRatio(), + QImage::Format_ARGB32_Premultiplied); + image.setDevicePixelRatio(style::DevicePixelRatio() * kHelloOversample); + image.fill(Qt::transparent); + auto p = QPainter(&image); + auto hq = PainterHighQualityEnabler(p); + + p.scale(1. / kHelloOversample, 1. / kHelloOversample); + p.setFont(font); + p.setPen(Qt::white); + p.drawText(QPointF(0, font->ascent), word); + p.end(); + return _words.emplace(word, std::move(image)).first->second; +} + +QSizeF HelloParticles::wordSize(const QString &word) { + const auto &image = lookupWord(word); + return QSizeF(image.size()) / image.devicePixelRatio(); +} + +void HelloParticles::setGeometry(QRect outer, QRect device) { + const auto inset = outer.height() * kVerticalFactor; + _rect = QRectF(outer).marginsRemoved(QMarginsF(0, inset, 0, inset)); + + const auto now = crl::now(); + auto index = 0; + for (auto &particle : _particles) { + generate(particle, index++, now, true); + } +} + +void HelloParticles::generate( + Particle &particle, + int index, + crl::time now, + bool reset) { + const auto &words = HelloWords(); + particle.word = words[_random.index(int(words.size()))]; + particle.duration = kHelloMinDuration + _random.index( + kHelloRandDuration); + particle.scale = _random.value( + kHelloMinScale, + kHelloMinScale + kHelloRandScale); + if (particle.word.size() > kHelloLongLength) { + particle.scale *= kHelloLongScale; + } else if (particle.word.size() > kHelloMediumLength) { + particle.scale *= kHelloMediumScale; + } + const auto size = wordSize(particle.word); + + const auto center = rect::center(_rect); + auto minX = _rect.left() + size.width() / 4.; + auto maxX = rect::right(_rect) - size.width() / 4.; + if (!(index % 2)) { + maxX = center.x() - size.width() / 2.; + } else { + minX = center.x() + size.width() / 2.; + } + if (maxX <= minX) { + maxX = minX + 1.; + } + auto bestDistance = 0.; + auto best = QPointF( + _random.value(minX, maxX), + _random.value(_rect.top(), _rect.bottom())); + for (auto i = 0; i != kDistributionTries; ++i) { + const auto candidate = QPointF( + _random.value(minX, maxX), + _random.value(_rect.top(), _rect.bottom())); + auto minDistance = std::numeric_limits::max(); + for (const auto &other : _particles) { + if (!other.set) { + continue; + } + const auto width = wordSize(other.word).width() * other.scale; + const auto dx = std::min( + std::abs(other.x + width * 1.1 - candidate.x()), + std::abs(other.x - candidate.x())); + const auto dy = other.y - candidate.y(); + minDistance = std::min(minDistance, dx * dx + dy * dy); + } + if (minDistance > bestDistance) { + bestDistance = minDistance; + best = candidate; + } + } + particle.x = best.x(); + particle.y = best.y(); + particle.alpha = int(base::SafeRound( + _random.value(0.5, 1.) * kBrightParticleAlpha)); + particle.bornTime = now - (reset + ? crl::time(_random.value() * 0.9 * particle.duration) + : 0); + particle.set = true; +} + +void HelloParticles::paint(QPainter &p) { + if (_rect.isEmpty()) { + return; + } + const auto now = crl::now(); + auto hq = PainterHighQualityEnabler(p); + const auto opacity = p.opacity(); + auto index = 0; + for (auto &particle : _particles) { + const auto &image = lookupWord(particle.word); + const auto size = QSizeF(image.size()) / image.devicePixelRatio(); + const auto progress = std::clamp( + (now - particle.bornTime) / float64(particle.duration), + 0., + 1.); + const auto shift = progress - 0.5; + const auto wave = 1. - 4. * shift * shift; + const auto scale = particle.scale * (0.7 + 0.4 * wave); + p.save(); + p.translate(particle.x, particle.y); + p.scale(scale, scale); + p.setOpacity(opacity * (particle.alpha / 255.) * wave); + p.drawImage( + QRectF( + -size.width() / 2., + -size.height() / 2., + size.width(), + size.height()), + image); + p.restore(); + + if (progress >= 1.) { + generate(particle, index, now, false); + } + ++index; + } + p.setOpacity(opacity); +} + +[[nodiscard]] std::vector IconSprites( + std::vector names, + std::vector sizes, + bool randomRotate) { + Expects(names.size() == sizes.size()); + + auto result = std::vector(); + result.reserve(names.size()); + for (auto i = 0; i != int(names.size()); ++i) { + result.push_back({ + .path = u":/gui/icons/premium/%1.svg"_q.arg(names[i]), + .size = sizes[i], + .randomRotate = randomRotate, + }); + } + return result; +} + +[[nodiscard]] std::vector DefaultIconSizes() { + return { + st::premiumPromoIconSmall, + st::premiumPromoIconLarge, + st::premiumPromoIconMedium, + }; +} + +[[nodiscard]] std::vector WideIconSizes() { + return { + st::premiumPromoIconSmall, + st::premiumPromoIconWide, + st::premiumPromoIconWide, + }; +} + +[[nodiscard]] std::unique_ptr MakeIcons( + std::vector names, + std::vector sizes, + bool randomRotate = true, + bool orbit = false) { + return std::make_unique(StarParticles::Config{ + .sprites = IconSprites( + std::move(names), + std::move(sizes), + randomRotate), + .orbit = orbit, + .aroundDevice = false, + }); +} + +[[nodiscard]] std::unique_ptr MakeStars() { + return std::make_unique(StarParticles::Config{ + .sprites = { + { .size = st::premiumPromoStarSmall, .bright = true }, + { .size = st::premiumPromoStarLarge, .bright = true }, + { .size = st::premiumPromoStarMedium, .bright = true }, + }, + .count = kStarParticlesCount, + .orbit = true, + .blur = true, + .roundEffect = false, + .checkBounds = true, + }); +} + +[[nodiscard]] std::unique_ptr MakeProfileBadge() { + const auto star = u":/gui/icons/settings/star.svg"_q; + return std::make_unique(StarParticles::Config{ + .sprites = { + { .size = st::premiumPromoIconSmall, .bright = true }, + { + .path = star, + .size = st::premiumPromoIconLarge, + .randomRotate = true, + .bright = true, + }, + { + .path = star, + .size = st::premiumPromoIconMedium, + .randomRotate = true, + .bright = true, + }, + }, + .aroundDevice = false, + }); +} + +} // namespace + +std::unique_ptr MakePromoParticles( + PromoParticles type) { + switch (type) { + case PromoParticles::Stars: + return MakeStars(); + case PromoParticles::Matrix: + return std::make_unique(); + case PromoParticles::SpeedLines: + return std::make_unique(); + case PromoParticles::Hello: + return std::make_unique(); + case PromoParticles::ChatManagement: + return MakeIcons( + { u"folder"_q, u"bubble"_q, u"settings"_q }, + DefaultIconSizes()); + case PromoParticles::Emoji: + return MakeIcons( + { u"smile1"_q, u"smile2"_q, u"like"_q }, + DefaultIconSizes()); + case PromoParticles::Ads: + return MakeIcons( + { u"adsbubble"_q, u"like"_q, u"noads"_q }, + WideIconSizes()); + case PromoParticles::Userpics: + return MakeIcons( + { u"video2"_q, u"video"_q, u"user"_q }, + DefaultIconSizes()); + case PromoParticles::Tags: + return MakeIcons( + { u"tag"_q, u"check"_q, u"star"_q }, + WideIconSizes()); + case PromoParticles::Formatting: + return MakeIcons({ + u"list"_q, + u"math"_q, + u"table"_q, + u"superscript"_q, + u"bold"_q, + u"code"_q, + }, { + st::premiumPromoIconSmall, + st::premiumPromoIconWide, + st::premiumPromoIconWide, + st::premiumPromoIconWide, + st::premiumPromoIconWide, + st::premiumPromoIconWide, + }, false, true); + case PromoParticles::ProfileBadge: + return MakeProfileBadge(); + } + Unexpected("Type in Ui::Premium::MakePromoParticles."); +} + +} // namespace Ui::Premium diff --git a/Telegram/SourceFiles/ui/effects/premium_promo_particles.h b/Telegram/SourceFiles/ui/effects/premium_promo_particles.h new file mode 100644 index 0000000000..b7e6a61966 --- /dev/null +++ b/Telegram/SourceFiles/ui/effects/premium_promo_particles.h @@ -0,0 +1,41 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +namespace Ui::Premium { + +enum class PromoParticles : uchar { + Stars, + Matrix, + SpeedLines, + Hello, + ChatManagement, + Emoji, + Ads, + Userpics, + Tags, + Formatting, + ProfileBadge, +}; + +class PromoParticlesPainter { +public: + virtual ~PromoParticlesPainter() = default; + + virtual void setGeometry(QRect outer, QRect device) = 0; + + virtual void setVideoProgress(float64 progress) { + } + + virtual void paint(QPainter &p) = 0; +}; + +[[nodiscard]] std::unique_ptr MakePromoParticles( + PromoParticles type); + +} // namespace Ui::Premium diff --git a/Telegram/cmake/td_ui.cmake b/Telegram/cmake/td_ui.cmake index 450dbd1884..d33f151caa 100644 --- a/Telegram/cmake/td_ui.cmake +++ b/Telegram/cmake/td_ui.cmake @@ -561,6 +561,8 @@ PRIVATE ui/effects/premium_diamond_renderer.h ui/effects/premium_graphics.cpp ui/effects/premium_graphics.h + ui/effects/premium_promo_particles.cpp + ui/effects/premium_promo_particles.h ui/effects/premium_star.cpp ui/effects/premium_star.h ui/effects/premium_star_model.cpp