// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ash/ambient/ui/ambient_animation_attribution_transformer.h"
#include <string>
#include <utility>
#include "ash/utility/lottie_util.h"
#include "base/check.h"
#include "base/logging.h"
#include "cc/paint/skottie_resource_metadata.h"
#include "cc/paint/skottie_text_property_value.h"
#include "cc/paint/skottie_transform_property_value.h"
#include "cc/paint/skottie_wrapper.h"
#include "ui/gfx/geometry/point.h"
#include "ui/gfx/geometry/point_f.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/rect_f.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/geometry/vector2d.h"
#include "ui/lottie/animation.h"
#include "ui/views/controls/animated_image_view.h"
namespace ash {
namespace {
// Amount of padding there should be from the bottom-right of the
// AnimatedImageView to the bottom-right of the attribution text box.
constexpr gfx::Vector2d kTextBoxPaddingDip = gfx::Vector2d(24, 24);
} // namespace
// This translates between 2 coordinate systems. The first coordinate system
// (the one translating from) is the views coordinate system where the origin
// is the top-left of the view. In practice, the typical case looks like this:
//
// Animation
// +-----------------------------------------------+
// | |
// |(0, 0) View |
// +-----------------------------------------------|
// | |
// | |
// | |
// | |
// | |
// | |
// | |
// |-------------------------------------------+ |
// | Attribution Text| |
// |-------------------------------------------+ |
// | |
// +-----------------------------------------------+
// | |
// | |
// +-----------------------------------------------+
//
// Note in the above, the animation's width matches the view's width, and its
// height exceeds that of the view (the upper and lower parts of the animation
// get cropped out). The origin is the top-left of the view, so the top-left of
// the animation has coordinates (0, <some negative number>). The attribution
// text box's bottom right corner has coordinates
// (view_width - 24, view_height - 24).
//
// The second set of coordinates (the one translating to) is the original
// animation's coordinate system. "Original" here refers to the
// coordinates baked into the Lottie file. Visually, it looks the same as the
// picture above, except:
// * The origin is the top-left of the animation.
// * The animation's width/height are those of the original animation (baked
// into the Lottie file), as opposed to those of the "scaled" animation that
// was scaled to reflect the view's bounds/dimensions.
//
// Note that although the typical case is illustrated above, the implementation
// was written generically to account for all cases.
void AmbientAnimationAttributionTransformer::TransformTextBox(
views::AnimatedImageView& animated_image_view) {
gfx::Transform view_to_animation_transform;
// 1) Change the origin from the top-left of the view to the top-left of the
// scaled animation.
DCHECK(!animated_image_view.GetImageBounds().IsEmpty());
gfx::Vector2d scaled_animation_origin_offset =
animated_image_view.GetImageBounds().origin().OffsetFromOrigin();
view_to_animation_transform.Translate(-scaled_animation_origin_offset);
// 2) Reset the coordinates from the "scaled" animation dimensions (scaled to
// fit the view) to the original animation dimensions baked into the Lottie
// file.
lottie::Animation* animation = animated_image_view.animated_image();
DCHECK(animation);
gfx::Size original_animation_size = animation->GetOriginalSize();
gfx::Size scaled_animation_size = animated_image_view.GetImageBounds().size();
view_to_animation_transform.PostScale(
static_cast<float>(original_animation_size.width()) /
scaled_animation_size.width(),
static_cast<float>(original_animation_size.height()) /
scaled_animation_size.height());
// Apply transformation to the bottom-right corner of the text box. The
// bottom-right corner is arbitrary here and is just used as a point of
// reference when building the final transformed text box's coordinates.
gfx::Rect view_bounds = animated_image_view.GetContentsBounds();
DCHECK(!view_bounds.IsEmpty())
<< "AnimatedImageView's content bounds must be initialized before "
"transforming the text box.";
gfx::Point text_box_bottom_right = view_to_animation_transform.MapPoint(
view_bounds.bottom_right() - kTextBoxPaddingDip);
// In the majority of cases, the bottom-right of the text box will already be
// within the boundaries of the original animation. There are some corner
// cases though (ex: fitting a landscape animation file to portrait view)
// where the bottom-right will be outside the animation's boundaries. In these
// cases, clamp the text box's coordinates to the bottom-right of the
// animation, or the text box will ultimately not be rendered.
text_box_bottom_right.SetToMin(
gfx::Rect(original_animation_size).bottom_right());
for (const std::string& text_node_name :
animation->skottie()->GetTextNodeNames()) {
if (!IsCustomizableLottieId(text_node_name)) {
DVLOG(4) << "Ignoring non-attribution text node";
continue;
}
cc::SkottieResourceIdHash attribution_node_id =
cc::HashSkottieResourceId(text_node_name);
DCHECK(animation->text_map().contains(attribution_node_id));
cc::SkottieTextPropertyValue& attribution_val =
animation->text_map().at(attribution_node_id);
// Text box's height stays the same as what's specified in the lottie file.
gfx::RectF new_text_box = attribution_val.box();
new_text_box.set_width(text_box_bottom_right.x());
new_text_box.set_origin(
gfx::PointF(0, text_box_bottom_right.y() - new_text_box.height()));
// One final transform: The text box's coordinates must be relative to the
// text attribution layer's "position" in the animation file (an arbitrary
// point specified in Adobe After-Effects when the animation is built). It
// is effectively the "local origin" for the text box, and may be different
// for each attribution node in the animation.
DCHECK(animation->skottie()->GetCurrentTransformPropertyValues().contains(
attribution_node_id));
gfx::Transform attribution_layer_shift;
attribution_layer_shift.Translate(-animation->skottie()
->GetCurrentTransformPropertyValues()
.at(attribution_node_id)
.position.OffsetFromOrigin());
attribution_val.set_box(attribution_layer_shift.MapRect(new_text_box));
}
}
} // namespace ash