Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 38 additions & 1 deletion core/animation/animation.cc
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

#include <math.h>

#include <atomic>
#include <cstdint>
#include <utility>

Expand All @@ -27,6 +28,10 @@ namespace {

constexpr int64_t kThirtyMinutesInSeconds = 1800;

// Allocates nonzero Animation ids that are not reused during the process
// lifetime, including across page and Element lifecycles.
std::atomic<int64_t> g_next_animation_id{1};

} // namespace

namespace lynx {
Expand All @@ -44,7 +49,24 @@ void SuppressSampleSideEffects(KeyframeEffect::KeyframeSampleResult& result) {
} // namespace

Animation::Animation(const base::String& name)
: name_(name), keyframe_effect_(nullptr) {}
: name_(name),
id_(g_next_animation_id.fetch_add(1, std::memory_order_relaxed)),
keyframe_effect_(nullptr) {}

fml::TimeDelta Animation::GetCurrentTime() const {
if (start_time_ == fml::TimePoint::Min() ||
start_time_ == GetAnimationDummyStartTime() ||
current_run_start_system_time_ == fml::TimePoint::Min()) {
return fml::TimeDelta::Zero();
}
if (state_ == State::kPause) {
return current_time_at_pause_;
}
fml::TimeDelta elapsed =
current_time_at_pause_ +
(fml::TimePoint::Now() - current_run_start_system_time_);
return elapsed < fml::TimeDelta::Zero() ? fml::TimeDelta::Zero() : elapsed;
}

void Animation::Play(bool play_handles_initial_frame) {
if (state_ == State::kPlay) {
Expand All @@ -55,12 +77,17 @@ void Animation::Play(bool play_handles_initial_frame) {
State temp_state = state_;
if (temp_state == State::kIdle || temp_state == State::kStop) {
ResetPauseTiming();
current_run_start_system_time_ = fml::TimePoint::Min();
ClearSampleHistory();
} else {
// Resume keeps the last valid sample history, but drops same-timestamp
// cache.
InvalidateSampleCache();
}
if (temp_state == State::kPause &&
current_run_start_system_time_ != fml::TimePoint::Min()) {
current_run_start_system_time_ = fml::TimePoint::Now();
}
// Since `DoFrame` may reads and modifies state_, the change of state_ must be
// completed before DoFrame is executed.
state_ = State::kPlay;
Expand Down Expand Up @@ -101,6 +128,7 @@ void Animation::Pause() {
if (state_ == State::kPause) {
return;
}
current_time_at_pause_ = GetCurrentTime();
InvalidateSampleCache();
state_ = State::kPause;
}
Expand Down Expand Up @@ -167,6 +195,10 @@ bool Animation::Tick(fml::TimePoint& time) {
start_time_ = time;
keyframe_effect_->SetStartTime(time, reset_effect_state);
}
if (state_ == State::kPlay && time != GetAnimationDummyStartTime() &&
current_run_start_system_time_ == fml::TimePoint::Min()) {
current_run_start_system_time_ = fml::TimePoint::Now();
}
return keyframe_effect_->TickKeyframeModel(time).has_finished_all;
}

Expand Down Expand Up @@ -271,6 +303,10 @@ KeyframeEffect::KeyframeSampleResult Animation::SampleAt(
start_time_ = frame_time;
keyframe_effect_->SetStartTime(frame_time, reset_effect_state);
}
if (state_ == State::kPlay && frame_time != GetAnimationDummyStartTime() &&
current_run_start_system_time_ == fml::TimePoint::Min()) {
current_run_start_system_time_ = fml::TimePoint::Now();
}

// Resolve the timestamp that should be sampled. Paused animations keep
// sampling at pause_time_ so repeated resolves return a frozen style.
Expand Down Expand Up @@ -371,6 +407,7 @@ void Animation::NotifyUnitValuesUpdatedToAnimation(tasm::CSSValuePattern type) {
void Animation::ResetPauseTiming() {
pause_time_ = fml::TimePoint::Min();
total_paused_duration_ = fml::TimeDelta::Zero();
current_time_at_pause_ = fml::TimeDelta::Zero();
was_paused_ = false;
}

Expand Down
32 changes: 32 additions & 0 deletions core/animation/animation.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@
#ifndef CORE_ANIMATION_ANIMATION_H_
#define CORE_ANIMATION_ANIMATION_H_

#include <cstdint>
#include <memory>
#include <string>
#include <unordered_set>

#include "base/include/fml/time/time_point.h"
#include "core/animation/keyframe_effect.h"
#include "core/base/lynx_export.h"

namespace lynx {
namespace base {
Expand Down Expand Up @@ -42,6 +44,17 @@ class Animation : public std::enable_shared_from_this<Animation> {
static fml::TimePoint& GetAnimationDummyStartTime();

enum class State { kIdle = 0, kPlay, kPause, kStop };

// Origin category used by the CDP Animation domain.
enum class Origin : uint8_t {
// Animation created from an author-defined CSS @keyframes rule.
kCSSAnimation = 0,
// Animation created by the CSS transition manager.
kCSSTransition,
// Animation created through Lynx's imperative Animate or AnimateV2 API.
kWebAnimation,
};

Animation(const base::String& name);
~Animation() = default;
void Play(bool play_handles_initial_frame = true);
Expand All @@ -61,6 +74,13 @@ class Animation : public std::enable_shared_from_this<Animation> {
void SendIterationEvent();

const base::String& name() { return name_; }

// Stable process-unique identifier used as CDP Animation.id.
int64_t id() const { return id_; }

// Returns the current timeline time without mutating animation state.
LYNX_EXPORT_FOR_DEVTOOL fml::TimeDelta GetCurrentTime() const;

const fml::TimePoint& start_time() const { return start_time_; }
const fml::TimePoint& pause_time() const { return pause_time_; }
const fml::TimeDelta& total_paused_duration() const {
Expand Down Expand Up @@ -115,6 +135,10 @@ class Animation : public std::enable_shared_from_this<Animation> {

bool GetTransitionFlag() { return is_transition_; }

Origin GetOrigin() const { return origin_; }

void SetOrigin(Origin origin) { origin_ = origin; }

void NotifyElementSizeUpdated();

void NotifyUnitValuesUpdatedToAnimation(tasm::CSSValuePattern);
Expand All @@ -137,6 +161,8 @@ class Animation : public std::enable_shared_from_this<Animation> {
void ClearSampleHistory();
AnimationDelegate* animation_delegate_{nullptr};
base::String name_;
// Process-unique identifier assigned once when the object is constructed.
int64_t id_{0};
std::unique_ptr<KeyframeEffect> keyframe_effect_;

starlight::AnimationData animation_data_;
Expand All @@ -149,9 +175,15 @@ class Animation : public std::enable_shared_from_this<Animation> {
State state_{State::kIdle};

bool is_transition_ = false;
// Creation entry point exposed through the CDP Animation domain.
Origin origin_{Origin::kCSSAnimation};
bool need_report_over_time_{true};
fml::TimePoint pause_time_{fml::TimePoint::Min()};
fml::TimeDelta total_paused_duration_{fml::TimeDelta::Zero()};
// Inspector timeline tracking. It uses the FML monotonic clock and does not
// depend on the platform-specific vsync timestamp epoch.
fml::TimeDelta current_time_at_pause_{fml::TimeDelta::Zero()};
fml::TimePoint current_run_start_system_time_{fml::TimePoint::Min()};
bool was_paused_{false};
bool has_cached_sample_{false};
fml::TimePoint cached_sample_time_{fml::TimePoint::Min()};
Expand Down
111 changes: 111 additions & 0 deletions core/animation/animation_unittest.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
// Licensed under the Apache License Version 2.0 that can be found in the
// LICENSE file in the root directory of this source tree.

#include <chrono>
#include <memory>
#include <thread>

#include "core/animation/css_keyframe_manager.h"
#include "core/animation/keyframe_effect.h"
Expand Down Expand Up @@ -775,6 +777,115 @@ TEST_F(AnimationTest, TestDurationZero) {
}
}

// CDP Animation: each Animation gets a stable, process-unique numeric id that
// is never reused, so records cleared on navigation can never collide with
// fresh ids.
TEST_F(AnimationTest, IdIsUniqueAndNonZero) {
auto a1 = InitTestAnimation();
auto a2 = InitTestAnimation();
EXPECT_GT(a1->id(), 0);
EXPECT_GT(a2->id(), 0);
EXPECT_NE(a1->id(), a2->id());
}

// CDP Animation: the origin tags an animation's entry point so the Inspector
// can map it to CSSAnimation / CSSTransition / WebAnimation. It defaults to
// CSSAnimation (CSS @keyframes) and is settable by the manager.
TEST_F(AnimationTest, OriginDefaultsToCSSAnimationAndIsSettable) {
auto a = InitTestAnimation();
EXPECT_EQ(a->GetOrigin(), animation::Animation::Origin::kCSSAnimation);
a->SetOrigin(animation::Animation::Origin::kCSSTransition);
EXPECT_EQ(a->GetOrigin(), animation::Animation::Origin::kCSSTransition);
a->SetOrigin(animation::Animation::Origin::kWebAnimation);
EXPECT_EQ(a->GetOrigin(), animation::Animation::Origin::kWebAnimation);
}

// CDP Animation.getCurrentTime: the timeline time is zero before the animation
// has a real start time.
TEST_F(AnimationTest, GetCurrentTimeIsZeroBeforeStart) {
auto a = InitTestAnimation();
EXPECT_EQ(a->GetCurrentTime(), fml::TimeDelta::Zero());

a->Pause();
EXPECT_EQ(a->GetCurrentTime(), fml::TimeDelta::Zero());
EXPECT_EQ(a->GetState(), animation::Animation::State::kPause);
}

// Pausing snapshots currentTime immediately. Inspector queries must remain
// stable before the animation receives another frame, and replaying a stopped
// animation must replace the old snapshot.
TEST_F(AnimationTest, GetCurrentTimeRemainsFrozenWhilePaused) {
auto a = InitTestAnimation();
auto data = InitAnimationData(lynx::base::String("test_animation"), 3000, 0,
starlight::TimingFunctionData(), 1,
starlight::AnimationFillModeType::kBoth,
starlight::AnimationDirectionType::kNormal,
starlight::AnimationPlayStateType::kRunning);
a->UpdateAnimationData(data);
a->Play(false);

auto start_time = fml::TimePoint::FromEpochDelta(
fml::TimeDelta::FromSecondsF(1000000000.0));
a->SampleAt(start_time);
std::this_thread::sleep_for(std::chrono::milliseconds(2));
EXPECT_GT(a->GetCurrentTime(), fml::TimeDelta::Zero());
a->Pause();

const auto first_pause_current_time = a->GetCurrentTime();
EXPECT_EQ(a->GetState(), animation::Animation::State::kPause);
std::this_thread::sleep_for(std::chrono::milliseconds(2));
EXPECT_EQ(a->GetCurrentTime(), first_pause_current_time);
EXPECT_EQ(a->GetState(), animation::Animation::State::kPause);

a->Play(false);
const auto current_time_at_resume = a->GetCurrentTime();
std::this_thread::sleep_for(std::chrono::milliseconds(2));
EXPECT_GT(a->GetCurrentTime(), current_time_at_resume);
a->Pause();

a->Stop();
a->Play(false);
EXPECT_EQ(a->GetCurrentTime(), fml::TimeDelta::Zero());
auto restart_time = start_time + fml::TimeDelta::FromSecondsF(1.0);
a->SampleAt(restart_time);
std::this_thread::sleep_for(std::chrono::milliseconds(2));
a->Pause();

const auto second_pause_current_time = a->GetCurrentTime();
EXPECT_GT(second_pause_current_time, fml::TimeDelta::Zero());
std::this_thread::sleep_for(std::chrono::milliseconds(2));
EXPECT_EQ(a->GetCurrentTime(), second_pause_current_time);
}

// CDP Animation.getCurrentTime is strictly read-only: querying it must not
// advance, pause, restart, or otherwise mutate the animation's state, either
// before start or after the first frame.
TEST_F(AnimationTest, GetCurrentTimeIsReadOnly) {
auto a = InitTestAnimation();
auto data = InitAnimationData(lynx::base::String("test_animation"), 3000, 0,
starlight::TimingFunctionData(), 1,
starlight::AnimationFillModeType::kBoth,
starlight::AnimationDirectionType::kNormal,
starlight::AnimationPlayStateType::kRunning);
a->UpdateAnimationData(data);

// Before start: the query must not change state.
auto state_before = a->GetState();
EXPECT_EQ(a->GetCurrentTime(), fml::TimeDelta::Zero());
EXPECT_EQ(a->GetState(), state_before);

a->Play(false);
fml::TimePoint t0 =
fml::TimePoint::FromEpochDelta(fml::TimeDelta::FromSecondsF(1.0));
a->DoFrame(t0);

// After the first frame: current time is non-negative and the query must not
// mutate state.
auto state_after_start = a->GetState();
EXPECT_GE(a->GetCurrentTime(), fml::TimeDelta::Zero());
EXPECT_EQ(a->GetState(), state_after_start);
}

} // namespace testing
} // namespace tasm
} // namespace lynx
18 changes: 15 additions & 3 deletions core/animation/css_keyframe_manager.cc
Original file line number Diff line number Diff line change
Expand Up @@ -313,14 +313,15 @@ void CSSKeyframeManager::SetAnimationDataAndPlayInternal(
// Update an existing animation, add it to temp_active_animations_map_ and
// delete it from animations_map_;
if (force_rebuild) {
const auto origin = animation->second->GetOrigin();
if (use_new_pipeline_cleanup) {
PrepareAnimationRemoval(animation->second, new_base_resolved_styles,
new_underlying_layout_only_styles);
} else {
animation->second->Destroy();
}
auto recreated_animation =
CreateAnimation(data, new_base_custom_properties);
CreateAnimation(data, new_base_custom_properties, origin);
if (recreated_animation != nullptr) {
temp_active_animations_map_[data.name] = recreated_animation;
}
Expand All @@ -334,14 +335,15 @@ void CSSKeyframeManager::SetAnimationDataAndPlayInternal(
if (animation->second->GetState() == Animation::State::kStop &&
HasNoSampleableKeyframes(animation->second,
has_custom_property_keyframes)) {
const auto origin = animation->second->GetOrigin();
if (use_new_pipeline_cleanup) {
PrepareAnimationRemoval(animation->second, new_base_resolved_styles,
new_underlying_layout_only_styles);
} else {
animation->second->Destroy();
}
auto recreated_animation =
CreateAnimation(data, new_base_custom_properties);
CreateAnimation(data, new_base_custom_properties, origin);
if (recreated_animation != nullptr) {
temp_active_animations_map_[data.name] = recreated_animation;
}
Expand Down Expand Up @@ -726,9 +728,17 @@ bool CSSKeyframeManager::NeedsFutureTickForNewPipeline() const {
has_running_animation(temp_keep_animations_map_);
}

Animation::Origin CSSKeyframeManager::ResolveAnimationOrigin(
const starlight::AnimationData& data) const {
return element_->HasImperativeAnimationMetadata(data.name)
? Animation::Origin::kWebAnimation
: Animation::Origin::kCSSAnimation;
}

std::shared_ptr<Animation> CSSKeyframeManager::CreateAnimation(
starlight::AnimationData& data,
const tasm::CustomPropertiesMap* base_custom_properties) {
const tasm::CustomPropertiesMap* base_custom_properties,
std::optional<Animation::Origin> origin) {
// 1. create animation & keyframe_effect according to animation data
auto animation = std::make_shared<Animation>(data.name);
animation->set_animation_data(data);
Expand All @@ -743,6 +753,8 @@ std::shared_ptr<Animation> CSSKeyframeManager::CreateAnimation(
animation->SetKeyframeEffect(std::move(keyframe_effect));
animation->BindDelegate(this);
animation->BindElement(this->element());
animation->SetOrigin(origin.has_value() ? *origin
: ResolveAnimationOrigin(data));
// 2. create keyframe Models& animation Curves according to CSS keyframe
// tokens
MakeKeyframeModel(animation.get(), data.name, base_custom_properties);
Expand Down
7 changes: 6 additions & 1 deletion core/animation/css_keyframe_manager.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#define CORE_ANIMATION_CSS_KEYFRAME_MANAGER_H_

#include <memory>
#include <optional>
#include <string>
#include <unordered_map>
#include <unordered_set>
Expand Down Expand Up @@ -155,9 +156,13 @@ class CSSKeyframeManager : public AnimationDelegate {
void NotifyUnitValuesUpdatedToAnimation(tasm::CSSValuePattern);

protected:
virtual Animation::Origin ResolveAnimationOrigin(
const starlight::AnimationData& data) const;

std::shared_ptr<Animation> CreateAnimation(
starlight::AnimationData& data,
const tasm::CustomPropertiesMap* base_custom_properties = nullptr);
const tasm::CustomPropertiesMap* base_custom_properties = nullptr,
std::optional<Animation::Origin> origin = std::nullopt);

void SetAnimationDataAndPlayInternal(
base::Vector<starlight::AnimationData>& anim_data, bool force_rebuild,
Expand Down
Loading
Loading