blob: fdd508e3f53a0084cbef15b64dd295673c0219de [file] [log] [blame]
// Copyright 2020 The Cobalt Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "cobalt/dom/lottie_player.h"
#include <memory>
#include <string>
#include "base/strings/string_number_conversions.h"
#include "base/trace_event/trace_event.h"
#include "cobalt/base/polymorphic_downcast.h"
#include "cobalt/dom/csp_delegate.h"
#include "cobalt/dom/document.h"
#include "cobalt/dom/dom_settings.h"
#include "cobalt/dom/html_element_context.h"
#include "cobalt/dom/lottie_frame_custom_event.h"
#include "cobalt/dom/window.h"
#include "cobalt/script/global_environment.h"
#include "url/gurl.h"
namespace cobalt {
namespace dom {
using render_tree::LottieAnimation;
const char LottiePlayer::kTagName[] = "lottie-player";
LottiePlayer::LottiePlayer(Document* document)
: HTMLElement(document, base::Token(kTagName)),
autoplaying_(true),
ALLOW_THIS_IN_INITIALIZER_LIST(event_queue_(this)),
callback_task_runner_(base::MessageLoop::current()->task_runner()) {
SetAnimationEventCallbacks();
}
std::string LottiePlayer::src() const {
return GetAttribute("src").value_or("");
}
void LottiePlayer::set_src(const std::string& src) { SetAttribute("src", src); }
bool LottiePlayer::autoplay() const { return GetBooleanAttribute("autoplay"); }
void LottiePlayer::set_autoplay(bool autoplay) {
// The value of 'autoplay' is true when the 'autoplay' attribute is present.
// The value of the attribute is irrelevant.
if (autoplay) {
SetBooleanAttribute("autoplay", true);
} else {
SetBooleanAttribute("autoplay", false);
}
}
std::string LottiePlayer::background() const {
return GetAttribute("background").value_or("");
}
void LottiePlayer::set_background(std::string background) {
SetAttribute("background", background);
}
int LottiePlayer::count() const { return properties_.count; }
void LottiePlayer::set_count(int count) {
SetAttribute("count", base::Int32ToString(count));
}
int LottiePlayer::direction() const { return properties_.direction; }
void LottiePlayer::set_direction(int direction) {
SetAttribute("direction", base::Int32ToString(direction));
}
bool LottiePlayer::hover() const { return GetBooleanAttribute("hover"); }
void LottiePlayer::set_hover(bool hover) {
// The value of 'hover' is true when the 'hover' attribute is present.
// The value of the attribute is irrelevant.
if (hover) {
SetBooleanAttribute("hover", true);
} else {
SetBooleanAttribute("hover", false);
}
}
bool LottiePlayer::loop() const { return properties_.loop; }
void LottiePlayer::set_loop(bool loop) {
// The value of 'loop' is true when the 'loop' attribute is present.
// The value of the attribute is irrelevant.
if (loop) {
SetBooleanAttribute("loop", true);
} else {
SetBooleanAttribute("loop", false);
}
}
std::string LottiePlayer::mode() const { return properties_.GetModeAsString(); }
void LottiePlayer::set_mode(std::string mode) { SetAttribute("mode", mode); }
double LottiePlayer::speed() const { return properties_.speed; }
void LottiePlayer::set_speed(double speed) {
SetAttribute("speed", base::NumberToString(speed));
}
std::string LottiePlayer::preserve_aspect_ratio() const {
// Skottie animations default to "xMidYMid meet", meaning that the animation
// will be uniformly scaled and centered relative to the element's width and
// height.
return "xMidYMid meet";
}
std::string LottiePlayer::renderer() const {
// Cobalt uses a custom compiled-in renderer.
return "skottie-m79";
}
void LottiePlayer::Load(std::string src) {
// https://github.com/LottieFiles/lottie-player#loadsrc-string--object--void
set_src(src);
}
void LottiePlayer::Play() {
// https://lottiefiles.github.io/lottie-player/methods.html#play--void
UpdateState(LottieAnimation::LottieState::kPlaying);
}
void LottiePlayer::Pause() {
// https://lottiefiles.github.io/lottie-player/methods.html#pause--void
UpdateState(LottieAnimation::LottieState::kPaused);
}
void LottiePlayer::Stop() {
// https://lottiefiles.github.io/lottie-player/methods.html#stop--void
UpdateState(LottieAnimation::LottieState::kStopped);
}
void LottiePlayer::Seek(FrameType frame) {
// https://lottiefiles.github.io/lottie-player/methods.html#seekvalue-number--string--void
if (frame.IsType<double>()) {
properties_.SeekFrame(frame.AsType<double>());
} else if (frame.IsType<std::string>()) {
// Check whether a valid percent string.
std::string frame_string = frame.AsType<std::string>();
if (frame_string.empty()) {
DLOG(WARNING) << "Percent string cannot be empty.";
return;
}
double frame_percent;
bool prefix_is_num = base::StringToDouble(
frame_string.substr(0, frame_string.length() - 1), &frame_percent);
if (frame_string.back() != '%' || !prefix_is_num) {
DLOG(WARNING) << "Not a valid percent string: "
<< frame.AsType<std::string>();
return;
}
properties_.SeekPercent(frame_percent);
}
UpdateLottieObjects();
}
void LottiePlayer::SetDirection(int direction) {
// https://lottiefiles.github.io/lottie-player/methods.html#setdirectionvalue-number--void
if (properties_.UpdateDirection(direction)) {
UpdateLottieObjects();
}
}
void LottiePlayer::SetLooping(bool loop) {
// https://lottiefiles.github.io/lottie-player/methods.html#setloopingvalue-boolean--void
if (properties_.UpdateLoop(loop)) {
UpdateLottieObjects();
}
}
void LottiePlayer::SetSpeed(double speed) {
// https://lottiefiles.github.io/lottie-player/methods.html#setspeedvalue-number--void
if (properties_.UpdateSpeed(speed)) {
UpdateLottieObjects();
}
}
void LottiePlayer::ToggleLooping() {
// https://github.com/LottieFiles/lottie-player#togglelooping--void
properties_.ToggleLooping();
UpdateLottieObjects();
}
void LottiePlayer::TogglePlay() {
// https://github.com/LottieFiles/lottie-player#toggleplay--void
properties_.TogglePlay();
UpdateLottieObjects();
}
LottieAnimation::LottieProperties LottiePlayer::GetProperties() const {
return properties_;
}
void LottiePlayer::OnHover() {
if (hover()) {
UpdateState(LottieAnimation::LottieState::kPlaying);
}
}
void LottiePlayer::OnUnHover() {
if (hover()) {
UpdateState(LottieAnimation::LottieState::kStopped);
}
}
void LottiePlayer::PurgeCachedBackgroundImagesOfNodeAndDescendants() {
if (!cached_image_loaded_callback_handler_) {
return;
}
// While we are still loading, treat this as an error.
OnLoadingError();
}
void LottiePlayer::OnSetAttribute(const std::string& name,
const std::string& value) {
if (name == "src") {
UpdateAnimationData();
} else if (name == "background") {
SetStyleAttribute("background:" + value);
} else if (name == "count") {
int count;
base::StringToInt32(value, &count);
SetCount(count);
} else if (name == "direction") {
int direction;
base::StringToInt32(value, &direction);
SetDirection(direction);
} else if (name == "loop") {
SetLooping(true);
} else if (name == "mode") {
SetMode(value);
} else if (name == "speed") {
double speed;
base::StringToDouble(value, &speed);
SetSpeed(speed);
} else {
HTMLElement::OnSetAttribute(name, value);
}
}
void LottiePlayer::OnRemoveAttribute(const std::string& name) {
if (name == "src") {
UpdateAnimationData();
} else if (name == "background") {
SetStyleAttribute("background:transparent");
} else if (name == "count") {
SetCount(LottieAnimation::LottieProperties::kDefaultCount);
} else if (name == "direction") {
SetDirection(LottieAnimation::LottieProperties::kDefaultDirection);
} else if (name == "loop") {
SetLooping(LottieAnimation::LottieProperties::kDefaultLoop);
} else if (name == "mode") {
SetMode(LottieAnimation::LottieProperties::kDefaultMode);
} else if (name == "speed") {
SetSpeed(LottieAnimation::LottieProperties::kDefaultSpeed);
} else {
HTMLElement::OnRemoveAttribute(name);
}
}
void LottiePlayer::UpdateAnimationData() {
DCHECK(base::MessageLoop::current());
DCHECK(node_document());
TRACE_EVENT0("cobalt::dom", "LottiePlayer::UpdateAnimationData()");
if (cached_image_loaded_callback_handler_) {
cached_image_loaded_callback_handler_.reset();
prevent_gc_until_load_complete_.reset();
node_document()->DecreaseLoadingCounter();
}
const std::string src = GetAttribute("src").value_or("");
if (!src.empty()) {
const GURL& base_url = node_document()->url_as_gurl();
const GURL selected_source = base_url.Resolve(src);
if (!selected_source.is_valid()) {
LOG(WARNING) << src << " cannot be resolved based on " << base_url << ".";
return;
}
auto image_cache = node_document()->html_element_context()->image_cache();
cached_image_ = image_cache->GetOrCreateCachedResource(selected_source,
loader::Origin());
if (cached_image_->TryGetResource()) {
PreventGarbageCollectionUntilEventIsDispatched(base::Tokens::load());
// The requested resource has already been loaded. Trigger the "load" and
// "ready" events to indicate that the animation data is already loaded
// and the DOM element is already ready.
ScheduleEvent(base::Tokens::load());
ScheduleEvent(base::Tokens::ready());
return;
}
} else {
PreventGarbageCollectionUntilEventIsDispatched(base::Tokens::error());
return;
}
DCHECK(!prevent_gc_until_load_complete_);
prevent_gc_until_load_complete_.reset(
new script::GlobalEnvironment::ScopedPreventGarbageCollection(
html_element_context()->script_runner()->GetGlobalEnvironment(),
this));
node_document()->IncreaseLoadingCounter();
cached_image_loaded_callback_handler_.reset(
new loader::image::CachedImage::OnLoadedCallbackHandler(
cached_image_,
base::Bind(&LottiePlayer::OnLoadingSuccess, base::Unretained(this)),
base::Bind(&LottiePlayer::OnLoadingError, base::Unretained(this))));
}
void LottiePlayer::OnLoadingSuccess() {
TRACE_EVENT0("cobalt::dom", "LottiePlayer::OnLoadingSuccess()");
AllowGarbageCollectionAfterEventIsDispatched(
base::Tokens::load(), std::move(prevent_gc_until_load_complete_));
if (node_document()) {
node_document()->DecreaseLoadingCounterAndMaybeDispatchLoadEvent();
}
cached_image_loaded_callback_handler_.reset();
// Set up the Lottie objects in the box and render trees once the file has
// successfully loaded.
UpdateLottieObjects();
// Trigger a load event to indicate that the animation data has loaded, and
// then a "ready" event to indicate the DOM element is ready.
ScheduleEvent(base::Tokens::load());
ScheduleEvent(base::Tokens::ready());
// If the animation is autoplaying and autoplay is true, then the animation
// playback state needs to be set to LottieAnimation::LottieState::kPlaying.
UpdatePlaybackStateIfAutoplaying();
}
void LottiePlayer::OnLoadingError() {
TRACE_EVENT0("cobalt::dom", "LottiePlayer::OnLoadingError()");
AllowGarbageCollectionAfterEventIsDispatched(
base::Tokens::error(), std::move(prevent_gc_until_load_complete_));
if (node_document()) {
node_document()->DecreaseLoadingCounterAndMaybeDispatchLoadEvent();
}
cached_image_loaded_callback_handler_.reset();
ScheduleEvent(base::Tokens::error());
}
void LottiePlayer::PreventGarbageCollectionUntilEventIsDispatched(
base::Token event_name) {
std::unique_ptr<script::GlobalEnvironment::ScopedPreventGarbageCollection>
prevent_gc_until_event_dispatch(
new script::GlobalEnvironment::ScopedPreventGarbageCollection(
html_element_context()->script_runner()->GetGlobalEnvironment(),
this));
AllowGarbageCollectionAfterEventIsDispatched(
event_name, std::move(prevent_gc_until_event_dispatch));
}
void LottiePlayer::AllowGarbageCollectionAfterEventIsDispatched(
base::Token event_name,
std::unique_ptr<script::GlobalEnvironment::ScopedPreventGarbageCollection>
scoped_prevent_gc) {
PostToDispatchEventNameAndRunCallback(
FROM_HERE, event_name,
base::Bind(&LottiePlayer::DestroyScopedPreventGC,
base::AsWeakPtr<LottiePlayer>(this),
base::Passed(&scoped_prevent_gc)));
}
void LottiePlayer::DestroyScopedPreventGC(
std::unique_ptr<script::GlobalEnvironment::ScopedPreventGarbageCollection>
scoped_prevent_gc) {
scoped_prevent_gc.reset();
}
void LottiePlayer::UpdateState(LottieAnimation::LottieState state) {
if (properties_.UpdateState(state)) {
UpdateLottieObjects();
}
}
void LottiePlayer::UpdatePlaybackStateIfAutoplaying() {
if (autoplaying_ && autoplay()) {
properties_.UpdateState(LottieAnimation::LottieState::kPlaying);
}
autoplaying_ = false;
}
void LottiePlayer::SetCount(int count) {
if (properties_.UpdateCount(count)) {
UpdateLottieObjects();
}
}
void LottiePlayer::SetMode(std::string mode) {
if (properties_.UpdateMode(mode)) {
UpdateLottieObjects();
}
}
void LottiePlayer::SetMode(LottieAnimation::LottieMode mode) {
if (properties_.UpdateMode(mode)) {
UpdateLottieObjects();
}
}
void LottiePlayer::UpdateLottieObjects() {
node_document()->RecordMutation();
InvalidateLayoutBoxesOfNodeAndAncestors();
}
void LottiePlayer::ScheduleEvent(base::Token event_name) {
// https://github.com/LottieFiles/lottie-player#events
scoped_refptr<Event> event = new Event(event_name);
event->set_target(this);
event_queue_.Enqueue(event);
}
void LottiePlayer::SetAnimationEventCallbacks() {
DCHECK(callback_task_runner_);
properties_.onplay_callback =
base::Bind(base::IgnoreResult(&base::SingleThreadTaskRunner::PostTask),
callback_task_runner_, FROM_HERE,
base::Bind(&LottiePlayer::OnPlay, base::AsWeakPtr(this)));
properties_.onpause_callback =
base::Bind(base::IgnoreResult(&base::SingleThreadTaskRunner::PostTask),
callback_task_runner_, FROM_HERE,
base::Bind(&LottiePlayer::OnPause, base::AsWeakPtr(this)));
properties_.onstop_callback =
base::Bind(base::IgnoreResult(&base::SingleThreadTaskRunner::PostTask),
callback_task_runner_, FROM_HERE,
base::Bind(&LottiePlayer::OnStop, base::AsWeakPtr(this)));
properties_.oncomplete_callback =
base::Bind(base::IgnoreResult(&base::SingleThreadTaskRunner::PostTask),
callback_task_runner_, FROM_HERE,
base::Bind(&LottiePlayer::OnComplete, base::AsWeakPtr(this)));
properties_.onloop_callback =
base::Bind(base::IgnoreResult(&base::SingleThreadTaskRunner::PostTask),
callback_task_runner_, FROM_HERE,
base::Bind(&LottiePlayer::OnLoop, base::AsWeakPtr(this)));
properties_.onenterframe_callback = base::Bind(
&LottiePlayer::CallOnEnterFrame, callback_task_runner_,
base::Bind(&LottiePlayer::OnEnterFrame, base::AsWeakPtr(this)));
properties_.onfreeze_callback =
base::Bind(base::IgnoreResult(&base::SingleThreadTaskRunner::PostTask),
callback_task_runner_, FROM_HERE,
base::Bind(&LottiePlayer::OnFreeze, base::AsWeakPtr(this)));
properties_.onunfreeze_callback =
base::Bind(base::IgnoreResult(&base::SingleThreadTaskRunner::PostTask),
callback_task_runner_, FROM_HERE,
base::Bind(&LottiePlayer::OnUnfreeze, base::AsWeakPtr(this)));
}
void LottiePlayer::OnPlay() { ScheduleEvent(base::Tokens::play()); }
void LottiePlayer::OnPause() { ScheduleEvent(base::Tokens::pause()); }
void LottiePlayer::OnStop() { ScheduleEvent(base::Tokens::stop()); }
void LottiePlayer::OnComplete() { ScheduleEvent(base::Tokens::complete()); }
void LottiePlayer::OnLoop() { ScheduleEvent(base::Tokens::loop()); }
void LottiePlayer::OnEnterFrame(double frame, double seeker) {
LottieFrameCustomEventDetail detail;
detail.set_frame(frame);
detail.set_seeker(seeker);
scoped_refptr<LottieFrameCustomEvent> lottie_frame_custom_event =
new LottieFrameCustomEvent("frame");
lottie_frame_custom_event->set_detail(detail);
lottie_frame_custom_event->set_target(this);
event_queue_.Enqueue(lottie_frame_custom_event);
}
void LottiePlayer::CallOnEnterFrame(
scoped_refptr<base::SingleThreadTaskRunner> callback_task_runner,
base::Callback<void(double, double)> enter_frame_callback, double frame,
double seeker) {
callback_task_runner->PostTask(
FROM_HERE, base::Bind(enter_frame_callback, frame, seeker));
}
void LottiePlayer::OnFreeze() {
if (properties_.FreezeAnimationState()) {
ScheduleEvent(base::Tokens::freeze());
UpdateLottieObjects();
}
}
void LottiePlayer::OnUnfreeze() {
if (properties_.UnfreezeAnimationState()) {
ScheduleEvent(base::Tokens::play());
UpdateLottieObjects();
}
}
} // namespace dom
} // namespace cobalt