From 2a99046bbd9175f228af158fa1f95f2763bbdbfa Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Fri, 16 Dec 2022 14:36:55 +0300 Subject: [PATCH] Added snowflakes effect. --- .../SourceFiles/ui/effects/snowflakes.cpp | 205 ++++++++++++++++++ Telegram/SourceFiles/ui/effects/snowflakes.h | 76 +++++++ .../SourceFiles/window/window_main_menu.cpp | 101 ++++++--- .../SourceFiles/window/window_main_menu.h | 2 + Telegram/cmake/td_ui.cmake | 2 + 5 files changed, 361 insertions(+), 25 deletions(-) create mode 100644 Telegram/SourceFiles/ui/effects/snowflakes.cpp create mode 100644 Telegram/SourceFiles/ui/effects/snowflakes.h diff --git a/Telegram/SourceFiles/ui/effects/snowflakes.cpp b/Telegram/SourceFiles/ui/effects/snowflakes.cpp new file mode 100644 index 000000000..23c005247 --- /dev/null +++ b/Telegram/SourceFiles/ui/effects/snowflakes.cpp @@ -0,0 +1,205 @@ +/* +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/snowflakes.h" + +#include "base/random.h" +#include "ui/effects/animation_value_f.h" +#include "ui/painter.h" + +#include + +namespace Ui { +namespace { + +[[nodiscard]] QImage PrepareSnowflake(QBrush brush) { + constexpr auto kPenWidth = 1.5; + constexpr auto kTailCount = 6; + constexpr auto kAngle = (-M_PI / 2.); + constexpr auto kTailSize = 8.; + constexpr auto kSubtailPositionRatio = 2 / 3.; + constexpr auto kSubtailSize = kTailSize / 3; + constexpr auto kSubtailAngle1 = -M_PI / 6.; + constexpr auto kSubtailAngle2 = -M_PI - kSubtailAngle1; + constexpr auto kSpriteSize = (kTailSize + kPenWidth / 2.) * 2; + + const auto x = float64(style::ConvertScaleExact(kSpriteSize / 2.)); + const auto y = float64(style::ConvertScaleExact(kSpriteSize / 2.)); + const auto tailSize = style::ConvertScaleExact(kTailSize); + const auto subtailSize = style::ConvertScaleExact(kSubtailSize); + const auto endTail = QPointF( + std::cos(kAngle) * tailSize, + std::sin(kAngle) * tailSize); + const auto startSubtail = endTail * kSubtailPositionRatio; + const auto endSubtail1 = startSubtail + QPointF( + subtailSize * std::cos(kSubtailAngle1), + subtailSize * std::sin(kSubtailAngle1)); + const auto endSubtail2 = startSubtail + QPointF( + subtailSize * std::cos(kSubtailAngle2), + subtailSize * std::sin(kSubtailAngle2)); + + const auto pen = QPen( + std::move(brush), + style::ConvertScaleExact(kPenWidth), + Qt::SolidLine, + Qt::RoundCap, + Qt::RoundJoin); + + const auto s = style::ConvertScaleExact(kSpriteSize) + * style::DevicePixelRatio(); + auto result = QImage(QSize(s, s), QImage::Format_ARGB32_Premultiplied); + result.setDevicePixelRatio(style::DevicePixelRatio()); + result.fill(Qt::transparent); + { + auto p = QPainter(&result); + PainterHighQualityEnabler hq(p); + p.setPen(pen); + p.setBrush(Qt::NoBrush); + p.translate(x, y); + const auto step = 360. / kTailCount; + for (auto i = 0; i < kTailCount; i++) { + p.rotate(step); + p.drawLine(QPointF(), endTail); + p.drawLine(startSubtail, endSubtail1); + p.drawLine(startSubtail, endSubtail2); + } + } + return result; +} + +} // namespace + +Snowflakes::Snowflakes(Fn updateCallback) +: _lifeLength({ 300, 100 }) +, _deathTime({ 2000, 100 }) +, _scale({ 60, 100 }) +, _velocity({ 20, 4 }) +, _angle({ 70, 40 }) +, _relativeX({ 0, 100 }) +, _appearProgressTill(200. / _deathTime.from) +, _disappearProgressAfter(_appearProgressTill) +, _dotMargins(3., 3., 3., 3.) +, _renderMargins(1., 1., 1., 1.) +, _animation([=](crl::time now) { + if (now > _nextBirthTime && !_paused) { + createParticle(now); + } + if (_rectToUpdate.isValid()) { + updateCallback(base::take(_rectToUpdate)); + } +}) { + if (anim::Disabled()) { + const auto from = _deathTime.from + _deathTime.length; + auto r = bytes::vector(from); + base::RandomFill(r.data(), r.size()); + for (auto i = -from; i < 0; i += randomInterval(_lifeLength, r[-i])) { + createParticle(i); + } + updateCallback(_rectToUpdate); + } else { + _animation.start(); + } +} + +int Snowflakes::randomInterval( + const Interval &interval, + const bytes::type &random) const { + return interval.from + (uchar(random) % interval.length); +} + +crl::time Snowflakes::timeNow() const { + return anim::Disabled() ? 0 : crl::now(); +} + +void Snowflakes::paint(QPainter &p, const QRectF &rect) { + const auto center = rect.center(); + const auto opacity = p.opacity(); + PainterHighQualityEnabler hq(p); + p.setPen(Qt::NoPen); + p.setBrush(_brush); + for (const auto &particle : _particles) { + const auto progress = (timeNow() - particle.birthTime) + / float64(particle.deathTime - particle.birthTime); + if (progress > 1.) { + continue; + } + const auto appearProgress = std::clamp( + progress / _appearProgressTill, + 0., + 1.); + const auto dissappearProgress = 1. + - (std::clamp(progress - _disappearProgressAfter, 0., 1.) + / (1. - _disappearProgressAfter)); + + p.setOpacity(appearProgress * dissappearProgress * opacity); + + const auto startX = rect.x() + rect.width() * particle.relativeX; + const auto startY = rect.y() + rect.height() * particle.relativeY; + const auto endX = startX + particle.velocityX; + const auto endY = startY + particle.velocityY; + + const auto x = anim::interpolateF(startX, endX, progress); + const auto y = anim::interpolateF(startY, endY, progress); + + if (particle.type == Type::Dot) { + const auto renderRect = QRectF(x, y, 0., 0.) + + _dotMargins * particle.scale; + p.drawEllipse(renderRect); + _rectToUpdate |= renderRect.toRect() + _renderMargins; + } else if (particle.type == Type::Snowflake) { + const auto s = _sprite.size() / style::DevicePixelRatio(); + const auto h = s.height() / 2.; + const auto pos = QPointF(x - h, y - h); + p.drawImage(pos, _sprite); + _rectToUpdate |= QRectF(pos, s).toRect() + _renderMargins; + } + } + p.setOpacity(opacity); +} + +void Snowflakes::setPaused(bool paused) { + _paused = paused; +} + +void Snowflakes::setBrush(QBrush brush) { + _brush = std::move(brush); + _sprite = PrepareSnowflake(_brush); +} + +void Snowflakes::createParticle(crl::time now) { + constexpr auto kRandomSize = 8; + auto random = bytes::vector(kRandomSize); + base::RandomFill(random.data(), random.size()); + + auto i = 0; + auto next = [&] { return random[i++]; }; + + _nextBirthTime = now + randomInterval(_lifeLength, next()); + + const auto angle = randomInterval(_angle, next()); + const auto velocity = randomInterval(_velocity, next()); + auto particle = Particle{ + .birthTime = now, + .deathTime = now + randomInterval(_deathTime, next()), + .scale = float64(randomInterval(_scale, next())) / 100., + .relativeX = float64(randomInterval(_relativeX, next())) / 100., + .relativeY = float64(randomInterval(_relativeX, next())) / 100., + .velocityX = std::cos(M_PI / 180. * angle) * velocity, + .velocityY = std::sin(M_PI / 180. * angle) * velocity, + .type = ((uchar(next()) % 2) == 1 ? Type::Snowflake : Type::Dot), + }; + for (auto i = 0; i < _particles.size(); i++) { + if (particle.birthTime > _particles[i].deathTime) { + _particles[i] = particle; + return; + } + } + _particles.push_back(particle); +} + + +} // namespace Ui diff --git a/Telegram/SourceFiles/ui/effects/snowflakes.h b/Telegram/SourceFiles/ui/effects/snowflakes.h new file mode 100644 index 000000000..fd7da55f7 --- /dev/null +++ b/Telegram/SourceFiles/ui/effects/snowflakes.h @@ -0,0 +1,76 @@ +/* +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 Snowflakes final { +public: + Snowflakes(Fn updateCallback); + + void paint(QPainter &p, const QRectF &rect); + void setPaused(bool paused); + void setBrush(QBrush brush); + +private: + enum class Type { + Dot, + Snowflake, + }; + + struct Interval { + int from = 0; + int length = 0; + }; + + struct Particle { + crl::time birthTime = 0; + crl::time deathTime = 0; + float64 scale = 0.; + float64 alpha = 0.; + float64 relativeX = 0.; // Relative to a width. + float64 relativeY = 0.; // Relative to a height. + float64 velocityX = 0.; + float64 velocityY = 0.; + Type type; + }; + + void createParticle(crl::time now); + [[nodiscard]] crl::time timeNow() const; + [[nodiscard]] int randomInterval( + const Interval &interval, + const gsl::byte &random) const; + + const Interval _lifeLength; + const Interval _deathTime; + const Interval _scale; + const Interval _velocity; + const Interval _angle; + const Interval _relativeX; + + const float64 _appearProgressTill; + const float64 _disappearProgressAfter; + const QMarginsF _dotMargins; + const QMargins _renderMargins; + + Ui::Animations::Basic _animation; + QImage _sprite; + + std::vector _particles; + + crl::time _nextBirthTime = 0; + bool _paused = false; + QBrush _brush; + + QRect _rectToUpdate; + +}; + +} // namespace Ui diff --git a/Telegram/SourceFiles/window/window_main_menu.cpp b/Telegram/SourceFiles/window/window_main_menu.cpp index 915617aae..394aca6c9 100644 --- a/Telegram/SourceFiles/window/window_main_menu.cpp +++ b/Telegram/SourceFiles/window/window_main_menu.cpp @@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "window/window_controller.h" #include "ui/chat/chat_theme.h" #include "ui/controls/userpic_button.h" +#include "ui/effects/snowflakes.h" #include "ui/widgets/buttons.h" #include "ui/widgets/labels.h" #include "ui/widgets/popup_menu.h" @@ -78,6 +79,20 @@ namespace { constexpr auto kPlayStatusLimit = 2; +[[nodiscard]] bool CanCheckSpecialEvent() { + static const auto result = [] { + const auto now = QDate::currentDate(); + return (now.month() == 12) || (now.month() == 1 && now.day() == 1); + }(); + return result; +} + +[[nodiscard]] bool CheckSpecialEvent() { + const auto now = QDate::currentDate(); + return (now.month() == 12 && now.day() >= 24) + || (now.month() == 1 && now.day() == 1); +} + void ShowCallsBox(not_null window) { auto controller = std::make_unique(window); const auto initBox = [ @@ -469,6 +484,38 @@ MainMenu::MainMenu( }, lifetime()); initResetScaleButton(); + + if (CanCheckSpecialEvent() && CheckSpecialEvent()) { + const auto snowLifetime = lifetime().make_state(); + const auto rebuild = [=] { + const auto snowRaw = Ui::CreateChild(this); + const auto snow = snowLifetime->make_state( + [=](const QRect &r) { snowRaw->update(r); }); + snow->setBrush(QColor(230, 230, 230)); + snowRaw->paintRequest( + ) | rpl::start_with_next([=](const QRect &r) { + auto p = Painter(snowRaw); + p.fillRect(r, st::mainMenuBg); + drawName(p); + snow->paint(p, snowRaw->rect()); + }, snowRaw->lifetime()); + widthValue( + ) | rpl::start_with_next([=](int width) { + snowRaw->setGeometry(0, 0, width, st::mainMenuCoverHeight); + }, snowRaw->lifetime()); + snowRaw->show(); + snowRaw->lower(); + snowRaw->setAttribute(Qt::WA_TransparentForMouseEvents); + snowLifetime->add([=] { base::unique_qptr{ snowRaw }; }); + }; + Window::Theme::IsNightModeValue( + ) | rpl::start_with_next([=](bool isNightMode) { + snowLifetime->destroy(); + if (isNightMode) { + rebuild(); + } + }, lifetime()); + } } MainMenu::~MainMenu() = default; @@ -812,39 +859,43 @@ void MainMenu::chooseEmojiStatus() { } void MainMenu::paintEvent(QPaintEvent *e) { - Painter p(this); + auto p = Painter(this); const auto clip = e->rect(); const auto cover = QRect(0, 0, width(), st::mainMenuCoverHeight); p.fillRect(clip, st::mainMenuBg); if (cover.intersects(clip)) { - const auto widthText = width() - - st::mainMenuCoverNameLeft - - _toggleAccounts->rightSkip(); - - const auto user = _controller->session().user(); - if (_nameVersion < user->nameVersion()) { - _nameVersion = user->nameVersion(); - _name.setText( - st::semiboldTextStyle, - user->name(), - Ui::NameTextOptions()); - moveBadge(); - } - p.setFont(st::semiboldFont); - p.setPen(st::windowBoldFg); - _name.drawLeftElided( - p, - st::mainMenuCoverNameLeft, - st::mainMenuCoverNameTop, - (widthText - - (_badge->widget() - ? (st::semiboldFont->spacew + _badge->widget()->width()) - : 0)), - width()); + drawName(p); } } +void MainMenu::drawName(Painter &p) { + const auto widthText = width() + - st::mainMenuCoverNameLeft + - _toggleAccounts->rightSkip(); + + const auto user = _controller->session().user(); + if (_nameVersion < user->nameVersion()) { + _nameVersion = user->nameVersion(); + _name.setText( + st::semiboldTextStyle, + user->name(), + Ui::NameTextOptions()); + moveBadge(); + } + p.setFont(st::semiboldFont); + p.setPen(st::windowBoldFg); + _name.drawLeftElided( + p, + st::mainMenuCoverNameLeft, + st::mainMenuCoverNameTop, + (widthText + - (_badge->widget() + ? (st::semiboldFont->spacew + _badge->widget()->width()) + : 0)), + width()); +} + void MainMenu::initResetScaleButton() { if (!window() || !window()->windowHandle()) { return; diff --git a/Telegram/SourceFiles/window/window_main_menu.h b/Telegram/SourceFiles/window/window_main_menu.h index 5da215391..a770699de 100644 --- a/Telegram/SourceFiles/window/window_main_menu.h +++ b/Telegram/SourceFiles/window/window_main_menu.h @@ -73,6 +73,8 @@ private: void toggleAccounts(); void chooseEmojiStatus(); + void drawName(Painter &p); + const not_null _controller; object_ptr _userpicButton; Ui::Text::String _name; diff --git a/Telegram/cmake/td_ui.cmake b/Telegram/cmake/td_ui.cmake index feba4ee62..7de65172d 100644 --- a/Telegram/cmake/td_ui.cmake +++ b/Telegram/cmake/td_ui.cmake @@ -250,6 +250,8 @@ PRIVATE ui/effects/round_checkbox.h ui/effects/scroll_content_shadow.cpp ui/effects/scroll_content_shadow.h + ui/effects/snowflakes.cpp + ui/effects/snowflakes.h ui/text/format_song_name.cpp ui/text/format_song_name.h ui/text/format_values.cpp