blob: 1a8a2a388e7c010e6efb181d9fd0f0dec048fd9a [file] [log] [blame]
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ash/media/media_notification_background.h"
#include <algorithm>
#include <vector>
#include "skia/ext/image_operations.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/color_analysis.h"
#include "ui/gfx/color_utils.h"
#include "ui/gfx/scoped_canvas.h"
#include "ui/views/style/typography.h"
#include "ui/views/view.h"
namespace ash {
namespace {
constexpr int kMediaImageGradientWidth = 40;
constexpr SkColor kMediaNotificationDefaultBackgroundColor = SK_ColorWHITE;
// The ratio for a background color option to be considered very popular.
constexpr double kMediaNotificationBackgroundColorVeryPopularRatio = 2.5;
// The ratio for the most popular foreground color to be used.
constexpr double kMediaNotificationForegroundColorMostPopularRatio = 0.01;
// The minimum saturation for the most popular foreground color to be used.
constexpr double kMediaNotificationForegroundColorMostPopularMinSaturation =
0.19;
// The ratio for the more vibrant foreground color to use.
constexpr double kMediaNotificationForegroundColorMoreVibrantRatio = 1.0;
bool IsNearlyWhiteOrBlack(SkColor color) {
color_utils::HSL hsl;
color_utils::SkColorToHSL(color, &hsl);
return hsl.l >= 0.9 || hsl.l <= 0.08;
}
int GetHueDegrees(const SkColor& color) {
color_utils::HSL hsl;
color_utils::SkColorToHSL(color, &hsl);
return hsl.h * 360;
}
double GetSaturation(const color_utils::Swatch& swatch) {
color_utils::HSL hsl;
color_utils::SkColorToHSL(swatch.color, &hsl);
return hsl.s;
}
bool IsForegroundColorSwatchAllowed(const SkColor& background,
const SkColor& candidate) {
if (IsNearlyWhiteOrBlack(candidate))
return false;
if (IsNearlyWhiteOrBlack(background))
return true;
int diff = abs(GetHueDegrees(candidate) - GetHueDegrees(background));
return diff > 10 && diff < 350;
}
base::Optional<SkColor> GetNotificationBackgroundColor(const SkBitmap* source) {
if (!source || source->empty() || source->isNull())
return base::nullopt;
std::vector<color_utils::Swatch> swatches =
color_utils::CalculateColorSwatches(
*source, 16, gfx::Rect(source->width() / 2, source->height()),
base::nullopt);
if (swatches.empty())
return base::nullopt;
base::Optional<color_utils::Swatch> most_popular;
base::Optional<color_utils::Swatch> non_white_black;
// Find the most popular color with the most weight and the color which
// is the color with the most weight that is not white or black.
for (auto& swatch : swatches) {
if (!IsNearlyWhiteOrBlack(swatch.color) &&
(!non_white_black || swatch.population > non_white_black->population)) {
non_white_black = swatch;
}
if (most_popular && swatch.population < most_popular->population)
continue;
most_popular = swatch;
}
DCHECK(most_popular);
// If the most popular color is not white or black then we should use that.
if (!IsNearlyWhiteOrBlack(most_popular->color))
return most_popular->color;
// If we could not find a color that is not white or black then we should
// use the most popular color.
if (!non_white_black)
return most_popular->color;
// If the most popular color is very popular then we should use that color.
if (static_cast<double>(most_popular->population) /
non_white_black->population >
kMediaNotificationBackgroundColorVeryPopularRatio) {
return most_popular->color;
}
return non_white_black->color;
}
color_utils::Swatch SelectVibrantSwatch(const color_utils::Swatch& more_vibrant,
const color_utils::Swatch& vibrant) {
if ((static_cast<double>(more_vibrant.population) / vibrant.population) <
kMediaNotificationForegroundColorMoreVibrantRatio) {
return vibrant;
}
return more_vibrant;
}
color_utils::Swatch SelectMutedSwatch(const color_utils::Swatch& muted,
const color_utils::Swatch& more_muted) {
double population_ratio =
static_cast<double>(muted.population) / more_muted.population;
// Use the swatch with the higher saturation ratio.
return (GetSaturation(muted) * population_ratio > GetSaturation(more_muted))
? muted
: more_muted;
}
base::Optional<SkColor> GetNotificationForegroundColor(
const base::Optional<SkColor>& background_color,
const SkBitmap* source) {
if (!background_color || !source || source->empty() || source->isNull())
return base::nullopt;
const bool is_light =
color_utils::GetRelativeLuminance(*background_color) > 0.5;
const SkColor fallback_color = is_light ? SK_ColorBLACK : SK_ColorWHITE;
gfx::Rect bitmap_area(source->width(), source->height());
bitmap_area.Inset(source->width() * 0.4, 0, 0, 0);
// If the background color is dark we want to look for colors that are darker
// and vice versa.
const color_utils::LumaRange more_luma_range =
is_light ? color_utils::LumaRange::DARK : color_utils::LumaRange::LIGHT;
std::vector<color_utils::ColorProfile> color_profiles;
color_profiles.push_back(color_utils::ColorProfile(
more_luma_range, color_utils::SaturationRange::VIBRANT));
color_profiles.push_back(color_utils::ColorProfile(
color_utils::LumaRange::NORMAL, color_utils::SaturationRange::VIBRANT));
color_profiles.push_back(color_utils::ColorProfile(
color_utils::LumaRange::NORMAL, color_utils::SaturationRange::MUTED));
color_profiles.push_back(color_utils::ColorProfile(
more_luma_range, color_utils::SaturationRange::MUTED));
color_profiles.push_back(color_utils::ColorProfile(
color_utils::LumaRange::ANY, color_utils::SaturationRange::ANY));
std::vector<color_utils::Swatch> best_swatches =
color_utils::CalculateProminentColorsOfBitmap(
*source, color_profiles, &bitmap_area,
base::BindRepeating(&IsForegroundColorSwatchAllowed,
background_color.value()));
if (best_swatches.empty())
return fallback_color;
DCHECK_EQ(5u, best_swatches.size());
const color_utils::Swatch& more_vibrant = best_swatches[0];
const color_utils::Swatch& vibrant = best_swatches[1];
const color_utils::Swatch& muted = best_swatches[2];
const color_utils::Swatch& more_muted = best_swatches[3];
const color_utils::Swatch& most_popular = best_swatches[4];
// We are looking for a fraction that is at least 0.2% of the image.
const size_t population_min =
std::min(bitmap_area.width() * bitmap_area.height(),
color_utils::kMaxConsideredPixelsForSwatches) *
0.002;
// This selection algorithm is an implementation of MediaNotificationProcessor
// from Android. It will select more vibrant colors first since they stand out
// better against the background. If not, it will fallback to muted colors,
// the most popular color and then either white/black. Any swatch has to be
// above a minimum population threshold to be determined significant enough in
// the artwork to be used.
base::Optional<color_utils::Swatch> swatch;
if (more_vibrant.population > population_min &&
vibrant.population > population_min) {
swatch = SelectVibrantSwatch(more_vibrant, vibrant);
} else if (more_vibrant.population > population_min) {
swatch = more_vibrant;
} else if (vibrant.population > population_min) {
swatch = vibrant;
} else if (muted.population > population_min &&
more_muted.population > population_min) {
swatch = SelectMutedSwatch(muted, more_muted);
} else if (muted.population > population_min) {
swatch = muted;
} else if (more_muted.population > population_min) {
swatch = more_muted;
} else if (most_popular.population > population_min) {
return most_popular.color;
} else {
return fallback_color;
}
if (most_popular == *swatch)
return swatch->color;
if (static_cast<double>(swatch->population) / most_popular.population <
kMediaNotificationForegroundColorMostPopularRatio &&
GetSaturation(most_popular) >
kMediaNotificationForegroundColorMostPopularMinSaturation) {
return most_popular.color;
}
return swatch->color;
}
} // namespace
MediaNotificationBackground::MediaNotificationBackground(
views::View* owner,
int top_radius,
int bottom_radius,
double artwork_max_width_pct)
: owner_(owner),
top_radius_(top_radius),
bottom_radius_(bottom_radius),
artwork_max_width_pct_(artwork_max_width_pct) {
DCHECK(owner);
}
MediaNotificationBackground::~MediaNotificationBackground() = default;
void MediaNotificationBackground::Paint(gfx::Canvas* canvas,
views::View* view) const {
DCHECK(view);
gfx::ScopedCanvas scoped_canvas(canvas);
gfx::Rect bounds = view->GetContentsBounds();
{
// Draw a rounded rectangle which the background will be clipped to. The
// radius is provided by the notification and can change based on where in
// the list the notification is.
const SkScalar top_radius = SkIntToScalar(top_radius_);
const SkScalar bottom_radius = SkIntToScalar(bottom_radius_);
const SkScalar radii[8] = {top_radius, top_radius, top_radius,
top_radius, bottom_radius, bottom_radius,
bottom_radius, bottom_radius};
SkPath path;
path.addRoundRect(gfx::RectToSkRect(bounds), radii, SkPath::kCW_Direction);
canvas->ClipPath(path, true);
}
{
// Draw the artwork. The artwork is resized to the height of the view while
// maintaining the aspect ratio.
gfx::Rect source_bounds =
gfx::Rect(0, 0, artwork_.width(), artwork_.height());
gfx::Rect artwork_bounds = GetArtworkBounds(bounds);
canvas->DrawImageInt(
artwork_, source_bounds.x(), source_bounds.y(), source_bounds.width(),
source_bounds.height(), artwork_bounds.x(), artwork_bounds.y(),
artwork_bounds.width(), artwork_bounds.height(), false /* filter */);
}
// Draw a filled rectangle which will act as the main background of the
// notification. This may cover up some of the artwork.
const SkColor background_color =
background_color_.value_or(kMediaNotificationDefaultBackgroundColor);
canvas->FillRect(GetFilledBackgroundBounds(bounds), background_color);
{
// Draw a gradient to fade the color background and the image together.
gfx::Rect draw_bounds = GetGradientBounds(bounds);
const SkColor colors[2] = {
background_color, SkColorSetA(background_color, SK_AlphaTRANSPARENT)};
const SkPoint points[2] = {GetGradientStartPoint(draw_bounds),
GetGradientEndPoint(draw_bounds)};
cc::PaintFlags flags;
flags.setAntiAlias(true);
flags.setStyle(cc::PaintFlags::kFill_Style);
flags.setShader(cc::PaintShader::MakeLinearGradient(points, colors, nullptr,
2, SkTileMode::kClamp));
canvas->DrawRect(draw_bounds, flags);
}
}
void MediaNotificationBackground::UpdateArtwork(const gfx::ImageSkia& image) {
if (artwork_.BackedBySameObjectAs(image))
return;
artwork_ = image;
background_color_ = GetNotificationBackgroundColor(artwork_.bitmap());
foreground_color_ =
GetNotificationForegroundColor(background_color_, artwork_.bitmap());
owner_->SchedulePaint();
}
void MediaNotificationBackground::UpdateCornerRadius(int top_radius,
int bottom_radius) {
if (top_radius_ == top_radius && bottom_radius_ == bottom_radius)
return;
top_radius_ = top_radius;
bottom_radius_ = bottom_radius;
owner_->SchedulePaint();
}
void MediaNotificationBackground::UpdateArtworkMaxWidthPct(
double max_width_pct) {
if (artwork_max_width_pct_ == max_width_pct)
return;
artwork_max_width_pct_ = max_width_pct;
owner_->SchedulePaint();
}
SkColor MediaNotificationBackground::GetBackgroundColor() const {
return background_color_.value_or(kMediaNotificationDefaultBackgroundColor);
}
SkColor MediaNotificationBackground::GetForegroundColor() const {
return color_utils::GetColorWithMinimumContrast(
foreground_color_.value_or(views::style::GetColor(
*owner_, views::style::CONTEXT_LABEL, views::style::STYLE_PRIMARY)),
GetBackgroundColor());
}
int MediaNotificationBackground::GetArtworkWidth(
const gfx::Size& view_size) const {
if (artwork_.isNull())
return 0;
// Calculate the aspect ratio of the image and determine what the width of the
// image should be based on that ratio and the height of the notification.
float aspect_ratio = (float)artwork_.width() / artwork_.height();
return ceil(view_size.height() * aspect_ratio);
}
int MediaNotificationBackground::GetArtworkVisibleWidth(
const gfx::Size& view_size) const {
// The artwork should only take up a maximum percentage of the notification.
return std::min(GetArtworkWidth(view_size),
(int)ceil(view_size.width() * artwork_max_width_pct_));
}
gfx::Rect MediaNotificationBackground::GetArtworkBounds(
const gfx::Rect& view_bounds) const {
int width = GetArtworkWidth(view_bounds.size());
// The artwork should be positioned on the far right hand side of the
// notification and be the same height.
return owner_->GetMirroredRect(
gfx::Rect(view_bounds.right() - width, 0, width, view_bounds.height()));
}
gfx::Rect MediaNotificationBackground::GetFilledBackgroundBounds(
const gfx::Rect& view_bounds) const {
// The filled background should take up the full notification except the area
// taken up by the artwork.
gfx::Rect bounds = gfx::Rect(view_bounds);
bounds.Inset(0, 0, GetArtworkVisibleWidth(view_bounds.size()), 0);
return owner_->GetMirroredRect(bounds);
}
gfx::Rect MediaNotificationBackground::GetGradientBounds(
const gfx::Rect& view_bounds) const {
if (artwork_.isNull())
return gfx::Rect(0, 0, 0, 0);
// The gradient should appear above the artwork on the left.
return owner_->GetMirroredRect(gfx::Rect(
view_bounds.width() - GetArtworkVisibleWidth(view_bounds.size()),
view_bounds.y(), kMediaImageGradientWidth, view_bounds.height()));
}
SkPoint MediaNotificationBackground::GetGradientStartPoint(
const gfx::Rect& draw_bounds) const {
return gfx::PointToSkPoint(base::i18n::IsRTL() ? draw_bounds.right_center()
: draw_bounds.left_center());
}
SkPoint MediaNotificationBackground::GetGradientEndPoint(
const gfx::Rect& draw_bounds) const {
return gfx::PointToSkPoint(base::i18n::IsRTL() ? draw_bounds.left_center()
: draw_bounds.right_center());
}
} // namespace ash