1 /*
2 This file is part of Telegram Desktop,
3 the official desktop application for the Telegram messaging service.
4
5 For license and copyright information please follow this link:
6 https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
7 */
8 #include "ui/effects/fireworks_animation.h"
9
10 #include "base/random.h"
11
12 namespace Ui {
13 namespace {
14
15 constexpr auto kParticlesCount = 60;
16 constexpr auto kFallCount = 30;
17 constexpr auto kFirstUpdateTime = crl::time(16);
18 constexpr auto kFireworkWidth = 480;
19 constexpr auto kFireworkHeight = 320;
20
Brush(int color)21 QBrush Brush(int color) {
22 return QBrush{ QColor(
23 color & 0xFF,
24 (color >> 8) & 0xFF,
25 (color >> 16) & 0xFF)
26 };
27 }
28
PrepareBrushes()29 std::vector<QBrush> PrepareBrushes() {
30 return {
31 Brush(0xff2CBCE8),
32 Brush(0xff9E04D0),
33 Brush(0xffFECB02),
34 Brush(0xffFD2357),
35 Brush(0xff278CFE),
36 Brush(0xff59B86C),
37 };
38 }
39
RandomFloat01()40 [[nodiscard]] float64 RandomFloat01() {
41 return base::RandomValue<uint32>()
42 / float64(std::numeric_limits<uint32>::max());
43 }
44
45 } // namespace
46
FireworksAnimation(Fn<void ()> repaint)47 FireworksAnimation::FireworksAnimation(Fn<void()> repaint)
48 : _brushes(PrepareBrushes())
49 , _animation([=](crl::time now) { update(now); })
50 , _repaint(std::move(repaint)) {
51 _smallSide = style::ConvertScale(2);
52 _particles.reserve(kParticlesCount + kFallCount);
53 for (auto i = 0; i != kParticlesCount; ++i) {
54 initParticle(_particles.emplace_back(), false);
55 }
56 _animation.start();
57 }
58
update(crl::time now)59 void FireworksAnimation::update(crl::time now) {
60 const auto passed = _lastUpdate ? (now - _lastUpdate) : kFirstUpdateTime;
61 _lastUpdate = now;
62 auto allFinished = true;
63 for (auto &particle : _particles) {
64 updateParticle(particle, passed);
65 if (!particle.finished) {
66 allFinished = false;
67 }
68 }
69 if (allFinished) {
70 _animation.stop();
71 } else if (_fallingDown >= kParticlesCount / 2 && _speedCoef > 0.2) {
72 startFall();
73 _speedCoef -= passed / 16.0 * 0.15;
74 if (_speedCoef < 0.2) {
75 _speedCoef = 0.2;
76 }
77 }
78 _repaint();
79 }
80
paint(QPainter & p,const QRect & rect)81 bool FireworksAnimation::paint(QPainter &p, const QRect &rect) {
82 if (rect.isEmpty()) {
83 return false;
84 }
85 PainterHighQualityEnabler hq(p);
86 p.setPen(Qt::NoPen);
87 p.setClipRect(rect);
88 for (auto &particle : _particles) {
89 if (!particle.finished) {
90 paintParticle(p, particle, rect);
91 }
92 }
93 p.setClipping(false);
94 return _animation.animating();
95 }
96
paintParticle(QPainter & p,const Particle & particle,const QRect & rect)97 void FireworksAnimation::paintParticle(
98 QPainter &p,
99 const Particle &particle,
100 const QRect &rect) {
101 const auto size = particle.size;
102 const auto x = rect.x() + (particle.x * rect.width() / kFireworkWidth);
103 const auto y = rect.y() + (particle.y * rect.height() / kFireworkHeight);
104 p.setBrush(_brushes[particle.color]);
105 if (particle.type == Particle::Type::Circle) {
106 p.drawEllipse(x, y, size, size);
107 } else {
108 const auto rect = QRect(-size, -_smallSide, size, _smallSide);
109 p.save();
110 p.translate(x, y);
111 p.rotate(particle.rotation);
112 p.drawRoundedRect(rect, _smallSide, _smallSide);
113 p.restore();
114 }
115 }
116
updateParticle(Particle & particle,crl::time dt)117 void FireworksAnimation::updateParticle(Particle &particle, crl::time dt) {
118 if (particle.finished) {
119 return;
120 }
121 const auto moveCoef = dt / 16.;
122 particle.x += particle.moveX * moveCoef;
123 particle.y += particle.moveY * moveCoef;
124 if (particle.xFinished != 0) {
125 const auto dp = 0.5;
126 if (particle.xFinished == 1) {
127 particle.moveX += dp * moveCoef * 0.05;
128 if (particle.moveX >= dp) {
129 particle.xFinished = 2;
130 }
131 } else {
132 particle.moveX -= dp * moveCoef * 0.05f;
133 if (particle.moveX <= -dp) {
134 particle.xFinished = 1;
135 }
136 }
137 } else {
138 if (particle.right) {
139 if (particle.moveX < 0) {
140 particle.moveX += moveCoef * 0.05f;
141 if (particle.moveX >= 0) {
142 particle.moveX = 0;
143 particle.xFinished = particle.finishedStart;
144 }
145 }
146 } else {
147 if (particle.moveX > 0) {
148 particle.moveX -= moveCoef * 0.05f;
149 if (particle.moveX <= 0) {
150 particle.moveX = 0;
151 particle.xFinished = particle.finishedStart;
152 }
153 }
154 }
155 }
156 const auto yEdge = -0.5;
157 const auto wasNegative = (particle.moveY < yEdge);
158 if (particle.moveY > yEdge) {
159 particle.moveY += (1. / 3.) * moveCoef * _speedCoef;
160 } else {
161 particle.moveY += (1. / 3.) * moveCoef;
162 }
163 if (wasNegative && particle.moveY > yEdge) {
164 ++_fallingDown;
165 }
166 if (particle.type == Particle::Type::Rectangle) {
167 particle.rotation += moveCoef * 10;
168 if (particle.rotation > 360) {
169 particle.rotation -= 360;
170 }
171 }
172 if (particle.y >= kFireworkHeight) {
173 particle.finished = true;
174 }
175 }
176
startFall()177 void FireworksAnimation::startFall() {
178 if (_startedFall) {
179 return;
180 }
181 _startedFall = true;
182 for (auto i = 0; i != kFallCount; ++i) {
183 initParticle(_particles.emplace_back(), true);
184 }
185 }
186
initParticle(Particle & particle,bool falling)187 void FireworksAnimation::initParticle(Particle &particle, bool falling) {
188 using Type = Particle::Type;
189 using base::RandomIndex;
190
191 particle.color = RandomIndex(_brushes.size());
192 particle.type = RandomIndex(2) ? Type::Rectangle : Type::Circle;
193 particle.right = (RandomIndex(2) == 1);
194 particle.finishedStart = 1 + RandomIndex(2);
195 if (particle.type == Type::Circle) {
196 particle.size = style::ConvertScale(6 + RandomFloat01() * 3);
197 } else {
198 particle.size = style::ConvertScale(6 + RandomFloat01() * 6);
199 particle.rotation = RandomIndex(360);
200 }
201 if (falling) {
202 particle.y = -RandomFloat01() * kFireworkHeight * 1.2f;
203 particle.x = 5 + RandomIndex(kFireworkWidth - 10);
204 particle.xFinished = particle.finishedStart;
205 } else {
206 const auto xOffset = 4 + RandomIndex(10);
207 const auto yOffset = kFireworkHeight / 4;
208 if (particle.right) {
209 particle.x = kFireworkWidth + xOffset;
210 } else {
211 particle.x = -xOffset;
212 }
213 particle.moveX = (particle.right ? -1 : 1) * (1.2 + RandomFloat01() * 4);
214 particle.moveY = -(4 + RandomFloat01() * 4);
215 particle.y = yOffset / 2 + RandomIndex(yOffset * 2);
216 }
217 }
218
219 } // namespace Ui
220