mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2025-06-05 22:54:01 +02:00
Added snowflakes effect.
This commit is contained in:
parent
a3a48a38c8
commit
2a99046bbd
5 changed files with 361 additions and 25 deletions
205
Telegram/SourceFiles/ui/effects/snowflakes.cpp
Normal file
205
Telegram/SourceFiles/ui/effects/snowflakes.cpp
Normal file
|
@ -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 <QtCore/QtMath>
|
||||||
|
|
||||||
|
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<void(const QRect &r)> 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
|
76
Telegram/SourceFiles/ui/effects/snowflakes.h
Normal file
76
Telegram/SourceFiles/ui/effects/snowflakes.h
Normal file
|
@ -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<void(const QRect &r)> 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<Particle> _particles;
|
||||||
|
|
||||||
|
crl::time _nextBirthTime = 0;
|
||||||
|
bool _paused = false;
|
||||||
|
QBrush _brush;
|
||||||
|
|
||||||
|
QRect _rectToUpdate;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Ui
|
|
@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
#include "window/window_controller.h"
|
#include "window/window_controller.h"
|
||||||
#include "ui/chat/chat_theme.h"
|
#include "ui/chat/chat_theme.h"
|
||||||
#include "ui/controls/userpic_button.h"
|
#include "ui/controls/userpic_button.h"
|
||||||
|
#include "ui/effects/snowflakes.h"
|
||||||
#include "ui/widgets/buttons.h"
|
#include "ui/widgets/buttons.h"
|
||||||
#include "ui/widgets/labels.h"
|
#include "ui/widgets/labels.h"
|
||||||
#include "ui/widgets/popup_menu.h"
|
#include "ui/widgets/popup_menu.h"
|
||||||
|
@ -78,6 +79,20 @@ namespace {
|
||||||
|
|
||||||
constexpr auto kPlayStatusLimit = 2;
|
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::SessionController*> window) {
|
void ShowCallsBox(not_null<Window::SessionController*> window) {
|
||||||
auto controller = std::make_unique<Calls::BoxController>(window);
|
auto controller = std::make_unique<Calls::BoxController>(window);
|
||||||
const auto initBox = [
|
const auto initBox = [
|
||||||
|
@ -469,6 +484,38 @@ MainMenu::MainMenu(
|
||||||
}, lifetime());
|
}, lifetime());
|
||||||
|
|
||||||
initResetScaleButton();
|
initResetScaleButton();
|
||||||
|
|
||||||
|
if (CanCheckSpecialEvent() && CheckSpecialEvent()) {
|
||||||
|
const auto snowLifetime = lifetime().make_state<rpl::lifetime>();
|
||||||
|
const auto rebuild = [=] {
|
||||||
|
const auto snowRaw = Ui::CreateChild<Ui::RpWidget>(this);
|
||||||
|
const auto snow = snowLifetime->make_state<Ui::Snowflakes>(
|
||||||
|
[=](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;
|
MainMenu::~MainMenu() = default;
|
||||||
|
@ -812,39 +859,43 @@ void MainMenu::chooseEmojiStatus() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainMenu::paintEvent(QPaintEvent *e) {
|
void MainMenu::paintEvent(QPaintEvent *e) {
|
||||||
Painter p(this);
|
auto p = Painter(this);
|
||||||
const auto clip = e->rect();
|
const auto clip = e->rect();
|
||||||
const auto cover = QRect(0, 0, width(), st::mainMenuCoverHeight);
|
const auto cover = QRect(0, 0, width(), st::mainMenuCoverHeight);
|
||||||
|
|
||||||
p.fillRect(clip, st::mainMenuBg);
|
p.fillRect(clip, st::mainMenuBg);
|
||||||
if (cover.intersects(clip)) {
|
if (cover.intersects(clip)) {
|
||||||
const auto widthText = width()
|
drawName(p);
|
||||||
- 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::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() {
|
void MainMenu::initResetScaleButton() {
|
||||||
if (!window() || !window()->windowHandle()) {
|
if (!window() || !window()->windowHandle()) {
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -73,6 +73,8 @@ private:
|
||||||
void toggleAccounts();
|
void toggleAccounts();
|
||||||
void chooseEmojiStatus();
|
void chooseEmojiStatus();
|
||||||
|
|
||||||
|
void drawName(Painter &p);
|
||||||
|
|
||||||
const not_null<SessionController*> _controller;
|
const not_null<SessionController*> _controller;
|
||||||
object_ptr<Ui::UserpicButton> _userpicButton;
|
object_ptr<Ui::UserpicButton> _userpicButton;
|
||||||
Ui::Text::String _name;
|
Ui::Text::String _name;
|
||||||
|
|
|
@ -250,6 +250,8 @@ PRIVATE
|
||||||
ui/effects/round_checkbox.h
|
ui/effects/round_checkbox.h
|
||||||
ui/effects/scroll_content_shadow.cpp
|
ui/effects/scroll_content_shadow.cpp
|
||||||
ui/effects/scroll_content_shadow.h
|
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.cpp
|
||||||
ui/text/format_song_name.h
|
ui/text/format_song_name.h
|
||||||
ui/text/format_values.cpp
|
ui/text/format_values.cpp
|
||||||
|
|
Loading…
Add table
Reference in a new issue