Commit 5420a4f2 authored by Orin Jaworski's avatar Orin Jaworski Committed by Commit Bot

Use contrast ratios for tab hover opacities

The hovered tab state was visually too close to active state
so this CL reduces the opacity. The method for calculating
interpolation range and hover radial gradient opacity is
changed to use target contrast ratios. This approach ideally
will adapt better to various themes and color schemes,
ensuring the right level of contrast by adjusting opacity
automatically.

Bug: 856893
Change-Id: I2799b722c1d84d7c323e4f587fa8f5e58106bf58
Reviewed-on: https://chromium-review.googlesource.com/1147605
Commit-Queue: Orin Jaworski <orinj@chromium.org>
Reviewed-by: default avatarMichael Wasserman <msw@chromium.org>
Reviewed-by: default avatarPeter Kasting <pkasting@chromium.org>
Cr-Commit-Position: refs/heads/master@{#581330}
parent 53d72b57
......@@ -6,7 +6,9 @@
#include "ui/views/view.h"
// Amount to scale the opacity.
// Amount to scale the opacity. The spec is in terms of a Sketch radial gradient
// from color A (#FFF, 0.9) at center to color B (#FFF, 0.0) at edge, with a
// gradient opacity of 0.5. So this premultiplies for a center opacity of 0.45.
static const double kSubtleOpacityScale = 0.45;
static const double kPronouncedOpacityScale = 1.0;
......@@ -14,7 +16,10 @@ static const double kPronouncedOpacityScale = 1.0;
static const int kTrackHoverDurationMs = 200;
GlowHoverController::GlowHoverController(views::View* view)
: view_(view), animation_(this), opacity_scale_(kSubtleOpacityScale) {
: view_(view),
animation_(this),
opacity_scale_(kSubtleOpacityScale),
subtle_opacity_scale_(kSubtleOpacityScale) {
animation_.set_delegate(this);
}
......@@ -31,10 +36,14 @@ void GlowHoverController::SetLocation(const gfx::Point& location) {
view_->SchedulePaint();
}
void GlowHoverController::SetSubtleOpacityScale(double opacity_scale) {
subtle_opacity_scale_ = opacity_scale;
}
void GlowHoverController::Show(Style style) {
switch (style) {
case SUBTLE:
opacity_scale_ = kSubtleOpacityScale;
opacity_scale_ = subtle_opacity_scale_;
animation_.SetSlideDuration(kTrackHoverDurationMs);
animation_.SetTweenType(gfx::Tween::EASE_OUT);
animation_.Show();
......
......@@ -39,6 +39,9 @@ class GlowHoverController : public gfx::AnimationDelegate {
// constructor.
void SetLocation(const gfx::Point& location);
// Set opacity scale to use when Show is called with SUBTLE.
void SetSubtleOpacityScale(double opacity_scale);
const gfx::Point& location() const { return location_; }
// Initiates showing the hover.
......@@ -73,6 +76,7 @@ class GlowHoverController : public gfx::AnimationDelegate {
// Location of the glow, relative to view.
gfx::Point location_;
double opacity_scale_;
double subtle_opacity_scale_;
DISALLOW_COPY_AND_ASSIGN(GlowHoverController);
};
......
......@@ -478,21 +478,6 @@ gfx::Path GetBorderPath(float scale,
return path;
}
float Lerp(float v0, float v1, float t) {
return v0 + (v1 - v0) * t;
}
// Produces lerp parameter from a range and value within the range, then uses
// it to Lerp from v0 to v1.
float LerpFromRange(float v0,
float v1,
float range_start,
float range_end,
float value_in_range) {
const float t = (value_in_range - range_start) / (range_end - range_start);
return Lerp(v0, v1, t);
}
} // namespace
// Tab -------------------------------------------------------------------------
......@@ -551,6 +536,8 @@ Tab::Tab(TabController* controller, gfx::AnimationContainer* container)
title_animation_.SetContainer(animation_container_.get());
hover_controller_.SetAnimationContainer(animation_container_.get());
UpdateOpacities();
}
Tab::~Tab() {
......@@ -865,6 +852,7 @@ void Tab::OnMouseMoved(const ui::MouseEvent& event) {
void Tab::OnMouseEntered(const ui::MouseEvent& event) {
mouse_hovered_ = true;
hover_controller_.SetSubtleOpacityScale(radial_highlight_opacity_);
hover_controller_.Show(GlowHoverController::SUBTLE);
Layout();
}
......@@ -980,6 +968,7 @@ void Tab::AddedToWidget() {
void Tab::OnThemeChanged() {
OnButtonColorMaybeChanged();
UpdateOpacities();
}
void Tab::SetClosing(bool closing) {
......@@ -1678,13 +1667,16 @@ float Tab::GetThrobValue() const {
// Wrapping in closure to only compute offset when needed (animate or hover).
const auto offset = [=] {
// Opacity boost varies on tab width.
constexpr float kHoverOpacityMin = 0.5f;
constexpr float kHoverOpacityMax = 0.65f;
const float hoverOpacity = LerpFromRange(
kHoverOpacityMin, kHoverOpacityMax, float{GetStandardWidth()},
float{GetMinimumInactiveWidth()}, float{bounds().width()});
return is_selected ? (kSelectedTabThrobScale * hoverOpacity) : hoverOpacity;
// Opacity boost varies on tab width. The interpolation is nonlinear so
// that most tabs will fall on the low end of the opacity range, but very
// narrow tabs will still stand out on the high end.
const float range_start = float{GetStandardWidth()};
const float range_end = float{GetMinimumInactiveWidth()};
const float value_in_range = float{bounds().width()};
const float t = (value_in_range - range_start) / (range_end - range_start);
const float opacity = gfx::Tween::FloatValueBetween(
t * t, hover_opacity_min_, hover_opacity_max_);
return is_selected ? (kSelectedTabThrobScale * opacity) : opacity;
};
if (pulse_animation_.is_animating())
......@@ -1735,5 +1727,44 @@ void Tab::UpdateTabIconNeedsAttentionBlocked() {
}
}
void Tab::UpdateOpacities() {
// The contrast ratio for the hover effect on standard-width tabs.
// In the default Refresh color scheme, this corresponds to a hover
// opacity of 0.4.
constexpr float kDesiredContrastHoveredStandardWidthTab = 1.11f;
// The contrast ratio for the hover effect on min-width tabs.
// In the default Refresh color scheme, this corresponds to a hover
// opacity of 0.65.
constexpr float kDesiredContrastHoveredMinWidthTab = 1.19f;
// The contrast ratio for the radial gradient effect on hovered tabs.
// In the default Refresh color scheme, this corresponds to a hover
// opacity of 0.45.
constexpr float kDesiredContrastRadialGradient = 1.13728f;
const SkColor active_tab_bg_color =
controller_->GetTabBackgroundColor(TAB_ACTIVE);
const SkColor inactive_tab_bg_color =
controller_->GetTabBackgroundColor(TAB_INACTIVE);
const SkAlpha hover_base_alpha_wide =
color_utils::GetBlendValueWithMinimumContrast(
inactive_tab_bg_color, active_tab_bg_color, inactive_tab_bg_color,
kDesiredContrastHoveredStandardWidthTab);
const SkAlpha hover_base_alpha_narrow =
color_utils::GetBlendValueWithMinimumContrast(
inactive_tab_bg_color, active_tab_bg_color, inactive_tab_bg_color,
kDesiredContrastHoveredMinWidthTab);
const SkAlpha radial_highlight_alpha =
color_utils::GetBlendValueWithMinimumContrast(
inactive_tab_bg_color, active_tab_bg_color, inactive_tab_bg_color,
kDesiredContrastRadialGradient);
hover_opacity_min_ = hover_base_alpha_wide / 255.0f;
hover_opacity_max_ = hover_base_alpha_narrow / 255.0f;
radial_highlight_opacity_ = radial_highlight_alpha / 255.0f;
}
Tab::BackgroundCache::BackgroundCache() = default;
Tab::BackgroundCache::~BackgroundCache() = default;
......@@ -310,6 +310,9 @@ class Tab : public gfx::AnimationDelegate,
// state; it is the responsibility of the caller to request a paint.
void UpdateTabIconNeedsAttentionBlocked();
// Computes and stores opacities derived from contrast ratios.
void UpdateOpacities();
// The controller, never NULL.
TabController* const controller_;
......@@ -383,6 +386,11 @@ class Tab : public gfx::AnimationDelegate,
// the view bounds.
bool mouse_hovered_ = false;
// These computed values are stored for fast use during mouse moves & hover.
float hover_opacity_min_;
float hover_opacity_max_;
float radial_highlight_opacity_;
class BackgroundCache {
public:
BackgroundCache();
......
......@@ -319,36 +319,60 @@ SkColor PickContrastingColor(SkColor foreground1,
SkColor GetColorWithMinimumContrast(SkColor default_foreground,
SkColor background) {
DCHECK_EQ(SkColorGetA(default_foreground), SK_AlphaOPAQUE);
const float background_luminance = GetRelativeLuminance(background);
if (GetContrastRatio(GetRelativeLuminance(default_foreground),
background_luminance) >= kMinimumReadableContrastRatio) {
return default_foreground;
}
const SkColor blend_direction =
IsDark(background) ? SK_ColorWHITE : g_color_utils_darkest;
// Binary search to find the smallest blend that gives us acceptable contrast.
SkAlpha lower_bound_alpha = SK_AlphaTRANSPARENT;
SkAlpha upper_bound_alpha = SK_AlphaOPAQUE;
SkColor best_color = blend_direction;
const SkAlpha alpha = GetBlendValueWithMinimumContrast(
default_foreground, blend_direction, background,
kMinimumReadableContrastRatio);
return AlphaBlend(blend_direction, default_foreground, alpha);
}
SkAlpha GetBlendValueWithMinimumContrast(SkColor source,
SkColor target,
SkColor base,
float contrast_ratio) {
DCHECK_EQ(SkColorGetA(base), SK_AlphaOPAQUE);
source = GetResultingPaintColor(source, base);
if (GetContrastRatio(source, base) >= contrast_ratio)
return 0;
target = GetResultingPaintColor(target, base);
constexpr int kCloseEnoughAlphaDelta = 0x04;
while (lower_bound_alpha + kCloseEnoughAlphaDelta < upper_bound_alpha) {
const SkAlpha next_alpha =
gfx::ToCeiledInt((lower_bound_alpha + upper_bound_alpha) / 2.f);
const SkColor next_foreground =
AlphaBlend(blend_direction, default_foreground, next_alpha);
if (GetContrastRatio(GetRelativeLuminance(next_foreground),
background_luminance) <
kMinimumReadableContrastRatio) {
lower_bound_alpha = next_alpha;
return FindBlendValueForContrastRatio(source, target, base, contrast_ratio,
kCloseEnoughAlphaDelta);
}
SkAlpha FindBlendValueForContrastRatio(SkColor source,
SkColor target,
SkColor base,
float contrast_ratio,
int alpha_error_tolerance) {
DCHECK_EQ(SkColorGetA(source), SK_AlphaOPAQUE);
DCHECK_EQ(SkColorGetA(target), SK_AlphaOPAQUE);
DCHECK_EQ(SkColorGetA(base), SK_AlphaOPAQUE);
DCHECK_GE(alpha_error_tolerance, 0);
const float base_luminance = GetRelativeLuminance(base);
// Use int for inclusive lower bound and exclusive upper bound, reserving
// conversion to SkAlpha for the end (reduces casts).
int low = SK_AlphaTRANSPARENT;
int high = SK_AlphaOPAQUE + 1;
int best = SK_AlphaOPAQUE;
while (low + alpha_error_tolerance < high) {
const int alpha = (low + high) / 2;
const SkColor blended = AlphaBlend(target, source, alpha);
const float luminance = GetRelativeLuminance(blended);
const float contrast = GetContrastRatio(luminance, base_luminance);
if (contrast >= contrast_ratio) {
best = alpha;
high = alpha;
} else {
upper_bound_alpha = next_alpha;
best_color = next_foreground;
low = alpha + 1;
}
}
return best_color;
return best;
}
SkColor InvertColor(SkColor color) {
......
......@@ -130,10 +130,33 @@ GFX_EXPORT SkColor PickContrastingColor(SkColor foreground1,
// |background|. If |default_foreground| already meets the minimum contrast
// ratio, this function will simply return it. Otherwise it will blend the color
// darker/lighter until either the contrast ratio is acceptable or the color
// cannot become any more extreme. Only use with opaque colors.
// cannot become any more extreme. Only use with opaque background.
GFX_EXPORT SkColor GetColorWithMinimumContrast(SkColor default_foreground,
SkColor background);
// Attempts to select an alpha value such that blending |target| onto |source|
// with that alpha produces a color of at least |contrast_ratio| against |base|.
// If |source| already meets the minimum contrast ratio, this function will
// simply return 0. Otherwise it will blend the |target| onto |source| until
// either the contrast ratio is acceptable or the color cannot become any more
// extreme. |base| must be opaque.
GFX_EXPORT SkAlpha GetBlendValueWithMinimumContrast(SkColor source,
SkColor target,
SkColor base,
float contrast_ratio);
// Returns the minimum alpha value such that blending |target| onto |source|
// produces a color that contrasts against |base| with at least |contrast_ratio|
// unless this is impossible, in which case SK_AlphaOPAQUE is returned.
// Use only with opaque colors. |alpha_error_tolerance| should normally be 0 for
// best accuracy, but if performance is critical then it can be a positive value
// (4 is recommended) to save a few cycles and give "close enough" alpha.
GFX_EXPORT SkAlpha FindBlendValueForContrastRatio(SkColor source,
SkColor target,
SkColor base,
float contrast_ratio,
int alpha_error_tolerance);
// Invert a color.
GFX_EXPORT SkColor InvertColor(SkColor color);
......
......@@ -221,4 +221,37 @@ TEST(ColorUtils, GetColorWithMinimumContrast_StopsAtDarkestColor) {
SetDarkestColor(old_black_color);
}
TEST(ColorUtils, GetBlendValueWithMinimumContrast_ComputesExpectedOpacities) {
const SkColor source = SkColorSetRGB(0xDE, 0xE1, 0xE6);
const SkColor target = SkColorSetRGB(0xFF, 0xFF, 0xFF);
const SkColor base = source;
SkAlpha alpha = GetBlendValueWithMinimumContrast(source, target, base, 1.11f);
EXPECT_NEAR(alpha / 255.0f, 0.4f, 0.03f);
alpha = GetBlendValueWithMinimumContrast(source, target, base, 1.19f);
EXPECT_NEAR(alpha / 255.0f, 0.65f, 0.03f);
alpha = GetBlendValueWithMinimumContrast(source, target, base, 1.13728f);
EXPECT_NEAR(alpha / 255.0f, 0.45f, 0.03f);
}
TEST(ColorUtils, FindBlendValueForContrastRatio_MatchesNaiveImplementation) {
const SkColor source = SkColorSetRGB(0xDE, 0xE1, 0xE6);
const SkColor target = SkColorSetRGB(0xFF, 0xFF, 0xFF);
const SkColor base = source;
const float contrast_ratio = 1.11f;
const SkAlpha alpha =
FindBlendValueForContrastRatio(source, target, base, contrast_ratio, 0);
// Naive implementation is direct translation of function description.
SkAlpha check = SK_AlphaTRANSPARENT;
for (SkAlpha alpha = SK_AlphaTRANSPARENT; alpha <= SK_AlphaOPAQUE; ++alpha) {
const SkColor blended = AlphaBlend(target, source, alpha);
const float contrast = GetContrastRatio(blended, base);
check = alpha;
if (contrast >= contrast_ratio)
break;
}
EXPECT_EQ(check, alpha);
}
} // namespace color_utils
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment