mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2025-06-05 06:33:57 +02:00
Add mini-copies animation for custom reactions.
This commit is contained in:
parent
7d77e8a203
commit
a256eb4bc8
4 changed files with 167 additions and 30 deletions
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.;
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue