/* 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 "info/profile/info_profile_birthday_effect.h" #include "apiwrap.h" #include "base/random.h" #include "base/weak_qptr.h" #include "chat_helpers/stickers_lottie.h" #include "data/data_birthday.h" #include "data/data_document.h" #include "data/data_document_media.h" #include "data/data_session.h" #include "data/data_user.h" #include "data/stickers/data_stickers.h" #include "data/stickers/data_stickers_set.h" #include "history/view/media/history_view_sticker_player.h" #include "lottie/lottie_common.h" #include "main/main_session.h" #include "ui/effects/animation_value.h" #include "ui/effects/animations.h" #include "ui/effects/fireworks_animation.h" #include "ui/emoji_config.h" #include "ui/power_saving.h" #include "ui/qt_object_factory.h" #include "ui/rect.h" #include "ui/rp_widget.h" #include "styles/style_info.h" namespace Info::Profile { namespace { constexpr auto kDuration = crl::time(4200); constexpr auto kMaxFrameStep = crl::time(20); constexpr auto kFadeOutFrom = 0.9; constexpr auto kCascadeWaveLength = 1.8; constexpr auto kCascadeScalePart = 0.4; constexpr auto kDigitPitchFactor = 0.88; constexpr auto kDigitSourceBelowFactor = 2; const auto kConfettiSetShortName = u"EmojiAnimations"_q; const auto kDigitsSetShortName = u"FestiveFontEmoji"_q; const auto kConfettiEmoji = { QString::fromUtf8("\xf0\x9f\x8e\x89"), QString::fromUtf8("\xf0\x9f\x8e\x86"), QString::fromUtf8("\xf0\x9f\x8e\x88"), }; [[nodiscard]] float64 Cascade( float64 fullProgress, float64 position, float64 count, float64 waveLength) { if (count <= 0) { return fullProgress; } const auto waveDuration = (1. / count) * std::min(waveLength, count); const auto waveOffset = (position / count) * (1. - waveDuration); return std::clamp( (fullProgress - waveOffset) / waveDuration, 0., 1.); } [[nodiscard]] std::vector DigitEmojiVariants(QChar digit) { const auto keycap = QChar(0x20E3); const auto selector = QChar(0xFE0F); return { QString() + digit + selector + keycap, QString() + digit + keycap, }; } [[nodiscard]] bool AdvanceFrame( not_null player, int index, bool frozen) { if (frozen) { return false; } const auto frames = player->framesCount(); return (frames <= 0) || (index + 1 < frames); } } // namespace class BirthdayEffect final : public Ui::RpWidget { public: BirthdayEffect( not_null parent, not_null session, int age, Fn userpicGeometry, Fn paused); void start(); protected: void paintEvent(QPaintEvent *e) override; private: using StickerPlayer = HistoryView::StickerPlayer; using EmojiDocuments = base::flat_map; struct Sticker { std::shared_ptr media; std::unique_ptr player; bool usesTextColor = false; }; void resolveSet(const QString &shortName, Fn done); void loadConfetti(); void useFireworksFallback(); void loadDigits(); void startDigits(const EmojiDocuments &byEmoji); void loadInto( not_null slot, not_null document, ChatHelpers::StickerLottieSize sizeTag, QSize box, Fn ready); void startAnimation(); void tick(crl::time now); void paintConfetti(QPainter &p); void paintDigits(QPainter &p); void destroy(); [[nodiscard]] bool paused() const { return _paused && _paused(); } const not_null _session; const int _age = 0; const Fn _userpicGeometry; const Fn _paused; Ui::Animations::Basic _animation; crl::time _lastTickTime = 0; float64 _progress = 0.; bool _started = false; bool _destroying = false; Sticker _confetti; int _confettiSide = 0; std::optional _fireworks; std::vector _digits; int _digitSide = 0; }; BirthdayEffect::BirthdayEffect( not_null parent, not_null session, int age, Fn userpicGeometry, Fn paused) : RpWidget(parent) , _session(session) , _age(age) , _userpicGeometry(std::move(userpicGeometry)) , _paused(std::move(paused)) , _animation([=](crl::time now) { tick(now); }) { } void BirthdayEffect::start() { _confettiSide = std::min(width(), st::infoProfileBirthdayConfettiSize); _digitSide = st::infoProfileBirthdayDigitSize; loadConfetti(); if (_age > 0) { loadDigits(); } } void BirthdayEffect::resolveSet( const QString &shortName, Fn done) { const auto build = [](not_null set) { auto byEmoji = EmojiDocuments(); for (const auto &[emoji, pack] : set->emoji) { if (!pack.isEmpty()) { byEmoji.emplace(emoji, pack.front()); } } return byEmoji; }; auto &stickers = _session->data().stickers(); for (const auto &[id, set] : stickers.sets()) { if (!set->emoji.empty() && !set->shortName.compare(shortName, Qt::CaseInsensitive)) { done(build(set.get())); return; } } auto identifier = StickerSetIdentifier(); identifier.shortName = shortName; _session->api().request(MTPmessages_GetStickerSet( Data::InputStickerSet(identifier), MTP_int(0) // hash )).done(crl::guard(this, [=](const MTPmessages_StickerSet &result) { auto byEmoji = EmojiDocuments(); result.match([&](const MTPDmessages_stickerSet &data) { byEmoji = build(_session->data().stickers().feedSetFull(data)); }, [](const MTPDmessages_stickerSetNotModified &) { }); done(std::move(byEmoji)); })).fail(crl::guard(this, [=] { done(EmojiDocuments()); })).send(); } void BirthdayEffect::loadConfetti() { resolveSet(kConfettiSetShortName, [=](EmojiDocuments byEmoji) { const auto count = int(kConfettiEmoji.size()); const auto from = count ? base::RandomIndex(count) : 0; auto document = (DocumentData*)nullptr; for (auto k = 0; k != count; ++k) { const auto &emoticon = *(kConfettiEmoji.begin() + ((from + k) % count)); if (const auto emoji = Ui::Emoji::Find(emoticon)) { const auto i = byEmoji.find(emoji->original()); if (i != byEmoji.end()) { document = i->second; break; } } } if (!document) { useFireworksFallback(); return; } loadInto( &_confetti, document, ChatHelpers::StickerLottieSize::EmojiInteraction, Size(_confettiSide), [=] { startAnimation(); }); }); } void BirthdayEffect::useFireworksFallback() { if (_started || _fireworks) { return; } _started = true; _fireworks.emplace([=] { update(); }); update(); } void BirthdayEffect::loadDigits() { resolveSet(kDigitsSetShortName, [=](EmojiDocuments byEmoji) { startDigits(byEmoji); }); } void BirthdayEffect::startDigits(const EmojiDocuments &byEmoji) { const auto text = QString::number(_age); _digits.clear(); _digits.resize(text.size()); for (auto i = 0; i != int(text.size()); ++i) { auto found = (DocumentData*)nullptr; for (const auto &variant : DigitEmojiVariants(text[i])) { const auto emoji = Ui::Emoji::Find(variant); if (!emoji) { continue; } const auto j = byEmoji.find(emoji->original()); if (j != byEmoji.end()) { found = j->second; break; } } if (found) { loadInto( &_digits[i], found, ChatHelpers::StickerLottieSize::StickerSet, Size(_digitSide), [=] { update(); }); } } } void BirthdayEffect::loadInto( not_null slot, not_null document, ChatHelpers::StickerLottieSize sizeTag, QSize box, Fn ready) { slot->media = document->createMediaView(); slot->media->checkStickerLarge(); slot->media->goodThumbnailWanted(); const auto media = slot->media; rpl::single() | rpl::then( document->session().downloaderTaskFinished() ) | rpl::filter([=] { return media->loaded(); }) | rpl::take(1) | rpl::on_next([=] { const auto sticker = document->sticker(); if (!sticker) { return; } auto player = std::unique_ptr(); if (sticker->isLottie()) { player = std::make_unique( ChatHelpers::LottiePlayerFromDocument( media.get(), sizeTag, box, Lottie::Quality::High)); } else if (sticker->isWebm()) { player = std::make_unique( media->owner()->location(), media->bytes(), box); } else { player = std::make_unique( media->owner()->location(), media->bytes(), box); } player->setRepaintCallback([=] { update(); }); slot->usesTextColor = media->owner()->emojiUsesTextColor(); slot->player = std::move(player); ready(); }, lifetime()); } void BirthdayEffect::startAnimation() { if (_started) { return; } _started = true; _lastTickTime = 0; _progress = 0.; _animation.start(); update(); } void BirthdayEffect::tick(crl::time now) { const auto delta = _lastTickTime ? std::min(now - _lastTickTime, kMaxFrameStep) : crl::time(0); _lastTickTime = now; if (!paused()) { _progress = std::clamp( _progress + (delta / float64(kDuration)), 0., 1.); } update(); if (_progress >= 1.) { _animation.stop(); destroy(); } } void BirthdayEffect::paintEvent(QPaintEvent *e) { auto p = QPainter(this); if (_fireworks) { if (!_fireworks->paint(p, rect())) { destroy(); } return; } else if (!_started) { return; } paintConfetti(p); if (_age > 0) { paintDigits(p); } } void BirthdayEffect::paintConfetti(QPainter &p) { if (!_confetti.player || !_confetti.player->ready()) { return; } const auto frozen = paused(); const auto colored = _confetti.usesTextColor ? st::windowFgActive->c : QColor(0, 0, 0, 0); const auto info = _confetti.player->frame( Size(_confettiSide), colored, true, crl::now(), frozen); const auto image = info.image; const auto size = image.size() / style::DevicePixelRatio(); const auto center = rect::center(_userpicGeometry()); const auto x = center.x() - (size.width() / 2); const auto y = std::max(0, center.y() - (size.height() / 2)); const auto opacity = (_progress < kFadeOutFrom) ? 1. : std::max(0., 1. - (_progress - kFadeOutFrom) / (1. - kFadeOutFrom)); p.setOpacity(opacity); p.drawImage(Rect(x, y, size), image); p.setOpacity(1.); if (AdvanceFrame(_confetti.player.get(), info.index, frozen)) { _confetti.player->markFrameShown(); } } void BirthdayEffect::paintDigits(QPainter &p) { const auto count = int(_digits.size()); if (!count) { return; } const auto frozen = paused(); const auto sz = float64(_digitSide); const auto pitch = sz * kDigitPitchFactor; const auto userpic = _userpicGeometry(); const auto sourceX = float64(rect::center(userpic).x()); const auto sourceY = float64( userpic.y() + userpic.height() * kDigitSourceBelowFactor); const auto rowLeft = (width() - pitch * (count - 1)) / 2.; const auto travelX = rowLeft - sourceX; const auto travelY = sourceY + sz; const auto t = _progress; for (auto i = count - 1; i >= 0; --i) { auto &digit = _digits[i]; if (!digit.player || !digit.player->ready()) { continue; } const auto local = Cascade(t, i, count, kCascadeWaveLength); const auto scale = anim::easeOutQuint( 1., std::clamp(local / kCascadeScalePart, 0., 1.)); const auto centerX = sourceX + (pitch * i) + (local * travelX); const auto centerY = sourceY - (travelY * t * t); const auto drawSide = sz * scale; const auto colored = digit.usesTextColor ? st::windowFgActive->c : QColor(0, 0, 0, 0); const auto info = digit.player->frame( Size(_digitSide), colored, false, crl::now(), frozen); p.drawImage( Rect( centerX - (drawSide / 2.), centerY - (drawSide / 2.), Size(drawSide)), info.image); if (AdvanceFrame(digit.player.get(), info.index, frozen)) { digit.player->markFrameShown(); } } } void BirthdayEffect::destroy() { if (_destroying) { return; } _destroying = true; crl::on_main(this, [=] { delete this; }); } void StartProfileBirthdayEffect( not_null cover, not_null user, Fn userpicGeometry, Fn paused) { if (PowerSaving::On(PowerSaving::kChatEffects)) { return; } const auto effect = Ui::CreateChild( cover.get(), &user->session(), Data::BirthdayAge(user->birthday()), std::move(userpicGeometry), std::move(paused)); effect->setAttribute(Qt::WA_TransparentForMouseEvents); effect->setGeometry(cover->rect()); cover->sizeValue( ) | rpl::on_next([=](QSize size) { effect->setGeometry(Rect(size)); }, effect->lifetime()); effect->show(); effect->raise(); effect->start(); } } // namespace Info::Profile