chromium/ash/ambient/model/ambient_topic_queue_animation_delegate.cc

// 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