| // 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/model/ambient_topic_queue_animation_delegate.h" |
| |
| #include <algorithm> |
| |
| #include "ash/ambient/util/ambient_util.h" |
| #include "base/logging.h" |
| #include "base/numerics/safe_conversions.h" |
| #include "cc/paint/skottie_resource_metadata.h" |
| #include "ui/gfx/geometry/size_f.h" |
| |
| namespace ash { |
| namespace { |
| |
| bool IsPortrait(const gfx::Size& size) { |
| DCHECK(!size.IsEmpty()); |
| return size.height() > size.width(); |
| } |
| |
| bool IsSquare(const gfx::Size& size) { |
| DCHECK(!size.IsEmpty()); |
| // This is arbitrary. Just a rough estimate that a "square" picture has an |
| // aspect ratio in the range [1 - kAspectRatioDelta, 1 + kAspectRatioDelta]. |
| static constexpr float kAspectRatioDelta = 0.05f; |
| static constexpr float kAspectRatioLowerBound = 1.f - kAspectRatioDelta; |
| static constexpr float kAspectRatioUpperBound = 1.f + kAspectRatioDelta; |
| float aspect_ratio = gfx::SizeF(size).AspectRatio(); |
| return aspect_ratio > kAspectRatioLowerBound && |
| aspect_ratio < kAspectRatioUpperBound; |
| } |
| |
| // Determines one size that best represents the group of image assets in the |
| // |resource_metadata| whose orientation matches |is_portrait|. The logic is |
| // currently as follows: |
| // * Compute the average aspect ratio of all assets with matching orientation. |
| // * Calculate the smallest size whose a) aspect ratio matches the average |
| // computed above and b) dimensions exceed those of all assets with matching |
| // orientation. This ensures that we ultimately download the largest |
| // possible resolution of photos from IMAX and any resizing that happens |
| // "shrinks" the photo to fit in the animation, which generally has better |
| // quality that "growing" a photo. |
| // * Discard any "square" orientations from the aspect ratio calculation. These |
| // are outliers that aren't quite portrait or landscape and bias the average |
| // aspect ratio. Since they are "square", it is a good enough compromise to |
| // use either a portrait or landscape photo and center-crop it to a square |
| // orientation before rendering. If this is not good enough in the future, we |
| // can return a third size in |GetTopicSizes()|, but it is currently not worth |
| // it. |
| // |
| // Returns an empty gfx::Size instance if there are no assets that match the |
| // |is_portrait| orientation. |
| gfx::Size SummarizeImageAssetSizes( |
| const cc::SkottieResourceMetadataMap& resource_metadata, |
| bool is_portrait) { |
| constexpr int kDimensionInvalid = -1; |
| int largest_width_observed = kDimensionInvalid; |
| int largest_height_observed = kDimensionInvalid; |
| float aspect_ratio_sum = 0.f; |
| int num_assets_found = 0; |
| for (const auto& [asset_id, asset_metadata] : |
| resource_metadata.asset_storage()) { |
| // IMAX photos are only assigned to dynamic image assets in the animation, |
| // so static image assets should be ignored when calculating. |
| ambient::util::ParsedDynamicAssetId parsed_dynamic_asset_id; |
| bool is_dynamic_image_asset = ambient::util::ParseDynamicLottieAssetId( |
| asset_id, parsed_dynamic_asset_id); |
| if (!is_dynamic_image_asset || !asset_metadata.size.has_value() || |
| IsPortrait(*asset_metadata.size) != is_portrait) { |
| continue; |
| } |
| |
| largest_width_observed = |
| std::max(asset_metadata.size->width(), largest_width_observed); |
| largest_height_observed = |
| std::max(asset_metadata.size->height(), largest_height_observed); |
| if (!IsSquare(*asset_metadata.size)) { |
| ++num_assets_found; |
| aspect_ratio_sum += gfx::SizeF(*asset_metadata.size).AspectRatio(); |
| } |
| } |
| |
| if (num_assets_found == 0) { |
| if (largest_width_observed == kDimensionInvalid) { |
| // There were no assets matching the desired orientation. |
| return gfx::Size(); |
| } else { |
| // There were assets matching the desired orientation, but all of them |
| // were closer to being "square". |
| int square_length = |
| std::max(largest_width_observed, largest_height_observed); |
| return gfx::Size(square_length, square_length); |
| } |
| } |
| |
| float average_aspect_ratio = aspect_ratio_sum / num_assets_found; |
| // There are corner cases here where an asset found above may ultimately have |
| // a dimension larger than the computed size, but it's not worth accounting |
| // for. |
| gfx::Size candidate_a = gfx::Size( |
| largest_width_observed, |
| base::ClampRound<int>(largest_width_observed / average_aspect_ratio)); |
| gfx::Size candidate_b = gfx::Size( |
| base::ClampRound<int>(largest_height_observed * average_aspect_ratio), |
| largest_height_observed); |
| // Both candidates should have the same aspect ratio, so comparing one of the |
| // dimensions (width in this case) is sufficient. |
| return candidate_a.width() > candidate_b.width() ? candidate_a : candidate_b; |
| } |
| |
| // The output will always have 1 size for landscape assets and 1 size for |
| // portrait assets (or 0 if there are no assets of a particular orientation). |
| std::vector<gfx::Size> ComputeTopicSizes( |
| const cc::SkottieResourceMetadataMap& resource_metadata) { |
| static constexpr gfx::Size kDefaultTopicSize = gfx::Size(500, 500); |
| |
| gfx::Size landscape_size = |
| SummarizeImageAssetSizes(resource_metadata, /*is_portrait=*/false); |
| gfx::Size portrait_size = |
| SummarizeImageAssetSizes(resource_metadata, /*is_portrait=*/true); |
| std::vector<gfx::Size> output; |
| if (!landscape_size.IsEmpty()) |
| output.push_back(std::move(landscape_size)); |
| if (!portrait_size.IsEmpty()) |
| output.push_back(std::move(portrait_size)); |
| |
| if (output.empty()) { |
| LOG(DFATAL) << "Failed to compute topic sizes for animation. Animation " |
| "file is likely invalid."; |
| return {kDefaultTopicSize}; |
| } |
| return output; |
| } |
| |
| } // namespace |
| |
| AmbientTopicQueueAnimationDelegate::AmbientTopicQueueAnimationDelegate( |
| const cc::SkottieResourceMetadataMap& resource_metadata) |
| : topic_sizes_(ComputeTopicSizes(resource_metadata)) {} |
| |
| AmbientTopicQueueAnimationDelegate::~AmbientTopicQueueAnimationDelegate() = |
| default; |
| |
| std::vector<gfx::Size> AmbientTopicQueueAnimationDelegate::GetTopicSizes() { |
| // At the time this was written, UX has agreed that the landscape and portrait |
| // versions of a given animation theme will have the same image asset sizes |
| // (only the animation's layout will be different). Thus, it is sufficient |
| // and simplest to just compute the desired topic sizes once with whichever |
| // version of the animation is loaded initially (either topic or landscape). |
| // |
| // If this changes in the future, this will need to recompute topic sizes with |
| // the new animation orientation. |
| return topic_sizes_; |
| } |
| |
| } // namespace ash |