Files
tdesktop/Telegram/SourceFiles/ui/effects/drifting_particles.cpp

348 lines
9.9 KiB
C++

/*
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/drifting_particles.h"
#include "ui/painter.h"
#include "ui/rect.h"
#include <QtCore/QtMath>
namespace Ui {
namespace {
constexpr auto kMinLifeTime = crl::time(2000);
constexpr auto kRandLifeTime = 1000;
constexpr auto kFadeOutTime = crl::time(200);
constexpr auto kFadeOutDuration = 150.;
constexpr auto kAppearDuration = 200.;
constexpr auto kSpeedDivider = 660.;
constexpr auto kMinDelta = crl::time(4);
constexpr auto kMaxDelta = crl::time(50);
constexpr auto kOvershootTension = 2.;
constexpr auto kDistributionTries = 10;
constexpr auto kRandomRotateDegrees = 45;
constexpr auto kOrbitPeriod = 40000.;
constexpr auto kOrbitPeriodStep = 10000.;
[[nodiscard]] float64 Overshoot(float64 progress) {
const auto t = progress - 1.;
return t * t * ((kOvershootTension + 1.) * t + kOvershootTension) + 1.;
}
[[nodiscard]] QPainterPath FourPointStarPath(
int size,
float64 corner,
int ratio) {
const auto half = (size * ratio / 2) / float64(ratio);
const auto mid = int(half * ratio * corner) / float64(ratio);
auto result = QPainterPath();
result.moveTo(0, half);
result.lineTo(mid, mid);
result.lineTo(half, 0);
result.lineTo(size - mid, mid);
result.lineTo(size, half);
result.lineTo(size - mid, size - mid);
result.lineTo(half, size);
result.lineTo(mid, size - mid);
result.closeSubpath();
return result;
}
[[nodiscard]] QPainterPath RoundCorners(
const QPainterPath &path,
float64 radius) {
auto points = std::vector<QPointF>();
for (auto i = 0; i != path.elementCount(); ++i) {
const auto element = path.elementAt(i);
if (element.type == QPainterPath::CurveToElement
|| element.type == QPainterPath::CurveToDataElement) {
return path;
}
points.push_back(QPointF(element.x, element.y));
}
const auto count = int(points.size());
if (count < 3) {
return path;
}
const auto cut = [&](QPointF from, QPointF to) {
const auto delta = to - from;
const auto length = std::hypot(delta.x(), delta.y());
return (length > 0.)
? (from + delta * (std::min(radius, length / 2.) / length))
: from;
};
auto result = QPainterPath();
for (auto i = 0; i != count; ++i) {
const auto previous = points[(i + count - 1) % count];
const auto current = points[i];
const auto next = points[(i + 1) % count];
if (!i) {
result.moveTo(cut(current, previous));
} else {
result.lineTo(cut(current, previous));
}
result.quadTo(current, cut(current, next));
}
result.closeSubpath();
return result;
}
void BlurCoverage(
std::vector<int> &values,
int width,
int height,
int radius) {
const auto div = 2 * radius + 1;
auto blurred = std::vector<int>(values.size());
for (auto y = 0; y != height; ++y) {
for (auto x = 0; x != width; ++x) {
auto sum = 0;
for (auto i = -radius; i <= radius; ++i) {
sum += values[y * width + std::clamp(x + i, 0, width - 1)];
}
blurred[y * width + x] = sum / div;
}
}
for (auto x = 0; x != width; ++x) {
for (auto y = 0; y != height; ++y) {
auto sum = 0;
for (auto i = -radius; i <= radius; ++i) {
sum += blurred[std::clamp(y + i, 0, height - 1) * width + x];
}
values[y * width + x] = sum / div;
}
}
}
} // namespace
QImage FourPointStarImage(FourPointStarArgs args) {
const auto ratio = style::DevicePixelRatio();
auto result = QImage(
Size(args.size) * ratio,
QImage::Format_ARGB32_Premultiplied);
result.setDevicePixelRatio(ratio);
result.fill(Qt::transparent);
auto path = FourPointStarPath(args.size, args.corner, ratio);
if (args.cornerRadius > 0.) {
path = RoundCorners(path, args.cornerRadius);
}
{
auto p = QPainter(&result);
auto hq = PainterHighQualityEnabler(p);
p.setPen(Qt::NoPen);
p.setBrush(Qt::white);
p.drawPath(path);
}
const auto width = result.width();
const auto height = result.height();
auto coverage = std::vector<int>(width * height);
for (auto y = 0; y != height; ++y) {
const auto line = reinterpret_cast<const uint32*>(
result.constScanLine(y));
for (auto x = 0; x != width; ++x) {
coverage[y * width + x] = qAlpha(line[x]);
}
}
if (args.blurRadius > 0) {
BlurCoverage(coverage, width, height, args.blurRadius);
}
const auto red = args.color.red();
const auto green = args.color.green();
const auto blue = args.color.blue();
const auto alpha = args.color.alpha();
for (auto y = 0; y != height; ++y) {
const auto line = reinterpret_cast<uint32*>(result.scanLine(y));
for (auto x = 0; x != width; ++x) {
const auto value = coverage[y * width + x] * alpha / 255;
line[x] = qRgba(
red * value / 255,
green * value / 255,
blue * value / 255,
value);
}
}
return result;
}
DriftingParticles::DriftingParticles(Config config)
: _config(std::move(config))
, _particles(_config.count)
, _orbitAngles(_config.sprites.size(), 0.) {
}
void DriftingParticles::setGeometry(
QRectF field,
QRectF bounds,
QRectF exclude) {
if (_field == field && _bounds == bounds && _exclude == exclude) {
return;
}
_field = field;
_bounds = bounds;
_exclude = exclude;
const auto now = crl::now() - _pauseOffset;
for (auto &particle : _particles) {
generate(particle, now);
}
}
void DriftingParticles::setPaused(bool paused) {
if (paused) {
if (!_pausedAt) {
_pausedAt = crl::now();
}
} else if (_pausedAt) {
_pauseOffset += crl::now() - _pausedAt;
_pausedAt = 0;
}
}
QPointF DriftingParticles::generatePosition() {
if (_config.spread) {
auto bestDistance = 0.;
auto best = QPointF(
_random.value(_field.left(), _field.right()),
_random.value(_field.top(), _field.bottom()));
for (auto i = 0; i != kDistributionTries; ++i) {
const auto candidate = QPointF(
_random.value(_field.left(), _field.right()),
_random.value(_field.top(), _field.bottom()));
auto minDistance = std::numeric_limits<float64>::max();
for (const auto &particle : _particles) {
const auto dx = particle.x - candidate.x();
const auto dy = particle.y - candidate.y();
minDistance = std::min(minDistance, dx * dx + dy * dy);
}
if (minDistance > bestDistance) {
bestDistance = minDistance;
best = candidate;
}
}
return best;
} else if (_config.spawnInCircle) {
const auto exclude = _config.excludeRadius;
const auto radius = exclude
+ _random.value() * (_field.width() - exclude);
const auto angle = _random.value() * 2. * M_PI;
const auto center = rect::center(_field);
return QPointF(
center.x() + radius * std::sin(angle),
center.y() + radius * std::cos(angle));
}
return QPointF(
_random.value(_field.left(), _field.right()),
_random.value(_field.top(), _field.bottom()));
}
void DriftingParticles::generate(Particle &particle, crl::time now) {
const auto index = _random.index(int(_config.sprites.size()));
const auto &sprite = _config.sprites[index];
particle.sprite = index;
particle.bornTime = now;
particle.lifeTime = now + kMinLifeTime + _random.index(kRandLifeTime);
particle.rotate = sprite.randomRotate
? _random.value(-kRandomRotateDegrees, kRandomRotateDegrees)
: 0.;
const auto position = generatePosition();
particle.x = position.x();
particle.y = position.y();
const auto center = rect::center(_field);
const auto angle = std::atan2(
particle.y - center.y(),
particle.x - center.x());
particle.vecX = std::cos(angle);
particle.vecY = std::sin(angle);
particle.alpha = int(base::SafeRound(
_random.value(0.5, 1.) * sprite.maxAlpha));
}
QPointF DriftingParticles::drawingPosition(const Particle &particle) const {
if (!_config.orbit) {
return QPointF(particle.x, particle.y);
}
const auto center = rect::center(_field);
const auto radians = _orbitAngles[particle.sprite] * M_PI / 180.;
const auto cosine = std::cos(radians);
const auto sine = std::sin(radians);
const auto dx = particle.x - center.x();
const auto dy = particle.y - center.y();
return QPointF(
center.x() + dx * cosine - dy * sine,
center.y() + dx * sine + dy * cosine);
}
void DriftingParticles::paint(QPainter &p) {
if (_field.isEmpty()) {
return;
}
const auto paused = (_pausedAt != 0);
const auto now = (paused ? _pausedAt : crl::now()) - _pauseOffset;
const auto delta = float64(
std::clamp(now - _lastTime, kMinDelta, kMaxDelta));
_lastTime = now;
if (_config.orbit && !paused) {
for (auto i = 0; i != int(_orbitAngles.size()); ++i) {
_orbitAngles[i] += 360.
* (delta / (kOrbitPeriod + i * kOrbitPeriodStep));
}
}
const auto speed = _config.speed * (delta / kSpeedDivider);
auto hq = PainterHighQualityEnabler(p);
const auto opacity = p.opacity();
for (auto &particle : _particles) {
const auto position = drawingPosition(particle);
if (!_exclude.contains(position)) {
const auto left = particle.lifeTime - now;
const auto out = (left < kFadeOutTime)
? std::clamp(1. - (left / kFadeOutDuration), 0., 1.)
: 0.;
const auto appear = std::clamp(
(now - particle.bornTime) / kAppearDuration,
0.,
1.);
const auto scale = (appear < 1.) ? Overshoot(appear) : 1.;
const auto &image = _config.sprites[particle.sprite].image;
const auto size = image.size() / image.devicePixelRatio();
const auto target = QRectF(
position.x() - size.width() / 2.,
position.y() - size.height() / 2.,
size.width(),
size.height());
p.setOpacity(opacity * (particle.alpha / 255.) * (1. - out));
if (scale == 1. && !particle.rotate) {
p.drawImage(target.topLeft(), image);
} else {
p.save();
p.translate(position);
p.scale(scale, scale);
p.rotate(particle.rotate);
p.translate(-position);
p.drawImage(target, image);
p.restore();
}
}
if (!paused) {
particle.x += particle.vecX * speed;
particle.y += particle.vecY * speed;
}
if (now > particle.lifeTime
|| (_config.checkBounds && !_bounds.contains(position))) {
generate(particle, now);
}
}
p.setOpacity(opacity);
}
} // namespace Ui