mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-09-12 04:22:28 +00:00
LibWeb: Implement the color-mix()
function
This takes 2 color values and returns the result of mixing them by a given amount.
This commit is contained in:
parent
a14711a9d7
commit
9cf04f40f6
Notes:
github-actions[bot]
2025-04-22 10:45:32 +00:00
Author: https://github.com/tcl3
Commit: 9cf04f40f6
Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/4419
18 changed files with 1946 additions and 0 deletions
|
@ -45,6 +45,7 @@ public:
|
|||
XYZD50,
|
||||
XYZD65,
|
||||
LightDark, // This is used by CSSLightDark for light-dark(..., ...).
|
||||
ColorMix,
|
||||
};
|
||||
ColorType color_type() const { return m_color_type; }
|
||||
ColorSyntax color_syntax() const { return m_color_syntax; }
|
||||
|
|
189
Libraries/LibWeb/CSS/StyleValues/ColorMixStyleValue.cpp
Normal file
189
Libraries/LibWeb/CSS/StyleValues/ColorMixStyleValue.cpp
Normal file
|
@ -0,0 +1,189 @@
|
|||
/*
|
||||
* Copyright (c) 2025, Tim Ledbetter <tim.ledbetter@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include "ColorMixStyleValue.h"
|
||||
#include <AK/TypeCasts.h>
|
||||
#include <LibWeb/CSS/Interpolation.h>
|
||||
#include <LibWeb/Layout/Node.h>
|
||||
|
||||
namespace Web::CSS {
|
||||
|
||||
ValueComparingNonnullRefPtr<ColorMixStyleValue const> ColorMixStyleValue::create(ColorInterpolationMethod interpolation_method, ColorMixComponent first_component, ColorMixComponent second_component)
|
||||
{
|
||||
return adopt_ref(*new (nothrow) ColorMixStyleValue(move(interpolation_method), move(first_component), move(second_component)));
|
||||
}
|
||||
|
||||
ColorMixStyleValue::ColorMixStyleValue(ColorInterpolationMethod color_interpolation_method, ColorMixComponent first_component, ColorMixComponent second_component)
|
||||
: CSSColorValue(ColorType::ColorMix, ColorSyntax::Modern)
|
||||
, m_properties {
|
||||
.color_interpolation_method = move(color_interpolation_method),
|
||||
.first_component = move(first_component),
|
||||
.second_component = move(second_component)
|
||||
}
|
||||
{
|
||||
}
|
||||
|
||||
bool ColorMixStyleValue::equals(CSSStyleValue const& other) const
|
||||
{
|
||||
if (type() != other.type())
|
||||
return false;
|
||||
auto const& other_color = other.as_color();
|
||||
if (color_type() != other_color.color_type())
|
||||
return false;
|
||||
auto const& other_color_mix = as<ColorMixStyleValue>(other_color);
|
||||
return m_properties == other_color_mix.m_properties;
|
||||
}
|
||||
|
||||
// https://drafts.csswg.org/css-color-5/#serial-color-mix
|
||||
String ColorMixStyleValue::to_string(SerializationMode mode) const
|
||||
{
|
||||
auto serialize_first_percentage = [](StringBuilder& builder, Optional<PercentageOrCalculated> const& p1, Optional<PercentageOrCalculated> const& p2) {
|
||||
// if BOTH the first percentage p1 and second percentage p2 are specified:
|
||||
if (p1.has_value() && p2.has_value()) {
|
||||
// If both p1 equals 50% and p2 equals 50%, nothing is serialized.
|
||||
if (!p1->is_calculated() && !p2->is_calculated() && p1->value().value() == 50 && p2->value().value() == 50)
|
||||
return;
|
||||
|
||||
// else, p1 is serialized as is.
|
||||
builder.appendff(" {}", p1->to_string());
|
||||
}
|
||||
// else if ONLY the first percentage p1 is specified:
|
||||
else if (p1.has_value()) {
|
||||
// If p1 is equal to 50%, nothing is serialized.
|
||||
if (!p1->is_calculated() && p1->value().value() == 50)
|
||||
return;
|
||||
|
||||
// else, p1 is serialized as is.
|
||||
builder.appendff(" {}", p1->to_string());
|
||||
}
|
||||
// else if ONLY the second percentage p2 is specified:
|
||||
else if (p2.has_value()) {
|
||||
// if p2 equals 50%, nothing is serialized.
|
||||
if (!p2->is_calculated() && p2->value().value() == 50)
|
||||
return;
|
||||
|
||||
// if p2 is not calc(), the value of 100% - p2 is serialized.
|
||||
if (!p2->is_calculated())
|
||||
builder.appendff(" {}%", 100 - p2->value().value());
|
||||
|
||||
// else, nothing is serialized.
|
||||
}
|
||||
// else if NEITHER is specified:
|
||||
else {
|
||||
// nothing is serialized.
|
||||
}
|
||||
};
|
||||
|
||||
auto serialize_second_percentage = [](StringBuilder& builder, Optional<PercentageOrCalculated> const& p1, Optional<PercentageOrCalculated> const& p2) {
|
||||
// If BOTH the first percentage p1 and second percentages p2 are specified:
|
||||
if (p1.has_value() && p2.has_value()) {
|
||||
// if neither p1 nor p2 is calc(), and p1 + p2 equals 100%, nothing is serialized.
|
||||
if (!p1->is_calculated() && !p2->is_calculated() && p1->value().value() + p2->value().value() == 100)
|
||||
return;
|
||||
|
||||
// else, p2 is serialized as is.
|
||||
builder.appendff(" {}", p2->to_string());
|
||||
}
|
||||
// else if ONLY the first percentage p1 is specified:
|
||||
else if (p1.has_value()) {
|
||||
// nothing is serialized.
|
||||
}
|
||||
// else if ONLY the second percentage p2 is specified:
|
||||
else if (p2.has_value()) {
|
||||
// if p2 equals 50%, nothing is serialized.
|
||||
if (!p2->is_calculated() && p2->value().value() == 50)
|
||||
return;
|
||||
|
||||
// if p2 is not calc(), nothing is serialized.
|
||||
if (!p2->is_calculated())
|
||||
return;
|
||||
|
||||
// else, p2 is serialized as is.
|
||||
builder.appendff(" {}", p2->to_string());
|
||||
}
|
||||
// else if NEITHER is specified:
|
||||
else {
|
||||
// nothing is serialized.
|
||||
}
|
||||
};
|
||||
|
||||
StringBuilder builder;
|
||||
builder.appendff("color-mix(in {}", m_properties.color_interpolation_method.color_space);
|
||||
if (m_properties.color_interpolation_method.hue_interpolation_method.value_or(HueInterpolationMethod::Shorter) != HueInterpolationMethod::Shorter)
|
||||
builder.appendff(" {} hue", CSS::to_string(*m_properties.color_interpolation_method.hue_interpolation_method));
|
||||
builder.append(", "sv);
|
||||
builder.append(m_properties.first_component.color->to_string(mode));
|
||||
serialize_first_percentage(builder, m_properties.first_component.percentage, m_properties.second_component.percentage);
|
||||
builder.appendff(", {}", m_properties.second_component.color->to_string(mode));
|
||||
serialize_second_percentage(builder, m_properties.first_component.percentage, m_properties.second_component.percentage);
|
||||
builder.append(')');
|
||||
return MUST(builder.to_string());
|
||||
}
|
||||
|
||||
// https://drafts.csswg.org/css-color-5/#color-mix-percent-norm
|
||||
ColorMixStyleValue::PercentageNormalizationResult ColorMixStyleValue::normalize_percentages() const
|
||||
{
|
||||
auto resolve_percentage = [&](Optional<PercentageOrCalculated> const& percentage_or_calculated) -> Optional<Percentage> {
|
||||
if (!percentage_or_calculated.has_value())
|
||||
return {};
|
||||
if (!percentage_or_calculated->is_calculated())
|
||||
return percentage_or_calculated->value();
|
||||
return percentage_or_calculated->resolved({});
|
||||
};
|
||||
|
||||
// 1. Let p1 be the first percentage and p2 the second one.
|
||||
auto p1 = resolve_percentage(m_properties.first_component.percentage);
|
||||
auto p2 = resolve_percentage(m_properties.second_component.percentage);
|
||||
double alpha_multiplier = 0;
|
||||
|
||||
// 2. If both percentages are omitted, they each default to 50% (an equal mix of the two colors).
|
||||
if (!p1.has_value() && !p2.has_value()) {
|
||||
p1 = Percentage(50);
|
||||
p2 = Percentage(50);
|
||||
}
|
||||
// 3. Otherwise, if p2 is omitted, it becomes 100% - p1
|
||||
else if (!p2.has_value()) {
|
||||
p2 = Percentage(100 - p1->value());
|
||||
}
|
||||
// 4. Otherwise, if p1 is omitted, it becomes 100% - p2
|
||||
else if (!p1.has_value()) {
|
||||
p1 = Percentage(100 - p2->value());
|
||||
}
|
||||
// 5. Otherwise, if both are provided and add up to greater than 100%, they are scaled accordingly so that they add up to 100%.
|
||||
else if (p1->value() + p2->value() > 100) {
|
||||
auto sum = p1->value() + p2->value();
|
||||
p1 = Percentage((p1->value() / sum) * 100);
|
||||
p2 = Percentage((p2->value() / sum) * 100);
|
||||
}
|
||||
// 6. Otherwise, if both are provided and add up to less than 100%, the sum is saved as an alpha multiplier. If the sum is greater than zero, they are then scaled accordingly so that they add up to 100%.
|
||||
else if (p1->value() + p2->value() < 100) {
|
||||
auto sum = p1->value() + p2->value();
|
||||
alpha_multiplier = sum / 100;
|
||||
if (sum > 0) {
|
||||
p1 = Percentage((p1->value() / sum) * 100);
|
||||
p2 = Percentage((p2->value() / sum) * 100);
|
||||
}
|
||||
}
|
||||
|
||||
VERIFY(p1.has_value());
|
||||
VERIFY(p2.has_value());
|
||||
|
||||
return PercentageNormalizationResult { .p1 = *p1, .p2 = *p2, .alpha_multiplier = alpha_multiplier };
|
||||
}
|
||||
|
||||
// https://drafts.csswg.org/css-color-5/#color-mix-result
|
||||
Color ColorMixStyleValue::to_color(Optional<Layout::NodeWithStyle const&> node) const
|
||||
{
|
||||
// FIXME: Do this in a spec-compliant way.
|
||||
// Our color interpolation doesn't currently take the color space or hue interpolation method into account.
|
||||
auto normalized_percentages = normalize_percentages();
|
||||
auto from_color = m_properties.first_component.color->to_color(node);
|
||||
auto to_color = m_properties.second_component.color->to_color(node);
|
||||
auto delta = normalized_percentages.p2.value() / 100;
|
||||
return interpolate_color(from_color, to_color, delta);
|
||||
}
|
||||
|
||||
}
|
56
Libraries/LibWeb/CSS/StyleValues/ColorMixStyleValue.h
Normal file
56
Libraries/LibWeb/CSS/StyleValues/ColorMixStyleValue.h
Normal file
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* Copyright (c) 2025, Tim Ledbetter <tim.ledbetter@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <LibWeb/CSS/CalculatedOr.h>
|
||||
#include <LibWeb/CSS/StyleValues/CSSColorValue.h>
|
||||
|
||||
namespace Web::CSS {
|
||||
|
||||
class ColorMixStyleValue final : public CSSColorValue {
|
||||
public:
|
||||
virtual ~ColorMixStyleValue() override = default;
|
||||
|
||||
struct ColorInterpolationMethod {
|
||||
String color_space;
|
||||
Optional<HueInterpolationMethod> hue_interpolation_method;
|
||||
bool operator==(ColorInterpolationMethod const&) const = default;
|
||||
};
|
||||
|
||||
struct ColorMixComponent {
|
||||
ValueComparingNonnullRefPtr<CSSStyleValue const> color;
|
||||
Optional<PercentageOrCalculated> percentage;
|
||||
bool operator==(ColorMixComponent const&) const = default;
|
||||
};
|
||||
|
||||
static ValueComparingNonnullRefPtr<ColorMixStyleValue const> create(ColorInterpolationMethod, ColorMixComponent first_component, ColorMixComponent second_component);
|
||||
|
||||
virtual bool equals(CSSStyleValue const&) const override;
|
||||
virtual Color to_color(Optional<Layout::NodeWithStyle const&>) const override;
|
||||
virtual String to_string(SerializationMode) const override;
|
||||
|
||||
private:
|
||||
struct Properties {
|
||||
ColorInterpolationMethod color_interpolation_method;
|
||||
ColorMixComponent first_component;
|
||||
ColorMixComponent second_component;
|
||||
bool operator==(Properties const&) const = default;
|
||||
};
|
||||
|
||||
ColorMixStyleValue(ColorInterpolationMethod, ColorMixComponent first_component, ColorMixComponent second_component);
|
||||
|
||||
struct PercentageNormalizationResult {
|
||||
Percentage p1;
|
||||
Percentage p2;
|
||||
double alpha_multiplier;
|
||||
};
|
||||
PercentageNormalizationResult normalize_percentages() const;
|
||||
|
||||
Properties m_properties;
|
||||
};
|
||||
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue