From e3517aceab2205d2cbc4bf4cd657d8f76d8c7326 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 14 Jan 2025 13:54:58 +0400 Subject: [PATCH] Improve stars in collectible emoji status. --- .../chat_helpers/emoji_list_widget.cpp | 23 +- .../ui/effects/premium_stars_colored.cpp | 271 ++++++++++++++---- 2 files changed, 221 insertions(+), 73 deletions(-) diff --git a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp index d719cc2e8..edfa0ff06 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp @@ -2387,8 +2387,6 @@ not_null EmojiListWidget::resolveCustomEmoji( EmojiStatusId id, not_null document, uint64 setId) { - Expects(document->sticker() != nullptr); - const auto documentId = document->id; const auto i = _customEmoji.find(id); const auto recentOnly = (i != end(_customEmoji)) && i->second.recentOnly; @@ -2469,9 +2467,6 @@ void EmojiListWidget::refreshEmojiStatusCollectibles() { } const auto type = Data::EmojiStatuses::Type::Collectibles; const auto &list = session().data().emojiStatuses().list(type); - if (list.empty()) { - return; - } const auto setId = Data::Stickers::CollectibleSetId; auto set = std::vector(); set.reserve(list.size()); @@ -2480,14 +2475,16 @@ void EmojiListWidget::refreshEmojiStatusCollectibles() { ? status.collectible->documentId : status.documentId; const auto document = session().data().document(documentId); - if (const auto sticker = document->sticker()) { - set.push_back({ - .collectible = status.collectible, - .custom = resolveCustomEmoji(status, document, setId), - .document = document, - .emoji = Ui::Emoji::Find(sticker->alt), - }); - } + const auto sticker = document->sticker(); + set.push_back({ + .collectible = status.collectible, + .custom = resolveCustomEmoji(status, document, setId), + .document = document, + .emoji = sticker ? Ui::Emoji::Find(sticker->alt) : nullptr, + }); + } + if (set.empty()) { + return; } const auto collectibles = session().data().stickers().collectibleSet(); _custom.push_back({ diff --git a/Telegram/SourceFiles/ui/effects/premium_stars_colored.cpp b/Telegram/SourceFiles/ui/effects/premium_stars_colored.cpp index 2aeb1b998..7b7157b50 100644 --- a/Telegram/SourceFiles/ui/effects/premium_stars_colored.cpp +++ b/Telegram/SourceFiles/ui/effects/premium_stars_colored.cpp @@ -7,11 +7,221 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "ui/effects/premium_stars_colored.h" +#include "base/random.h" #include "ui/effects/premium_graphics.h" // GiftGradientStops. #include "ui/text/text_custom_emoji.h" +#include "ui/painter.h" #include "ui/rp_widget.h" namespace Ui::Premium { +namespace { + +constexpr auto kStarsCount = 16; +constexpr auto kTravelMax = 0.5; +constexpr auto kExcludeRadius = 0.7; +constexpr auto kFading = crl::time(200); +constexpr auto kLifetimeMin = crl::time(1000); +constexpr auto kLifetimeMax = 3 * kLifetimeMin; +constexpr auto kSizeMin = 0.1; +constexpr auto kSizeMax = 0.15; + +[[nodiscard]] crl::time ChooseLife(base::BufferedRandom &random) { + return kLifetimeMin + + base::RandomIndex(kLifetimeMax - kLifetimeMin, random); +} + +class CollectibleEmoji final : public Text::CustomEmoji { +public: + CollectibleEmoji( + QStringView entityData, + QColor centerColor, + QColor edgeColor, + std::unique_ptr inner, + Fn update, + int size); + + int width() override; + QString entityData() override; + void paint(QPainter &p, const Context &context) override; + void unload() override; + bool ready() override; + bool readyInDefaultState() override; + +private: + struct Star { + QPointF start; + QPointF delta; + float64 size = 0.; + crl::time birthTime = 0; + crl::time deathTime = 0; + }; + + void fill(); + void refill(Star &star, base::BufferedRandom &random); + void prepareFrame(); + + QString _entityData; + QSvgRenderer _svg; + std::vector _stars; + QColor _centerColor; + QColor _edgeColor; + std::unique_ptr _inner; + Ui::Animations::Basic _animation; + QImage _frame; + int _size = 0; + +}; + +CollectibleEmoji::CollectibleEmoji( + QStringView entityData, + QColor centerColor, + QColor edgeColor, + std::unique_ptr inner, + Fn update, + int size) +: _entityData(entityData.toString()) +, _svg(u":/gui/icons/settings/starmini.svg"_q) +, _centerColor(centerColor) +, _edgeColor(edgeColor) +, _inner(std::move(inner)) +, _animation(std::move(update)) +, _size(size) { + fill(); +} + +void CollectibleEmoji::fill() { + _stars.reserve(kStarsCount); + const auto now = crl::now(); + auto random = base::BufferedRandom(kStarsCount * 12); + for (auto i = 0; i != kStarsCount; ++i) { + const auto life = ChooseLife(random); + const auto shift = base::RandomIndex(life - kFading, random); + _stars.push_back({ + .birthTime = now - crl::time(shift), + .deathTime = now - crl::time(shift) + crl::time(life), + }); + refill(_stars.back(), random); + } +} + +void CollectibleEmoji::refill( + Star &star, + base::BufferedRandom &random) { + const auto take = [&] { + return base::RandomIndex(_size * 16, random) / (_size * 15.); + }; + const auto stake = [&] { + return take() * 2. - 1.; + }; + const auto exclude = kExcludeRadius * kExcludeRadius; + auto square = 0.; + while (true) { + const auto start = QPointF(stake(), stake()); + square = start.x() * start.x() + start.y() * start.y(); + if (square > exclude) { + star.start = start * _size; + break; + } + } + square *= _size * _size; + while (true) { + const auto delta = QPointF(stake(), stake()) * kTravelMax * _size; + const auto end = star.start + delta; + if (end.x() * end.x() + end.y() * end.y() > square) { + star.delta = delta; + break; + } + } + star.start = (star.start + QPointF(_size, _size)) / 2.; + star.size = (kSizeMin + (kSizeMax - kSizeMin) * take()) * _size; +} + +int CollectibleEmoji::width() { + return _inner->width(); +} + +QString CollectibleEmoji::entityData() { + return _entityData; +} + +void CollectibleEmoji::prepareFrame() { + const auto clip = QSize(_size, _size); + if (_frame.isNull()) { + const auto ratio = style::DevicePixelRatio(); + _frame = QImage(clip * ratio, QImage::Format_ARGB32_Premultiplied); + _frame.setDevicePixelRatio(ratio); + } + _frame.fill(Qt::transparent); + auto p = QPainter(&_frame); + auto hq = PainterHighQualityEnabler(p); + auto random = std::optional>(); + const auto now = crl::now(); + for (auto &star : _stars) { + if (star.deathTime <= now) { + if (!random) { + random.emplace(kStarsCount * 10); + } + const auto life = ChooseLife(*random); + star.birthTime = now; + star.deathTime = now + crl::time(life); + refill(star, *random); + } + Assert(star.birthTime <= now && now <= star.deathTime); + + const auto time = (now - star.birthTime) + / float64(star.deathTime - star.birthTime); + const auto position = star.start + star.delta * time; + const auto scale = ((now - star.birthTime) < kFading) + ? ((now - star.birthTime) / float64(kFading)) + : ((star.deathTime - now) < kFading) + ? ((star.deathTime - now) / float64(kFading)) + : 1.; + if (scale > 0.) { + const auto size = star.size * scale; + const auto rect = QRectF( + position - QPointF(size, size), + QSizeF(size, size) * 2); + _svg.render(&p, rect); + } + } + + p.setCompositionMode(QPainter::CompositionMode_SourceIn); + auto gradient = QRadialGradient( + QRect(QPoint(), clip).center(), + clip.height() / 2); + gradient.setStops({ + { 0., _centerColor }, + { 1., _edgeColor }, + }); + p.setBrush(gradient); + p.setPen(Qt::NoPen); + p.drawRect(QRect(QPoint(), clip)); +} + +void CollectibleEmoji::paint(QPainter &p, const Context &context) { + prepareFrame(); + p.drawImage(context.position, _frame); + if (context.paused) { + _animation.stop(); + } else if (!_animation.animating()) { + _animation.start(); + } + _inner->paint(p, context); +} + +void CollectibleEmoji::unload() { + _inner->unload(); +} + +bool CollectibleEmoji::ready() { + return _inner->ready(); +} + +bool CollectibleEmoji::readyInDefaultState() { + return _inner->readyInDefaultState(); +} + +} // namespace ColoredMiniStars::ColoredMiniStars( not_null parent, @@ -113,66 +323,7 @@ std::unique_ptr MakeCollectibleEmoji( std::unique_ptr inner, Fn update, int size) { - class Emoji final : public Text::CustomEmoji { - public: - Emoji( - QStringView entityData, - QColor centerColor, - QColor edgeColor, - std::unique_ptr inner, - Fn update, - int size) - : _entityData(entityData.toString()) - , _stars([=](QRect) { update(); }, MiniStars::Type::SlowStars) - , _centerColor(centerColor) - , _edgeColor(edgeColor) - , _inner(std::move(inner)) - , _size(size) { - _stars.setColorOverride(QGradientStops{ - { 0., edgeColor }, - { 1., centerColor }, - }); - _stars.setSize(QSize(size, size)); - } - - int width() override { - return _inner->width(); - } - - QString entityData() override { - return _entityData; - } - - void paint(QPainter &p, const Context &context) override { - _stars.setPosition(context.position); - _stars.paint(p); - - _inner->paint(p, context); - } - - void unload() override { - _inner->unload(); - } - - bool ready() override { - return _inner->ready(); - } - - bool readyInDefaultState() override { - return _inner->readyInDefaultState(); - } - - private: - QString _entityData; - ColoredMiniStars _stars; - QColor _centerColor; - QColor _edgeColor; - std::unique_ptr _inner; - int _size = 0; - - }; - - return std::make_unique( + return std::make_unique( entityData, centerColor, edgeColor,