/* 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