diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 1b86c2cd1..ceaf1205d 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -845,6 +845,8 @@ PRIVATE support/support_helper.h support/support_templates.cpp support/support_templates.h + ui/effects/fireworks_animation.cpp + ui/effects/fireworks_animation.h ui/effects/round_checkbox.cpp ui/effects/round_checkbox.h ui/effects/send_action_animations.cpp diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index a5a325926..84bbd8f0d 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -505,6 +505,10 @@ void Message::draw( const auto fastShareTop = g.top() + g.height() - fastShareSkip - st::historyFastShareSize; drawRightAction(p, fastShareLeft, fastShareTop, width()); } + + if (media) { + media->paintBubbleFireworks(p, g, ms); + } } else if (media && media->isDisplayed()) { p.translate(g.topLeft()); media->draw(p, clip.translated(-g.topLeft()), skipTextSelection(selection), ms); diff --git a/Telegram/SourceFiles/history/view/media/history_view_media.h b/Telegram/SourceFiles/history/view/media/history_view_media.h index 4d3b6c0a0..0708ca2a0 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media.h +++ b/Telegram/SourceFiles/history/view/media/history_view_media.h @@ -247,6 +247,11 @@ public: [[nodiscard]] virtual QMargins bubbleRollRepaintMargins() const { return QMargins(); } + virtual void paintBubbleFireworks( + Painter &p, + const QRect &bubble, + crl::time ms) const { + } virtual void unloadHeavyPart() { } diff --git a/Telegram/SourceFiles/history/view/media/history_view_poll.cpp b/Telegram/SourceFiles/history/view/media/history_view_poll.cpp index 3ec1e633a..9bfc98ae5 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_poll.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_poll.cpp @@ -18,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/effects/animations.h" #include "ui/effects/radial_animation.h" #include "ui/effects/ripple_animation.h" +#include "ui/effects/fireworks_animation.h" #include "data/data_media_types.h" #include "data/data_poll.h" #include "data/data_user.h" @@ -36,6 +37,7 @@ constexpr auto kRotateSegments = 8; constexpr auto kRotateAmplitude = 3.; constexpr auto kScaleSegments = 2; constexpr auto kScaleAmplitude = 0.03; +constexpr auto kRollDuration = crl::time(400); struct PercentCounterItem { int index = 0; @@ -380,26 +382,30 @@ void Poll::updateTexts() { if (willStartAnimation) { startAnswersAnimation(); if (!voted) { - checkQuizAnsweredWrong(); + checkQuizAnswered(); } } } -void Poll::checkQuizAnsweredWrong() { - if (!_voted || !_poll->quiz()) { +void Poll::checkQuizAnswered() { + if (!_voted || !_votedFromHere || !_poll->quiz() || anim::Disabled()) { return; } const auto i = ranges::find(_answers, true, &Answer::chosen); - if (i == end(_answers) || i->correct) { + if (i == end(_answers)) { return; } - constexpr auto kDuration = crl::time(400); - _wrongAnswerAnimation.start( - [=] { history()->owner().requestViewRepaint(_parent); }, - 0., - 1., - kDuration, - anim::linear); + if (i->correct) { + _fireworksAnimation = std::make_unique( + [=] { history()->owner().requestViewRepaint(_parent); }); + } else { + _wrongAnswerAnimation.start( + [=] { history()->owner().requestViewRepaint(_parent); }, + 0., + 1., + kRollDuration, + anim::linear); + } } void Poll::updateRecentVoters() { @@ -451,6 +457,7 @@ ClickHandlerPtr Poll::createAnswerClickHandler( })); } return std::make_shared(crl::guard(this, [=] { + _votedFromHere = true; history()->session().api().sendPollVotes( _parent->data()->fullId(), { option }); @@ -490,9 +497,7 @@ void Poll::sendMultiOptions() { &Answer::option ) | ranges::to_vector; if (!chosen.empty()) { - for (auto &answer : _answers) { - answer.selected = false; - } + _votedFromHere = true; history()->session().api().sendPollVotes( _parent->data()->fullId(), std::move(chosen)); @@ -506,7 +511,17 @@ void Poll::showResults() { } void Poll::updateVotes() { - _voted = _poll->voted(); + const auto voted = _poll->voted(); + if (_voted != voted) { + _voted = voted; + if (_voted) { + for (auto &answer : _answers) { + answer.selected = false; + } + } else { + _votedFromHere = false; + } + } updateAnswerVotes(); updateTotalVotes(); } @@ -1179,6 +1194,16 @@ QMargins Poll::bubbleRollRepaintMargins() const { return QMargins(kAdd, kAdd, kAdd, kAdd); } +void Poll::paintBubbleFireworks( + Painter &p, + const QRect &bubble, + crl::time ms) const { + if (!_fireworksAnimation || _fireworksAnimation->paint(p, bubble)) { + return; + } + _fireworksAnimation = nullptr; +} + void Poll::clickHandlerPressedChanged( const ClickHandlerPtr &handler, bool pressed) { diff --git a/Telegram/SourceFiles/history/view/media/history_view_poll.h b/Telegram/SourceFiles/history/view/media/history_view_poll.h index 404c050ca..b689fa96a 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_poll.h +++ b/Telegram/SourceFiles/history/view/media/history_view_poll.h @@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Ui { class RippleAnimation; +class FireworksAnimation; } // namespace Ui namespace HistoryView { @@ -43,6 +44,10 @@ public: BubbleRoll bubbleRoll() const override; QMargins bubbleRollRepaintMargins() const override; + void paintBubbleFireworks( + Painter &p, + const QRect &bubble, + crl::time ms) const override; void clickHandlerPressedChanged( const ClickHandlerPtr &handler, @@ -149,7 +154,7 @@ private: void toggleMultiOption(const QByteArray &option); void sendMultiOptions(); void showResults(); - void checkQuizAnsweredWrong(); + void checkQuizAnswered(); [[nodiscard]] int bottomButtonHeight() const; @@ -172,10 +177,12 @@ private: mutable std::unique_ptr _answersAnimation; mutable std::unique_ptr _sendingAnimation; + mutable std::unique_ptr _fireworksAnimation; Ui::Animations::Simple _wrongAnswerAnimation; mutable QPoint _lastLinkPoint; bool _hasSelected = false; + bool _votedFromHere = false; mutable bool _wrongAnswerAnimated = false; }; diff --git a/Telegram/SourceFiles/ui/effects/fireworks_animation.cpp b/Telegram/SourceFiles/ui/effects/fireworks_animation.cpp new file mode 100644 index 000000000..2a6bb7f35 --- /dev/null +++ b/Telegram/SourceFiles/ui/effects/fireworks_animation.cpp @@ -0,0 +1,221 @@ +/* +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/fireworks_animation.h" + +#include "base/openssl_help.h" + +namespace Ui { +namespace { + +constexpr auto kParticlesCount = 60; +constexpr auto kFallCount = 30; +constexpr auto kFirstUpdateTime = crl::time(16); +constexpr auto kFireworkWidth = 480; +constexpr auto kFireworkHeight = 320; + +QBrush Brush(int color) { + return QBrush{ QColor( + color & 0xFF, + (color >> 8) & 0xFF, + (color >> 16) & 0xFF) + }; +} + +std::vector PrepareBrushes() { + return { + Brush(0xff2CBCE8), + Brush(0xff9E04D0), + Brush(0xffFECB02), + Brush(0xffFD2357), + Brush(0xff278CFE), + Brush(0xff59B86C), + }; +} + +int RandomInt(uint32 till) { + return int(openssl::RandomValue() % till); +} + +[[nodiscard]] float64 RandomFloat01() { + return openssl::RandomValue() + / float64(std::numeric_limits::max()); +} + +} // namespace + +FireworksAnimation::FireworksAnimation(Fn repaint) +: _brushes(PrepareBrushes()) +, _animation([=](crl::time now) { update(now); }) +, _repaint(std::move(repaint)) { + _smallSide = style::ConvertScale(2); + _particles.reserve(kParticlesCount + kFallCount); + for (auto i = 0; i != kParticlesCount; ++i) { + initParticle(_particles.emplace_back(), false); + } + _animation.start(); +} + +void FireworksAnimation::update(crl::time now) { + const auto passed = _lastUpdate ? (now - _lastUpdate) : kFirstUpdateTime; + _lastUpdate = now; + auto allFinished = true; + for (auto &particle : _particles) { + updateParticle(particle, passed); + if (!particle.finished) { + allFinished = false; + } + } + if (allFinished) { + _animation.stop(); + } else if (_fallingDown >= kParticlesCount / 2 && _speedCoef > 0.2) { + startFall(); + _speedCoef -= passed / 16.0 * 0.15; + if (_speedCoef < 0.2) { + _speedCoef = 0.2; + } + } + _repaint(); +} + +bool FireworksAnimation::paint(QPainter &p, const QRect &rect) { + if (rect.isEmpty()) { + return false; + } + PainterHighQualityEnabler hq(p); + p.setPen(Qt::NoPen); + p.setClipRect(rect); + for (auto &particle : _particles) { + if (!particle.finished) { + paintParticle(p, particle, rect); + } + } + p.setClipping(false); + return _animation.animating(); +} + +void FireworksAnimation::paintParticle( + QPainter &p, + const Particle &particle, + const QRect &rect) { + const auto size = particle.size; + const auto x = rect.x() + (particle.x * rect.width() / kFireworkWidth); + const auto y = rect.y() + (particle.y * rect.height() / kFireworkHeight); + p.setBrush(_brushes[particle.color]); + if (particle.type == Particle::Type::Circle) { + p.drawEllipse(x, y, size, size); + } else { + const auto rect = QRect(-size, -_smallSide, size, _smallSide); + p.save(); + p.translate(x, y); + p.rotate(particle.rotation); + p.drawRoundedRect(rect, _smallSide, _smallSide); + p.restore(); + } +} + +void FireworksAnimation::updateParticle(Particle &particle, crl::time dt) { + if (particle.finished) { + return; + } + const auto moveCoef = dt / 16.; + particle.x += particle.moveX * moveCoef; + particle.y += particle.moveY * moveCoef; + if (particle.xFinished != 0) { + const auto dp = 0.5; + if (particle.xFinished == 1) { + particle.moveX += dp * moveCoef * 0.05; + if (particle.moveX >= dp) { + particle.xFinished = 2; + } + } else { + particle.moveX -= dp * moveCoef * 0.05f; + if (particle.moveX <= -dp) { + particle.xFinished = 1; + } + } + } else { + if (particle.right) { + if (particle.moveX < 0) { + particle.moveX += moveCoef * 0.05f; + if (particle.moveX >= 0) { + particle.moveX = 0; + particle.xFinished = particle.finishedStart; + } + } + } else { + if (particle.moveX > 0) { + particle.moveX -= moveCoef * 0.05f; + if (particle.moveX <= 0) { + particle.moveX = 0; + particle.xFinished = particle.finishedStart; + } + } + } + } + const auto yEdge = -0.5; + const auto wasNegative = (particle.moveY < yEdge); + if (particle.moveY > yEdge) { + particle.moveY += (1. / 3.) * moveCoef * _speedCoef; + } else { + particle.moveY += (1. / 3.) * moveCoef; + } + if (wasNegative && particle.moveY > yEdge) { + ++_fallingDown; + } + if (particle.type == Particle::Type::Rectangle) { + particle.rotation += moveCoef * 10; + if (particle.rotation > 360) { + particle.rotation -= 360; + } + } + if (particle.y >= kFireworkHeight) { + particle.finished = true; + } +} + +void FireworksAnimation::startFall() { + if (_startedFall) { + return; + } + _startedFall = true; + for (auto i = 0; i != kFallCount; ++i) { + initParticle(_particles.emplace_back(), true); + } +} + +void FireworksAnimation::initParticle(Particle &particle, bool falling) { + using Type = Particle::Type; + particle.color = RandomInt(_brushes.size()); + particle.type = RandomInt(2) ? Type::Rectangle : Type::Circle; + particle.right = (RandomInt(2) == 1); + particle.finishedStart = 1 + RandomInt(2); + if (particle.type == Type::Circle) { + particle.size = style::ConvertScale(6 + RandomFloat01() * 3); + } else { + particle.size = style::ConvertScale(6 + RandomFloat01() * 6); + particle.rotation = RandomInt(360); + } + if (falling) { + particle.y = -RandomFloat01() * kFireworkHeight * 1.2f; + particle.x = 5 + RandomInt(kFireworkWidth - 10); + particle.xFinished = particle.finishedStart; + } else { + const auto xOffset = 4 + RandomInt(10); + const auto yOffset = kFireworkHeight / 4; + if (particle.right) { + particle.x = kFireworkWidth + xOffset; + } else { + particle.x = -xOffset; + } + particle.moveX = (particle.right ? -1 : 1) * (1.2 + RandomFloat01() * 4); + particle.moveY = -(4 + RandomFloat01() * 4); + particle.y = yOffset / 2 + RandomInt(yOffset * 2); + } +} + +} // namespace Ui diff --git a/Telegram/SourceFiles/ui/effects/fireworks_animation.h b/Telegram/SourceFiles/ui/effects/fireworks_animation.h new file mode 100644 index 000000000..168e3adbc --- /dev/null +++ b/Telegram/SourceFiles/ui/effects/fireworks_animation.h @@ -0,0 +1,63 @@ +/* +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 + +#include "ui/effects/animations.h" + +namespace Ui { + +class FireworksAnimation final { +public: + explicit FireworksAnimation(Fn repaint); + + bool paint(QPainter &p, const QRect &rect); + +private: + struct Particle { + enum class Type : uchar { + Circle, + Rectangle + }; + + float64 x = 0.; + float64 y = 0.; + float64 moveX = 0.; + float64 moveY = 0.; + uint16 rotation = 0; + + Type type = Type::Circle; + uchar color = 0; + bool right = false; + uchar size = 0; + uchar xFinished = 0; + uchar finishedStart = 0; + bool finished = false; + }; + + void update(crl::time now); + void startFall(); + void paintParticle( + QPainter &p, + const Particle &particle, + const QRect &rect); + void initParticle(Particle &particle, bool falling); + void updateParticle(Particle &particle, crl::time dt); + + std::vector _particles; + std::vector _brushes; + Ui::Animations::Basic _animation; + Fn _repaint; + crl::time _lastUpdate = 0; + float64 _speedCoef = 1.; + int _fallingDown = 0; + int _smallSide = 0; + bool _startedFall = false; + +}; + +} // namespace Ui