mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2026-07-25 15:04:57 +00:00
1997 lines
52 KiB
C++
1997 lines
52 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 "boxes/star_gift_craft_animation.h"
|
|
|
|
#include "base/call_delayed.h"
|
|
#include "base/unixtime.h"
|
|
#include "boxes/star_gift_box.h"
|
|
#include "boxes/star_gift_cover_box.h"
|
|
#include "boxes/star_gift_craft_box.h"
|
|
#include "chat_helpers/compose/compose_show.h"
|
|
#include "chat_helpers/stickers_lottie.h"
|
|
#include "data/data_credits.h"
|
|
#include "data/data_document.h"
|
|
#include "data/data_document_media.h"
|
|
#include "data/data_session.h"
|
|
#include "history/view/media/history_view_sticker_player.h"
|
|
#include "info/peer_gifts/info_peer_gifts_common.h"
|
|
#include "lang/lang_keys.h"
|
|
#include "lang/lang_tag.h"
|
|
#include "lottie/lottie_common.h"
|
|
#include "lottie/lottie_icon.h"
|
|
#include "main/main_session.h"
|
|
#include "settings/settings_credits_graphics.h"
|
|
#include "ui/boxes/boost_box.h"
|
|
#include "ui/boxes/confirm_box.h"
|
|
#include "ui/image/image_prepare.h"
|
|
#include "ui/layers/generic_box.h"
|
|
#include "ui/widgets/gradient_round_button.h"
|
|
#include "ui/widgets/labels.h"
|
|
#include "ui/wrap/vertical_layout.h"
|
|
#include "ui/top_background_gradient.h"
|
|
#include "ui/painter.h"
|
|
#include "ui/ui_utility.h"
|
|
#include "styles/style_boxes.h"
|
|
#include "styles/style_credits.h"
|
|
#include "styles/style_layers.h"
|
|
|
|
namespace Ui {
|
|
namespace {
|
|
|
|
using namespace Info::PeerGifts;
|
|
|
|
constexpr auto kFrameDuration = 1000. / 60.;
|
|
constexpr auto kFlightDuration = crl::time(400);
|
|
constexpr auto kLoadingFadeInDuration = crl::time(150);
|
|
constexpr auto kLoadingFadeOutDuration = crl::time(300);
|
|
constexpr auto kLoadingMinDuration = crl::time(600);
|
|
constexpr auto kFailureFadeDuration = crl::time(400);
|
|
constexpr auto kSuccessFadeInDuration = crl::time(300);
|
|
constexpr auto kSuccessExpandDuration = crl::time(400);
|
|
constexpr auto kSuccessExpandStart = crl::time(100);
|
|
constexpr auto kProgressFadeInDuration = crl::time(300);
|
|
|
|
[[nodiscard]] QString FormatPercent(int permille) {
|
|
const auto rounded = (permille + 5) / 10;
|
|
return QString::number(rounded) + '%';
|
|
}
|
|
|
|
struct Rotation {
|
|
float64 x = 0.;
|
|
float64 y = 0.;
|
|
|
|
Rotation operator+(Rotation other) const {
|
|
return { x + other.x, y + other.y };
|
|
}
|
|
Rotation operator*(float64 scale) const {
|
|
return { x * scale, y * scale };
|
|
}
|
|
};
|
|
|
|
using RotationFn = Fn<Rotation(Rotation initial, crl::time t)>;
|
|
|
|
[[nodiscard]] int Slowing() {
|
|
return anim::SlowMultiplier();
|
|
}
|
|
|
|
[[nodiscard]] RotationFn DecayingRotation(Rotation impulse, float64 decay) {
|
|
const auto lambda = -std::log(decay) / kFrameDuration;
|
|
return [=](Rotation initial, crl::time t) {
|
|
const auto factor = (1. - std::exp(-lambda * t)) / lambda / 1000.;
|
|
return initial + impulse * factor;
|
|
};
|
|
}
|
|
|
|
[[nodiscard]] RotationFn DecayingEndRotation(
|
|
Rotation impulse,
|
|
float64 decay,
|
|
Rotation target,
|
|
crl::time duration) {
|
|
const auto lambda = -std::log(decay) / kFrameDuration;
|
|
target = target * M_PI;
|
|
return [=](Rotation initial, crl::time t) {
|
|
const auto factor = (1. - std::exp(-lambda * t)) / lambda / 1000.;
|
|
const auto result = initial + impulse * factor;
|
|
if (t <= duration - 200) {
|
|
return result;
|
|
} else if (t >= duration) {
|
|
return target;
|
|
}
|
|
const auto progress = (duration - t) / 200.;
|
|
return result * progress + target * (1. - progress);
|
|
};
|
|
}
|
|
|
|
struct GiftAnimationConfig {
|
|
RotationFn rotation;
|
|
crl::time duration = 0;
|
|
crl::time revealTime = 0;
|
|
int targetFaceIndex = 0;
|
|
int targetFaceRotation = 0;
|
|
};
|
|
|
|
const auto kGiftAnimations = std::array<GiftAnimationConfig, 7>{{ {
|
|
.rotation = DecayingEndRotation({ 4.1, -8.2 }, 0.98, { 1, -2 }, 2200),
|
|
.duration = 2200,
|
|
.revealTime = 600,
|
|
.targetFaceIndex = 1,
|
|
.targetFaceRotation = 180,
|
|
}, {
|
|
.rotation = DecayingRotation({ 3.2, -5.9 }, 0.97),
|
|
.duration = 1200,
|
|
.revealTime = 0,
|
|
.targetFaceIndex = 4,
|
|
.targetFaceRotation = 180,
|
|
}, {
|
|
.rotation = DecayingEndRotation({ 6.2, 7.8 }, 0.98, { 2, 1 }, 2200),
|
|
.duration = 2200,
|
|
.revealTime = 1000,
|
|
.targetFaceIndex = 1,
|
|
.targetFaceRotation = 0,
|
|
}, {
|
|
.rotation = DecayingRotation({ 7.0, 4.5 }, 0.97),
|
|
.duration = 1200,
|
|
.revealTime = 200,
|
|
.targetFaceIndex = 5,
|
|
.targetFaceRotation = 0,
|
|
}, {
|
|
.rotation = DecayingEndRotation({ -6.5, 5.0 }, 0.98, { 0, 1 }, 2200),
|
|
.duration = 2200,
|
|
.revealTime = 800,
|
|
.targetFaceIndex = 1,
|
|
.targetFaceRotation = 0,
|
|
}, {
|
|
.rotation = DecayingRotation({ -2.5, 3.5 }, 0.98),
|
|
.duration = 1200,
|
|
.revealTime = 200,
|
|
.targetFaceIndex = 2,
|
|
.targetFaceRotation = 180,
|
|
}, {
|
|
.rotation = DecayingEndRotation({ -4.4, -8.2 }, 0.98, { 0, -1.5 }, 2200),
|
|
.duration = 2200,
|
|
.revealTime = 600,
|
|
.targetFaceIndex = 3,
|
|
.targetFaceRotation = 0,
|
|
} }};
|
|
|
|
[[nodiscard]] QImage CreateBgGradient(
|
|
QSize size,
|
|
const Data::UniqueGiftBackdrop &backdrop) {
|
|
const auto ratio = style::DevicePixelRatio();
|
|
auto result = QImage(size * ratio, QImage::Format_ARGB32_Premultiplied);
|
|
result.setDevicePixelRatio(ratio);
|
|
|
|
auto p = QPainter(&result);
|
|
auto hq = PainterHighQualityEnabler(p);
|
|
auto gradient = QRadialGradient(
|
|
QPoint(size.width() / 2, size.height() / 2),
|
|
size.height() / 2);
|
|
gradient.setStops({
|
|
{ 0., backdrop.centerColor },
|
|
{ 1., backdrop.edgeColor },
|
|
});
|
|
p.setBrush(gradient);
|
|
p.setPen(Qt::NoPen);
|
|
p.drawRect(0, 0, size.width(), size.height());
|
|
p.end();
|
|
|
|
const auto mask = Images::CornersMask(st::boxRadius);
|
|
return Images::Round(std::move(result), mask);
|
|
}
|
|
|
|
[[nodiscard]] std::optional<std::array<QPointF, 4>> ComputeCubeFaceCorners(
|
|
QPointF center,
|
|
float64 size,
|
|
float64 rotationX,
|
|
float64 rotationY,
|
|
int faceIndex) {
|
|
struct Vec3 {
|
|
float64 x, y, z;
|
|
|
|
Vec3 operator+(const Vec3 &o) const {
|
|
return { x + o.x, y + o.y, z + o.z };
|
|
}
|
|
Vec3 operator-(const Vec3 &o) const {
|
|
return { x - o.x, y - o.y, z - o.z };
|
|
}
|
|
Vec3 operator*(float64 s) const {
|
|
return { x * s, y * s, z * s };
|
|
}
|
|
[[nodiscard]] Vec3 cross(const Vec3 &o) const {
|
|
return {
|
|
y * o.z - z * o.y,
|
|
z * o.x - x * o.z,
|
|
x * o.y - y * o.x
|
|
};
|
|
}
|
|
[[nodiscard]] float64 dot(const Vec3 &o) const {
|
|
return x * o.x + y * o.y + z * o.z;
|
|
}
|
|
[[nodiscard]] Vec3 normalized() const {
|
|
const auto len = std::sqrt(x * x + y * y + z * z);
|
|
return (len > 0.) ? Vec3{ x / len, y / len, z / len } : *this;
|
|
}
|
|
};
|
|
|
|
const auto rotateY = [](Vec3 v, float64 angle) {
|
|
const auto c = std::cos(angle);
|
|
const auto s = std::sin(angle);
|
|
return Vec3{ v.x * c + v.z * s, v.y, -v.x * s + v.z * c };
|
|
};
|
|
const auto rotateX = [](Vec3 v, float64 angle) {
|
|
const auto c = std::cos(angle);
|
|
const auto s = std::sin(angle);
|
|
return Vec3{ v.x, v.y * c - v.z * s, v.y * s + v.z * c };
|
|
};
|
|
|
|
const auto half = size / 2.;
|
|
constexpr auto kFocalLength = 800.;
|
|
|
|
struct FaceDefinition {
|
|
std::array<Vec3, 4> corners;
|
|
Vec3 normal;
|
|
};
|
|
|
|
const auto faces = std::array<FaceDefinition, 6>{{
|
|
{{{{ -half, -half, -half },
|
|
{ half, -half, -half },
|
|
{ half, half, -half },
|
|
{ -half, half, -half }}},
|
|
{ 0., 0., -1. }},
|
|
|
|
{{{{ half, -half, half },
|
|
{ -half, -half, half },
|
|
{ -half, half, half },
|
|
{ half, half, half }}},
|
|
{ 0., 0., 1. }},
|
|
|
|
{{{{ -half, -half, half },
|
|
{ -half, -half, -half },
|
|
{ -half, half, -half },
|
|
{ -half, half, half }}},
|
|
{ -1., 0., 0. }},
|
|
|
|
{{{{ half, -half, -half },
|
|
{ half, -half, half },
|
|
{ half, half, half },
|
|
{ half, half, -half }}},
|
|
{ 1., 0., 0. }},
|
|
|
|
{{{{ -half, -half, half },
|
|
{ half, -half, half },
|
|
{ half, -half, -half },
|
|
{ -half, -half, -half }}},
|
|
{ 0., -1., 0. }},
|
|
|
|
{{{{ -half, half, -half },
|
|
{ half, half, -half },
|
|
{ half, half, half },
|
|
{ -half, half, half }}},
|
|
{ 0., 1., 0. }},
|
|
}};
|
|
|
|
if (faceIndex < 0 || faceIndex >= 6) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
const auto &face = faces[faceIndex];
|
|
|
|
const auto transformedNormal = rotateX(
|
|
rotateY(face.normal, rotationY),
|
|
rotationX);
|
|
if (transformedNormal.z >= 0.) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
auto result = std::array<QPointF, 4>();
|
|
for (auto i = 0; i != 4; ++i) {
|
|
const auto p = rotateX(
|
|
rotateY(face.corners[i], rotationY),
|
|
rotationX);
|
|
|
|
const auto viewZ = p.z + half + kFocalLength;
|
|
if (viewZ <= 0.) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
const auto scale = kFocalLength / viewZ;
|
|
result[i] = QPointF(
|
|
center.x() + p.x * scale,
|
|
center.y() + p.y * scale);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
void PaintCubeFace(
|
|
QPainter &p,
|
|
const std::array<QPointF, 4> &corners,
|
|
const QImage &face,
|
|
QSize faceSize = QSize(),
|
|
const QRect &source = QRect(),
|
|
int rotation = 0) {
|
|
if (face.isNull()) {
|
|
return;
|
|
}
|
|
|
|
const auto ratio = style::DevicePixelRatio();
|
|
const auto origin = source.isEmpty() ? face.size() : source.size();
|
|
if (faceSize.isEmpty()) {
|
|
faceSize = origin / ratio;
|
|
}
|
|
const auto w = faceSize.width();
|
|
const auto h = faceSize.height();
|
|
|
|
const auto srcRect = QPolygonF({
|
|
QPointF(0, 0),
|
|
QPointF(w, 0),
|
|
QPointF(w, h),
|
|
QPointF(0, h)
|
|
});
|
|
|
|
const auto rotationSteps = (rotation / 90) % 4;
|
|
auto rotatedCorners = corners;
|
|
for (auto i = 0; i < rotationSteps; ++i) {
|
|
rotatedCorners = {
|
|
rotatedCorners[1],
|
|
rotatedCorners[2],
|
|
rotatedCorners[3],
|
|
rotatedCorners[0],
|
|
};
|
|
}
|
|
|
|
const auto dstRect = QPolygonF({
|
|
rotatedCorners[0],
|
|
rotatedCorners[1],
|
|
rotatedCorners[2],
|
|
rotatedCorners[3]
|
|
});
|
|
|
|
auto transform = QTransform();
|
|
if (!QTransform::quadToQuad(srcRect, dstRect, transform)) {
|
|
return;
|
|
}
|
|
|
|
PainterHighQualityEnabler hq(p);
|
|
p.save();
|
|
p.setTransform(transform, true);
|
|
const auto delta = source.isEmpty()
|
|
? QPointF()
|
|
: QPointF(
|
|
((origin.width() / ratio) - faceSize.width()) / 2.,
|
|
((origin.height() / ratio) - faceSize.height()) / 2.);
|
|
p.drawImage(QRectF(-delta, QSizeF(origin) / ratio), face, source);
|
|
p.restore();
|
|
}
|
|
|
|
[[nodiscard]] std::vector<int> GetVisibleCubeFaces(
|
|
float64 rotationX,
|
|
float64 rotationY) {
|
|
struct Vec3 {
|
|
float64 x, y, z;
|
|
|
|
Vec3 operator*(float64 s) const {
|
|
return { x * s, y * s, z * s };
|
|
}
|
|
};
|
|
|
|
const auto rotateY = [](Vec3 v, float64 angle) {
|
|
const auto c = std::cos(angle);
|
|
const auto s = std::sin(angle);
|
|
return Vec3{ v.x * c + v.z * s, v.y, -v.x * s + v.z * c };
|
|
};
|
|
const auto rotateX = [](Vec3 v, float64 angle) {
|
|
const auto c = std::cos(angle);
|
|
const auto s = std::sin(angle);
|
|
return Vec3{ v.x, v.y * c - v.z * s, v.y * s + v.z * c };
|
|
};
|
|
|
|
constexpr auto normals = std::array<Vec3, 6>{{
|
|
{ 0., 0., -1. },
|
|
{ 0., 0., 1. },
|
|
{ -1., 0., 0. },
|
|
{ 1., 0., 0. },
|
|
{ 0., -1., 0. },
|
|
{ 0., 1., 0. },
|
|
}};
|
|
|
|
struct FaceDepth {
|
|
int index;
|
|
float64 z;
|
|
};
|
|
auto visible = std::vector<FaceDepth>();
|
|
visible.reserve(3);
|
|
|
|
for (auto i = 0; i != 6; ++i) {
|
|
const auto transformed = rotateX(
|
|
rotateY(normals[i], rotationY),
|
|
rotationX);
|
|
if (transformed.z < 0.) {
|
|
visible.push_back({ i, transformed.z });
|
|
}
|
|
}
|
|
|
|
ranges::sort(visible, ranges::greater(), &FaceDepth::z);
|
|
|
|
auto result = std::vector<int>();
|
|
result.reserve(visible.size());
|
|
for (const auto &face : visible) {
|
|
result.push_back(face.index);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void PaintCubeFirstFlight(
|
|
QPainter &p,
|
|
not_null<const CraftAnimationState*> state,
|
|
float64 progress,
|
|
bool skipForgeIcon = false) {
|
|
const auto shared = state->shared.get();
|
|
|
|
const auto overlayBg = shared->forgeBgOverlay;
|
|
auto sideBg = shared->forgeBg1;
|
|
sideBg.setAlphaF(progress);
|
|
|
|
auto hq = PainterHighQualityEnabler(p);
|
|
const auto forge = shared->forgeRect;
|
|
const auto radius = (1. - progress) * st::boxRadius;
|
|
p.setPen(Qt::NoPen);
|
|
p.setBrush(overlayBg);
|
|
p.drawRoundedRect(forge, radius, radius);
|
|
p.setBrush(sideBg);
|
|
p.drawRoundedRect(forge, radius, radius);
|
|
|
|
if (!skipForgeIcon) {
|
|
st::craftForge.paintInCenter(p, forge, st::white->c);
|
|
}
|
|
}
|
|
|
|
std::unique_ptr<CraftFailAnimation> SetupFailureAnimation(
|
|
not_null<RpWidget*> canvas,
|
|
not_null<CraftAnimationState*> state) {
|
|
auto result = std::make_unique<CraftFailAnimation>();
|
|
result->lottie = Lottie::MakeIcon({
|
|
.name = u"craft_failed"_q,
|
|
.sizeOverride = st::craftFailLottieSize,
|
|
});
|
|
return result;
|
|
}
|
|
|
|
void FailureAnimationCheck(
|
|
not_null<CraftAnimationState*> state,
|
|
not_null<RpWidget*> canvas,
|
|
crl::time now) {
|
|
const auto animation = state->failureAnimation.get();
|
|
if (!animation || animation->started) {
|
|
return;
|
|
}
|
|
const auto elapsed = (now - state->animationStartTime);
|
|
const auto left = (state->animationDuration * Slowing()) - elapsed;
|
|
if (left <= 0 || left > kFailureFadeDuration * Slowing()) {
|
|
return;
|
|
}
|
|
animation->started = true;
|
|
auto &covers = state->shared->covers;
|
|
for (auto i = 0; i != 5; ++i) {
|
|
auto &cover = covers[i];
|
|
if (!cover.shown) {
|
|
animation->finalCoverIndex = i;
|
|
if (i != 4) {
|
|
auto &last = covers.back();
|
|
cover.backdrop = last.backdrop;
|
|
cover.pattern = std::move(last.pattern);
|
|
cover.button1 = last.button1;
|
|
cover.button2 = last.button2;
|
|
}
|
|
cover.shown = true;
|
|
cover.shownAnimation.start([=] {
|
|
canvas->update();
|
|
}, 0., 1., left);
|
|
break;
|
|
}
|
|
}
|
|
|
|
const auto lottie = animation->lottie.get();
|
|
if (lottie->framesCount() > 1) {
|
|
lottie->animate([=] {
|
|
canvas->update();
|
|
}, 0, lottie->framesCount() - 1);
|
|
}
|
|
|
|
state->failureShown = true;
|
|
state->progressShown = false;
|
|
}
|
|
|
|
void FailureAnimationPrepareFrame(
|
|
not_null<CraftAnimationState*> state,
|
|
not_null<RpWidget*> canvas,
|
|
int faceIndex) {
|
|
if (!state->failureAnimation) {
|
|
state->failureAnimation = SetupFailureAnimation(
|
|
canvas,
|
|
state);
|
|
}
|
|
const auto shared = state->shared.get();
|
|
const auto animation = state->failureAnimation.get();
|
|
if (animation->frame.isNull()) {
|
|
animation->frame = shared->forgeSides[faceIndex].frame;
|
|
}
|
|
const auto lastIndex = animation->finalCoverIndex;
|
|
const auto progress = (lastIndex >= 0)
|
|
? shared->covers[lastIndex].shownAnimation.value(1.)
|
|
: 0.;
|
|
const auto bg = anim::color(
|
|
shared->forgeSides[faceIndex].bg,
|
|
shared->forgeFail,
|
|
progress);
|
|
const auto radius = progress * st::boxRadius;
|
|
const auto rounded = (radius > 0.);
|
|
if (rounded) {
|
|
animation->frame.fill(Qt::transparent);
|
|
}
|
|
|
|
auto p = QPainter(&animation->frame);
|
|
const auto size = shared->forgeRect.size();
|
|
const auto rect = QRect(QPoint(), size);
|
|
if (rounded) {
|
|
auto hq = PainterHighQualityEnabler(p);
|
|
p.setBrush(bg);
|
|
p.setPen(Qt::NoPen);
|
|
p.drawRoundedRect(rect, radius, radius);
|
|
} else {
|
|
p.fillRect(rect, bg);
|
|
}
|
|
animation->lottie->paintInCenter(p, rect);
|
|
p.setOpacity(progress);
|
|
p.drawImage(
|
|
st::craftForgePadding,
|
|
st::craftForgePadding,
|
|
state->shared->lostRadial);
|
|
}
|
|
|
|
[[nodiscard]] QRect PrepareCraftFrame(
|
|
not_null<CraftAnimationState*> state,
|
|
int faceIndex,
|
|
crl::time now) {
|
|
Expects(state->successAnimation != nullptr);
|
|
|
|
const auto &shared = state->shared.get();
|
|
const auto success = state->successAnimation.get();
|
|
const auto elapsed = now - state->animationStartTime;
|
|
const auto end = state->animationDuration * Slowing();
|
|
const auto left = end - elapsed;
|
|
const auto fadeDuration = kSuccessFadeInDuration * Slowing();
|
|
const auto fadeInProgress = std::clamp(
|
|
(1. - (left / float64(fadeDuration))),
|
|
0.,
|
|
1.);
|
|
const auto expandStart = kSuccessExpandStart * Slowing();
|
|
const auto expanding = kSuccessExpandDuration * Slowing();
|
|
const auto expandProgress = std::clamp(
|
|
(expandStart - left) / float64(expanding),
|
|
0.,
|
|
1.);
|
|
if (expandProgress == 1. && !success->finished) {
|
|
success->finished = true;
|
|
}
|
|
const auto expanded = anim::easeOutCubic(1., expandProgress);
|
|
success->expanded = expanded;
|
|
return success->widget->prepareCraftFrame(success->frame, {
|
|
.initial = shared->forgeRect.size(),
|
|
.initialBg = shared->forgeSides[faceIndex].bg,
|
|
.fadeInProgress = fadeInProgress,
|
|
.expandProgress = expanded,
|
|
});
|
|
}
|
|
|
|
void PaintCube(
|
|
QPainter &p,
|
|
not_null<CraftAnimationState*> state,
|
|
not_null<RpWidget*> canvas,
|
|
QPointF center,
|
|
float64 size) {
|
|
const auto shared = state->shared.get();
|
|
|
|
const auto now = crl::now();
|
|
FailureAnimationCheck(state, canvas, now);
|
|
|
|
const auto success = state->successAnimation.get();
|
|
const auto failure = state->failureAnimation.get();
|
|
|
|
const auto finalCoverIndex = failure ? failure->finalCoverIndex : -1;
|
|
const auto finishingFailure = (finalCoverIndex >= 0)
|
|
? shared->covers[finalCoverIndex].shownAnimation.value(1.)
|
|
: 0.;
|
|
|
|
if (!state->nextFaceRevealed
|
|
&& state->currentPhaseIndex >= 0) {
|
|
const auto &config = kGiftAnimations[state->currentPhaseIndex];
|
|
const auto elapsed = (crl::now() - state->animationStartTime);
|
|
if (elapsed >= config.revealTime * Slowing()) {
|
|
state->nextFaceRevealed = true;
|
|
}
|
|
}
|
|
|
|
const auto faces = GetVisibleCubeFaces(
|
|
state->rotationX,
|
|
state->rotationY);
|
|
|
|
auto paintFailure = Fn<void()>();
|
|
for (const auto faceIndex : faces) {
|
|
const auto corners = ComputeCubeFaceCorners(
|
|
center,
|
|
size,
|
|
state->rotationX,
|
|
state->rotationY,
|
|
faceIndex);
|
|
if (!corners) {
|
|
continue;
|
|
}
|
|
|
|
const auto thisFaceRevealed = state->nextFaceRevealed
|
|
&& (faceIndex == state->nextFaceIndex);
|
|
auto faceSize = shared->forgeRect.size();
|
|
auto faceImage = shared->forgeSides[faceIndex].frame;
|
|
auto faceSource = QRect();
|
|
auto faceRotation = thisFaceRevealed
|
|
? state->nextFaceRotation
|
|
: 0;
|
|
|
|
for (auto i = 0; i < 4; ++i) {
|
|
if (i != state->currentlyFlying
|
|
&& state->giftToSide[i].face == faceIndex
|
|
&& shared->corners[i].giftButton) {
|
|
faceSize = QSize(); // Take from the frame.
|
|
faceImage = shared->corners[i].gift(1.);
|
|
faceRotation = state->giftToSide[i].rotation;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (thisFaceRevealed && state->allGiftsLanded) {
|
|
const auto cover = success ? success->widget : nullptr;
|
|
if (cover) {
|
|
faceSource = PrepareCraftFrame(state, faceIndex, now);
|
|
faceImage = success->frame;
|
|
} else {
|
|
FailureAnimationPrepareFrame(
|
|
state,
|
|
canvas,
|
|
faceIndex);
|
|
faceImage = state->failureAnimation->frame;
|
|
}
|
|
}
|
|
if (finishingFailure > 0.) {
|
|
p.setOpacity((faceIndex != state->nextFaceIndex)
|
|
? (1. - finishingFailure)
|
|
: 1.);
|
|
}
|
|
PaintCubeFace(
|
|
p,
|
|
*corners,
|
|
faceImage,
|
|
faceSize,
|
|
faceSource,
|
|
faceRotation);
|
|
}
|
|
if (finishingFailure > 0.) {
|
|
p.setOpacity(1.);
|
|
}
|
|
}
|
|
|
|
struct GiftFlightPosition {
|
|
QPointF origin;
|
|
float64 scale;
|
|
};
|
|
|
|
[[nodiscard]] GiftFlightPosition ComputeGiftFlightPosition(
|
|
QRect originalRect,
|
|
QPointF targetCenter,
|
|
float64 targetSize,
|
|
float64 progress,
|
|
float64 startOffsetY) {
|
|
const auto eased = progress;
|
|
|
|
const auto startOrigin = QPointF(originalRect.topLeft())
|
|
+ QPointF(0, startOffsetY);
|
|
const auto targetOrigin = targetCenter
|
|
- QPointF(targetSize, targetSize) / 2.;
|
|
const auto currentOrigin = startOrigin
|
|
+ (targetOrigin - startOrigin) * eased;
|
|
|
|
const auto originalSize = std::max(
|
|
originalRect.width(),
|
|
originalRect.height());
|
|
const auto endScale = (originalSize > 0.)
|
|
? (targetSize / float64(originalSize))
|
|
: 1.;
|
|
const auto currentScale = 1. + (endScale - 1.) * eased;
|
|
|
|
return {
|
|
.origin = currentOrigin,
|
|
.scale = currentScale,
|
|
};
|
|
}
|
|
|
|
[[nodiscard]] FacePlacement GetCameraFacingFreeFace(
|
|
not_null<const CraftAnimationState*> state) {
|
|
struct Vec3 {
|
|
float64 x, y, z;
|
|
};
|
|
|
|
const auto rotateY = [](Vec3 v, float64 angle) {
|
|
const auto c = std::cos(angle);
|
|
const auto s = std::sin(angle);
|
|
return Vec3{ v.x * c + v.z * s, v.y, -v.x * s + v.z * c };
|
|
};
|
|
const auto rotateX = [](Vec3 v, float64 angle) {
|
|
const auto c = std::cos(angle);
|
|
const auto s = std::sin(angle);
|
|
return Vec3{ v.x, v.y * c - v.z * s, v.y * s + v.z * c };
|
|
};
|
|
|
|
constexpr auto normals = std::array<Vec3, 6>{{
|
|
{ 0., 0., -1. },
|
|
{ 0., 0., 1. },
|
|
{ -1., 0., 0. },
|
|
{ 1., 0., 0. },
|
|
{ 0., -1., 0. },
|
|
{ 0., 1., 0. },
|
|
}};
|
|
|
|
constexpr auto faceUpVectors = std::array<Vec3, 6>{{
|
|
{ 0., -1., 0. },
|
|
{ 0., -1., 0. },
|
|
{ 0., -1., 0. },
|
|
{ 0., -1., 0. },
|
|
{ 0., 0., -1. },
|
|
{ 0., 0., 1. },
|
|
}};
|
|
|
|
const auto isOccupied = [&](int faceIndex) {
|
|
for (const auto &placement : state->giftToSide) {
|
|
if (placement.face == faceIndex) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
|
|
auto bestFace = -1;
|
|
auto bestZ = 0.;
|
|
|
|
for (auto i = 0; i != 6; ++i) {
|
|
if (isOccupied(i)) {
|
|
continue;
|
|
}
|
|
const auto transformed = rotateX(
|
|
rotateY(normals[i], state->rotationY),
|
|
state->rotationX);
|
|
if (bestFace < 0 || transformed.z < bestZ) {
|
|
bestFace = i;
|
|
bestZ = transformed.z;
|
|
}
|
|
}
|
|
|
|
if (bestFace < 0) {
|
|
return {};
|
|
}
|
|
|
|
const auto faceUp = rotateX(
|
|
rotateY(faceUpVectors[bestFace], state->rotationY),
|
|
state->rotationX);
|
|
|
|
const auto screenUpY = faceUp.y;
|
|
const auto screenUpX = faceUp.x;
|
|
|
|
auto rotation = 0;
|
|
if (std::abs(screenUpY) >= std::abs(screenUpX)) {
|
|
rotation = (screenUpY < 0.) ? 0 : 180;
|
|
} else {
|
|
rotation = (screenUpX < 0.) ? 90 : 270;
|
|
}
|
|
|
|
return { .face = bestFace, .rotation = rotation };
|
|
}
|
|
|
|
[[nodiscard]] std::array<QPointF, 4> InterpolateQuadCorners(
|
|
const std::array<QPointF, 4> &from,
|
|
const std::array<QPointF, 4> &to,
|
|
float64 progress) {
|
|
return {
|
|
from[0] + (to[0] - from[0]) * progress,
|
|
from[1] + (to[1] - from[1]) * progress,
|
|
from[2] + (to[2] - from[2]) * progress,
|
|
from[3] + (to[3] - from[3]) * progress,
|
|
};
|
|
}
|
|
|
|
[[nodiscard]] std::array<QPointF, 4> RectToCorners(QRectF rect) {
|
|
return {
|
|
rect.topLeft(),
|
|
rect.topRight(),
|
|
rect.bottomRight(),
|
|
rect.bottomLeft(),
|
|
};
|
|
}
|
|
|
|
void PaintFlyingGiftWithQuad(
|
|
QPainter &p,
|
|
const CraftState::CornerSnapshot &corner,
|
|
const std::array<QPointF, 4> &corners,
|
|
float64 progress) {
|
|
if (!corner.giftButton) {
|
|
return;
|
|
}
|
|
|
|
const auto frame = corner.gift(progress);
|
|
PaintCubeFace(p, corners, frame);
|
|
}
|
|
|
|
void PaintFlyingGift(
|
|
QPainter &p,
|
|
const CraftState::CornerSnapshot &corner,
|
|
const GiftFlightPosition &position,
|
|
float64 progress) {
|
|
if (!corner.giftButton) {
|
|
return;
|
|
}
|
|
|
|
const auto ratio = style::DevicePixelRatio();
|
|
const auto frame = corner.gift(progress);
|
|
const auto giftSize = QSizeF(frame.size()) / ratio;
|
|
const auto scaledSize = giftSize * position.scale;
|
|
const auto giftTopLeft = position.origin;
|
|
|
|
auto hq = PainterHighQualityEnabler(p);
|
|
p.drawImage(QRectF(giftTopLeft, scaledSize), frame);
|
|
}
|
|
|
|
void PaintSlideOutCorner(
|
|
QPainter &p,
|
|
const CraftState::CornerSnapshot &corner,
|
|
float64 progress) {
|
|
if (corner.originalRect.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
const auto ratio = style::DevicePixelRatio();
|
|
const auto currentTopLeft = QPointF(corner.originalRect.topLeft());
|
|
if (corner.giftButton) {
|
|
const auto frame = corner.gift(0.);
|
|
const auto giftSize = QSizeF(frame.size()) / ratio;
|
|
const auto giftTopLeft = currentTopLeft;
|
|
|
|
const auto &extend = st::defaultDropdownMenu.wrap.shadow.extend;
|
|
const auto innerTopLeft = giftTopLeft
|
|
+ QPointF(extend.left(), extend.top());
|
|
const auto innerWidth = giftSize.width()
|
|
- extend.left()
|
|
- extend.right();
|
|
|
|
p.drawImage(giftTopLeft, frame);
|
|
|
|
const auto badgeFade = std::clamp(1. - progress / 0.7, 0., 1.);
|
|
if (!corner.percentBadge.isNull() && badgeFade > 0.) {
|
|
const auto badgeSize = QSizeF(corner.percentBadge.size())
|
|
/ ratio;
|
|
const auto badgePos = innerTopLeft
|
|
- QPointF(badgeSize.width() / 3., badgeSize.height() / 3.);
|
|
p.setOpacity(badgeFade);
|
|
p.drawImage(badgePos, corner.percentBadge);
|
|
p.setOpacity(1.);
|
|
}
|
|
|
|
if (!corner.removeButton.isNull() && badgeFade > 0.) {
|
|
const auto removeSize = QSizeF(corner.removeButton.size())
|
|
/ ratio;
|
|
const auto removePos = innerTopLeft
|
|
+ QPointF(
|
|
(innerWidth
|
|
- removeSize.width()
|
|
+ removeSize.width() / 3.),
|
|
-removeSize.height() / 3.);
|
|
p.setOpacity(badgeFade);
|
|
p.drawImage(removePos, corner.removeButton);
|
|
p.setOpacity(1.);
|
|
}
|
|
} else if (!corner.addButton.isNull()) {
|
|
const auto fadeProgress = std::clamp(progress * 1.5, 0., 1.);
|
|
const auto addTopLeft = currentTopLeft;
|
|
p.setOpacity(1. - fadeProgress);
|
|
p.drawImage(addTopLeft, corner.addButton);
|
|
p.setOpacity(1.);
|
|
}
|
|
}
|
|
|
|
void PaintSlideOutPhase(
|
|
QPainter &p,
|
|
not_null<CraftState*> shared,
|
|
QSize canvasSize,
|
|
float64 progress) {
|
|
const auto ratio = style::DevicePixelRatio();
|
|
|
|
if (!shared->bottomPart.isNull()) {
|
|
const auto slideDistance = QSizeF(shared->bottomPart.size()).height()
|
|
/ ratio;
|
|
const auto offsetY = slideDistance * progress;
|
|
const auto opacity = 1. - progress;
|
|
|
|
p.setOpacity(opacity);
|
|
p.drawImage(
|
|
QPointF(0, shared->bottomPartY + offsetY),
|
|
shared->bottomPart);
|
|
p.setOpacity(1.);
|
|
}
|
|
|
|
for (const auto &corner : shared->corners) {
|
|
PaintSlideOutCorner(p, corner, progress);
|
|
}
|
|
|
|
auto hq = PainterHighQualityEnabler(p);
|
|
const auto forge = shared->forgeRect;
|
|
const auto radius = st::boxRadius;
|
|
p.setPen(Qt::NoPen);
|
|
p.setBrush(shared->forgeBgOverlay);
|
|
p.drawRoundedRect(forge, radius, radius);
|
|
st::craftForge.paintInCenter(p, forge, st::white->c);
|
|
p.setOpacity(1. - progress);
|
|
p.drawImage(
|
|
forge.x() + st::craftForgePadding,
|
|
forge.y() + st::craftForgePadding,
|
|
shared->forgePercent);
|
|
}
|
|
|
|
void PaintFailureThumbnails(
|
|
QPainter &p,
|
|
not_null<const CraftState*> shared,
|
|
QSize canvasSize,
|
|
float64 fadeProgress) {
|
|
if (fadeProgress <= 0. || shared->lostGifts.empty()) {
|
|
return;
|
|
}
|
|
|
|
p.setOpacity(fadeProgress);
|
|
|
|
const auto width = canvasSize.width();
|
|
const auto giftsCount = int(shared->lostGifts.size());
|
|
const auto &firstCorner = shared->corners[
|
|
shared->lostGifts.front().cornerIndex];
|
|
if (!firstCorner.giftButton) {
|
|
p.setOpacity(1.);
|
|
return;
|
|
}
|
|
const auto thumbSize = firstCorner.giftButton->size();
|
|
const auto &extend = st::defaultDropdownMenu.wrap.shadow.extend;
|
|
const auto thumbSpacing = st::boxRowPadding.left() / 2;
|
|
const auto totalThumbWidth = giftsCount * thumbSize.width()
|
|
+ (giftsCount - 1) * thumbSpacing;
|
|
const auto available = width - extend.left() - extend.right();
|
|
const auto skip = (totalThumbWidth > available)
|
|
? (available - giftsCount * thumbSize.width()) / (giftsCount - 1)
|
|
: thumbSpacing;
|
|
const auto full = giftsCount * thumbSize.width()
|
|
+ (giftsCount - 1) * skip;
|
|
auto x = (width - full) / 2;
|
|
const auto y = shared->forgeRect.bottom() + st::craftFailureThumbsTop;
|
|
const auto rubberOut = st::lineWidth;
|
|
|
|
for (const auto &gift : shared->lostGifts) {
|
|
if (gift.cornerIndex < 0) {
|
|
x += thumbSize.width() + skip;
|
|
continue;
|
|
}
|
|
const auto &corner = shared->corners[gift.cornerIndex];
|
|
const auto giftFrame = corner.gift(0.);
|
|
if (!giftFrame.isNull()) {
|
|
const auto targetRect = QRect(QPoint(x, y), thumbSize);
|
|
p.drawImage(targetRect, giftFrame);
|
|
|
|
if (!gift.number.isEmpty()) {
|
|
if (gift.badgeCache.isNull()) {
|
|
const auto burnedBg = BurnedBadgeBg();
|
|
gift.badgeCache = ValidateRotatedBadge(GiftBadge{
|
|
.text = gift.number,
|
|
.bg1 = burnedBg,
|
|
.bg2 = burnedBg,
|
|
.fg = st::white->c,
|
|
.small = true,
|
|
}, QMargins());
|
|
}
|
|
const auto inner = targetRect.marginsRemoved(extend);
|
|
p.save();
|
|
p.setClipRect(inner.marginsAdded(
|
|
{ rubberOut, rubberOut, rubberOut, rubberOut }));
|
|
const auto badgeW = gift.badgeCache.width()
|
|
/ gift.badgeCache.devicePixelRatio();
|
|
p.drawImage(
|
|
inner.x() + inner.width() + rubberOut - badgeW,
|
|
inner.y() - rubberOut,
|
|
gift.badgeCache);
|
|
p.restore();
|
|
}
|
|
}
|
|
x += thumbSize.width() + skip;
|
|
}
|
|
|
|
p.setOpacity(1.);
|
|
}
|
|
|
|
[[nodiscard]] std::array<QPointF, 4> RotateCornersForFace(
|
|
std::array<QPointF, 4> corners,
|
|
int rotation) {
|
|
const auto steps = (rotation / 90) % 4;
|
|
for (auto i = 0; i < steps; ++i) {
|
|
corners = {
|
|
corners[1],
|
|
corners[2],
|
|
corners[3],
|
|
corners[0],
|
|
};
|
|
}
|
|
return corners;
|
|
}
|
|
|
|
void StartGiftFlight(
|
|
not_null<CraftAnimationState*> state,
|
|
not_null<RpWidget*> canvas,
|
|
int startIndex) {
|
|
const auto &corners = state->shared->corners;
|
|
|
|
auto nextGift = -1;
|
|
for (auto i = startIndex; i < 4; ++i) {
|
|
if (corners[i].giftButton) {
|
|
nextGift = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (nextGift < 0) {
|
|
return;
|
|
}
|
|
|
|
state->currentlyFlying = nextGift;
|
|
state->giftToSide[nextGift] = GetCameraFacingFreeFace(state);
|
|
state->flightTargetCorners = std::nullopt;
|
|
|
|
state->flightAnimation.start(
|
|
[=] { canvas->update(); },
|
|
0.,
|
|
1.,
|
|
kFlightDuration,
|
|
anim::easeInCubic);
|
|
|
|
if (!state->continuousAnimation.animating()) {
|
|
state->continuousAnimation.start();
|
|
}
|
|
}
|
|
|
|
void StartGiftFlightToFace(
|
|
not_null<CraftAnimationState*> state,
|
|
not_null<RpWidget*> canvas,
|
|
int targetFace) {
|
|
const auto shared = state->shared.get();
|
|
const auto &corners = shared->corners;
|
|
|
|
auto nextCorner = -1;
|
|
for (auto i = 0; i < 4; ++i) {
|
|
if (corners[i].giftButton && state->giftToSide[i].face < 0) {
|
|
nextCorner = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (nextCorner < 0) {
|
|
return;
|
|
}
|
|
|
|
state->currentlyFlying = nextCorner;
|
|
|
|
const auto faceRotation = state->nextFaceRotation;
|
|
state->giftToSide[nextCorner] = FacePlacement{
|
|
.face = targetFace,
|
|
.rotation = faceRotation,
|
|
};
|
|
|
|
if (state->currentPhaseIndex >= 0) {
|
|
const auto &config = kGiftAnimations[state->currentPhaseIndex];
|
|
const auto initial = Rotation{
|
|
state->initialRotationX,
|
|
state->initialRotationY,
|
|
};
|
|
const auto endRotation = config.rotation(initial, config.duration);
|
|
|
|
const auto cubeSize = float64(shared->forgeRect.width());
|
|
const auto cubeCenter = QPointF(shared->forgeRect.topLeft())
|
|
+ QPointF(cubeSize, cubeSize) / 2.;
|
|
|
|
const auto targetCorners = ComputeCubeFaceCorners(
|
|
cubeCenter,
|
|
cubeSize,
|
|
endRotation.x,
|
|
endRotation.y,
|
|
targetFace);
|
|
if (targetCorners) {
|
|
state->flightTargetCorners = RotateCornersForFace(
|
|
*targetCorners,
|
|
faceRotation);
|
|
} else {
|
|
state->flightTargetCorners = std::nullopt;
|
|
}
|
|
} else {
|
|
state->flightTargetCorners = std::nullopt;
|
|
}
|
|
|
|
state->flightAnimation.start([=] {
|
|
canvas->update();
|
|
}, 0., 1., kFlightDuration, anim::easeInCubic);
|
|
|
|
if (!state->continuousAnimation.animating()) {
|
|
state->continuousAnimation.start();
|
|
}
|
|
}
|
|
|
|
void LandCurrentGift(not_null<CraftAnimationState*> state, crl::time now) {
|
|
if (state->currentlyFlying < 0) {
|
|
return;
|
|
}
|
|
|
|
const auto giftNumber = ++state->giftsLanded;
|
|
const auto isLastGift
|
|
= state->allGiftsLanded
|
|
= (giftNumber >= state->totalGifts);
|
|
|
|
const auto configIndex = (giftNumber - 1) * 2 + (isLastGift ? 0 : 1);
|
|
Assert(configIndex < 7);
|
|
|
|
const auto &config = kGiftAnimations[configIndex];
|
|
|
|
state->currentlyFlying = -1;
|
|
|
|
state->initialRotationX = state->rotationX;
|
|
state->initialRotationY = state->rotationY;
|
|
state->currentPhaseIndex = configIndex;
|
|
state->currentPhaseFinished = false;
|
|
state->animationStartTime = now;
|
|
state->animationDuration = config.duration;
|
|
state->nextFaceIndex = config.targetFaceIndex;
|
|
state->nextFaceRotation = config.targetFaceRotation;
|
|
state->nextFaceRevealed = false;
|
|
}
|
|
|
|
void PaintLoadingAnimation(
|
|
QPainter &p,
|
|
not_null<CraftAnimationState*> state) {
|
|
const auto &loading = state->loadingAnimation;
|
|
if (!loading || !state->loadingStartedTime) {
|
|
return;
|
|
}
|
|
|
|
const auto loadingShown = state->loadingShownAnimation.value(
|
|
state->loadingFadingOut ? 0. : 1.);
|
|
if (loadingShown <= 0.) {
|
|
return;
|
|
}
|
|
|
|
const auto shared = state->shared.get();
|
|
const auto forge = shared->forgeRect;
|
|
const auto inner = forge.marginsRemoved({
|
|
st::craftForgePadding,
|
|
st::craftForgePadding,
|
|
st::craftForgePadding,
|
|
st::craftForgePadding,
|
|
});
|
|
|
|
const auto radial = loading->computeState();
|
|
const auto adjustedState = RadialState{
|
|
.shown = radial.shown * loadingShown,
|
|
.arcFrom = radial.arcFrom,
|
|
.arcLength = radial.arcLength,
|
|
};
|
|
|
|
auto pen = QPen(st::white->c);
|
|
pen.setCapStyle(Qt::RoundCap);
|
|
InfiniteRadialAnimation::Draw(
|
|
p,
|
|
adjustedState,
|
|
inner.topLeft(),
|
|
inner.size(),
|
|
inner.width(),
|
|
pen,
|
|
st::craftForgeLoading.thickness);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
QImage CraftState::CornerSnapshot::gift(float64 progress) const {
|
|
if (!giftButton) {
|
|
return QImage();
|
|
} else if (progress == 1. && giftFrameFinal) {
|
|
return giftFrame;
|
|
} else if (progress < 1.) {
|
|
giftFrameFinal = false;
|
|
}
|
|
if (giftButton->makeCraftFrameIsFinal(giftFrame, progress)) {
|
|
giftFrameFinal = true;
|
|
}
|
|
return giftFrame;
|
|
}
|
|
|
|
void CraftState::paint(
|
|
QPainter &p,
|
|
QSize size,
|
|
int craftingHeight,
|
|
float64 slideProgress) {
|
|
const auto width = size.width();
|
|
const auto getBackdrop = [&](BackdropView &backdrop) {
|
|
const auto ratio = style::DevicePixelRatio();
|
|
const auto gradientSize = size;
|
|
auto &gradient = backdrop.gradient;
|
|
if (gradient.size() != gradientSize * ratio) {
|
|
gradient = CreateBgGradient(gradientSize, backdrop.colors);
|
|
}
|
|
return gradient;
|
|
};
|
|
auto patternOffsetY = 0.;
|
|
const auto paintPattern = [&](
|
|
QPainter &q,
|
|
PatternView &pattern,
|
|
const BackdropView &backdrop,
|
|
float64 shown) {
|
|
const auto color = backdrop.colors.patternColor;
|
|
const auto key = (color.red() << 16)
|
|
| (color.green() << 8)
|
|
| color.blue();
|
|
if (patternOffsetY != 0.) {
|
|
q.translate(0, patternOffsetY);
|
|
}
|
|
PaintBgPoints(
|
|
q,
|
|
PatternBgPoints(),
|
|
pattern.emojis[key],
|
|
pattern.emoji.get(),
|
|
color,
|
|
QRect(0, 0, width, st::boxTitleHeight + craftingHeight),
|
|
shown);
|
|
if (patternOffsetY != 0.) {
|
|
q.translate(0, -patternOffsetY);
|
|
}
|
|
};
|
|
auto animating = false;
|
|
auto newEdgeColor = std::optional<QColor>();
|
|
auto newButton1 = QColor();
|
|
auto newButton2 = QColor();
|
|
for (auto i = 0; i != 5; ++i) {
|
|
auto &cover = covers[i];
|
|
if (cover.shownAnimation.animating()) {
|
|
animating = true;
|
|
}
|
|
const auto finalValue = cover.shown ? 1. : 0.;
|
|
const auto shown = cover.shownAnimation.value(finalValue);
|
|
if (shown <= 0.) {
|
|
break;
|
|
} else if (shown == 1.) {
|
|
const auto next = i + 1;
|
|
if (next < 4
|
|
&& covers[next].shown
|
|
&& !covers[next].shownAnimation.animating()) {
|
|
continue;
|
|
}
|
|
}
|
|
p.setOpacity(shown);
|
|
p.drawImage(0, 0, getBackdrop(cover.backdrop));
|
|
paintPattern(p, cover.pattern, cover.backdrop, 1.);
|
|
if (coversAnimate) {
|
|
const auto edge = cover.backdrop.colors.edgeColor;
|
|
if (!newEdgeColor) {
|
|
newEdgeColor = edge;
|
|
newButton1 = cover.button1;
|
|
newButton2 = cover.button2;
|
|
} else {
|
|
newEdgeColor = anim::color(*newEdgeColor, edge, shown);
|
|
newButton1 = anim::color(newButton1, cover.button1, shown);
|
|
newButton2 = anim::color(newButton2, cover.button2, shown);
|
|
}
|
|
}
|
|
}
|
|
if (newEdgeColor) {
|
|
edgeColor = *newEdgeColor;
|
|
button1 = newButton1;
|
|
button2 = newButton2;
|
|
}
|
|
if (animating) {
|
|
p.setOpacity(1.);
|
|
} else {
|
|
coversAnimate = false;
|
|
}
|
|
}
|
|
|
|
void CraftState::updateForGiftCount(int count, Fn<void()> repaint) {
|
|
for (auto i = 5; i != 0;) {
|
|
auto &cover = covers[--i];
|
|
const auto shown = (i < count);
|
|
if (cover.shown != shown) {
|
|
cover.shown = shown;
|
|
const auto from = shown ? 0. : 1.;
|
|
const auto to = shown ? 1. : 0.;
|
|
cover.shownAnimation.start(
|
|
repaint,
|
|
from,
|
|
to,
|
|
st::fadeWrapDuration);
|
|
coversAnimate = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
CraftState::EmptySide CraftState::prepareEmptySide(int index) const {
|
|
const auto size = forgeRect.size();
|
|
const auto ratio = style::DevicePixelRatio();
|
|
auto result = QImage(size * ratio, QImage::Format_ARGB32_Premultiplied);
|
|
result.setDevicePixelRatio(ratio);
|
|
|
|
const auto bg = anim::color(forgeBg1, forgeBg2, index / 5.);
|
|
result.fill(bg);
|
|
|
|
auto p = QPainter(&result);
|
|
st::craftForge.paintInCenter(p, QRect(QPoint(), size), st::white->c);
|
|
|
|
//#if _DEBUG
|
|
// p.setFont(st::semiboldFont);
|
|
// p.setPen(st::white);
|
|
// p.drawText(
|
|
// size.width() / 10,
|
|
// size.width() / 10 + st::semiboldFont->ascent,
|
|
// QString::number(index));
|
|
//#endif // _DEBUG
|
|
|
|
p.end();
|
|
|
|
return { .bg = bg, .frame = result };
|
|
}
|
|
|
|
void SetupCraftProgressTitle(
|
|
not_null<VerticalLayout*> container,
|
|
not_null<rpl::variable<float64>*> opacity) {
|
|
const auto row = container->add(
|
|
object_ptr<RpWidget>(container),
|
|
st::boxRowPadding + st::craftProgressTitleMargin,
|
|
style::al_top);
|
|
|
|
const auto label = CreateChild<FlatLabel>(
|
|
row,
|
|
tr::lng_gift_craft_progress(),
|
|
st::uniqueGiftTitle);
|
|
label->setTextColorOverride(st::white->c);
|
|
|
|
const auto iconSize = st::craftProgressIconSize;
|
|
const auto iconWidget = CreateChild<RpWidget>(row);
|
|
iconWidget->resize(iconSize);
|
|
|
|
auto owned = Lottie::MakeIcon({
|
|
.name = u"craft_progress"_q,
|
|
.sizeOverride = iconSize,
|
|
.limitFps = true,
|
|
});
|
|
const auto icon = owned.get();
|
|
iconWidget->lifetime().add([kept = std::move(owned)] {});
|
|
|
|
const auto startAnimation = [=] {
|
|
icon->animate(
|
|
[=] { iconWidget->update(); },
|
|
0,
|
|
icon->framesCount() - 1);
|
|
};
|
|
startAnimation();
|
|
|
|
iconWidget->paintRequest(
|
|
) | rpl::on_next([=] {
|
|
auto p = QPainter(iconWidget);
|
|
p.setOpacity(opacity->current());
|
|
icon->paint(p, 0, 0);
|
|
if (!icon->animating() && icon->frameIndex() > 0) {
|
|
startAnimation();
|
|
}
|
|
}, iconWidget->lifetime());
|
|
|
|
rpl::combine(
|
|
label->sizeValue(),
|
|
row->sizeValue()
|
|
) | rpl::on_next([=](QSize labelSize, QSize rowSize) {
|
|
const auto skip = st::craftProgressIconSkip;
|
|
const auto totalWidth = iconSize.width() + skip + labelSize.width();
|
|
const auto rowHeight = std::max(iconSize.height(), labelSize.height());
|
|
if (rowSize.height() != rowHeight) {
|
|
row->resize(rowSize.width(), rowHeight);
|
|
return;
|
|
}
|
|
const auto left = (rowSize.width() - totalWidth) / 2;
|
|
iconWidget->move(left, (rowHeight - iconSize.height()) / 2);
|
|
label->move(
|
|
left + iconSize.width() + skip,
|
|
(rowHeight - labelSize.height()) / 2);
|
|
}, row->lifetime());
|
|
|
|
opacity->value(
|
|
) | rpl::on_next([=](float64 value) {
|
|
label->setOpacity(value);
|
|
iconWidget->update();
|
|
}, row->lifetime());
|
|
|
|
}
|
|
|
|
void SetupProgressControls(
|
|
not_null<CraftAnimationState*> state,
|
|
not_null<RpWidget*> canvas) {
|
|
const auto add = [&](not_null<FlatLabel*> label) {
|
|
state->progressOpacity.value() | rpl::on_next([=](float64 opacity) {
|
|
label->setOpacity(opacity);
|
|
}, label->lifetime());
|
|
};
|
|
|
|
const auto controls = CreateChild<VerticalLayout>(canvas);
|
|
controls->resizeToWidth(canvas->width());
|
|
controls->move(0, state->shared->craftingBottom);
|
|
|
|
SetupCraftProgressTitle(controls, &state->progressOpacity);
|
|
|
|
const auto subColor = QColor(255, 255, 255, 178);
|
|
const auto about = controls->add(
|
|
object_ptr<FlatLabel>(
|
|
controls,
|
|
tr::lng_gift_craft_progress_about(
|
|
lt_gift,
|
|
rpl::single(tr::marked(state->shared->giftName)),
|
|
tr::rich),
|
|
st::uniqueGiftSubtitle),
|
|
st::boxRowPadding + st::craftProgressAboutMargin,
|
|
style::al_top);
|
|
add(about);
|
|
about->setTextColorOverride(subColor);
|
|
|
|
const auto warning = controls->add(
|
|
object_ptr<FlatLabel>(
|
|
controls,
|
|
tr::lng_gift_craft_progress_fails(),
|
|
st::craftAbout),
|
|
st::boxRowPadding + st::craftProgressWarningMargin,
|
|
style::al_top);
|
|
add(warning);
|
|
warning->setTextColorOverride(subColor);
|
|
|
|
const auto chance = controls->add(
|
|
object_ptr<FlatLabel>(
|
|
controls,
|
|
tr::lng_gift_craft_progress_chance(
|
|
tr::now,
|
|
lt_percent,
|
|
FormatPercent(state->shared->successPermille)),
|
|
st::craftChance),
|
|
st::boxRowPadding + st::craftProgressChanceMargin,
|
|
style::al_top);
|
|
add(chance);
|
|
chance->setTextColorOverride(st::white->c);
|
|
chance->paintOn([=](QPainter &p) {
|
|
if (const auto opacity = state->progressOpacity.current()) {
|
|
p.setPen(Qt::NoPen);
|
|
p.setOpacity(opacity);
|
|
p.setBrush(state->shared->forgeBgOverlay);
|
|
const auto radius = chance->height() / 2.;
|
|
p.drawRoundedRect(chance->rect(), radius, radius);
|
|
}
|
|
});
|
|
|
|
controls->showOn(state->progressOpacity.value(
|
|
) | rpl::map(
|
|
rpl::mappers::_1 > 0.
|
|
) | rpl::distinct_until_changed());
|
|
}
|
|
|
|
void SetupFailureControls(
|
|
not_null<CraftAnimationState*> state,
|
|
not_null<RpWidget*> canvas) {
|
|
const auto add = [&](not_null<FlatLabel*> label) {
|
|
state->failureOpacity.value() | rpl::on_next([=](float64 opacity) {
|
|
label->setOpacity(opacity);
|
|
}, label->lifetime());
|
|
};
|
|
|
|
const auto controls = CreateChild<VerticalLayout>(canvas);
|
|
controls->resizeToWidth(canvas->width());
|
|
controls->move(0, state->shared->craftingBottom);
|
|
|
|
const auto title = controls->add(
|
|
object_ptr<FlatLabel>(
|
|
controls,
|
|
tr::lng_gift_craft_failed_title(),
|
|
st::uniqueGiftTitle),
|
|
st::boxRowPadding + st::craftFailureTitleMargin,
|
|
style::al_top);
|
|
add(title);
|
|
title->setTextColorOverride(QColor(0xF8, 0x4A, 0x4A));
|
|
|
|
const auto about = controls->add(
|
|
object_ptr<FlatLabel>(
|
|
controls,
|
|
tr::lng_gift_craft_failed_about(
|
|
lt_count,
|
|
rpl::single(state->shared->lostGifts.size() * 1.),
|
|
tr::rich),
|
|
st::uniqueGiftSubtitle),
|
|
st::boxRowPadding + st::craftFailureAboutMargin,
|
|
style::al_top);
|
|
add(about);
|
|
about->setTextColorOverride(QColor(0xFF, 0xBC, 0x9B));
|
|
|
|
controls->showOn(state->failureOpacity.value(
|
|
) | rpl::map(
|
|
rpl::mappers::_1 > 0.
|
|
) | rpl::distinct_until_changed());
|
|
}
|
|
|
|
void SetupCraftNewButton(
|
|
not_null<CraftAnimationState*> state,
|
|
not_null<RpWidget*> canvas) {
|
|
const auto button = CreateChild<GradientButton>(
|
|
canvas,
|
|
QGradientStops{
|
|
{ 0., QColor(0xE2, 0x75, 0x19) },
|
|
{ 1., QColor(0xDD, 0x48, 0x19) },
|
|
});
|
|
button->setFullRadius(true);
|
|
button->setClickedCallback([=] {
|
|
state->retryWithNewGift();
|
|
});
|
|
|
|
const auto buttonLabel = CreateChild<FlatLabel>(
|
|
button,
|
|
tr::lng_gift_craft_new_button(),
|
|
st::creditsBoxButtonLabel);
|
|
buttonLabel->setTextColorOverride(st::white->c);
|
|
buttonLabel->setAttribute(
|
|
Qt::WA_TransparentForMouseEvents);
|
|
button->sizeValue() | rpl::on_next([=](QSize size) {
|
|
buttonLabel->moveToLeft(
|
|
(size.width() - buttonLabel->width()) / 2,
|
|
st::giftBox.button.textTop);
|
|
}, buttonLabel->lifetime());
|
|
|
|
const auto buttonWidth = canvas->width()
|
|
- st::craftBoxButtonPadding.left()
|
|
- st::craftBoxButtonPadding.right();
|
|
const auto buttonHeight = st::giftBox.button.height;
|
|
button->setNaturalWidth(buttonWidth);
|
|
button->setGeometry(
|
|
st::craftBoxButtonPadding.left(),
|
|
state->shared->originalButtonY,
|
|
buttonWidth,
|
|
buttonHeight);
|
|
button->show();
|
|
|
|
button->shownValue() | rpl::filter(
|
|
rpl::mappers::_1
|
|
) | rpl::take(1) | rpl::on_next([=] {
|
|
button->startGlareAnimation();
|
|
}, button->lifetime());
|
|
}
|
|
|
|
void StartCraftAnimation(
|
|
not_null<GenericBox*> box,
|
|
std::shared_ptr<ChatHelpers::Show> show,
|
|
std::shared_ptr<CraftState> shared,
|
|
Fn<void(Fn<void(CraftResult)> callback)> startRequest,
|
|
Fn<void(Fn<void()> closeCurrent)> retryWithNewGift) {
|
|
const auto container = box->verticalLayout();
|
|
while (container->count() > 0) {
|
|
delete container->widgetAt(0);
|
|
}
|
|
|
|
auto canvas = object_ptr<RpWidget>(container);
|
|
const auto raw = canvas.data();
|
|
const auto state = raw->lifetime().make_state<CraftAnimationState>();
|
|
|
|
state->retryWithNewGift = [=] {
|
|
retryWithNewGift(crl::guard(box, [=] { box->closeBox(); }));
|
|
};
|
|
|
|
const auto title = CreateChild<FlatLabel>(
|
|
raw,
|
|
tr::lng_gift_craft_title(),
|
|
st::uniqueGiftTitle);
|
|
title->naturalWidthValue() | rpl::on_next([=](int titleWidth) {
|
|
const auto width = st::boxWideWidth;
|
|
title->moveToLeft(
|
|
(width - titleWidth) / 2,
|
|
st::craftTitleMargin.top(),
|
|
width);
|
|
}, title->lifetime());
|
|
title->setTextColorOverride(st::white->c);
|
|
|
|
const auto height = shared->containerHeight;
|
|
const auto craftingHeight = shared->craftingBottom - shared->craftingTop;
|
|
state->shared = std::move(shared);
|
|
state->shared->craftingStarted = true;
|
|
|
|
const auto craftingSize = QSize(container->width(), height);
|
|
raw->resize(craftingSize);
|
|
|
|
for (auto &corner : state->shared->corners) {
|
|
if (corner.giftButton) {
|
|
++state->totalGifts;
|
|
}
|
|
}
|
|
|
|
state->loadingAnimation = std::make_unique<InfiniteRadialAnimation>(
|
|
[=] { raw->update(); },
|
|
st::craftForgeLoading);
|
|
|
|
SetupProgressControls(state, raw);
|
|
SetupFailureControls(state, raw);
|
|
|
|
state->progressShown.value(
|
|
) | rpl::combine_previous(
|
|
) | rpl::on_next([=](bool wasShown, bool nowShown) {
|
|
if (wasShown == nowShown) {
|
|
return;
|
|
}
|
|
const auto from = wasShown ? 1. : 0.;
|
|
const auto to = nowShown ? 1. : 0.;
|
|
state->progressFadeIn.start([=] {
|
|
raw->update();
|
|
}, from, to, kProgressFadeInDuration, anim::easeOutCubic);
|
|
}, raw->lifetime());
|
|
|
|
state->failureShown.value(
|
|
) | rpl::combine_previous(
|
|
) | rpl::on_next([=](bool wasShown, bool nowShown) {
|
|
if (wasShown == nowShown) {
|
|
return;
|
|
}
|
|
const auto from = wasShown ? 1. : 0.;
|
|
const auto to = nowShown ? 1. : 0.;
|
|
state->failureFadeIn.start([=] {
|
|
raw->update();
|
|
if (!state->failureFadeIn.animating()) {
|
|
SetupCraftNewButton(state, raw);
|
|
}
|
|
}, from, to, kFailureFadeDuration, anim::easeOutCubic);
|
|
}, raw->lifetime());
|
|
|
|
raw->paintOn([=](QPainter &p) {
|
|
const auto shared = state->shared.get();
|
|
const auto success = state->successAnimation.get();
|
|
const auto failure = state->failureAnimation.get();
|
|
const auto slideProgress = state->slideOutAnimation.value(1.);
|
|
if (slideProgress < 1. || (failure && failure->started)) {
|
|
shared->paint(p, craftingSize, craftingHeight, slideProgress);
|
|
} else {
|
|
if (shared->craftBg.isNull()) {
|
|
const auto ratio = style::DevicePixelRatio();
|
|
shared->craftBg = QImage(
|
|
craftingSize * ratio,
|
|
QImage::Format_ARGB32_Premultiplied);
|
|
shared->craftBg.setDevicePixelRatio(ratio);
|
|
shared->craftBg.fill(Qt::transparent);
|
|
|
|
auto q = QPainter(&shared->craftBg);
|
|
shared->paint(q, craftingSize, craftingHeight, 1.);
|
|
}
|
|
if (success && success->hiding) {
|
|
p.setOpacity(1. - success->heightAnimation.value(1.));
|
|
}
|
|
p.drawImage(0, 0, shared->craftBg);
|
|
if (success && success->hiding) {
|
|
p.setOpacity(1.);
|
|
}
|
|
}
|
|
|
|
if (slideProgress < 1.) {
|
|
PaintSlideOutPhase(p, shared, craftingSize, slideProgress);
|
|
} else {
|
|
const auto craftingOffsetY = success
|
|
? (-success->coverShift * success->expanded)
|
|
: 0.;
|
|
if (success && success->expanded > 0.) {
|
|
const auto width = st::boxWideWidth;
|
|
const auto top = st::craftTitleMargin.top();
|
|
title->moveToLeft(
|
|
(width - title->naturalWidth()) / 2,
|
|
top - (success->expanded * (top + title->height())),
|
|
width);
|
|
}
|
|
const auto cubeSize = float64(shared->forgeRect.width());
|
|
const auto cubeCenter = QPointF(shared->forgeRect.topLeft())
|
|
+ QPointF(cubeSize, cubeSize) / 2.
|
|
+ QPointF(0, craftingOffsetY);
|
|
|
|
for (auto i = 0; i < 4; ++i) {
|
|
const auto &corner = shared->corners[i];
|
|
if (corner.giftButton && state->giftToSide[i].face < 0) {
|
|
const auto giftTopLeft = QPointF()
|
|
+ QPointF(corner.originalRect.topLeft())
|
|
+ QPointF(0, craftingOffsetY);
|
|
p.drawImage(giftTopLeft, corner.gift(0));
|
|
}
|
|
}
|
|
|
|
const auto flying = state->currentlyFlying;
|
|
const auto firstFlyProgress = (flying == 0)
|
|
? state->flightAnimation.value(1.)
|
|
: state->giftsLanded
|
|
? 1.
|
|
: 0.;
|
|
const auto loadingShown = state->loadingStartedTime
|
|
&& !state->loadingFadingOut;
|
|
if (firstFlyProgress < 1.) {
|
|
const auto skipForgeIcon = loadingShown && anim::Disabled();
|
|
PaintCubeFirstFlight(
|
|
p,
|
|
state,
|
|
firstFlyProgress,
|
|
skipForgeIcon);
|
|
} else {
|
|
PaintCube(p, state, raw, cubeCenter, cubeSize);
|
|
}
|
|
|
|
if (flying >= 0) {
|
|
const auto &corner = shared->corners[flying];
|
|
const auto progress = (flying > 0)
|
|
? state->flightAnimation.value(1.)
|
|
: firstFlyProgress;
|
|
|
|
if (state->flightTargetCorners) {
|
|
const auto sourceRect = QRectF(corner.originalRect)
|
|
.translated(0, craftingOffsetY);
|
|
const auto sourceCorners = RectToCorners(sourceRect);
|
|
const auto interpolatedCorners = InterpolateQuadCorners(
|
|
sourceCorners,
|
|
*state->flightTargetCorners,
|
|
progress);
|
|
PaintFlyingGiftWithQuad(
|
|
p,
|
|
corner,
|
|
interpolatedCorners,
|
|
progress);
|
|
} else {
|
|
const auto position = ComputeGiftFlightPosition(
|
|
corner.originalRect,
|
|
cubeCenter,
|
|
cubeSize,
|
|
progress,
|
|
craftingOffsetY);
|
|
PaintFlyingGift(p, corner, position, progress);
|
|
}
|
|
}
|
|
|
|
PaintLoadingAnimation(p, state);
|
|
|
|
if (const auto opacity = state->failureOpacity.current()) {
|
|
PaintFailureThumbnails(
|
|
p,
|
|
shared,
|
|
craftingSize,
|
|
opacity);
|
|
}
|
|
}
|
|
|
|
state->progressOpacity = state->progressFadeIn.value(
|
|
state->progressShown.current() ? 1. : 0.);
|
|
state->failureOpacity = state->failureFadeIn.value(
|
|
state->failureShown.current() ? 1. : 0.);
|
|
});
|
|
|
|
const auto startFlying = [=] {
|
|
if (state->loadingStartedTime > 0) {
|
|
state->loadingFadingOut = true;
|
|
state->loadingShownAnimation.start(
|
|
[=] { raw->update(); },
|
|
1.,
|
|
0.,
|
|
kLoadingFadeOutDuration);
|
|
}
|
|
StartGiftFlight(state, raw, 0);
|
|
};
|
|
|
|
const auto tryStartFlying = [=] {
|
|
const auto slideFinished = !state->slideOutAnimation.animating();
|
|
const auto resultArrived = state->craftResult.has_value();
|
|
const auto notYetFlying = (state->currentlyFlying < 0)
|
|
&& (state->giftsLanded == 0);
|
|
if (!slideFinished || !notYetFlying) {
|
|
return;
|
|
}
|
|
if (!resultArrived) {
|
|
if (!state->loadingStartedTime) {
|
|
state->loadingStartedTime = crl::now();
|
|
state->loadingAnimation->start();
|
|
state->loadingShownAnimation.start(
|
|
[=] { raw->update(); },
|
|
0.,
|
|
1.,
|
|
kLoadingFadeInDuration);
|
|
}
|
|
return;
|
|
}
|
|
if (!state->loadingStartedTime) {
|
|
startFlying();
|
|
return;
|
|
}
|
|
const auto elapsed = crl::now() - state->loadingStartedTime;
|
|
if (elapsed >= kLoadingMinDuration) {
|
|
startFlying();
|
|
} else {
|
|
base::call_delayed(
|
|
kLoadingMinDuration - elapsed,
|
|
raw,
|
|
startFlying);
|
|
}
|
|
};
|
|
|
|
state->progressShown = true;
|
|
|
|
state->slideOutAnimation.start([=] {
|
|
raw->update();
|
|
|
|
const auto progress = state->slideOutAnimation.value(1.);
|
|
if (progress >= 1.) {
|
|
tryStartFlying();
|
|
}
|
|
}, 0., 1., crl::time(300), anim::easeOutCubic);
|
|
|
|
state->continuousAnimation.init([=](crl::time now) {
|
|
if (state->currentPhaseIndex >= 0) {
|
|
const auto &config = kGiftAnimations[state->currentPhaseIndex];
|
|
const auto elapsed = state->currentPhaseFinished
|
|
? (config.duration * Slowing())
|
|
: (now - state->animationStartTime);
|
|
const auto initial = Rotation{
|
|
state->initialRotationX,
|
|
state->initialRotationY,
|
|
};
|
|
const auto r = config.rotation(initial, elapsed / Slowing());
|
|
state->rotationX = r.x;
|
|
state->rotationY = r.y;
|
|
}
|
|
|
|
raw->update();
|
|
|
|
if (state->currentlyFlying >= 0
|
|
&& !state->flightAnimation.animating()) {
|
|
LandCurrentGift(state, now);
|
|
}
|
|
|
|
if (!state->flightAnimation.animating()
|
|
&& state->currentlyFlying < 0
|
|
&& state->giftsLanded > 0
|
|
&& state->giftsLanded < state->totalGifts
|
|
&& state->currentPhaseIndex >= 0) {
|
|
const auto elapsed = now - state->animationStartTime;
|
|
const auto start = state->animationDuration - kFlightDuration;
|
|
if (elapsed >= start * Slowing()) {
|
|
StartGiftFlightToFace(state, raw, state->nextFaceIndex);
|
|
}
|
|
}
|
|
|
|
const auto success = state->successAnimation.get();
|
|
if (!state->currentPhaseFinished
|
|
&& !state->flightAnimation.animating()
|
|
&& state->currentlyFlying < 0
|
|
&& state->giftsLanded >= state->totalGifts
|
|
&& state->totalGifts > 0
|
|
&& state->currentPhaseIndex >= 0) {
|
|
const auto elapsed = now - state->animationStartTime;
|
|
if (elapsed >= state->animationDuration * Slowing()) {
|
|
state->currentPhaseFinished = true;
|
|
if (success) {
|
|
const auto wasContent = box->height();
|
|
const auto wasTotal = box->parentWidget()->height();
|
|
const auto wasButtons = wasTotal - wasContent;
|
|
|
|
container->clear();
|
|
container->add(std::move(success->owned));
|
|
|
|
box->setStyle(st::giftBox);
|
|
|
|
auto craftAnotherCallback = crl::guard(box, [=] {
|
|
retryWithNewGift([=] { box->closeBox(); });
|
|
});
|
|
|
|
const auto entry = EntryForUpgradedGift(
|
|
*state->craftResult,
|
|
0,
|
|
nullptr,
|
|
craftAnotherCallback);
|
|
Settings::GenericCreditsEntryBody(box, show, entry);
|
|
container->resizeToWidth(st::boxWideWidth);
|
|
|
|
const auto nowContent = box->height();
|
|
const auto nowTotal = box->parentWidget()->height();
|
|
const auto nowButtons = nowTotal - nowContent;
|
|
|
|
box->animateHeightFrom(
|
|
wasContent + wasButtons - nowButtons);
|
|
success->hiding = true;
|
|
const auto duration = kSuccessExpandDuration
|
|
- kSuccessExpandStart;
|
|
success->heightAnimation.start([=] {
|
|
const auto height = anim::interpolate(
|
|
craftingSize.height(),
|
|
success->coverHeight,
|
|
success->heightAnimation.value(1.));
|
|
raw->resize(craftingSize.width(), height);
|
|
}, 0., 1., duration, anim::easeOutCubic);
|
|
|
|
raw->setParent(box->parentWidget());
|
|
raw->show();
|
|
}
|
|
}
|
|
}
|
|
if (success && success->finished) {
|
|
state->continuousAnimation.stop();
|
|
raw->hide();
|
|
|
|
StartFireworks(box->parentWidget());
|
|
}
|
|
return true;
|
|
});
|
|
|
|
container->add(std::move(canvas));
|
|
|
|
if (startRequest) {
|
|
const auto weak = base::make_weak(raw);
|
|
startRequest([=](CraftResult result) {
|
|
if (!weak) {
|
|
return;
|
|
} else if (v::is<CraftResultError>(result)) {
|
|
box->uiShow()->show(MakeInformBox({
|
|
.text = v::get<CraftResultError>(result).type,
|
|
}));
|
|
box->closeBox();
|
|
return;
|
|
} else if (v::is<CraftResultWait>(result)) {
|
|
const auto when = base::unixtime::now()
|
|
+ v::get<CraftResultWait>(result).seconds;
|
|
ShowCraftLaterError(box->uiShow(), when);
|
|
box->closeBox();
|
|
return;
|
|
}
|
|
using Result = std::shared_ptr<Data::GiftUpgradeResult>;
|
|
state->craftResult = v::get<Result>(result);
|
|
if (const auto result = state->craftResult->get()) {
|
|
auto numberText = (result->info.unique && result->info.unique->number > 0)
|
|
? rpl::single(u"#"_q + Lang::FormatCountDecimal(result->info.unique->number))
|
|
: rpl::producer<QString>();
|
|
|
|
state->successAnimation = std::make_unique<
|
|
CraftDoneAnimation
|
|
>(CraftDoneAnimation{
|
|
.owned = MakeUniqueGiftCover(
|
|
container,
|
|
rpl::single(UniqueGiftCover{ *result->info.unique }),
|
|
{ .numberText = std::move(numberText) }),
|
|
});
|
|
const auto success = state->successAnimation.get();
|
|
|
|
const auto raw = success->owned.get();
|
|
success->widget = raw;
|
|
raw->hide();
|
|
raw->resizeToWidth(st::boxWideWidth);
|
|
SendPendingMoveResizeEvents(raw);
|
|
|
|
success->coverHeight = raw->height();
|
|
success->coverShift = state->shared->forgeRect.y()
|
|
+ (state->shared->forgeRect.height() / 2)
|
|
- (raw->height() / 2);
|
|
}
|
|
tryStartFlying();
|
|
});
|
|
}
|
|
}
|
|
|
|
} // namespace Ui
|