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;
 | 
			
		||||
	}
 | 
			
		||||
	if (!animations.empty()) {
 | 
			
		||||
		const auto now = context.now;
 | 
			
		||||
		context.reactionInfo->effectPaint = [=](QPainter &p) {
 | 
			
		||||
			auto result = QRect();
 | 
			
		||||
			for (const auto &single : animations) {
 | 
			
		||||
				const auto area = single.animation->paintGetArea(
 | 
			
		||||
					p,
 | 
			
		||||
					origin,
 | 
			
		||||
					single.target);
 | 
			
		||||
					single.target,
 | 
			
		||||
					now);
 | 
			
		||||
				result = result.isEmpty() ? area : result.united(area);
 | 
			
		||||
			}
 | 
			
		||||
			return result;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -452,13 +452,15 @@ void InlineList::paint(
 | 
			
		|||
		}
 | 
			
		||||
	}
 | 
			
		||||
	if (!animations.empty()) {
 | 
			
		||||
		const auto now = context.now;
 | 
			
		||||
		context.reactionInfo->effectPaint = [=](QPainter &p) {
 | 
			
		||||
			auto result = QRect();
 | 
			
		||||
			for (const auto &single : animations) {
 | 
			
		||||
				const auto area = single.animation->paintGetArea(
 | 
			
		||||
					p,
 | 
			
		||||
					QPoint(),
 | 
			
		||||
					single.target);
 | 
			
		||||
					single.target,
 | 
			
		||||
					now);
 | 
			
		||||
				result = result.isEmpty() ? area : result.united(area);
 | 
			
		||||
			}
 | 
			
		||||
			return result;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -15,12 +15,20 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 | 
			
		|||
#include "data/data_session.h"
 | 
			
		||||
#include "data/data_document.h"
 | 
			
		||||
#include "data/data_document_media.h"
 | 
			
		||||
#include "base/random.h"
 | 
			
		||||
#include "styles/style_chat.h"
 | 
			
		||||
 | 
			
		||||
namespace HistoryView::Reactions {
 | 
			
		||||
namespace {
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -61,6 +69,7 @@ Animation::Animation(
 | 
			
		|||
		const auto data = &owner->owner();
 | 
			
		||||
		const auto document = data->document(customId);
 | 
			
		||||
		_custom = data->customEmojiManager().create(document, callback());
 | 
			
		||||
		_customSize = centerIconSize;
 | 
			
		||||
		aroundAnimation = owner->chooseGenericAnimation(document);
 | 
			
		||||
	} else {
 | 
			
		||||
		const auto i = ranges::find(list, args.id, &::Data::Reaction::id);
 | 
			
		||||
| 
						 | 
				
			
			@ -91,10 +100,11 @@ Animation::Animation(
 | 
			
		|||
		return;
 | 
			
		||||
	}
 | 
			
		||||
	resolve(_effect, aroundAnimation, size * 2);
 | 
			
		||||
	generateMiniCopies(size + size / 2);
 | 
			
		||||
	if (!args.flyIcon.isNull()) {
 | 
			
		||||
		_flyIcon = std::move(args.flyIcon);
 | 
			
		||||
		_fly.start(flyCallback(), 0., 1., kFlyDuration);
 | 
			
		||||
	} else if (!_center && !_effect) {
 | 
			
		||||
	} else if (!_center && !_effect && _miniCopies.empty()) {
 | 
			
		||||
		return;
 | 
			
		||||
	} else {
 | 
			
		||||
		startAnimations();
 | 
			
		||||
| 
						 | 
				
			
			@ -108,16 +118,22 @@ Animation::~Animation() = default;
 | 
			
		|||
QRect Animation::paintGetArea(
 | 
			
		||||
		QPainter &p,
 | 
			
		||||
		QPoint origin,
 | 
			
		||||
		QRect target) const {
 | 
			
		||||
		QRect target,
 | 
			
		||||
		crl::time now) const {
 | 
			
		||||
	if (_flyIcon.isNull()) {
 | 
			
		||||
		paintCenterFrame(p, target);
 | 
			
		||||
		paintCenterFrame(p, target, now);
 | 
			
		||||
		const auto wide = QRect(
 | 
			
		||||
			target.topLeft() - QPoint(target.width(), target.height()) / 2,
 | 
			
		||||
			target.size() * 2);
 | 
			
		||||
		if (const auto effect = _effect.get()) {
 | 
			
		||||
			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 lshift = target.width() / 4;
 | 
			
		||||
| 
						 | 
				
			
			@ -127,7 +143,12 @@ QRect Animation::paintGetArea(
 | 
			
		|||
	const auto progress = _fly.value(1.);
 | 
			
		||||
	const auto rect = QRect(
 | 
			
		||||
		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.height(), target.height(), progress));
 | 
			
		||||
	const auto wide = rect.marginsAdded(margins);
 | 
			
		||||
| 
						 | 
				
			
			@ -137,13 +158,16 @@ QRect Animation::paintGetArea(
 | 
			
		|||
	}
 | 
			
		||||
	if (progress > 0.) {
 | 
			
		||||
		p.setOpacity(progress);
 | 
			
		||||
		paintCenterFrame(p, wide);
 | 
			
		||||
		paintCenterFrame(p, wide, now);
 | 
			
		||||
	}
 | 
			
		||||
	p.setOpacity(1.);
 | 
			
		||||
	return wide;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Animation::paintCenterFrame(QPainter &p, QRect target) const {
 | 
			
		||||
void Animation::paintCenterFrame(
 | 
			
		||||
		QPainter &p,
 | 
			
		||||
		QRect target,
 | 
			
		||||
		crl::time now) const {
 | 
			
		||||
	Expects(_center || _custom);
 | 
			
		||||
 | 
			
		||||
	const auto size = QSize(
 | 
			
		||||
| 
						 | 
				
			
			@ -157,24 +181,103 @@ void Animation::paintCenterFrame(QPainter &p, QRect target) const {
 | 
			
		|||
			size.height());
 | 
			
		||||
		p.drawImage(rect, _center->frame());
 | 
			
		||||
	} else {
 | 
			
		||||
		const auto side = Ui::Text::AdjustCustomEmojiSize(st::emojiSize);
 | 
			
		||||
		const auto scaled = (size.width() != side);
 | 
			
		||||
		const auto scaled = (size.width() != _customSize);
 | 
			
		||||
		_custom->paint(p, {
 | 
			
		||||
			.preview = Qt::transparent,
 | 
			
		||||
			.size = { side, side },
 | 
			
		||||
			.now = crl::now(),
 | 
			
		||||
			.scale = (scaled ? (size.width() / float64(side)) : 1.),
 | 
			
		||||
			.preview = QColor(0, 0, 0, 0),
 | 
			
		||||
			.size = { _customSize, _customSize },
 | 
			
		||||
			.now = now,
 | 
			
		||||
			.scale = (scaled ? (size.width() / float64(_customSize)) : 1.),
 | 
			
		||||
			.position = QPoint(
 | 
			
		||||
				target.x() + (target.width() - side) / 2,
 | 
			
		||||
				target.y() + (target.height() - side) / 2),
 | 
			
		||||
				target.x() + (target.width() - _customSize) / 2,
 | 
			
		||||
				target.y() + (target.height() - _customSize) / 2),
 | 
			
		||||
			.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(
 | 
			
		||||
		Parabolic &cache,
 | 
			
		||||
		int from,
 | 
			
		||||
		int to,
 | 
			
		||||
		int top,
 | 
			
		||||
		float64 progress) const {
 | 
			
		||||
	const auto t = progress;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -189,8 +292,8 @@ int Animation::computeParabolicTop(
 | 
			
		|||
	// 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))
 | 
			
		||||
	const auto y_1 = to - from;
 | 
			
		||||
	if (_cachedKey != y_1) {
 | 
			
		||||
		const auto y_0 = std::min(0, y_1) - st::reactionFlyUp;
 | 
			
		||||
	if (cache.key != y_1) {
 | 
			
		||||
		const auto y_0 = std::min(0, y_1) - top;
 | 
			
		||||
		const auto ratio = y_1 ? (float64(y_0) / y_1) : 0.;
 | 
			
		||||
		const auto root = y_1 ? sqrt(ratio * (ratio - 1)) : 0.;
 | 
			
		||||
		const auto t_0 = !y_1
 | 
			
		||||
| 
						 | 
				
			
			@ -200,12 +303,12 @@ int Animation::computeParabolicTop(
 | 
			
		|||
			: (ratio - root);
 | 
			
		||||
		const auto a = y_1 ? (y_1 / (1 - 2 * t_0)) : (-4 * y_0);
 | 
			
		||||
		const auto b = y_1 - a;
 | 
			
		||||
		_cachedKey = y_1;
 | 
			
		||||
		_cachedA = a;
 | 
			
		||||
		_cachedB = b;
 | 
			
		||||
		cache.key = y_1;
 | 
			
		||||
		cache.a = a;
 | 
			
		||||
		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() {
 | 
			
		||||
| 
						 | 
				
			
			@ -215,6 +318,9 @@ void Animation::startAnimations() {
 | 
			
		|||
	if (const auto effect = _effect.get()) {
 | 
			
		||||
		_effect->animate(callback());
 | 
			
		||||
	}
 | 
			
		||||
	if (!_miniCopies.empty()) {
 | 
			
		||||
		_minis.start(callback(), 0., 1., kMiniCopiesDurationMax);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Animation::setRepaintCallback(Fn<void()> repaint) {
 | 
			
		||||
| 
						 | 
				
			
			@ -233,7 +339,8 @@ bool Animation::finished() const {
 | 
			
		|||
	return !_valid
 | 
			
		||||
		|| (_flyIcon.isNull()
 | 
			
		||||
			&& (!_center || !_center->animating())
 | 
			
		||||
			&& (!_effect || !_effect->animating()));
 | 
			
		||||
			&& (!_effect || !_effect->animating())
 | 
			
		||||
			&& !_minis.animating());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
} // namespace HistoryView::Reactions
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -37,18 +37,43 @@ public:
 | 
			
		|||
	~Animation();
 | 
			
		||||
 | 
			
		||||
	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]] float64 flyingProgress() const;
 | 
			
		||||
	[[nodiscard]] bool finished() const;
 | 
			
		||||
 | 
			
		||||
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 callback();
 | 
			
		||||
	void startAnimations();
 | 
			
		||||
	int computeParabolicTop(int from, int to, float64 progress) const;
 | 
			
		||||
	void paintCenterFrame(QPainter &p, QRect target) const;
 | 
			
		||||
	int computeParabolicTop(
 | 
			
		||||
		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;
 | 
			
		||||
	Fn<void()> _repaint;
 | 
			
		||||
| 
						 | 
				
			
			@ -56,14 +81,15 @@ private:
 | 
			
		|||
	std::unique_ptr<Ui::Text::CustomEmoji> _custom;
 | 
			
		||||
	std::unique_ptr<Ui::AnimatedIcon> _center;
 | 
			
		||||
	std::unique_ptr<Ui::AnimatedIcon> _effect;
 | 
			
		||||
	std::vector<MiniCopy> _miniCopies;
 | 
			
		||||
	Ui::Animations::Simple _fly;
 | 
			
		||||
	Ui::Animations::Simple _minis;
 | 
			
		||||
	QRect _flyFrom;
 | 
			
		||||
	float64 _centerSizeMultiplier = 0.;
 | 
			
		||||
	int _customSize = 0;
 | 
			
		||||
	bool _valid = false;
 | 
			
		||||
 | 
			
		||||
	mutable std::optional<int> _cachedKey;
 | 
			
		||||
	mutable float64 _cachedA = 0.;
 | 
			
		||||
	mutable float64 _cachedB = 0.;
 | 
			
		||||
	mutable Parabolic _cached;
 | 
			
		||||
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		
		Reference in a new issue