Make complex gradients animate on outgoing messages.

This commit is contained in:
John Preston 2021-08-16 16:07:43 +03:00
parent a1f81e4de8
commit 7a06eccaec
5 changed files with 268 additions and 69 deletions

View file

@ -4929,6 +4929,9 @@ void HistoryWidget::startItemRevealAnimations() {
1.,
HistoryView::ListWidget::kItemRevealDuration,
anim::easeOutCirc);
if (item->out()) {
controller()->rotateComplexGradientBackground();
}
}
}
}

View file

@ -875,6 +875,12 @@ QImage ChatBackground::gradientForFill() const {
return _gradient;
}
void ChatBackground::recacheGradientForFill(QImage gradient) {
if (_gradient.size() == gradient.size()) {
_gradient = std::move(gradient);
}
}
QImage ChatBackground::createCurrentImage() const {
if (const auto fill = colorForFill()) {
auto result = QImage(

View file

@ -178,6 +178,7 @@ public:
}
[[nodiscard]] std::optional<QColor> colorForFill() const;
[[nodiscard]] QImage gradientForFill() const;
void recacheGradientForFill(QImage gradient);
[[nodiscard]] QImage createCurrentImage() const;
[[nodiscard]] bool tile() const;
[[nodiscard]] bool tileDay() const;

View file

@ -74,6 +74,83 @@ constexpr auto kCacheBackgroundTimeout = 3 * crl::time(1000);
constexpr auto kCacheBackgroundFastTimeout = crl::time(200);
constexpr auto kBackgroundFadeDuration = crl::time(200);
[[nodiscard]] CacheBackgroundResult CacheBackground(
const CacheBackgroundRequest &request) {
const auto gradient = request.gradient.isNull()
? QImage()
: request.recreateGradient
? Images::GenerateGradient(
request.gradient.size(),
request.gradientColors,
request.gradientRotation)
: request.gradient;
if (request.tile || request.prepared.isNull()) {
auto result = gradient.isNull()
? QImage(
request.area * cIntRetinaFactor(),
QImage::Format_ARGB32_Premultiplied)
: gradient.scaled(
request.area * cIntRetinaFactor(),
Qt::IgnoreAspectRatio,
Qt::SmoothTransformation);
result.setDevicePixelRatio(cRetinaFactor());
if (!request.prepared.isNull()) {
QPainter p(&result);
if (!gradient.isNull()) {
p.setCompositionMode(QPainter::CompositionMode_SoftLight);
p.setOpacity(request.patternOpacity);
}
const auto &tiled = request.preparedForTiled;
const auto w = tiled.width() / cRetinaFactor();
const auto h = tiled.height() / cRetinaFactor();
auto sx = 0;
auto sy = 0;
const auto cx = qCeil(request.area.width() / w);
const auto cy = qCeil(request.area.height() / h);
for (int i = sx; i < cx; ++i) {
for (int j = sy; j < cy; ++j) {
p.drawImage(QPointF(i * w, j * h), tiled);
}
}
}
return {
.image = std::move(result),
.gradient = gradient,
.area = request.area,
};
} else {
const auto rects = Window::Theme::ComputeBackgroundRects(
request.area,
request.prepared.size());
auto image = request.prepared.copy(rects.from).scaled(
rects.to.width() * cIntRetinaFactor(),
rects.to.height() * cIntRetinaFactor(),
Qt::IgnoreAspectRatio,
Qt::SmoothTransformation);
auto result = gradient.isNull()
? std::move(image)
: gradient.scaled(
image.size(),
Qt::IgnoreAspectRatio,
Qt::SmoothTransformation);
result.setDevicePixelRatio(cRetinaFactor());
if (!gradient.isNull()) {
QPainter p(&result);
p.setCompositionMode(QPainter::CompositionMode_SoftLight);
p.setOpacity(request.patternOpacity);
p.drawImage(QRect(QPoint(), rects.to.size()), image);
}
image = QImage();
return {
.image = std::move(result),
.gradient = gradient,
.area = request.area,
.x = rects.to.x(),
.y = rects.to.y(),
};
}
}
} // namespace
void ActivateWindow(not_null<SessionController*> controller) {
@ -460,6 +537,32 @@ void SessionNavigation::showPollResults(
showSection(std::make_shared<Info::Memento>(poll, contextId), params);
}
bool operator==(
const CacheBackgroundRequest &a,
const CacheBackgroundRequest &b) {
return (a.prepared.cacheKey() == b.prepared.cacheKey())
&& (a.area == b.area)
&& (a.gradientRotation == b.gradientRotation)
&& (a.tile == b.tile)
&& (a.recreateGradient == b.recreateGradient)
&& (a.gradient.cacheKey() == b.gradient.cacheKey())
&& (a.gradientProgress == b.gradientProgress)
&& (a.patternOpacity == b.patternOpacity);
}
bool operator!=(
const CacheBackgroundRequest &a,
const CacheBackgroundRequest &b) {
return !(a == b);
}
CachedBackground::CachedBackground(CacheBackgroundResult &&result)
: pixmap(Ui::PixmapFromImage(std::move(result.image)))
, area(result.area)
, x(result.x)
, y(result.y) {
}
SessionController::SessionController(
not_null<Main::Session*> session,
not_null<Controller*> window)
@ -1328,95 +1431,123 @@ const BackgroundState &SessionController::backgroundState(QSize area) {
_cacheBackgroundTimer.callOnce(kCacheBackgroundFastTimeout);
}
}
generateNextBackgroundRotation();
return _backgroundState;
}
void SessionController::cacheBackground() {
const auto background = Window::Theme::Background();
if (background->colorForFill()) {
bool SessionController::readyForBackgroundRotation() const {
return !anim::Disabled()
&& !_backgroundFade.animating()
&& !_cacheBackgroundTimer.isActive()
&& !_backgroundState.now.pixmap.isNull();
}
void SessionController::generateNextBackgroundRotation() {
if (_backgroundCachingRequest
|| !_backgroundNext.image.isNull()
|| !readyForBackgroundRotation()) {
return;
}
const auto background = Window::Theme::Background();
if (background->paper().backgroundColors().size() < 3) {
return;
}
const auto request = currentCacheRequest(_backgroundState.now.area, 45);
if (!request) {
return;
}
cacheBackgroundAsync(request, [=](CacheBackgroundResult &&result) {
const auto forRequest = base::take(_backgroundCachingRequest);
if (!readyForBackgroundRotation()) {
return;
}
const auto request = currentCacheRequest(
_backgroundState.now.area,
45);
if (forRequest == request) {
_backgroundAddRotation = (_backgroundAddRotation + 45) % 360;
_backgroundNext = std::move(result);
}
});
}
auto SessionController::currentCacheRequest(QSize area, int addRotation) const
-> CacheBackgroundRequest {
const auto background = Window::Theme::Background();
if (background->colorForFill()) {
return {};
}
const auto rotation = background->paper().gradientRotation();
const auto gradient = background->gradientForFill();
return {
.prepared = background->prepared(),
.preparedForTiled = background->preparedForTiled(),
.area = area,
.gradientRotation = (rotation
+ _backgroundAddRotation
+ addRotation) % 360,
.tile = background->tile(),
.recreateGradient = (addRotation != 0),
.gradient = gradient,
.gradientColors = (gradient.isNull()
? std::vector<QColor>()
: background->paper().backgroundColors()),
.gradientProgress = 1.,
.patternOpacity = background->paper().patternOpacity(),
};
}
void SessionController::cacheBackground() {
const auto now = crl::now();
if (now - _lastAreaChangeTime < kCacheBackgroundTimeout
&& QGuiApplication::mouseButtons() != 0) {
_cacheBackgroundTimer.callOnce(kCacheBackgroundFastTimeout);
return;
}
const auto gradient = background->gradientForFill();
const auto patternOpacity = background->paper().patternOpacity();
const auto &prepared = background->prepared();
if (background->tile() || prepared.isNull()) {
auto result = gradient.isNull()
? QImage(
_willCacheForArea * cIntRetinaFactor(),
QImage::Format_ARGB32_Premultiplied)
: gradient.scaled(
_willCacheForArea * cIntRetinaFactor(),
Qt::IgnoreAspectRatio,
Qt::SmoothTransformation);
result.setDevicePixelRatio(cRetinaFactor());
if (!prepared.isNull()) {
QPainter p(&result);
if (!gradient.isNull()) {
p.setCompositionMode(QPainter::CompositionMode_SoftLight);
p.setOpacity(patternOpacity);
}
const auto &tiled = background->preparedForTiled();
const auto w = tiled.width() / cRetinaFactor();
const auto h = tiled.height() / cRetinaFactor();
auto sx = 0;
auto sy = 0;
const auto cx = qCeil(_willCacheForArea.width() / w);
const auto cy = qCeil(_willCacheForArea.height() / h);
for (int i = sx; i < cx; ++i) {
for (int j = sy; j < cy; ++j) {
p.drawImage(QPointF(i * w, j * h), tiled);
}
}
cacheBackgroundNow();
}
void SessionController::cacheBackgroundNow() {
if (!_backgroundCachingRequest) {
if (const auto request = currentCacheRequest(_willCacheForArea)) {
cacheBackgroundAsync(request);
}
setCachedBackground({
.pixmap = Ui::PixmapFromImage(std::move(result)),
.area = _willCacheForArea,
});
} else {
const auto rects = Window::Theme::ComputeBackgroundRects(
_willCacheForArea,
prepared.size());
auto image = prepared.copy(rects.from).scaled(
rects.to.width() * cIntRetinaFactor(),
rects.to.height() * cIntRetinaFactor(),
Qt::IgnoreAspectRatio,
Qt::SmoothTransformation);
auto result = gradient.isNull()
? std::move(image)
: gradient.scaled(
image.size(),
Qt::IgnoreAspectRatio,
Qt::SmoothTransformation);
result.setDevicePixelRatio(cRetinaFactor());
if (!gradient.isNull()) {
QPainter p(&result);
p.setCompositionMode(QPainter::CompositionMode_SoftLight);
p.setOpacity(patternOpacity);
p.drawImage(QRect(QPoint(), rects.to.size()), image);
}
image = QImage();
setCachedBackground({
.pixmap = Ui::PixmapFromImage(std::move(result)),
.area = _willCacheForArea,
.x = rects.to.x(),
.y = rects.to.y(),
});
}
}
void SessionController::setCachedBackground(CachedBackground &&cached) {
void SessionController::cacheBackgroundAsync(
const CacheBackgroundRequest &request,
Fn<void(CacheBackgroundResult&&)> done) {
_backgroundCachingRequest = request;
const auto weak = base::make_weak(this);
crl::async([=] {
if (!weak) {
return;
}
crl::on_main(weak, [=, result = CacheBackground(request)]() mutable {
if (done) {
done(std::move(result));
} else if (const auto request = currentCacheRequest(
_willCacheForArea)) {
if (_backgroundCachingRequest != request) {
cacheBackgroundAsync(request);
} else {
_backgroundCachingRequest = {};
setCachedBackground(std::move(result));
}
}
});
});
}
void SessionController::setCachedBackground(CacheBackgroundResult &&cached) {
const auto background = Window::Theme::Background();
if (background->gradientForFill().isNull()
|| _backgroundState.now.pixmap.isNull()) {
_backgroundFade.stop();
_backgroundState.shown = 1.;
_backgroundState.now = std::move(cached);
_backgroundNext = {};
return;
}
// #TODO themes compose several transitions.
@ -1439,6 +1570,8 @@ void SessionController::setCachedBackground(CachedBackground &&cached) {
void SessionController::clearCachedBackground() {
_backgroundState = {};
_backgroundAddRotation = 0;
_backgroundNext = {};
_backgroundFade.stop();
_cacheBackgroundTimer.cancel();
_repaintBackgroundRequests.fire({});
@ -1448,6 +1581,14 @@ rpl::producer<> SessionController::repaintBackgroundRequests() const {
return _repaintBackgroundRequests.events();
}
void SessionController::rotateComplexGradientBackground() {
if (!_backgroundFade.animating() && !_backgroundNext.image.isNull()) {
Window::Theme::Background()->recacheGradientForFill(
std::move(_backgroundNext.gradient));
setCachedBackground(base::take(_backgroundNext));
}
}
SessionController::~SessionController() {
resetFakeUnreadWhileOpened();
}

View file

@ -55,7 +55,42 @@ class SectionMemento;
class Controller;
class FiltersMenu;
struct CacheBackgroundRequest {
QImage prepared;
QImage preparedForTiled;
QSize area;
int gradientRotation = 0;
bool tile = false;
bool recreateGradient = false;
QImage gradient;
std::vector<QColor> gradientColors;
float64 gradientProgress = 1.;
float64 patternOpacity = 1.;
explicit operator bool() const {
return !prepared.isNull() || !gradient.isNull();
}
};
bool operator==(
const CacheBackgroundRequest &a,
const CacheBackgroundRequest &b);
bool operator!=(
const CacheBackgroundRequest &a,
const CacheBackgroundRequest &b);
struct CacheBackgroundResult {
QImage image;
QImage gradient;
QSize area;
int x = 0;
int y = 0;
};
struct CachedBackground {
CachedBackground() = default;
CachedBackground(CacheBackgroundResult &&result);
QPixmap pixmap;
QSize area;
int x = 0;
@ -406,6 +441,7 @@ public:
[[nodiscard]] const BackgroundState &backgroundState(QSize area);
[[nodiscard]] rpl::producer<> repaintBackgroundRequests() const;
void rotateComplexGradientBackground();
rpl::lifetime &lifetime() {
return _lifetime;
@ -437,8 +473,17 @@ private:
void checkInvitePeek();
void cacheBackground();
void cacheBackgroundNow();
void cacheBackgroundAsync(
const CacheBackgroundRequest &request,
Fn<void(CacheBackgroundResult&&)> done = nullptr);
void clearCachedBackground();
void setCachedBackground(CachedBackground &&cached);
void setCachedBackground(CacheBackgroundResult &&cached);
[[nodiscard]] CacheBackgroundRequest currentCacheRequest(
QSize area,
int addRotation = 0) const;
[[nodiscard]] bool readyForBackgroundRotation() const;
void generateNextBackgroundRotation();
const not_null<Controller*> _window;
@ -469,6 +514,9 @@ private:
BackgroundState _backgroundState;
Ui::Animations::Simple _backgroundFade;
CacheBackgroundRequest _backgroundCachingRequest;
CacheBackgroundResult _backgroundNext;
int _backgroundAddRotation = 0;
QSize _willCacheForArea;
crl::time _lastAreaChangeTime = 0;
base::Timer _cacheBackgroundTimer;