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