From 9106714cc3f2bf7063773a4d19d0763d05c461ea Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Sat, 30 May 2026 13:39:31 +0300 Subject: [PATCH] Added particle cloud engine behind to premium cover top bar. --- .../ui/effects/premium_star_particles.cpp | 296 ++++++++++++++++++ .../ui/effects/premium_star_particles.h | 70 +++++ Telegram/cmake/td_ui.cmake | 2 + 3 files changed, 368 insertions(+) create mode 100644 Telegram/SourceFiles/ui/effects/premium_star_particles.cpp create mode 100644 Telegram/SourceFiles/ui/effects/premium_star_particles.h diff --git a/Telegram/SourceFiles/ui/effects/premium_star_particles.cpp b/Telegram/SourceFiles/ui/effects/premium_star_particles.cpp new file mode 100644 index 0000000000..37d95487c4 --- /dev/null +++ b/Telegram/SourceFiles/ui/effects/premium_star_particles.cpp @@ -0,0 +1,296 @@ +/* +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/premium_star_particles.h" + +#include "base/algorithm.h" +#include "ui/painter.h" +#include "ui/rect.h" +#include "ui/style/style_core.h" + +namespace Ui::Premium { +namespace { + +constexpr auto kParticleCount = 100; +constexpr auto kIdleLimit = 5; +constexpr auto kMinDelta = crl::time(4); +constexpr auto kMaxDelta = crl::time(50); +constexpr auto kLifeMin = crl::time(2000); +constexpr auto kLifeRand = 1000; +constexpr auto kAppearMs = 200.; +constexpr auto kFadeOutSpan = 150.; +constexpr auto kRadiusResolution = 1000; +constexpr auto kAngleSteps = 360; +constexpr auto kAlphaBasePercent = 50; +constexpr auto kAlphaRangePercent = 50; +constexpr auto kFatness = 0.98; +constexpr auto kSpriteAlpha = 200; +constexpr auto kBoundsFactor = 1.6; +constexpr auto kDriftDp = 4.; +constexpr auto kDriftDivisor = 660.; +constexpr auto kSoftScale = 0.66; +constexpr auto kMinSoftRatio = 2; +constexpr auto kRoundDp = 0.8; +constexpr auto kMsPerSecond = 1000.; +constexpr auto kDegreesToRadians = M_PI / 180.; +constexpr auto k2Pi = 2. * M_PI; +constexpr auto kRandomBuffer = 1024; + +constexpr auto kSizesDp = std::array{ 4., 12., 10. }; +constexpr auto kRotationDegPerSec = std::array{ 9., 7.2, 6. }; + +[[nodiscard]] QPainterPath StarPath(float64 size) { + const auto half = size / 2.; + const auto mid = half * kFatness; + auto path = QPainterPath(); + path.moveTo(0., half); + path.lineTo(mid, mid); + path.lineTo(half, 0.); + path.lineTo(size - mid, mid); + path.lineTo(size, half); + path.lineTo(size - mid, size - mid); + path.lineTo(half, size); + path.lineTo(mid, size - mid); + path.closeSubpath(); + return path; +} + +[[nodiscard]] float64 Overshoot(float64 t) { + constexpr auto kTension = 2.; + const auto u = t - 1.; + return u * u * ((kTension + 1.) * u + kTension) + 1.; +} + +} // namespace + +StarParticles::StarParticles(Fn update) +: _update(std::move(update)) +, _animation([=](crl::time now) { + if (++_idleCounter >= kIdleLimit) { + _animation.stop(); + return; + } + tick(now); + if (_rectToUpdate.isValid()) { + _update(base::take(_rectToUpdate)); + } +}) +, _random(kRandomBuffer) { +} + +void StarParticles::setColor(QColor color) { + if (_color == color) { + return; + } + _color = color; + _spritesDirty = true; +} + +void StarParticles::setPaused(bool paused) { + if (_paused == paused) { + return; + } + _paused = paused; + if (paused) { + _pausedAt = crl::now(); + _animation.stop(); + } else { + if (_pausedAt) { + const auto delta = crl::now() - _pausedAt; + for (auto &particle : _particles) { + particle.birthTime += delta; + particle.deathTime += delta; + } + _pausedAt = 0; + } + _lastTime = crl::now(); + } +} + +float64 StarParticles::driftStep(crl::time delta) const { + return style::ConvertScale(kDriftDp) * (delta / kDriftDivisor); +} + +void StarParticles::createParticle(crl::time now, Particle &particle) { + particle.radiusFactor = base::RandomIndex(kRadiusResolution, _random) + / float64(kRadiusResolution); + particle.angle = base::RandomIndex(kAngleSteps, _random) + * kDegreesToRadians; + particle.alpha = (kAlphaBasePercent + + base::RandomIndex(kAlphaRangePercent, _random)) / 100.; + particle.sizeIndex = base::RandomIndex(int(_sprites.size()), _random); + particle.birthTime = now; + particle.deathTime = now + + kLifeMin + + base::RandomIndex(kLifeRand, _random); + particle.distance = 0.; +} + +void StarParticles::ensureParticles() { + if (!_particles.empty()) { + return; + } + const auto now = crl::now(); + const auto perMs = driftStep(crl::time(1)); + _particles.resize(kParticleCount); + for (auto &particle : _particles) { + createParticle(now, particle); + const auto life = particle.deathTime - particle.birthTime; + const auto shift = base::RandomIndex( + int(std::max(life, 1)), + _random); + particle.birthTime = now - shift; + particle.deathTime = particle.birthTime + life; + particle.distance = perMs * shift; + } + _lastTime = now; +} + +void StarParticles::tick(crl::time now) { + ensureParticles(); + const auto delta = std::clamp(now - _lastTime, kMinDelta, kMaxDelta); + _lastTime = now; + + const auto radius = _field.isEmpty() + ? 0. + : std::min(_field.width(), _field.height()) / 2.; + const auto bound = radius * kBoundsFactor; + const auto step = driftStep(delta); + for (auto &particle : _particles) { + if (now >= particle.deathTime) { + createParticle(now, particle); + continue; + } + particle.distance += step; + if (radius > 0. + && (particle.radiusFactor * radius + particle.distance) > bound) { + createParticle(now, particle); + } + } + for (auto i = 0; i != int(_fieldAngle.size()); ++i) { + _fieldAngle[i] += kRotationDegPerSec[i] + * (delta / kMsPerSecond) + * kDegreesToRadians; + _fieldAngle[i] -= k2Pi * std::floor(_fieldAngle[i] / k2Pi); + } + + if (radius > 0.) { + const auto center = rect::center(_field); + const auto margin = bound + _maxSpriteExtent; + _rectToUpdate |= QRectF( + center - QPointF(margin, margin), + Size(2. * margin)).toAlignedRect(); + } +} + +void StarParticles::rebuildSprites(int ratio) { + _spritesDirty = false; + _spritesRatio = ratio; + const auto round = style::ConvertScale(kRoundDp) * ratio; + const auto pad = int(base::SafeRound(round)) + ratio; + auto fill = _color; + fill.setAlpha(kSpriteAlpha); + _maxSpriteExtent = 0.; + for (auto i = 0; i != int(_sprites.size()); ++i) { + const auto side = std::max( + 1, + int(base::SafeRound(style::ConvertScale(kSizesDp[i]) * ratio))); + const auto full = side + 2 * pad; + auto image = QImage(full, full, QImage::Format_ARGB32_Premultiplied); + image.fill(Qt::transparent); + { + auto p = QPainter(&image); + auto hq = PainterHighQualityEnabler(p); + p.translate(pad, pad); + auto pen = QPen(fill); + pen.setWidthF(round * 2.); + pen.setJoinStyle(Qt::RoundJoin); + pen.setCapStyle(Qt::RoundCap); + p.setPen(pen); + p.setBrush(fill); + p.drawPath(StarPath(side)); + } + const auto soft = std::max( + 1, + int(base::SafeRound(full * kSoftScale))); + image = image.scaled( + soft, + soft, + Qt::IgnoreAspectRatio, + Qt::SmoothTransformation + ).scaled( + full, + full, + Qt::IgnoreAspectRatio, + Qt::SmoothTransformation); + image.setDevicePixelRatio(ratio); + const auto extent = full / float64(ratio); + _sprites[i] = Sprite{ + .image = std::move(image), + .size = Size(extent), + }; + _maxSpriteExtent = std::max(_maxSpriteExtent, extent); + } +} + +void StarParticles::paint(QPainter &p, const QRectF &field) { + if (!_color.isValid() || field.isEmpty()) { + return; + } + ensureParticles(); + _idleCounter = 0; + const auto ratio = std::max( + int(std::ceil(p.device()->devicePixelRatioF())), + kMinSoftRatio); + if (_spritesDirty || _spritesRatio != ratio) { + rebuildSprites(ratio); + } + if (!_paused && !_animation.animating()) { + _lastTime = crl::now(); + _animation.start(); + } + _field = field; + + const auto center = rect::center(field); + const auto radius = std::min(field.width(), field.height()) / 2.; + const auto now = (_paused && _pausedAt) ? _pausedAt : crl::now(); + const auto baseOpacity = p.opacity(); + for (const auto &particle : _particles) { + const auto age = now - particle.birthTime; + const auto remaining = particle.deathTime - now; + if (age < 0 || remaining <= 0) { + continue; + } + const auto appear = std::clamp(age / kAppearMs, 0., 1.); + const auto scale = Overshoot(appear); + if (scale <= 0.) { + continue; + } + const auto fade = std::clamp(remaining / kFadeOutSpan, 0., 1.); + const auto alpha = particle.alpha * fade; + const auto radial = particle.radiusFactor * radius + + particle.distance; + const auto theta = particle.angle + _fieldAngle[particle.sizeIndex]; + const auto position = QPointF( + center.x() + radial * std::cos(theta), + center.y() + radial * std::sin(theta)); + const auto &sprite = _sprites[particle.sizeIndex]; + const auto width = sprite.size.width() * scale; + const auto height = sprite.size.height() * scale; + p.setOpacity(baseOpacity * alpha); + p.drawImage( + QRectF( + position.x() - width / 2., + position.y() - height / 2., + width, + height), + sprite.image); + } + p.setOpacity(baseOpacity); +} + +} // namespace Ui::Premium diff --git a/Telegram/SourceFiles/ui/effects/premium_star_particles.h b/Telegram/SourceFiles/ui/effects/premium_star_particles.h new file mode 100644 index 0000000000..b3b2cdac1f --- /dev/null +++ b/Telegram/SourceFiles/ui/effects/premium_star_particles.h @@ -0,0 +1,70 @@ +/* +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" +#include "base/random.h" + +namespace Ui::Premium { + +class StarParticles final { +public: + explicit StarParticles(Fn update); + + void setColor(QColor color); + void paint(QPainter &p, const QRectF &field); + void setPaused(bool paused); + +private: + struct Particle { + crl::time birthTime = 0; + crl::time deathTime = 0; + float64 angle = 0.; + float64 radiusFactor = 0.; + float64 distance = 0.; + float64 alpha = 0.; + int sizeIndex = 0; + }; + struct Sprite { + QImage image; + QSizeF size; + }; + + void ensureParticles(); + void createParticle(crl::time now, Particle &particle); + void rebuildSprites(int ratio); + void tick(crl::time now); + [[nodiscard]] float64 driftStep(crl::time delta) const; + + const Fn _update; + + std::array _sprites; + std::vector _particles; + + Ui::Animations::Basic _animation; + base::BufferedRandom _random; + + QColor _color; + float64 _maxSpriteExtent = 0.; + bool _spritesDirty = true; + int _spritesRatio = 0; + + std::array _fieldAngle = { { 0., 0., 0. } }; + QRectF _field; + + crl::time _lastTime = 0; + crl::time _pausedAt = 0; + + bool _paused = false; + uint8_t _idleCounter = 0; + + QRect _rectToUpdate; + +}; + +} // namespace Ui::Premium diff --git a/Telegram/cmake/td_ui.cmake b/Telegram/cmake/td_ui.cmake index 536c1b9d3a..eae2bdfdfd 100644 --- a/Telegram/cmake/td_ui.cmake +++ b/Telegram/cmake/td_ui.cmake @@ -491,6 +491,8 @@ PRIVATE ui/effects/premium_star.h ui/effects/premium_star_model.cpp ui/effects/premium_star_model.h + ui/effects/premium_star_particles.cpp + ui/effects/premium_star_particles.h ui/effects/premium_star_renderer.cpp ui/effects/premium_star_renderer.h ui/effects/premium_stars.cpp