Finish upgrading gifts animation.

This commit is contained in:
John Preston
2025-12-12 20:53:38 +04:00
parent c758f05a7a
commit ffd50c5582
4 changed files with 357 additions and 113 deletions

View File

@@ -83,9 +83,12 @@ namespace {
constexpr auto kTooltipDuration = 3 * crl::time(1000);
constexpr auto kHorizontalBar = QChar(0x2015);
constexpr auto kSpinnerRows = 6;
constexpr auto kSpinDuration = crl::time(160);
using Ui::AddTableRow;
using Ui::TableRowTooltipData;
using SpinnerState = Data::GiftUpgradeSpinner::State;
[[nodiscard]] QString CreateMessageLink(
not_null<Main::Session*> session,
@@ -276,6 +279,174 @@ using Ui::TableRowTooltipData;
return MakeValueWithSmallButton(table, label, std::move(text), handler);
}
[[nodiscard]] object_ptr<Ui::RpWidget> MakeAttributeValue(
not_null<Ui::TableLayout*> table,
const Data::UniqueGiftAttribute &attribute,
Fn<void(not_null<Ui::RpWidget*>, int)> showTooltip,
std::shared_ptr<Data::GiftUpgradeSpinner> spinner,
std::vector<Data::UniqueGiftAttribute> spinning,
SpinnerState finishedState) {
if (!spinner) {
return MakeAttributeValue(table, attribute, showTooltip);
}
auto result = object_ptr<Ui::RpWidget>(table);
const auto raw = result.get();
struct Row {
Data::UniqueGiftAttribute attribute;
object_ptr<Ui::RpWidget> widget;
QImage frame;
};
struct State {
int wasIndex = 0;
int nowIndex = 0;
std::vector<Row> rows;
Ui::Animations::Simple animation;
QImage fading;
Fn<void()> repaint;
bool finished = false;
};
const auto state = raw->lifetime().make_state<State>();
state->repaint = [=] { raw->update(); };
const auto margin = st::giveawayGiftCodeValueMargin;
const auto startWithin = [=] {
Expects(!state->rows.empty());
state->wasIndex = state->nowIndex;
state->nowIndex = (state->nowIndex + 1) % state->rows.size();
state->animation.start(state->repaint, 0., 1., kSpinDuration);
};
const auto startToTarget = [=] {
if (state->nowIndex != 0) {
state->wasIndex = state->nowIndex;
state->nowIndex = 0;
state->animation.start(
state->repaint,
0.,
1.,
kSpinDuration * 3,
anim::easeOutCubic);
}
};
const auto add = [&](const Data::UniqueGiftAttribute &value) {
if (state->rows.size() >= kSpinnerRows) {
return false;
}
const auto already = ranges::contains(
state->rows,
value,
&Row::attribute);
if (!already) {
state->rows.push_back(Row{
.attribute = value,
.widget = MakeAttributeValue(table, value, showTooltip),
});
const auto widget = state->rows.back().widget.get();
widget->setParent(raw);
widget->hide();
}
return true;
};
add(attribute);
for (const auto &item : spinning) {
if (!add(item)) {
break;
}
}
raw->widthValue() | rpl::on_next([=](int width) {
auto height = 0;
const auto inner = width - margin.left() - margin.right();
if (inner > 0) {
for (const auto &row : state->rows) {
row.widget->resizeToWidth(inner);
row.widget->move(margin.left(), margin.top());
height = std::max(height, row.widget->height());
}
}
raw->resize(width, margin.top() + height + margin.bottom());
crl::on_main(raw, [=] {
for (const auto &row : state->rows) {
Ui::SendPendingMoveResizeEvents(row.widget.get());
}
});
}, raw->lifetime());
style::PaletteChanged() | rpl::on_next([=] {
for (auto &row : state->rows) {
row.frame = QImage();
}
state->fading = QImage();
}, raw->lifetime());
raw->paintOn([=](QPainter &p) {
if (state->finished) {
return;
}
auto progress = state->animation.value(1.);
if (progress >= 1.) {
const auto ending = (spinner->state.current() >= finishedState);
if (ending) {
startToTarget();
} else {
startWithin();
}
progress = state->animation.value(1.);
if (ending && progress >= 1.) {
state->rows.front().widget->show();
state->finished = true;
return;
}
}
const auto ratio = style::DevicePixelRatio();
const auto h = raw->height();
if (state->fading.height() != h * ratio) {
state->fading = QImage(
QSize(1, h) * ratio,
QImage::Format_ARGB32_Premultiplied);
state->fading.setDevicePixelRatio(ratio);
state->fading.fill(Qt::transparent);
auto q = QPainter(&state->fading);
auto brush = QLinearGradient(0, 0, 0, margin.top());
brush.setStops({
{ 0., QColor(255, 255, 255, 255) },
{ 1., QColor(255, 255, 255, 0) },
});
q.fillRect(0, 0, 1, margin.top(), brush);
brush.setStart(0, h);
brush.setFinalStop(0, h - margin.bottom());
q.fillRect(0, h - margin.bottom(), 1, margin.bottom(), brush);
}
auto &was = state->rows[state->wasIndex];
auto &now = state->rows[state->nowIndex];
const auto validate = [&](Row &row) {
const auto size = row.widget->size();
if (row.frame.size() != size * ratio) {
row.frame = Ui::GrabWidgetToImage(row.widget.get());
}
};
validate(was);
validate(now);
const auto t = progress * (margin.top() + was.widget->height());
const auto b = progress * (now.widget->height() + margin.bottom());
p.drawImage(
margin.left(),
margin.top() - int(base::SafeRound(t)),
was.frame);
p.drawImage(
margin.left(),
raw->height() - int(base::SafeRound(b)),
now.frame);
p.drawImage(raw->rect(), state->fading);
});
startWithin();
return result;
}
void AddUniqueGiftPropertyRows(
not_null<Ui::RpWidget*> container,
not_null<Ui::TableLayout*> table,
@@ -297,18 +468,55 @@ void AddUniqueGiftPropertyRows(
rpl::single(TextWithEntities{ percent }),
Ui::Text::WithEntities));
};
const auto empty = std::vector<Data::UniqueGiftAttribute>();
const auto extract = [&](const auto &list) {
auto result = empty;
result.reserve(list.size());
for (const auto &item : list) {
result.push_back(item);
}
return result;
};
const auto attributes = spinner ? &spinner->attributes : nullptr;
const auto models = spinner ? extract(attributes->models) : empty;
const auto patterns = spinner ? extract(attributes->patterns) : empty;
const auto backdrops = spinner ? extract(attributes->backdrops) : empty;
const auto margin = spinner
? style::margins()
: st::giveawayGiftCodeValueMargin;
AddTableRow(
table,
tr::lng_gift_unique_model(),
MakeAttributeValue(table, unique->model, showRarity));
AddTableRow(
table,
tr::lng_gift_unique_backdrop(),
MakeAttributeValue(table, unique->backdrop, showRarity));
MakeAttributeValue(
table,
unique->model,
showRarity,
spinner,
models,
SpinnerState::FinishedModel),
margin);
AddTableRow(
table,
tr::lng_gift_unique_symbol(),
MakeAttributeValue(table, unique->pattern, showRarity));
MakeAttributeValue(
table,
unique->pattern,
showRarity,
spinner,
backdrops,
SpinnerState::FinishedPattern),
margin);
AddTableRow(
table,
tr::lng_gift_unique_backdrop(),
MakeAttributeValue(
table,
unique->backdrop,
showRarity,
spinner,
patterns,
SpinnerState::FinishedBackdrop),
margin);
}
[[nodiscard]] object_ptr<Ui::RpWidget> MakeStarGiftStarsValue(

View File

@@ -3603,53 +3603,65 @@ void AddUniqueGiftCover(
const auto now = crl::now();
const auto elapsed = now - state->spinStarted;
auto current = state->spinner->state.current();
const auto switchTo = [&](SpinnerState to, crl::time at) {
if (elapsed >= at && int(current) < int(to)) {
current = to;
state->spinner->state = to;
const auto stateTo = [&](SpinnerState to) {
if (int(current) < int(to)) {
state->spinner->state = current = to;
}
};
const auto switchTo = [&](SpinnerState to, crl::time at) {
if (elapsed >= at) {
stateTo(to);
}
};
if (anim::Disabled()) {
current = SpinnerState::FinishedModel;
state->spinner->state = current;
}
if (state->backdropSpin.willIndex != 0) {
switchTo(SpinnerState::FinishedBackdrop, kBackdropStopsAt);
}
if (current == SpinnerState::FinishedBackdrop
&& state->patternSpin.willIndex != 0) {
switchTo(SpinnerState::FinishedPattern, kPatternStopsAt);
}
if (current == SpinnerState::FinishedPattern
&& state->modelSpin.willIndex != 0) {
// We want to start final model move not from the middle.
switchTo(SpinnerState::FinishedModel, kModelStopsAt);
}
const auto actualize = [&](
AttributeSpin &spin,
auto &&list,
SpinnerState checkState,
SpinnerState finishState,
int slowdown = 1) {
if (spin.progress() < 1.) {
return;
} else if (current >= finishState) {
} else if (current >= checkState) {
spin.startToTarget([=] { cover->update(); }, slowdown);
stateTo(finishState);
} else {
spin.startWithin(list.size(), [=] { cover->update(); });
}
};
if (anim::Disabled() && current < SpinnerState::FinishedModel) {
stateTo(SpinnerState::FinishedModel);
state->backdropSpin.startToTarget([=] { cover->update(); });
state->patternSpin.startToTarget([=] { cover->update(); });
state->modelSpin.startToTarget([=] { cover->update(); });
}
if (state->backdropSpin.willIndex != 0) {
switchTo(SpinnerState::FinishBackdrop, kBackdropStopsAt);
}
actualize(
state->backdropSpin,
state->spinnerBackdrops,
SpinnerState::FinishBackdrop,
SpinnerState::FinishedBackdrop);
if (current == SpinnerState::FinishedBackdrop
&& state->patternSpin.willIndex != 0) {
switchTo(SpinnerState::FinishPattern, kPatternStopsAt);
}
actualize(
state->patternSpin,
state->spinnerPatterns,
SpinnerState::FinishPattern,
SpinnerState::FinishedPattern);
if (current == SpinnerState::FinishedPattern
&& state->modelSpin.willIndex != 0) {
// We want to start final model move not from the middle.
switchTo(SpinnerState::FinishModel, kModelStopsAt);
}
actualize(
state->modelSpin,
state->spinnerModels,
SpinnerState::FinishModel,
SpinnerState::FinishedModel,
2);
@@ -3739,6 +3751,9 @@ void AddUniqueGiftCover(
state->now.model);
const auto modelProgress = state->modelSpin.progress();
const auto paintOne = [&](ModelView &view, float64 progress) {
if (progress >= 1. || progress <= -1.) {
return;
}
auto scale = 1.;
if (progress != 0.) {
const auto shift = progress * width / 2.;