mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2026-04-19 02:10:26 +00:00
1036 lines
46 KiB
C++
1036 lines
46 KiB
C++
|
|
/*
|
||
|
|
* Copyright (c) 2026, Tim Ledbetter <tim.ledbetter@ladybird.org>
|
||
|
|
*
|
||
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
||
|
|
*/
|
||
|
|
|
||
|
|
#include <AK/IntegralMath.h>
|
||
|
|
#include <LibGfx/ColorConversion.h>
|
||
|
|
#include <LibWeb/CSS/ColorInterpolation.h>
|
||
|
|
#include <LibWeb/CSS/Interpolation.h>
|
||
|
|
#include <LibWeb/CSS/StyleValues/ColorFunctionStyleValue.h>
|
||
|
|
#include <LibWeb/CSS/StyleValues/HSLColorStyleValue.h>
|
||
|
|
#include <LibWeb/CSS/StyleValues/HWBColorStyleValue.h>
|
||
|
|
#include <LibWeb/CSS/StyleValues/KeywordStyleValue.h>
|
||
|
|
#include <LibWeb/CSS/StyleValues/LCHLikeColorStyleValue.h>
|
||
|
|
#include <LibWeb/CSS/StyleValues/LabLikeColorStyleValue.h>
|
||
|
|
#include <LibWeb/CSS/StyleValues/NumberStyleValue.h>
|
||
|
|
#include <LibWeb/CSS/StyleValues/RGBColorStyleValue.h>
|
||
|
|
|
||
|
|
namespace Web::CSS {
|
||
|
|
|
||
|
|
static float interpolate_color_component(float from, float to, float delta)
|
||
|
|
{
|
||
|
|
return from + (to - from) * delta;
|
||
|
|
}
|
||
|
|
|
||
|
|
// https://drafts.csswg.org/css-color-4/#hue-interpolation
|
||
|
|
static void fixup_hue(float& hue1, float& hue2, HueInterpolationMethod hue_interpolation_method)
|
||
|
|
{
|
||
|
|
auto difference = hue2 - hue1;
|
||
|
|
switch (hue_interpolation_method) {
|
||
|
|
// https://drafts.csswg.org/css-color-4/#hue-shorter
|
||
|
|
case HueInterpolationMethod::Shorter:
|
||
|
|
if (difference > 180.0f)
|
||
|
|
hue1 += 360.0f;
|
||
|
|
else if (difference < -180.0f)
|
||
|
|
hue2 += 360.0f;
|
||
|
|
break;
|
||
|
|
// https://drafts.csswg.org/css-color-4/#hue-longer
|
||
|
|
case HueInterpolationMethod::Longer:
|
||
|
|
if (difference > 0.0f && difference < 180.0f)
|
||
|
|
hue1 += 360.0f;
|
||
|
|
else if (difference > -180.0f && difference <= 0.0f)
|
||
|
|
hue2 += 360.0f;
|
||
|
|
break;
|
||
|
|
// https://drafts.csswg.org/css-color-4/#hue-increasing
|
||
|
|
case HueInterpolationMethod::Increasing:
|
||
|
|
if (hue2 < hue1)
|
||
|
|
hue2 += 360.0f;
|
||
|
|
break;
|
||
|
|
// https://drafts.csswg.org/css-color-4/#hue-decreasing
|
||
|
|
case HueInterpolationMethod::Decreasing:
|
||
|
|
if (hue1 < hue2)
|
||
|
|
hue1 += 360.0f;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
static Gfx::ColorComponents srgb_to_rectangular_color_space(Gfx::ColorComponents srgb, RectangularColorSpace space)
|
||
|
|
{
|
||
|
|
if (space == RectangularColorSpace::Srgb)
|
||
|
|
return srgb;
|
||
|
|
if (space == RectangularColorSpace::SrgbLinear)
|
||
|
|
return Gfx::srgb_to_linear_srgb(srgb);
|
||
|
|
|
||
|
|
auto xyz65 = Gfx::linear_srgb_to_xyz65(Gfx::srgb_to_linear_srgb(srgb));
|
||
|
|
switch (space) {
|
||
|
|
case RectangularColorSpace::Srgb:
|
||
|
|
case RectangularColorSpace::SrgbLinear:
|
||
|
|
VERIFY_NOT_REACHED();
|
||
|
|
case RectangularColorSpace::DisplayP3:
|
||
|
|
return Gfx::linear_display_p3_to_display_p3(Gfx::xyz65_to_linear_display_p3(xyz65));
|
||
|
|
case RectangularColorSpace::DisplayP3Linear:
|
||
|
|
return Gfx::xyz65_to_linear_display_p3(xyz65);
|
||
|
|
case RectangularColorSpace::A98Rgb:
|
||
|
|
return Gfx::linear_a98_rgb_to_a98_rgb(Gfx::xyz65_to_linear_a98_rgb(xyz65));
|
||
|
|
case RectangularColorSpace::ProphotoRgb:
|
||
|
|
return Gfx::linear_prophoto_rgb_to_prophoto_rgb(Gfx::xyz50_to_linear_prophoto_rgb(Gfx::xyz65_to_xyz50(xyz65)));
|
||
|
|
case RectangularColorSpace::Rec2020:
|
||
|
|
return Gfx::linear_rec2020_to_rec2020(Gfx::xyz65_to_linear_rec2020(xyz65));
|
||
|
|
case RectangularColorSpace::Lab:
|
||
|
|
return Gfx::xyz50_to_lab(Gfx::xyz65_to_xyz50(xyz65));
|
||
|
|
case RectangularColorSpace::Oklab:
|
||
|
|
return Gfx::xyz65_to_oklab(xyz65);
|
||
|
|
case RectangularColorSpace::Xyz:
|
||
|
|
case RectangularColorSpace::XyzD65:
|
||
|
|
return xyz65;
|
||
|
|
case RectangularColorSpace::XyzD50:
|
||
|
|
return Gfx::xyz65_to_xyz50(xyz65);
|
||
|
|
}
|
||
|
|
VERIFY_NOT_REACHED();
|
||
|
|
}
|
||
|
|
|
||
|
|
static Gfx::ColorComponents srgb_to_polar_color_space(Gfx::ColorComponents srgb, PolarColorSpace space)
|
||
|
|
{
|
||
|
|
switch (space) {
|
||
|
|
case PolarColorSpace::Hsl:
|
||
|
|
return Gfx::srgb_to_hsl(srgb);
|
||
|
|
case PolarColorSpace::Hwb:
|
||
|
|
return Gfx::srgb_to_hwb(srgb);
|
||
|
|
case PolarColorSpace::Lch: {
|
||
|
|
auto xyz65 = Gfx::linear_srgb_to_xyz65(Gfx::srgb_to_linear_srgb(srgb));
|
||
|
|
return Gfx::lab_to_lch(Gfx::xyz50_to_lab(Gfx::xyz65_to_xyz50(xyz65)));
|
||
|
|
}
|
||
|
|
case PolarColorSpace::Oklch: {
|
||
|
|
auto xyz65 = Gfx::linear_srgb_to_xyz65(Gfx::srgb_to_linear_srgb(srgb));
|
||
|
|
return Gfx::oklab_to_oklch(Gfx::xyz65_to_oklab(xyz65));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
VERIFY_NOT_REACHED();
|
||
|
|
}
|
||
|
|
|
||
|
|
static bool is_component_none(StyleValue const& component)
|
||
|
|
{
|
||
|
|
return component.to_keyword() == Keyword::None;
|
||
|
|
}
|
||
|
|
|
||
|
|
static MissingComponents extract_missing_components(StyleValue const& style_value)
|
||
|
|
{
|
||
|
|
if (!style_value.is_color())
|
||
|
|
return {};
|
||
|
|
|
||
|
|
auto const& color = style_value.as_color();
|
||
|
|
switch (color.color_type()) {
|
||
|
|
case ColorStyleValue::ColorType::HSL: {
|
||
|
|
auto const& hsl = as<HSLColorStyleValue>(color);
|
||
|
|
return { is_component_none(hsl.h()), is_component_none(hsl.s()), is_component_none(hsl.l()), is_component_none(hsl.alpha()) };
|
||
|
|
}
|
||
|
|
case ColorStyleValue::ColorType::HWB: {
|
||
|
|
auto const& hwb = as<HWBColorStyleValue>(color);
|
||
|
|
return { is_component_none(hwb.h()), is_component_none(hwb.w()), is_component_none(hwb.b()), is_component_none(hwb.alpha()) };
|
||
|
|
}
|
||
|
|
case ColorStyleValue::ColorType::Lab: {
|
||
|
|
auto const& lab = as<LabColorStyleValue>(color);
|
||
|
|
return { is_component_none(lab.l()), is_component_none(lab.a()), is_component_none(lab.b()), is_component_none(lab.alpha()) };
|
||
|
|
}
|
||
|
|
case ColorStyleValue::ColorType::OKLab: {
|
||
|
|
auto const& oklab = as<OKLabColorStyleValue>(color);
|
||
|
|
return { is_component_none(oklab.l()), is_component_none(oklab.a()), is_component_none(oklab.b()), is_component_none(oklab.alpha()) };
|
||
|
|
}
|
||
|
|
case ColorStyleValue::ColorType::LCH: {
|
||
|
|
auto const& lch = as<LCHColorStyleValue>(color);
|
||
|
|
return { is_component_none(lch.l()), is_component_none(lch.c()), is_component_none(lch.h()), is_component_none(lch.alpha()) };
|
||
|
|
}
|
||
|
|
case ColorStyleValue::ColorType::OKLCH: {
|
||
|
|
auto const& oklch = as<OKLCHColorStyleValue>(color);
|
||
|
|
return { is_component_none(oklch.l()), is_component_none(oklch.c()), is_component_none(oklch.h()), is_component_none(oklch.alpha()) };
|
||
|
|
}
|
||
|
|
case ColorStyleValue::ColorType::RGB: {
|
||
|
|
auto const& rgb = as<RGBColorStyleValue>(color);
|
||
|
|
return { is_component_none(rgb.r()), is_component_none(rgb.g()), is_component_none(rgb.b()), is_component_none(rgb.alpha()) };
|
||
|
|
}
|
||
|
|
default:
|
||
|
|
if (color.is_color_function()) {
|
||
|
|
auto const& func = as<ColorFunctionStyleValue>(color);
|
||
|
|
return { is_component_none(func.channel(0)), is_component_none(func.channel(1)), is_component_none(func.channel(2)), is_component_none(func.alpha()) };
|
||
|
|
}
|
||
|
|
return {};
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// https://drafts.csswg.org/css-color-4/#interpolation-missing
|
||
|
|
// Analogous component categories for carrying forward missing components across color spaces.
|
||
|
|
// Each input color space component is classified into a category. If a missing component in
|
||
|
|
// the source space has an analogous component in the interpolation space, it is carried forward.
|
||
|
|
// https://drafts.csswg.org/css-color-4/#interpolation-missing
|
||
|
|
static ComponentCategories categories_for_rectangular_space(RectangularColorSpace space)
|
||
|
|
{
|
||
|
|
switch (space) {
|
||
|
|
case RectangularColorSpace::Srgb:
|
||
|
|
case RectangularColorSpace::SrgbLinear:
|
||
|
|
case RectangularColorSpace::DisplayP3:
|
||
|
|
case RectangularColorSpace::DisplayP3Linear:
|
||
|
|
case RectangularColorSpace::A98Rgb:
|
||
|
|
case RectangularColorSpace::ProphotoRgb:
|
||
|
|
case RectangularColorSpace::Rec2020:
|
||
|
|
return { ComponentCategory::Red, ComponentCategory::Green, ComponentCategory::Blue };
|
||
|
|
case RectangularColorSpace::Xyz:
|
||
|
|
case RectangularColorSpace::XyzD50:
|
||
|
|
case RectangularColorSpace::XyzD65:
|
||
|
|
// NOTE: The spec says XYZ spaces are considered super-saturated RGB for this purpose.
|
||
|
|
return { ComponentCategory::Red, ComponentCategory::Green, ComponentCategory::Blue };
|
||
|
|
case RectangularColorSpace::Lab:
|
||
|
|
case RectangularColorSpace::Oklab:
|
||
|
|
return { ComponentCategory::Lightness, ComponentCategory::OpponentA, ComponentCategory::OpponentB };
|
||
|
|
}
|
||
|
|
VERIFY_NOT_REACHED();
|
||
|
|
}
|
||
|
|
|
||
|
|
static ComponentCategories categories_for_polar_space(PolarColorSpace space)
|
||
|
|
{
|
||
|
|
switch (space) {
|
||
|
|
case PolarColorSpace::Hsl:
|
||
|
|
return { ComponentCategory::Hue, ComponentCategory::Colorfulness, ComponentCategory::Lightness };
|
||
|
|
case PolarColorSpace::Hwb:
|
||
|
|
// NOTE: Whiteness and Blackness have no analogs in other color spaces.
|
||
|
|
return { ComponentCategory::Hue, ComponentCategory::NotAnalogous, ComponentCategory::NotAnalogous };
|
||
|
|
case PolarColorSpace::Lch:
|
||
|
|
case PolarColorSpace::Oklch:
|
||
|
|
return { ComponentCategory::Lightness, ComponentCategory::Colorfulness, ComponentCategory::Hue };
|
||
|
|
}
|
||
|
|
VERIFY_NOT_REACHED();
|
||
|
|
}
|
||
|
|
|
||
|
|
static ComponentCategories categories_for_color_type(ColorStyleValue::ColorType color_type)
|
||
|
|
{
|
||
|
|
switch (color_type) {
|
||
|
|
case ColorStyleValue::ColorType::HSL:
|
||
|
|
return { ComponentCategory::Hue, ComponentCategory::Colorfulness, ComponentCategory::Lightness };
|
||
|
|
case ColorStyleValue::ColorType::HWB:
|
||
|
|
return { ComponentCategory::Hue, ComponentCategory::NotAnalogous, ComponentCategory::NotAnalogous };
|
||
|
|
case ColorStyleValue::ColorType::Lab:
|
||
|
|
case ColorStyleValue::ColorType::OKLab:
|
||
|
|
return { ComponentCategory::Lightness, ComponentCategory::OpponentA, ComponentCategory::OpponentB };
|
||
|
|
case ColorStyleValue::ColorType::LCH:
|
||
|
|
case ColorStyleValue::ColorType::OKLCH:
|
||
|
|
return { ComponentCategory::Lightness, ComponentCategory::Colorfulness, ComponentCategory::Hue };
|
||
|
|
case ColorStyleValue::ColorType::RGB:
|
||
|
|
case ColorStyleValue::ColorType::A98RGB:
|
||
|
|
case ColorStyleValue::ColorType::DisplayP3:
|
||
|
|
case ColorStyleValue::ColorType::DisplayP3Linear:
|
||
|
|
case ColorStyleValue::ColorType::sRGB:
|
||
|
|
case ColorStyleValue::ColorType::sRGBLinear:
|
||
|
|
case ColorStyleValue::ColorType::ProPhotoRGB:
|
||
|
|
case ColorStyleValue::ColorType::Rec2020:
|
||
|
|
case ColorStyleValue::ColorType::XYZD50:
|
||
|
|
case ColorStyleValue::ColorType::XYZD65:
|
||
|
|
return { ComponentCategory::Red, ComponentCategory::Green, ComponentCategory::Blue };
|
||
|
|
default:
|
||
|
|
return { ComponentCategory::NotAnalogous, ComponentCategory::NotAnalogous, ComponentCategory::NotAnalogous };
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// https://drafts.csswg.org/css-color-4/#interpolation-missing
|
||
|
|
// Carry forward missing components from the input color space to the interpolation color space.
|
||
|
|
// A missing component is carried forward if it has an analogous component in the target space.
|
||
|
|
// Additionally, if ALL components of an analogous set are missing, they are all carried forward.
|
||
|
|
static MissingComponents carry_forward_missing_components(
|
||
|
|
MissingComponents const& source_missing,
|
||
|
|
ComponentCategories const& source_categories,
|
||
|
|
ComponentCategories const& target_categories)
|
||
|
|
{
|
||
|
|
MissingComponents result;
|
||
|
|
|
||
|
|
// Same-space: all components map to themselves, including NotAnalogous ones (e.g. HWB W/B).
|
||
|
|
if (source_categories == target_categories) {
|
||
|
|
for (size_t i = 0; i < 3; ++i)
|
||
|
|
result.component(i) = source_missing.component(i);
|
||
|
|
result.alpha = source_missing.alpha;
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Carry forward individual analogous components
|
||
|
|
for (size_t target_index = 0; target_index < 3; ++target_index) {
|
||
|
|
if (target_categories.component(target_index) == ComponentCategory::NotAnalogous)
|
||
|
|
continue;
|
||
|
|
for (size_t source_index = 0; source_index < 3; ++source_index) {
|
||
|
|
if (source_missing.component(source_index) && source_categories.component(source_index) == target_categories.component(target_index)) {
|
||
|
|
result.component(target_index) = true;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// If every component of an analogous set is missing in the source, carry forward as a set.
|
||
|
|
// The analogous set consists of the components that remain after removing individually analogous ones.
|
||
|
|
bool all_non_analogous_missing = true;
|
||
|
|
bool has_non_analogous = false;
|
||
|
|
for (size_t i = 0; i < 3; ++i) {
|
||
|
|
bool is_individually_analogous = false;
|
||
|
|
for (size_t j = 0; j < 3; ++j) {
|
||
|
|
if (source_categories.component(i) != ComponentCategory::NotAnalogous && source_categories.component(i) == target_categories.component(j)) {
|
||
|
|
is_individually_analogous = true;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (!is_individually_analogous) {
|
||
|
|
has_non_analogous = true;
|
||
|
|
if (!source_missing.component(i))
|
||
|
|
all_non_analogous_missing = false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (has_non_analogous && all_non_analogous_missing) {
|
||
|
|
for (size_t i = 0; i < 3; ++i) {
|
||
|
|
bool is_individually_analogous = false;
|
||
|
|
for (size_t j = 0; j < 3; ++j) {
|
||
|
|
if (target_categories.component(i) != ComponentCategory::NotAnalogous && target_categories.component(i) == source_categories.component(j)) {
|
||
|
|
is_individually_analogous = true;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (!is_individually_analogous)
|
||
|
|
result.component(i) = true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Alpha is always analogous to itself.
|
||
|
|
result.alpha = source_missing.alpha;
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
|
||
|
|
static ValueComparingNonnullRefPtr<StyleValue const> number_or_none(float value, bool is_missing)
|
||
|
|
{
|
||
|
|
if (is_missing)
|
||
|
|
return KeywordStyleValue::create(Keyword::None);
|
||
|
|
return NumberStyleValue::create(value);
|
||
|
|
}
|
||
|
|
|
||
|
|
static ValueComparingNonnullRefPtr<StyleValue const> style_value_from_rectangular_color_space(Gfx::ColorComponents const& components, RectangularColorSpace space, MissingComponents const& missing = {})
|
||
|
|
{
|
||
|
|
auto c1 = number_or_none(components[0], missing.component(0));
|
||
|
|
auto c2 = number_or_none(components[1], missing.component(1));
|
||
|
|
auto c3 = number_or_none(components[2], missing.component(2));
|
||
|
|
auto alpha = number_or_none(components.alpha(), missing.alpha);
|
||
|
|
|
||
|
|
switch (space) {
|
||
|
|
case RectangularColorSpace::Lab:
|
||
|
|
return LabLikeColorStyleValue::create<LabColorStyleValue>(c1, c2, c3, alpha);
|
||
|
|
case RectangularColorSpace::Oklab:
|
||
|
|
return LabLikeColorStyleValue::create<OKLabColorStyleValue>(c1, c2, c3, alpha);
|
||
|
|
case RectangularColorSpace::Srgb:
|
||
|
|
return ColorFunctionStyleValue::create("srgb"sv, c1, c2, c3, alpha);
|
||
|
|
case RectangularColorSpace::SrgbLinear:
|
||
|
|
return ColorFunctionStyleValue::create("srgb-linear"sv, c1, c2, c3, alpha);
|
||
|
|
case RectangularColorSpace::DisplayP3:
|
||
|
|
return ColorFunctionStyleValue::create("display-p3"sv, c1, c2, c3, alpha);
|
||
|
|
case RectangularColorSpace::DisplayP3Linear:
|
||
|
|
return ColorFunctionStyleValue::create("display-p3-linear"sv, c1, c2, c3, alpha);
|
||
|
|
case RectangularColorSpace::A98Rgb:
|
||
|
|
return ColorFunctionStyleValue::create("a98-rgb"sv, c1, c2, c3, alpha);
|
||
|
|
case RectangularColorSpace::ProphotoRgb:
|
||
|
|
return ColorFunctionStyleValue::create("prophoto-rgb"sv, c1, c2, c3, alpha);
|
||
|
|
case RectangularColorSpace::Rec2020:
|
||
|
|
return ColorFunctionStyleValue::create("rec2020"sv, c1, c2, c3, alpha);
|
||
|
|
case RectangularColorSpace::Xyz:
|
||
|
|
case RectangularColorSpace::XyzD65:
|
||
|
|
return ColorFunctionStyleValue::create("xyz-d65"sv, c1, c2, c3, alpha);
|
||
|
|
case RectangularColorSpace::XyzD50:
|
||
|
|
return ColorFunctionStyleValue::create("xyz-d50"sv, c1, c2, c3, alpha);
|
||
|
|
}
|
||
|
|
VERIFY_NOT_REACHED();
|
||
|
|
}
|
||
|
|
|
||
|
|
static ValueComparingNonnullRefPtr<StyleValue const> style_value_from_polar_color_space(Gfx::ColorComponents const& components, PolarColorSpace space, MissingComponents const& missing = {})
|
||
|
|
{
|
||
|
|
auto alpha = number_or_none(components.alpha(), missing.alpha);
|
||
|
|
|
||
|
|
switch (space) {
|
||
|
|
case PolarColorSpace::Hsl: {
|
||
|
|
// HSL/HWB resolve to sRGB in computed values, so convert and express as color(srgb ...).
|
||
|
|
auto srgb = Gfx::hsl_to_srgb(components);
|
||
|
|
return ColorFunctionStyleValue::create("srgb"sv,
|
||
|
|
NumberStyleValue::create(srgb[0]),
|
||
|
|
NumberStyleValue::create(srgb[1]),
|
||
|
|
NumberStyleValue::create(srgb[2]),
|
||
|
|
alpha);
|
||
|
|
}
|
||
|
|
case PolarColorSpace::Hwb: {
|
||
|
|
auto srgb = Gfx::hwb_to_srgb(components);
|
||
|
|
return ColorFunctionStyleValue::create("srgb"sv,
|
||
|
|
NumberStyleValue::create(srgb[0]),
|
||
|
|
NumberStyleValue::create(srgb[1]),
|
||
|
|
NumberStyleValue::create(srgb[2]),
|
||
|
|
alpha);
|
||
|
|
}
|
||
|
|
case PolarColorSpace::Lch:
|
||
|
|
return LCHLikeColorStyleValue::create<LCHColorStyleValue>(
|
||
|
|
number_or_none(components[0], missing.component(0)),
|
||
|
|
number_or_none(components[1], missing.component(1)),
|
||
|
|
number_or_none(components[2], missing.component(2)),
|
||
|
|
alpha);
|
||
|
|
case PolarColorSpace::Oklch:
|
||
|
|
return LCHLikeColorStyleValue::create<OKLCHColorStyleValue>(
|
||
|
|
number_or_none(components[0], missing.component(0)),
|
||
|
|
number_or_none(components[1], missing.component(1)),
|
||
|
|
number_or_none(components[2], missing.component(2)),
|
||
|
|
alpha);
|
||
|
|
}
|
||
|
|
VERIFY_NOT_REACHED();
|
||
|
|
}
|
||
|
|
|
||
|
|
static Optional<Gfx::ColorComponents> style_value_to_color_components(StyleValue const& style_value, CalculationResolutionContext const& context)
|
||
|
|
{
|
||
|
|
if (!style_value.is_color())
|
||
|
|
return {};
|
||
|
|
|
||
|
|
auto const& color = style_value.as_color();
|
||
|
|
auto resolve_alpha = [&](StyleValue const& alpha_sv) -> Optional<float> {
|
||
|
|
auto result = ColorStyleValue::resolve_alpha(alpha_sv, context);
|
||
|
|
if (!result.has_value())
|
||
|
|
return {};
|
||
|
|
return static_cast<float>(result.value());
|
||
|
|
};
|
||
|
|
|
||
|
|
switch (color.color_type()) {
|
||
|
|
case ColorStyleValue::ColorType::HSL: {
|
||
|
|
auto const& hsl = as<HSLColorStyleValue>(color);
|
||
|
|
auto h = ColorStyleValue::resolve_hue(hsl.h(), context);
|
||
|
|
auto s = ColorStyleValue::resolve_with_reference_value(hsl.s(), 100.0f, context);
|
||
|
|
auto l = ColorStyleValue::resolve_with_reference_value(hsl.l(), 100.0f, context);
|
||
|
|
auto a = resolve_alpha(hsl.alpha());
|
||
|
|
if (!h.has_value() || !s.has_value() || !l.has_value() || !a.has_value())
|
||
|
|
return {};
|
||
|
|
// ColorConversion expects S and L as fractions (0-1), not percentages
|
||
|
|
return Gfx::ColorComponents { static_cast<float>(h.value()), static_cast<float>(s.value() / 100.0), static_cast<float>(l.value() / 100.0), a.value() };
|
||
|
|
}
|
||
|
|
case ColorStyleValue::ColorType::HWB: {
|
||
|
|
auto const& hwb = as<HWBColorStyleValue>(color);
|
||
|
|
auto h = ColorStyleValue::resolve_hue(hwb.h(), context);
|
||
|
|
auto w = ColorStyleValue::resolve_with_reference_value(hwb.w(), 100.0f, context);
|
||
|
|
auto b = ColorStyleValue::resolve_with_reference_value(hwb.b(), 100.0f, context);
|
||
|
|
auto a = resolve_alpha(hwb.alpha());
|
||
|
|
if (!h.has_value() || !w.has_value() || !b.has_value() || !a.has_value())
|
||
|
|
return {};
|
||
|
|
return Gfx::ColorComponents { static_cast<float>(h.value()), static_cast<float>(w.value() / 100.0), static_cast<float>(b.value() / 100.0), a.value() };
|
||
|
|
}
|
||
|
|
case ColorStyleValue::ColorType::Lab: {
|
||
|
|
auto const& lab = as<LabColorStyleValue>(color);
|
||
|
|
auto l = ColorStyleValue::resolve_with_reference_value(lab.l(), 100.0f, context);
|
||
|
|
auto a_comp = ColorStyleValue::resolve_with_reference_value(lab.a(), 125.0f, context);
|
||
|
|
auto b_comp = ColorStyleValue::resolve_with_reference_value(lab.b(), 125.0f, context);
|
||
|
|
auto a = resolve_alpha(lab.alpha());
|
||
|
|
if (!l.has_value() || !a_comp.has_value() || !b_comp.has_value() || !a.has_value())
|
||
|
|
return {};
|
||
|
|
return Gfx::ColorComponents { static_cast<float>(l.value()), static_cast<float>(a_comp.value()), static_cast<float>(b_comp.value()), a.value() };
|
||
|
|
}
|
||
|
|
case ColorStyleValue::ColorType::OKLab: {
|
||
|
|
auto const& oklab = as<OKLabColorStyleValue>(color);
|
||
|
|
auto l = ColorStyleValue::resolve_with_reference_value(oklab.l(), 1.0f, context);
|
||
|
|
auto a_comp = ColorStyleValue::resolve_with_reference_value(oklab.a(), 0.4f, context);
|
||
|
|
auto b_comp = ColorStyleValue::resolve_with_reference_value(oklab.b(), 0.4f, context);
|
||
|
|
auto a = resolve_alpha(oklab.alpha());
|
||
|
|
if (!l.has_value() || !a_comp.has_value() || !b_comp.has_value() || !a.has_value())
|
||
|
|
return {};
|
||
|
|
return Gfx::ColorComponents { static_cast<float>(l.value()), static_cast<float>(a_comp.value()), static_cast<float>(b_comp.value()), a.value() };
|
||
|
|
}
|
||
|
|
case ColorStyleValue::ColorType::LCH: {
|
||
|
|
auto const& lch = as<LCHColorStyleValue>(color);
|
||
|
|
auto l = ColorStyleValue::resolve_with_reference_value(lch.l(), 100.0f, context);
|
||
|
|
auto c = ColorStyleValue::resolve_with_reference_value(lch.c(), 150.0f, context);
|
||
|
|
auto h = ColorStyleValue::resolve_hue(lch.h(), context);
|
||
|
|
auto a = resolve_alpha(lch.alpha());
|
||
|
|
if (!l.has_value() || !c.has_value() || !h.has_value() || !a.has_value())
|
||
|
|
return {};
|
||
|
|
return Gfx::ColorComponents { static_cast<float>(l.value()), static_cast<float>(c.value()), static_cast<float>(h.value()), a.value() };
|
||
|
|
}
|
||
|
|
case ColorStyleValue::ColorType::OKLCH: {
|
||
|
|
auto const& oklch = as<OKLCHColorStyleValue>(color);
|
||
|
|
auto l = ColorStyleValue::resolve_with_reference_value(oklch.l(), 1.0f, context);
|
||
|
|
auto c = ColorStyleValue::resolve_with_reference_value(oklch.c(), 0.4f, context);
|
||
|
|
auto h = ColorStyleValue::resolve_hue(oklch.h(), context);
|
||
|
|
auto a = resolve_alpha(oklch.alpha());
|
||
|
|
if (!l.has_value() || !c.has_value() || !h.has_value() || !a.has_value())
|
||
|
|
return {};
|
||
|
|
return Gfx::ColorComponents { static_cast<float>(l.value()), static_cast<float>(c.value()), static_cast<float>(h.value()), a.value() };
|
||
|
|
}
|
||
|
|
case ColorStyleValue::ColorType::RGB: {
|
||
|
|
auto const& rgb = as<RGBColorStyleValue>(color);
|
||
|
|
auto r = ColorStyleValue::resolve_with_reference_value(rgb.r(), 255.0f, context);
|
||
|
|
auto g = ColorStyleValue::resolve_with_reference_value(rgb.g(), 255.0f, context);
|
||
|
|
auto b = ColorStyleValue::resolve_with_reference_value(rgb.b(), 255.0f, context);
|
||
|
|
auto a = resolve_alpha(rgb.alpha());
|
||
|
|
if (!r.has_value() || !g.has_value() || !b.has_value() || !a.has_value())
|
||
|
|
return {};
|
||
|
|
// rgb() computed values clamp channels to [0, 255] before normalizing to [0, 1].
|
||
|
|
return Gfx::ColorComponents {
|
||
|
|
static_cast<float>(clamp(r.value(), 0.0, 255.0) / 255.0),
|
||
|
|
static_cast<float>(clamp(g.value(), 0.0, 255.0) / 255.0),
|
||
|
|
static_cast<float>(clamp(b.value(), 0.0, 255.0) / 255.0),
|
||
|
|
a.value(),
|
||
|
|
};
|
||
|
|
}
|
||
|
|
default:
|
||
|
|
if (color.is_color_function()) {
|
||
|
|
auto const& func = as<ColorFunctionStyleValue>(color);
|
||
|
|
auto c1 = ColorStyleValue::resolve_with_reference_value(func.channel(0), 1.0f, context);
|
||
|
|
auto c2 = ColorStyleValue::resolve_with_reference_value(func.channel(1), 1.0f, context);
|
||
|
|
auto c3 = ColorStyleValue::resolve_with_reference_value(func.channel(2), 1.0f, context);
|
||
|
|
auto a = resolve_alpha(func.alpha());
|
||
|
|
if (!c1.has_value() || !c2.has_value() || !c3.has_value() || !a.has_value())
|
||
|
|
return {};
|
||
|
|
return Gfx::ColorComponents { static_cast<float>(c1.value()), static_cast<float>(c2.value()), static_cast<float>(c3.value()), a.value() };
|
||
|
|
}
|
||
|
|
return {};
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
static Gfx::ColorComponents native_components_to_srgb(Gfx::ColorComponents native, ColorStyleValue::ColorType source_type)
|
||
|
|
{
|
||
|
|
switch (source_type) {
|
||
|
|
case ColorStyleValue::ColorType::RGB:
|
||
|
|
case ColorStyleValue::ColorType::sRGB:
|
||
|
|
return native;
|
||
|
|
case ColorStyleValue::ColorType::sRGBLinear:
|
||
|
|
return Gfx::linear_srgb_to_srgb(native);
|
||
|
|
case ColorStyleValue::ColorType::HSL:
|
||
|
|
return Gfx::hsl_to_srgb(native);
|
||
|
|
case ColorStyleValue::ColorType::HWB:
|
||
|
|
return Gfx::hwb_to_srgb(native);
|
||
|
|
case ColorStyleValue::ColorType::Lab: {
|
||
|
|
auto xyz50 = Gfx::lab_to_xyz50(native);
|
||
|
|
auto xyz65 = Gfx::xyz50_to_xyz65(xyz50);
|
||
|
|
return Gfx::linear_srgb_to_srgb(Gfx::xyz65_to_linear_srgb(xyz65));
|
||
|
|
}
|
||
|
|
case ColorStyleValue::ColorType::OKLab: {
|
||
|
|
auto xyz65 = Gfx::oklab_to_xyz65(native);
|
||
|
|
return Gfx::linear_srgb_to_srgb(Gfx::xyz65_to_linear_srgb(xyz65));
|
||
|
|
}
|
||
|
|
case ColorStyleValue::ColorType::LCH: {
|
||
|
|
auto lab = Gfx::lch_to_lab(native);
|
||
|
|
auto xyz50 = Gfx::lab_to_xyz50(lab);
|
||
|
|
auto xyz65 = Gfx::xyz50_to_xyz65(xyz50);
|
||
|
|
return Gfx::linear_srgb_to_srgb(Gfx::xyz65_to_linear_srgb(xyz65));
|
||
|
|
}
|
||
|
|
case ColorStyleValue::ColorType::OKLCH: {
|
||
|
|
auto oklab = Gfx::oklch_to_oklab(native);
|
||
|
|
auto xyz65 = Gfx::oklab_to_xyz65(oklab);
|
||
|
|
return Gfx::linear_srgb_to_srgb(Gfx::xyz65_to_linear_srgb(xyz65));
|
||
|
|
}
|
||
|
|
case ColorStyleValue::ColorType::DisplayP3: {
|
||
|
|
auto linear_p3 = Gfx::display_p3_to_linear_display_p3(native);
|
||
|
|
auto xyz65 = Gfx::linear_display_p3_to_xyz65(linear_p3);
|
||
|
|
return Gfx::linear_srgb_to_srgb(Gfx::xyz65_to_linear_srgb(xyz65));
|
||
|
|
}
|
||
|
|
case ColorStyleValue::ColorType::DisplayP3Linear: {
|
||
|
|
auto xyz65 = Gfx::linear_display_p3_to_xyz65(native);
|
||
|
|
return Gfx::linear_srgb_to_srgb(Gfx::xyz65_to_linear_srgb(xyz65));
|
||
|
|
}
|
||
|
|
case ColorStyleValue::ColorType::A98RGB: {
|
||
|
|
auto linear_a98 = Gfx::a98_rgb_to_linear_a98_rgb(native);
|
||
|
|
auto xyz65 = Gfx::linear_a98_rgb_to_xyz65(linear_a98);
|
||
|
|
return Gfx::linear_srgb_to_srgb(Gfx::xyz65_to_linear_srgb(xyz65));
|
||
|
|
}
|
||
|
|
case ColorStyleValue::ColorType::ProPhotoRGB: {
|
||
|
|
auto linear_prophoto = Gfx::prophoto_rgb_to_linear_prophoto_rgb(native);
|
||
|
|
auto xyz50 = Gfx::linear_prophoto_rgb_to_xyz50(linear_prophoto);
|
||
|
|
auto xyz65 = Gfx::xyz50_to_xyz65(xyz50);
|
||
|
|
return Gfx::linear_srgb_to_srgb(Gfx::xyz65_to_linear_srgb(xyz65));
|
||
|
|
}
|
||
|
|
case ColorStyleValue::ColorType::Rec2020: {
|
||
|
|
auto linear_rec2020 = Gfx::rec2020_to_linear_rec2020(native);
|
||
|
|
auto xyz65 = Gfx::linear_rec2020_to_xyz65(linear_rec2020);
|
||
|
|
return Gfx::linear_srgb_to_srgb(Gfx::xyz65_to_linear_srgb(xyz65));
|
||
|
|
}
|
||
|
|
case ColorStyleValue::ColorType::XYZD50: {
|
||
|
|
auto xyz65 = Gfx::xyz50_to_xyz65(native);
|
||
|
|
return Gfx::linear_srgb_to_srgb(Gfx::xyz65_to_linear_srgb(xyz65));
|
||
|
|
}
|
||
|
|
case ColorStyleValue::ColorType::XYZD65:
|
||
|
|
return Gfx::linear_srgb_to_srgb(Gfx::xyz65_to_linear_srgb(native));
|
||
|
|
default:
|
||
|
|
VERIFY_NOT_REACHED();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
static bool color_type_matches_rectangular_space(ColorStyleValue::ColorType source_type, RectangularColorSpace space)
|
||
|
|
{
|
||
|
|
switch (space) {
|
||
|
|
case RectangularColorSpace::Srgb:
|
||
|
|
return source_type == ColorStyleValue::ColorType::sRGB || source_type == ColorStyleValue::ColorType::RGB;
|
||
|
|
case RectangularColorSpace::SrgbLinear:
|
||
|
|
return source_type == ColorStyleValue::ColorType::sRGBLinear;
|
||
|
|
case RectangularColorSpace::DisplayP3:
|
||
|
|
return source_type == ColorStyleValue::ColorType::DisplayP3;
|
||
|
|
case RectangularColorSpace::DisplayP3Linear:
|
||
|
|
return source_type == ColorStyleValue::ColorType::DisplayP3Linear;
|
||
|
|
case RectangularColorSpace::A98Rgb:
|
||
|
|
return source_type == ColorStyleValue::ColorType::A98RGB;
|
||
|
|
case RectangularColorSpace::ProphotoRgb:
|
||
|
|
return source_type == ColorStyleValue::ColorType::ProPhotoRGB;
|
||
|
|
case RectangularColorSpace::Rec2020:
|
||
|
|
return source_type == ColorStyleValue::ColorType::Rec2020;
|
||
|
|
case RectangularColorSpace::Lab:
|
||
|
|
return source_type == ColorStyleValue::ColorType::Lab;
|
||
|
|
case RectangularColorSpace::Oklab:
|
||
|
|
return source_type == ColorStyleValue::ColorType::OKLab;
|
||
|
|
case RectangularColorSpace::Xyz:
|
||
|
|
case RectangularColorSpace::XyzD65:
|
||
|
|
return source_type == ColorStyleValue::ColorType::XYZD65;
|
||
|
|
case RectangularColorSpace::XyzD50:
|
||
|
|
return source_type == ColorStyleValue::ColorType::XYZD50;
|
||
|
|
}
|
||
|
|
VERIFY_NOT_REACHED();
|
||
|
|
}
|
||
|
|
|
||
|
|
static bool color_type_matches_polar_space(ColorStyleValue::ColorType source_type, PolarColorSpace space)
|
||
|
|
{
|
||
|
|
switch (space) {
|
||
|
|
case PolarColorSpace::Hsl:
|
||
|
|
return source_type == ColorStyleValue::ColorType::HSL;
|
||
|
|
case PolarColorSpace::Hwb:
|
||
|
|
return source_type == ColorStyleValue::ColorType::HWB;
|
||
|
|
case PolarColorSpace::Lch:
|
||
|
|
return source_type == ColorStyleValue::ColorType::LCH;
|
||
|
|
case PolarColorSpace::Oklch:
|
||
|
|
return source_type == ColorStyleValue::ColorType::OKLCH;
|
||
|
|
}
|
||
|
|
VERIFY_NOT_REACHED();
|
||
|
|
}
|
||
|
|
|
||
|
|
// https://drafts.csswg.org/css-color-4/#powerless
|
||
|
|
static void mark_powerless_for_zero_alpha(bool has_native_components, float alpha, MissingComponents& missing)
|
||
|
|
{
|
||
|
|
if (has_native_components)
|
||
|
|
return;
|
||
|
|
if (!missing.alpha && alpha == 0.0f) {
|
||
|
|
missing.component(0) = true;
|
||
|
|
missing.component(1) = true;
|
||
|
|
missing.component(2) = true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// NB: Achromatic colors converted through the sRGB -> XYZ-D65 -> XYZ-D50 -> Lab -> LCH chain accumulate
|
||
|
|
// floating-point error of ~0.016 in the chroma component due to the Bradford chromatic adaptation matrices.
|
||
|
|
// This is the worst case for all color conversion types, so the threshold is large enough to account for this.
|
||
|
|
static constexpr float achromatic_threshold = 0.02f;
|
||
|
|
|
||
|
|
static void mark_powerless_hue_after_conversion(
|
||
|
|
Gfx::ColorComponents const& components, MissingComponents& interp_missing,
|
||
|
|
StyleValue const& style_value, PolarColorSpace polar_color_space,
|
||
|
|
ComponentCategories const& target_categories)
|
||
|
|
{
|
||
|
|
if (style_value.is_color() && color_type_matches_polar_space(style_value.as_color().color_type(), polar_color_space))
|
||
|
|
return;
|
||
|
|
|
||
|
|
bool has_zero_colorfulness = false;
|
||
|
|
for (size_t i = 0; i < 3; ++i) {
|
||
|
|
if (target_categories.component(i) == ComponentCategory::Colorfulness && fabsf(components[i]) < achromatic_threshold)
|
||
|
|
has_zero_colorfulness = true;
|
||
|
|
}
|
||
|
|
if (polar_color_space == PolarColorSpace::Hwb
|
||
|
|
&& components[1] + components[2] >= 1.0f - achromatic_threshold)
|
||
|
|
has_zero_colorfulness = true;
|
||
|
|
|
||
|
|
if (has_zero_colorfulness) {
|
||
|
|
for (size_t i = 0; i < 3; ++i) {
|
||
|
|
if (target_categories.component(i) == ComponentCategory::Hue)
|
||
|
|
interp_missing.component(i) = true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
static void substitute_missing_components(
|
||
|
|
Gfx::ColorComponents& from_components, Gfx::ColorComponents& to_components,
|
||
|
|
MissingComponents const& from_missing, MissingComponents const& to_missing)
|
||
|
|
{
|
||
|
|
for (size_t i = 0; i < 3; ++i) {
|
||
|
|
if (from_missing.component(i) && !to_missing.component(i))
|
||
|
|
from_components[i] = to_components[i];
|
||
|
|
else if (to_missing.component(i) && !from_missing.component(i))
|
||
|
|
to_components[i] = from_components[i];
|
||
|
|
}
|
||
|
|
|
||
|
|
if (from_missing.alpha && !to_missing.alpha)
|
||
|
|
from_components.set_alpha(to_components.alpha());
|
||
|
|
else if (to_missing.alpha && !from_missing.alpha)
|
||
|
|
to_components.set_alpha(from_components.alpha());
|
||
|
|
else if (from_missing.alpha && to_missing.alpha) {
|
||
|
|
from_components.set_alpha(1.0f);
|
||
|
|
to_components.set_alpha(1.0f);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
using ColorInterpolationMethod = ColorInterpolationMethodStyleValue::ColorInterpolationMethod;
|
||
|
|
|
||
|
|
struct PreparedInterpolationColor {
|
||
|
|
MissingComponents missing_components;
|
||
|
|
ComponentCategories source_categories;
|
||
|
|
Optional<Gfx::ColorComponents> native_components;
|
||
|
|
Optional<Gfx::ColorComponents> srgb_components;
|
||
|
|
};
|
||
|
|
|
||
|
|
static ColorSyntax color_syntax_for_interpolation(StyleValue const& style_value)
|
||
|
|
{
|
||
|
|
if (style_value.is_keyword())
|
||
|
|
return ColorSyntax::Legacy;
|
||
|
|
|
||
|
|
auto const& color = style_value.as_color();
|
||
|
|
switch (color.color_type()) {
|
||
|
|
case ColorStyleValue::ColorType::RGB:
|
||
|
|
case ColorStyleValue::ColorType::HSL:
|
||
|
|
case ColorStyleValue::ColorType::HWB:
|
||
|
|
return ColorSyntax::Legacy;
|
||
|
|
default:
|
||
|
|
return color.color_syntax();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
static ComponentCategories source_categories_for_interpolation(StyleValue const& style_value)
|
||
|
|
{
|
||
|
|
if (style_value.is_color())
|
||
|
|
return categories_for_color_type(style_value.as_color().color_type());
|
||
|
|
return { ComponentCategory::Red, ComponentCategory::Green, ComponentCategory::Blue };
|
||
|
|
}
|
||
|
|
|
||
|
|
static InterpolationPolicy resolve_interpolation_policy(
|
||
|
|
StyleValue const& from,
|
||
|
|
StyleValue const& to,
|
||
|
|
Optional<ColorInterpolationMethod> color_interpolation_method)
|
||
|
|
{
|
||
|
|
// https://drafts.csswg.org/css-color-4/#interpolation-space
|
||
|
|
// If the host syntax does not define what color space interpolation should take place in, it defaults to Oklab.
|
||
|
|
// However, user agents must handle interpolation between legacy sRGB color formats (hex colors, named colors,
|
||
|
|
// rgb(), hsl() or hwb() and the equivalent alpha-including forms) in gamma-encoded sRGB space.
|
||
|
|
auto color_syntax = ColorSyntax::Legacy;
|
||
|
|
if (color_syntax_for_interpolation(from) == ColorSyntax::Modern
|
||
|
|
|| color_syntax_for_interpolation(to) == ColorSyntax::Modern) {
|
||
|
|
color_syntax = ColorSyntax::Modern;
|
||
|
|
}
|
||
|
|
|
||
|
|
// NB: When no explicit method is provided, derive from the color syntax.
|
||
|
|
// When an explicit method IS provided (e.g. color-mix), always use modern output format.
|
||
|
|
return {
|
||
|
|
.use_legacy_output = !color_interpolation_method.has_value() && color_syntax == ColorSyntax::Legacy,
|
||
|
|
.color_interpolation_method = color_interpolation_method.value_or(
|
||
|
|
ColorInterpolationMethodStyleValue::default_color_interpolation_method(color_syntax)),
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
static PreparedInterpolationColor initialize_interpolation_color(
|
||
|
|
StyleValue const& style_value,
|
||
|
|
ColorResolutionContext const& color_resolution_context)
|
||
|
|
{
|
||
|
|
return {
|
||
|
|
.missing_components = extract_missing_components(style_value),
|
||
|
|
.source_categories = source_categories_for_interpolation(style_value),
|
||
|
|
.native_components = style_value_to_color_components(
|
||
|
|
style_value,
|
||
|
|
color_resolution_context.calculation_resolution_context),
|
||
|
|
.srgb_components = {},
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
static Optional<Gfx::ColorComponents> resolve_interpolation_color_to_srgb(
|
||
|
|
StyleValue const& style_value,
|
||
|
|
PreparedInterpolationColor& color,
|
||
|
|
ColorResolutionContext const& color_resolution_context)
|
||
|
|
{
|
||
|
|
if (color.srgb_components.has_value())
|
||
|
|
return color.srgb_components;
|
||
|
|
|
||
|
|
if (color.native_components.has_value()) {
|
||
|
|
color.srgb_components = native_components_to_srgb(
|
||
|
|
color.native_components.value(),
|
||
|
|
style_value.as_color().color_type());
|
||
|
|
return color.srgb_components;
|
||
|
|
}
|
||
|
|
|
||
|
|
auto resolved = style_value.to_color(color_resolution_context);
|
||
|
|
if (!resolved.has_value())
|
||
|
|
return {};
|
||
|
|
|
||
|
|
color.srgb_components = Gfx::color_to_srgb(resolved.value());
|
||
|
|
return color.srgb_components;
|
||
|
|
}
|
||
|
|
|
||
|
|
static Optional<float> resolve_interpolation_color_alpha(
|
||
|
|
StyleValue const& style_value,
|
||
|
|
PreparedInterpolationColor& color,
|
||
|
|
ColorResolutionContext const& color_resolution_context)
|
||
|
|
{
|
||
|
|
if (color.native_components.has_value())
|
||
|
|
return color.native_components->alpha();
|
||
|
|
|
||
|
|
auto srgb = resolve_interpolation_color_to_srgb(style_value, color, color_resolution_context);
|
||
|
|
if (!srgb.has_value())
|
||
|
|
return {};
|
||
|
|
return srgb->alpha();
|
||
|
|
}
|
||
|
|
|
||
|
|
static bool prepare_interpolation_color_for_conversion(
|
||
|
|
StyleValue const& style_value,
|
||
|
|
PreparedInterpolationColor& color,
|
||
|
|
ColorResolutionContext const& color_resolution_context)
|
||
|
|
{
|
||
|
|
auto alpha = resolve_interpolation_color_alpha(style_value, color, color_resolution_context);
|
||
|
|
if (!alpha.has_value())
|
||
|
|
return false;
|
||
|
|
|
||
|
|
// https://drafts.csswg.org/css-color-4/#powerless
|
||
|
|
// NB: When a color has zero alpha, all color components are powerless and we mark them all as missing.
|
||
|
|
// However, if the alpha is itself `none`, it resolves to 0 but is not truly zero - it will be substituted
|
||
|
|
// with the other color's alpha during interpolation.
|
||
|
|
mark_powerless_for_zero_alpha(color.native_components.has_value(), alpha.value(), color.missing_components);
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
static Gfx::ColorComponents convert_interpolation_color_to_rectangular_space(
|
||
|
|
StyleValue const& style_value,
|
||
|
|
PreparedInterpolationColor& color,
|
||
|
|
RectangularColorSpace space,
|
||
|
|
ColorResolutionContext const& color_resolution_context)
|
||
|
|
{
|
||
|
|
if (color.native_components.has_value() && style_value.is_color()
|
||
|
|
&& color_type_matches_rectangular_space(style_value.as_color().color_type(), space)) {
|
||
|
|
return color.native_components.value();
|
||
|
|
}
|
||
|
|
|
||
|
|
auto srgb = resolve_interpolation_color_to_srgb(style_value, color, color_resolution_context);
|
||
|
|
VERIFY(srgb.has_value());
|
||
|
|
return srgb_to_rectangular_color_space(srgb.value(), space);
|
||
|
|
}
|
||
|
|
|
||
|
|
static Gfx::ColorComponents convert_interpolation_color_to_polar_space(
|
||
|
|
StyleValue const& style_value,
|
||
|
|
PreparedInterpolationColor& color,
|
||
|
|
PolarColorSpace space,
|
||
|
|
ColorResolutionContext const& color_resolution_context)
|
||
|
|
{
|
||
|
|
if (color.native_components.has_value() && style_value.is_color()
|
||
|
|
&& color_type_matches_polar_space(style_value.as_color().color_type(), space)) {
|
||
|
|
return color.native_components.value();
|
||
|
|
}
|
||
|
|
|
||
|
|
auto srgb = resolve_interpolation_color_to_srgb(style_value, color, color_resolution_context);
|
||
|
|
VERIFY(srgb.has_value());
|
||
|
|
return srgb_to_polar_color_space(srgb.value(), space);
|
||
|
|
}
|
||
|
|
|
||
|
|
static size_t hue_index_for_color_space(PolarColorSpace space)
|
||
|
|
{
|
||
|
|
switch (space) {
|
||
|
|
case PolarColorSpace::Hsl:
|
||
|
|
case PolarColorSpace::Hwb:
|
||
|
|
return 0;
|
||
|
|
case PolarColorSpace::Lch:
|
||
|
|
case PolarColorSpace::Oklch:
|
||
|
|
return 2;
|
||
|
|
}
|
||
|
|
VERIFY_NOT_REACHED();
|
||
|
|
}
|
||
|
|
|
||
|
|
static InterpolationSpaceState convert_to_interpolation_space(
|
||
|
|
StyleValue const& from,
|
||
|
|
PreparedInterpolationColor& from_color,
|
||
|
|
StyleValue const& to,
|
||
|
|
PreparedInterpolationColor& to_color,
|
||
|
|
ColorInterpolationMethod const& color_interpolation_method,
|
||
|
|
ColorResolutionContext const& color_resolution_context)
|
||
|
|
{
|
||
|
|
InterpolationSpaceState state;
|
||
|
|
|
||
|
|
color_interpolation_method.visit(
|
||
|
|
[&](RectangularColorSpace space) {
|
||
|
|
state.rectangular_color_space = space;
|
||
|
|
state.from_components = convert_interpolation_color_to_rectangular_space(
|
||
|
|
from, from_color, space, color_resolution_context);
|
||
|
|
state.to_components = convert_interpolation_color_to_rectangular_space(
|
||
|
|
to, to_color, space, color_resolution_context);
|
||
|
|
|
||
|
|
auto target_categories = categories_for_rectangular_space(space);
|
||
|
|
state.from_missing = carry_forward_missing_components(
|
||
|
|
from_color.missing_components, from_color.source_categories, target_categories);
|
||
|
|
state.to_missing = carry_forward_missing_components(
|
||
|
|
to_color.missing_components, to_color.source_categories, target_categories);
|
||
|
|
},
|
||
|
|
[&](ColorInterpolationMethodStyleValue::PolarColorInterpolationMethod const& polar_color_interpolation_method) {
|
||
|
|
state.is_polar = true;
|
||
|
|
state.polar_color_space = polar_color_interpolation_method.color_space;
|
||
|
|
state.hue_interpolation_method = polar_color_interpolation_method.hue_interpolation_method;
|
||
|
|
state.hue_index = hue_index_for_color_space(polar_color_interpolation_method.color_space);
|
||
|
|
state.from_components = convert_interpolation_color_to_polar_space(
|
||
|
|
from, from_color, polar_color_interpolation_method.color_space, color_resolution_context);
|
||
|
|
state.to_components = convert_interpolation_color_to_polar_space(
|
||
|
|
to, to_color, polar_color_interpolation_method.color_space, color_resolution_context);
|
||
|
|
|
||
|
|
auto target_categories = categories_for_polar_space(polar_color_interpolation_method.color_space);
|
||
|
|
state.from_missing = carry_forward_missing_components(
|
||
|
|
from_color.missing_components, from_color.source_categories, target_categories);
|
||
|
|
state.to_missing = carry_forward_missing_components(
|
||
|
|
to_color.missing_components, to_color.source_categories, target_categories);
|
||
|
|
state.polar_target_categories = target_categories;
|
||
|
|
});
|
||
|
|
|
||
|
|
if (state.is_polar) {
|
||
|
|
mark_powerless_hue_after_conversion(
|
||
|
|
state.from_components, state.from_missing, from, state.polar_color_space, state.polar_target_categories);
|
||
|
|
mark_powerless_hue_after_conversion(
|
||
|
|
state.to_components, state.to_missing, to, state.polar_color_space, state.polar_target_categories);
|
||
|
|
}
|
||
|
|
|
||
|
|
return state;
|
||
|
|
}
|
||
|
|
|
||
|
|
static bool reinsert_carried_forward_values(InterpolationSpaceState& state)
|
||
|
|
{
|
||
|
|
bool both_alpha_missing = state.from_missing.alpha && state.to_missing.alpha;
|
||
|
|
substitute_missing_components(state.from_components, state.to_components, state.from_missing, state.to_missing);
|
||
|
|
return both_alpha_missing;
|
||
|
|
}
|
||
|
|
|
||
|
|
static void fixup_hues_if_required(InterpolationSpaceState& state)
|
||
|
|
{
|
||
|
|
if (!state.is_polar)
|
||
|
|
return;
|
||
|
|
fixup_hue(state.from_components[state.hue_index], state.to_components[state.hue_index], state.hue_interpolation_method);
|
||
|
|
}
|
||
|
|
|
||
|
|
static Gfx::ColorComponents premultiply_color_components(
|
||
|
|
Gfx::ColorComponents const& components,
|
||
|
|
bool is_polar,
|
||
|
|
size_t hue_index)
|
||
|
|
{
|
||
|
|
Gfx::ColorComponents premultiplied;
|
||
|
|
premultiplied.set_alpha(components.alpha());
|
||
|
|
|
||
|
|
for (size_t i = 0; i < 3; ++i) {
|
||
|
|
if (is_polar && i == hue_index)
|
||
|
|
premultiplied[i] = components[i];
|
||
|
|
else
|
||
|
|
premultiplied[i] = components[i] * components.alpha();
|
||
|
|
}
|
||
|
|
|
||
|
|
return premultiplied;
|
||
|
|
}
|
||
|
|
|
||
|
|
static Gfx::ColorComponents interpolate_premultiplied_components(
|
||
|
|
Gfx::ColorComponents const& from,
|
||
|
|
Gfx::ColorComponents const& to,
|
||
|
|
float delta)
|
||
|
|
{
|
||
|
|
Gfx::ColorComponents interpolated;
|
||
|
|
for (size_t i = 0; i < 3; ++i)
|
||
|
|
interpolated[i] = interpolate_color_component(from[i], to[i], delta);
|
||
|
|
return interpolated;
|
||
|
|
}
|
||
|
|
|
||
|
|
static Gfx::ColorComponents unpremultiply_color_components(
|
||
|
|
Gfx::ColorComponents const& premultiplied,
|
||
|
|
float interpolated_alpha,
|
||
|
|
bool is_polar,
|
||
|
|
size_t hue_index)
|
||
|
|
{
|
||
|
|
Gfx::ColorComponents result;
|
||
|
|
result.set_alpha(interpolated_alpha);
|
||
|
|
|
||
|
|
for (size_t i = 0; i < 3; ++i) {
|
||
|
|
bool was_premultiplied = !is_polar || i != hue_index;
|
||
|
|
result[i] = was_premultiplied ? premultiplied[i] / interpolated_alpha : premultiplied[i];
|
||
|
|
}
|
||
|
|
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
|
||
|
|
static MissingComponents result_missing_components(InterpolationSpaceState const& state)
|
||
|
|
{
|
||
|
|
// https://drafts.csswg.org/css-color-4/#interpolation-missing
|
||
|
|
// NB: If both input colors have a component as missing, the result also has that component as missing.
|
||
|
|
return {
|
||
|
|
state.from_missing.component(0) && state.to_missing.component(0),
|
||
|
|
state.from_missing.component(1) && state.to_missing.component(1),
|
||
|
|
state.from_missing.component(2) && state.to_missing.component(2),
|
||
|
|
state.from_missing.alpha && state.to_missing.alpha,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
RefPtr<StyleValue const> style_value_for_interpolated_color(InterpolatedColor const& interpolated)
|
||
|
|
{
|
||
|
|
// https://drafts.csswg.org/css-color-4/#interpolation-space
|
||
|
|
// NB: Legacy sRGB content interpolates in sRGB and produces a legacy rgb() result.
|
||
|
|
if (interpolated.policy.use_legacy_output)
|
||
|
|
return ColorStyleValue::create_from_color(Gfx::srgb_to_color(interpolated.components), ColorSyntax::Legacy);
|
||
|
|
|
||
|
|
// NB: Return as a StyleValue in the interpolation color space.
|
||
|
|
if (interpolated.state.is_polar)
|
||
|
|
return style_value_from_polar_color_space(interpolated.components, interpolated.state.polar_color_space, interpolated.missing);
|
||
|
|
return style_value_from_rectangular_color_space(interpolated.components, interpolated.state.rectangular_color_space, interpolated.missing);
|
||
|
|
}
|
||
|
|
|
||
|
|
// https://drafts.csswg.org/css-color-4/#interpolation
|
||
|
|
Optional<InterpolatedColor> perform_color_interpolation(
|
||
|
|
StyleValue const& from, StyleValue const& to, float delta,
|
||
|
|
Optional<ColorInterpolationMethod> color_interpolation_method,
|
||
|
|
ColorResolutionContext const& color_resolution_context)
|
||
|
|
{
|
||
|
|
// 1. checking the two colors for analogous components and analogous sets which will be carried forward
|
||
|
|
auto from_color = initialize_interpolation_color(from, color_resolution_context);
|
||
|
|
auto to_color = initialize_interpolation_color(to, color_resolution_context);
|
||
|
|
|
||
|
|
// 2. prepare both colors for conversion. this changes any powerless components to missing values
|
||
|
|
if (!prepare_interpolation_color_for_conversion(from, from_color, color_resolution_context)
|
||
|
|
|| !prepare_interpolation_color_for_conversion(to, to_color, color_resolution_context)) {
|
||
|
|
return {};
|
||
|
|
}
|
||
|
|
|
||
|
|
// 3. converting them both to a given color space which will be referred to as the interpolation color space
|
||
|
|
// below.
|
||
|
|
auto interpolation_policy = resolve_interpolation_policy(from, to, color_interpolation_method);
|
||
|
|
auto state = convert_to_interpolation_space(
|
||
|
|
from, from_color, to, to_color, interpolation_policy.color_interpolation_method, color_resolution_context);
|
||
|
|
|
||
|
|
// 4. (if required) re-inserting carried forward values in the converted colors
|
||
|
|
auto both_alpha_missing = reinsert_carried_forward_values(state);
|
||
|
|
|
||
|
|
// 5. (if required) fixing up the hues, depending on the selected <hue-interpolation-method>
|
||
|
|
fixup_hues_if_required(state);
|
||
|
|
|
||
|
|
auto interpolated_alpha = interpolate_color_component(state.from_components.alpha(), state.to_components.alpha(), delta);
|
||
|
|
auto clamped_alpha = clamp(interpolated_alpha, 0.0f, 1.0f);
|
||
|
|
if (clamped_alpha == 0.0f && !both_alpha_missing) {
|
||
|
|
// OPTIMIZATION: Fully transparent results can skip the premultiply/interpolate/unpremultiply cycle.
|
||
|
|
Gfx::ColorComponents zero_result { 0.0f, 0.0f, 0.0f, 0.0f };
|
||
|
|
return InterpolatedColor { zero_result, {}, interpolation_policy, move(state) };
|
||
|
|
}
|
||
|
|
|
||
|
|
// 6. changing the color components to premultiplied form
|
||
|
|
// https://drafts.csswg.org/css-color-4/#interpolation-alpha
|
||
|
|
// For rectangular orthogonal color coordinate systems, all component values are multiplied by the alpha value.
|
||
|
|
// For cylindrical polar color coordinate systems, the hue angle is NOT premultiplied.
|
||
|
|
auto from_premultiplied = premultiply_color_components(state.from_components, state.is_polar, state.hue_index);
|
||
|
|
auto to_premultiplied = premultiply_color_components(state.to_components, state.is_polar, state.hue_index);
|
||
|
|
|
||
|
|
// 7. linearly interpolating each component of the computed value of the color separately
|
||
|
|
auto interpolated_components = interpolate_premultiplied_components(from_premultiplied, to_premultiplied, delta);
|
||
|
|
|
||
|
|
// 8. undoing premultiplication
|
||
|
|
auto result = unpremultiply_color_components(interpolated_components, clamped_alpha, state.is_polar, state.hue_index);
|
||
|
|
|
||
|
|
auto missing = result_missing_components(state);
|
||
|
|
return InterpolatedColor { result, missing, interpolation_policy, move(state) };
|
||
|
|
}
|
||
|
|
|
||
|
|
// https://drafts.csswg.org/css-color-4/#interpolation
|
||
|
|
RefPtr<StyleValue const> interpolate_color(
|
||
|
|
StyleValue const& from, StyleValue const& to, float delta,
|
||
|
|
Optional<ColorInterpolationMethod> color_interpolation_method,
|
||
|
|
ColorResolutionContext const& color_resolution_context)
|
||
|
|
{
|
||
|
|
auto interpolated = perform_color_interpolation(from, to, delta, color_interpolation_method, color_resolution_context);
|
||
|
|
if (!interpolated.has_value())
|
||
|
|
return {};
|
||
|
|
return style_value_for_interpolated_color(*interpolated);
|
||
|
|
}
|
||
|
|
|
||
|
|
}
|