Add mini-copies animation for custom reactions.

This commit is contained in:
John Preston 2022-09-01 16:49:25 +04:00
parent 7d77e8a203
commit a256eb4bc8
4 changed files with 167 additions and 30 deletions

View file

@ -389,13 +389,15 @@ void BottomInfo::paintReactions(
widthLeft -= width + add; widthLeft -= width + add;
} }
if (!animations.empty()) { if (!animations.empty()) {
const auto now = context.now;
context.reactionInfo->effectPaint = [=](QPainter &p) { context.reactionInfo->effectPaint = [=](QPainter &p) {
auto result = QRect(); auto result = QRect();
for (const auto &single : animations) { for (const auto &single : animations) {
const auto area = single.animation->paintGetArea( const auto area = single.animation->paintGetArea(
p, p,
origin, origin,
single.target); single.target,
now);
result = result.isEmpty() ? area : result.united(area); result = result.isEmpty() ? area : result.united(area);
} }
return result; return result;

View file

@ -452,13 +452,15 @@ void InlineList::paint(
} }
} }
if (!animations.empty()) { if (!animations.empty()) {
const auto now = context.now;
context.reactionInfo->effectPaint = [=](QPainter &p) { context.reactionInfo->effectPaint = [=](QPainter &p) {
auto result = QRect(); auto result = QRect();
for (const auto &single : animations) { for (const auto &single : animations) {
const auto area = single.animation->paintGetArea( const auto area = single.animation->paintGetArea(
p, p,
QPoint(), QPoint(),
single.target); single.target,
now);
result = result.isEmpty() ? area : result.united(area); result = result.isEmpty() ? area : result.united(area);
} }
return result; return result;

View file

@ -15,12 +15,20 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_session.h" #include "data/data_session.h"
#include "data/data_document.h" #include "data/data_document.h"
#include "data/data_document_media.h" #include "data/data_document_media.h"
#include "base/random.h"
#include "styles/style_chat.h" #include "styles/style_chat.h"
namespace HistoryView::Reactions { namespace HistoryView::Reactions {
namespace { namespace {
constexpr auto kFlyDuration = crl::time(300); constexpr auto kFlyDuration = crl::time(300);
constexpr auto kMiniCopies = 7;
constexpr auto kMiniCopiesDurationMax = crl::time(1400);
constexpr auto kMiniCopiesDurationMin = crl::time(700);
constexpr auto kMiniCopiesScaleInDuration = crl::time(200);
constexpr auto kMiniCopiesScaleOutDuration = crl::time(200);
constexpr auto kMiniCopiesMaxScaleMin = 0.6;
constexpr auto kMiniCopiesMaxScaleMax = 0.9;
} // namespace } // namespace
@ -61,6 +69,7 @@ Animation::Animation(
const auto data = &owner->owner(); const auto data = &owner->owner();
const auto document = data->document(customId); const auto document = data->document(customId);
_custom = data->customEmojiManager().create(document, callback()); _custom = data->customEmojiManager().create(document, callback());
_customSize = centerIconSize;
aroundAnimation = owner->chooseGenericAnimation(document); aroundAnimation = owner->chooseGenericAnimation(document);
} else { } else {
const auto i = ranges::find(list, args.id, &::Data::Reaction::id); const auto i = ranges::find(list, args.id, &::Data::Reaction::id);
@ -91,10 +100,11 @@ Animation::Animation(
return; return;
} }
resolve(_effect, aroundAnimation, size * 2); resolve(_effect, aroundAnimation, size * 2);
generateMiniCopies(size + size / 2);
if (!args.flyIcon.isNull()) { if (!args.flyIcon.isNull()) {
_flyIcon = std::move(args.flyIcon); _flyIcon = std::move(args.flyIcon);
_fly.start(flyCallback(), 0., 1., kFlyDuration); _fly.start(flyCallback(), 0., 1., kFlyDuration);
} else if (!_center && !_effect) { } else if (!_center && !_effect && _miniCopies.empty()) {
return; return;
} else { } else {
startAnimations(); startAnimations();
@ -108,16 +118,22 @@ Animation::~Animation() = default;
QRect Animation::paintGetArea( QRect Animation::paintGetArea(
QPainter &p, QPainter &p,
QPoint origin, QPoint origin,
QRect target) const { QRect target,
crl::time now) const {
if (_flyIcon.isNull()) { if (_flyIcon.isNull()) {
paintCenterFrame(p, target); paintCenterFrame(p, target, now);
const auto wide = QRect( const auto wide = QRect(
target.topLeft() - QPoint(target.width(), target.height()) / 2, target.topLeft() - QPoint(target.width(), target.height()) / 2,
target.size() * 2); target.size() * 2);
if (const auto effect = _effect.get()) { if (const auto effect = _effect.get()) {
p.drawImage(wide, effect->frame()); p.drawImage(wide, effect->frame());
} }
return wide; paintMiniCopies(p, target.center(), now);
return _miniCopies.empty()
? wide
: QRect(
target.topLeft() - QPoint(target.width(), target.height()),
target.size() * 3);
} }
const auto from = _flyFrom.translated(origin); const auto from = _flyFrom.translated(origin);
const auto lshift = target.width() / 4; const auto lshift = target.width() / 4;
@ -127,7 +143,12 @@ QRect Animation::paintGetArea(
const auto progress = _fly.value(1.); const auto progress = _fly.value(1.);
const auto rect = QRect( const auto rect = QRect(
anim::interpolate(from.x(), target.x(), progress), anim::interpolate(from.x(), target.x(), progress),
computeParabolicTop(from.y(), target.y(), progress), computeParabolicTop(
_cached,
from.y(),
target.y(),
st::reactionFlyUp,
progress),
anim::interpolate(from.width(), target.width(), progress), anim::interpolate(from.width(), target.width(), progress),
anim::interpolate(from.height(), target.height(), progress)); anim::interpolate(from.height(), target.height(), progress));
const auto wide = rect.marginsAdded(margins); const auto wide = rect.marginsAdded(margins);
@ -137,13 +158,16 @@ QRect Animation::paintGetArea(
} }
if (progress > 0.) { if (progress > 0.) {
p.setOpacity(progress); p.setOpacity(progress);
paintCenterFrame(p, wide); paintCenterFrame(p, wide, now);
} }
p.setOpacity(1.); p.setOpacity(1.);
return wide; return wide;
} }
void Animation::paintCenterFrame(QPainter &p, QRect target) const { void Animation::paintCenterFrame(
QPainter &p,
QRect target,
crl::time now) const {
Expects(_center || _custom); Expects(_center || _custom);
const auto size = QSize( const auto size = QSize(
@ -157,24 +181,103 @@ void Animation::paintCenterFrame(QPainter &p, QRect target) const {
size.height()); size.height());
p.drawImage(rect, _center->frame()); p.drawImage(rect, _center->frame());
} else { } else {
const auto side = Ui::Text::AdjustCustomEmojiSize(st::emojiSize); const auto scaled = (size.width() != _customSize);
const auto scaled = (size.width() != side);
_custom->paint(p, { _custom->paint(p, {
.preview = Qt::transparent, .preview = QColor(0, 0, 0, 0),
.size = { side, side }, .size = { _customSize, _customSize },
.now = crl::now(), .now = now,
.scale = (scaled ? (size.width() / float64(side)) : 1.), .scale = (scaled ? (size.width() / float64(_customSize)) : 1.),
.position = QPoint( .position = QPoint(
target.x() + (target.width() - side) / 2, target.x() + (target.width() - _customSize) / 2,
target.y() + (target.height() - side) / 2), target.y() + (target.height() - _customSize) / 2),
.scaled = scaled, .scaled = scaled,
}); });
} }
} }
void Animation::paintMiniCopies(
QPainter &p,
QPoint center,
crl::time now) const {
Expects(_miniCopies.empty() || _custom != nullptr);
if (!_minis.animating()) {
return;
}
auto hq = PainterHighQualityEnabler(p);
const auto size = QSize(_customSize, _customSize);
const auto preview = QColor(0, 0, 0, 0);
const auto progress = _minis.value(1.);
const auto middle = center - QPoint(_customSize / 2, _customSize / 2);
const auto scaleIn = kMiniCopiesScaleInDuration
/ float64(kMiniCopiesDurationMax);
const auto scaleOut = kMiniCopiesScaleOutDuration
/ float64(kMiniCopiesDurationMax);
auto context = Ui::Text::CustomEmoji::Context{
.preview = preview,
.size = size,
.now = now,
.scaled = true,
};
for (const auto &mini : _miniCopies) {
if (progress >= mini.duration) {
continue;
}
const auto value = progress / mini.duration;
context.scale = (progress < scaleIn)
? (mini.maxScale * progress / scaleIn)
: (progress <= mini.duration - scaleOut)
? mini.maxScale
: (mini.maxScale * (mini.duration - progress) / scaleOut);
context.position = middle + QPoint(
anim::interpolate(0, mini.finalX, value),
computeParabolicTop(
mini.cached,
0,
mini.finalY,
mini.flyUp,
value));
_custom->paint(p, context);
}
}
void Animation::generateMiniCopies(int size) {
if (!_custom) {
return;
}
const auto random = [] {
constexpr auto count = 16384;
return base::RandomIndex(count) / float64(count - 1);
};
const auto between = [](int a, int b) {
return (a > b)
? (b + base::RandomIndex(a - b + 1))
: (a + base::RandomIndex(b - a + 1));
};
_miniCopies.reserve(kMiniCopies);
for (auto i = 0; i != kMiniCopies; ++i) {
const auto maxScale = kMiniCopiesMaxScaleMin
+ (kMiniCopiesMaxScaleMax - kMiniCopiesMaxScaleMin) * random();
const auto duration = between(
kMiniCopiesDurationMin,
kMiniCopiesDurationMax);
const auto maxSize = int(std::ceil(maxScale * _customSize));
const auto maxHalf = (maxSize + 1) / 2;
_miniCopies.push_back({
.maxScale = maxScale,
.duration = duration / float64(kMiniCopiesDurationMax),
.flyUp = between(size / 4, size - maxHalf),
.finalX = between(-size, size),
.finalY = between(size - (size / 4), size),
});
}
}
int Animation::computeParabolicTop( int Animation::computeParabolicTop(
Parabolic &cache,
int from, int from,
int to, int to,
int top,
float64 progress) const { float64 progress) const {
const auto t = progress; const auto t = progress;
@ -189,8 +292,8 @@ int Animation::computeParabolicTop(
// b = 2 * t_0 * y_1 / (2 * t_0 - 1) // b = 2 * t_0 * y_1 / (2 * t_0 - 1)
// t_0 = (y_0 / y_1) +- sqrt((y_0 / y_1) * (y_0 / y_1 - 1)) // t_0 = (y_0 / y_1) +- sqrt((y_0 / y_1) * (y_0 / y_1 - 1))
const auto y_1 = to - from; const auto y_1 = to - from;
if (_cachedKey != y_1) { if (cache.key != y_1) {
const auto y_0 = std::min(0, y_1) - st::reactionFlyUp; const auto y_0 = std::min(0, y_1) - top;
const auto ratio = y_1 ? (float64(y_0) / y_1) : 0.; const auto ratio = y_1 ? (float64(y_0) / y_1) : 0.;
const auto root = y_1 ? sqrt(ratio * (ratio - 1)) : 0.; const auto root = y_1 ? sqrt(ratio * (ratio - 1)) : 0.;
const auto t_0 = !y_1 const auto t_0 = !y_1
@ -200,12 +303,12 @@ int Animation::computeParabolicTop(
: (ratio - root); : (ratio - root);
const auto a = y_1 ? (y_1 / (1 - 2 * t_0)) : (-4 * y_0); const auto a = y_1 ? (y_1 / (1 - 2 * t_0)) : (-4 * y_0);
const auto b = y_1 - a; const auto b = y_1 - a;
_cachedKey = y_1; cache.key = y_1;
_cachedA = a; cache.a = a;
_cachedB = b; cache.b = b;
} }
return int(base::SafeRound(_cachedA * t * t + _cachedB * t + from)); return int(base::SafeRound(cache.a * t * t + cache.b * t + from));
} }
void Animation::startAnimations() { void Animation::startAnimations() {
@ -215,6 +318,9 @@ void Animation::startAnimations() {
if (const auto effect = _effect.get()) { if (const auto effect = _effect.get()) {
_effect->animate(callback()); _effect->animate(callback());
} }
if (!_miniCopies.empty()) {
_minis.start(callback(), 0., 1., kMiniCopiesDurationMax);
}
} }
void Animation::setRepaintCallback(Fn<void()> repaint) { void Animation::setRepaintCallback(Fn<void()> repaint) {
@ -233,7 +339,8 @@ bool Animation::finished() const {
return !_valid return !_valid
|| (_flyIcon.isNull() || (_flyIcon.isNull()
&& (!_center || !_center->animating()) && (!_center || !_center->animating())
&& (!_effect || !_effect->animating())); && (!_effect || !_effect->animating())
&& !_minis.animating());
} }
} // namespace HistoryView::Reactions } // namespace HistoryView::Reactions

View file

@ -37,18 +37,43 @@ public:
~Animation(); ~Animation();
void setRepaintCallback(Fn<void()> repaint); void setRepaintCallback(Fn<void()> repaint);
QRect paintGetArea(QPainter &p, QPoint origin, QRect target) const; QRect paintGetArea(
QPainter &p,
QPoint origin,
QRect target,
crl::time now) const;
[[nodiscard]] bool flying() const; [[nodiscard]] bool flying() const;
[[nodiscard]] float64 flyingProgress() const; [[nodiscard]] float64 flyingProgress() const;
[[nodiscard]] bool finished() const; [[nodiscard]] bool finished() const;
private: private:
struct Parabolic {
float64 a = 0.;
float64 b = 0.;
std::optional<int> key;
};
struct MiniCopy {
mutable Parabolic cached;
float64 maxScale = 1.;
float64 duration = 1.;
int flyUp = 0;
int finalX = 0;
int finalY = 0;
};
[[nodiscard]] auto flyCallback(); [[nodiscard]] auto flyCallback();
[[nodiscard]] auto callback(); [[nodiscard]] auto callback();
void startAnimations(); void startAnimations();
int computeParabolicTop(int from, int to, float64 progress) const; int computeParabolicTop(
void paintCenterFrame(QPainter &p, QRect target) const; Parabolic &cache,
int from,
int to,
int top,
float64 progress) const;
void paintCenterFrame(QPainter &p, QRect target, crl::time now) const;
void paintMiniCopies(QPainter &p, QPoint center, crl::time now) const;
void generateMiniCopies(int size);
const not_null<::Data::Reactions*> _owner; const not_null<::Data::Reactions*> _owner;
Fn<void()> _repaint; Fn<void()> _repaint;
@ -56,14 +81,15 @@ private:
std::unique_ptr<Ui::Text::CustomEmoji> _custom; std::unique_ptr<Ui::Text::CustomEmoji> _custom;
std::unique_ptr<Ui::AnimatedIcon> _center; std::unique_ptr<Ui::AnimatedIcon> _center;
std::unique_ptr<Ui::AnimatedIcon> _effect; std::unique_ptr<Ui::AnimatedIcon> _effect;
std::vector<MiniCopy> _miniCopies;
Ui::Animations::Simple _fly; Ui::Animations::Simple _fly;
Ui::Animations::Simple _minis;
QRect _flyFrom; QRect _flyFrom;
float64 _centerSizeMultiplier = 0.; float64 _centerSizeMultiplier = 0.;
int _customSize = 0;
bool _valid = false; bool _valid = false;
mutable std::optional<int> _cachedKey; mutable Parabolic _cached;
mutable float64 _cachedA = 0.;
mutable float64 _cachedB = 0.;
}; };