LibWeb: Properly clamp interpolated opacity values

Opacity values are unique in that the range which calculated and
interpolated values should be clamped to [0,1] is different from the
range of allowed values [-∞,∞].

This fixes 28 WPT tests that were regressed in #6112
This commit is contained in:
Callum Law 2025-09-09 22:03:23 +12:00 committed by Sam Atkins
parent 517e3f5f1d
commit 43dd0f2dda
4 changed files with 251 additions and 21 deletions

View File

@ -3514,7 +3514,7 @@ NonnullRefPtr<StyleValue const> StyleComputer::compute_opacity(NonnullRefPtr<Sty
// NOTE: We also support calc()'d numbers
if (specified_value->is_calculated() && specified_value->as_calculated().resolves_to_number())
return NumberStyleValue::create(clamp(specified_value->as_calculated().resolve_number({ .length_resolution_context = computation_context.length_resolution_context }).value(), 0, 1));
return NumberStyleValue::create(specified_value->as_calculated().resolve_number({ .length_resolution_context = computation_context.length_resolution_context }).value());
// <percentage>
if (specified_value->is_percentage())
@ -3522,7 +3522,7 @@ NonnullRefPtr<StyleValue const> StyleComputer::compute_opacity(NonnullRefPtr<Sty
// NOTE: We also support calc()'d percentages
if (specified_value->is_calculated() && specified_value->as_calculated().resolves_to_percentage())
return NumberStyleValue::create(clamp(specified_value->as_calculated().resolve_percentage({ .length_resolution_context = computation_context.length_resolution_context })->as_fraction(), 0, 1));
return NumberStyleValue::create(specified_value->as_calculated().resolve_percentage({ .length_resolution_context = computation_context.length_resolution_context })->as_fraction());
VERIFY_NOT_REACHED();
}

View File

@ -891,35 +891,44 @@ AcceptedTypeRangeMap property_accepted_type_ranges(PropertyID property_id)
StringBuilder ranges_builder;
for (auto& type : valid_types.values()) {
VERIFY(type.is_string());
// Opacity values are unique in that the range which calculated and interpolated values should be clamped
// to [0,1] is different from the range of allowed values [-∞,∞]. To handle this we set the allowed range
// in Properties.json to [-∞,∞] but overwrite it to [0,1] here.
// FIXME: This is confusing as property_accepts_{number,percentage}() has a different range from this
// despite the names sounding similar.
if (first_is_one_of(name, "opacity"sv, "fill-opacity"sv, "flood-opacity"sv, "stop-opacity"sv, "stroke-opacity"sv)) {
ranges_builder.append("{ ValueType::Number, { 0, 1 } }, { ValueType::Percentage, { 0, 100 } }"sv);
} else {
for (auto& type : valid_types.values()) {
VERIFY(type.is_string());
Vector<String> type_parts = MUST(type.as_string().split(' '));
Vector<String> type_parts = MUST(type.as_string().split(' '));
if (type_parts.size() < 2)
continue;
if (type_parts.size() < 2)
continue;
auto type_name = type_parts.first();
auto type_name = type_parts.first();
if (type_name == "custom-ident")
continue;
if (type_name == "custom-ident")
continue;
// Drop the brackets on the range e.g. "[-∞,∞]" -> "-∞,∞"
auto type_range = MUST(type_parts.get(1)->substring_from_byte_offset(1, type_parts.get(1)->byte_count() - 2));
// Drop the brackets on the range e.g. "[-∞,∞]" -> "-∞,∞"
auto type_range = MUST(type_parts.get(1)->substring_from_byte_offset(1, type_parts.get(1)->byte_count() - 2));
auto limits = MUST(type_range.split(','));
auto limits = MUST(type_range.split(','));
if (limits.size() != 2)
VERIFY_NOT_REACHED();
if (limits.size() != 2)
VERIFY_NOT_REACHED();
// FIXME: Use min and max values for i32 instead of float where applicable (e.g. for "integer")
auto min = limits.get(0) == "-∞" ? "AK::NumericLimits<float>::lowest()"_string : *limits.get(0);
auto max = limits.get(1) == "" ? "AK::NumericLimits<float>::max()"_string : *limits.get(1);
// FIXME: Use min and max values for i32 instead of float where applicable (e.g. for "integer")
auto min = limits.get(0) == "-∞" ? "AK::NumericLimits<float>::lowest()"_string : *limits.get(0);
auto max = limits.get(1) == "" ? "AK::NumericLimits<float>::max()"_string : *limits.get(1);
if (!ranges_builder.is_empty())
ranges_builder.appendff(", ");
if (!ranges_builder.is_empty())
ranges_builder.appendff(", ");
ranges_builder.appendff("{{ ValueType::{}, {{ {}, {} }} }}", title_casify(type_name), min, max);
ranges_builder.appendff("{{ ValueType::{}, {{ {}, {} }} }}", title_casify(type_name), min, max);
}
}
property_generator.set("ranges", ranges_builder.to_string_without_validation());

View File

@ -0,0 +1,125 @@
Harness status: OK
Found 120 tests
120 Pass
Pass CSS Transitions: property <opacity> from neutral to [0.2] at (-0.3) should be [0.07]
Pass CSS Transitions: property <opacity> from neutral to [0.2] at (0) should be [0.1]
Pass CSS Transitions: property <opacity> from neutral to [0.2] at (0.3) should be [0.13]
Pass CSS Transitions: property <opacity> from neutral to [0.2] at (0.6) should be [0.16]
Pass CSS Transitions: property <opacity> from neutral to [0.2] at (1) should be [0.2]
Pass CSS Transitions: property <opacity> from neutral to [0.2] at (1.5) should be [0.25]
Pass CSS Transitions with transition: all: property <opacity> from neutral to [0.2] at (-0.3) should be [0.07]
Pass CSS Transitions with transition: all: property <opacity> from neutral to [0.2] at (0) should be [0.1]
Pass CSS Transitions with transition: all: property <opacity> from neutral to [0.2] at (0.3) should be [0.13]
Pass CSS Transitions with transition: all: property <opacity> from neutral to [0.2] at (0.6) should be [0.16]
Pass CSS Transitions with transition: all: property <opacity> from neutral to [0.2] at (1) should be [0.2]
Pass CSS Transitions with transition: all: property <opacity> from neutral to [0.2] at (1.5) should be [0.25]
Pass CSS Animations: property <opacity> from neutral to [0.2] at (-0.3) should be [0.07]
Pass CSS Animations: property <opacity> from neutral to [0.2] at (0) should be [0.1]
Pass CSS Animations: property <opacity> from neutral to [0.2] at (0.3) should be [0.13]
Pass CSS Animations: property <opacity> from neutral to [0.2] at (0.6) should be [0.16]
Pass CSS Animations: property <opacity> from neutral to [0.2] at (1) should be [0.2]
Pass CSS Animations: property <opacity> from neutral to [0.2] at (1.5) should be [0.25]
Pass Web Animations: property <opacity> from neutral to [0.2] at (-0.3) should be [0.07]
Pass Web Animations: property <opacity> from neutral to [0.2] at (0) should be [0.1]
Pass Web Animations: property <opacity> from neutral to [0.2] at (0.3) should be [0.13]
Pass Web Animations: property <opacity> from neutral to [0.2] at (0.6) should be [0.16]
Pass Web Animations: property <opacity> from neutral to [0.2] at (1) should be [0.2]
Pass Web Animations: property <opacity> from neutral to [0.2] at (1.5) should be [0.25]
Pass CSS Transitions: property <opacity> from [initial] to [0.2] at (-0.3) should be [1]
Pass CSS Transitions: property <opacity> from [initial] to [0.2] at (0) should be [1]
Pass CSS Transitions: property <opacity> from [initial] to [0.2] at (0.3) should be [0.76]
Pass CSS Transitions: property <opacity> from [initial] to [0.2] at (0.6) should be [0.52]
Pass CSS Transitions: property <opacity> from [initial] to [0.2] at (1) should be [0.2]
Pass CSS Transitions: property <opacity> from [initial] to [0.2] at (1.5) should be [0]
Pass CSS Transitions with transition: all: property <opacity> from [initial] to [0.2] at (-0.3) should be [1]
Pass CSS Transitions with transition: all: property <opacity> from [initial] to [0.2] at (0) should be [1]
Pass CSS Transitions with transition: all: property <opacity> from [initial] to [0.2] at (0.3) should be [0.76]
Pass CSS Transitions with transition: all: property <opacity> from [initial] to [0.2] at (0.6) should be [0.52]
Pass CSS Transitions with transition: all: property <opacity> from [initial] to [0.2] at (1) should be [0.2]
Pass CSS Transitions with transition: all: property <opacity> from [initial] to [0.2] at (1.5) should be [0]
Pass CSS Animations: property <opacity> from [initial] to [0.2] at (-0.3) should be [1]
Pass CSS Animations: property <opacity> from [initial] to [0.2] at (0) should be [1]
Pass CSS Animations: property <opacity> from [initial] to [0.2] at (0.3) should be [0.76]
Pass CSS Animations: property <opacity> from [initial] to [0.2] at (0.6) should be [0.52]
Pass CSS Animations: property <opacity> from [initial] to [0.2] at (1) should be [0.2]
Pass CSS Animations: property <opacity> from [initial] to [0.2] at (1.5) should be [0]
Pass Web Animations: property <opacity> from [initial] to [0.2] at (-0.3) should be [1]
Pass Web Animations: property <opacity> from [initial] to [0.2] at (0) should be [1]
Pass Web Animations: property <opacity> from [initial] to [0.2] at (0.3) should be [0.76]
Pass Web Animations: property <opacity> from [initial] to [0.2] at (0.6) should be [0.52]
Pass Web Animations: property <opacity> from [initial] to [0.2] at (1) should be [0.2]
Pass Web Animations: property <opacity> from [initial] to [0.2] at (1.5) should be [0]
Pass CSS Transitions: property <opacity> from [inherit] to [0.2] at (-0.3) should be [0.98]
Pass CSS Transitions: property <opacity> from [inherit] to [0.2] at (0) should be [0.8]
Pass CSS Transitions: property <opacity> from [inherit] to [0.2] at (0.3) should be [0.62]
Pass CSS Transitions: property <opacity> from [inherit] to [0.2] at (0.6) should be [0.44]
Pass CSS Transitions: property <opacity> from [inherit] to [0.2] at (1) should be [0.2]
Pass CSS Transitions: property <opacity> from [inherit] to [0.2] at (1.5) should be [0]
Pass CSS Transitions with transition: all: property <opacity> from [inherit] to [0.2] at (-0.3) should be [0.98]
Pass CSS Transitions with transition: all: property <opacity> from [inherit] to [0.2] at (0) should be [0.8]
Pass CSS Transitions with transition: all: property <opacity> from [inherit] to [0.2] at (0.3) should be [0.62]
Pass CSS Transitions with transition: all: property <opacity> from [inherit] to [0.2] at (0.6) should be [0.44]
Pass CSS Transitions with transition: all: property <opacity> from [inherit] to [0.2] at (1) should be [0.2]
Pass CSS Transitions with transition: all: property <opacity> from [inherit] to [0.2] at (1.5) should be [0]
Pass CSS Animations: property <opacity> from [inherit] to [0.2] at (-0.3) should be [0.98]
Pass CSS Animations: property <opacity> from [inherit] to [0.2] at (0) should be [0.8]
Pass CSS Animations: property <opacity> from [inherit] to [0.2] at (0.3) should be [0.62]
Pass CSS Animations: property <opacity> from [inherit] to [0.2] at (0.6) should be [0.44]
Pass CSS Animations: property <opacity> from [inherit] to [0.2] at (1) should be [0.2]
Pass CSS Animations: property <opacity> from [inherit] to [0.2] at (1.5) should be [0]
Pass Web Animations: property <opacity> from [inherit] to [0.2] at (-0.3) should be [0.98]
Pass Web Animations: property <opacity> from [inherit] to [0.2] at (0) should be [0.8]
Pass Web Animations: property <opacity> from [inherit] to [0.2] at (0.3) should be [0.62]
Pass Web Animations: property <opacity> from [inherit] to [0.2] at (0.6) should be [0.44]
Pass Web Animations: property <opacity> from [inherit] to [0.2] at (1) should be [0.2]
Pass Web Animations: property <opacity> from [inherit] to [0.2] at (1.5) should be [0]
Pass CSS Transitions: property <opacity> from [unset] to [0.2] at (-0.3) should be [1]
Pass CSS Transitions: property <opacity> from [unset] to [0.2] at (0) should be [1]
Pass CSS Transitions: property <opacity> from [unset] to [0.2] at (0.3) should be [0.76]
Pass CSS Transitions: property <opacity> from [unset] to [0.2] at (0.6) should be [0.52]
Pass CSS Transitions: property <opacity> from [unset] to [0.2] at (1) should be [0.2]
Pass CSS Transitions: property <opacity> from [unset] to [0.2] at (1.5) should be [0]
Pass CSS Transitions with transition: all: property <opacity> from [unset] to [0.2] at (-0.3) should be [1]
Pass CSS Transitions with transition: all: property <opacity> from [unset] to [0.2] at (0) should be [1]
Pass CSS Transitions with transition: all: property <opacity> from [unset] to [0.2] at (0.3) should be [0.76]
Pass CSS Transitions with transition: all: property <opacity> from [unset] to [0.2] at (0.6) should be [0.52]
Pass CSS Transitions with transition: all: property <opacity> from [unset] to [0.2] at (1) should be [0.2]
Pass CSS Transitions with transition: all: property <opacity> from [unset] to [0.2] at (1.5) should be [0]
Pass CSS Animations: property <opacity> from [unset] to [0.2] at (-0.3) should be [1]
Pass CSS Animations: property <opacity> from [unset] to [0.2] at (0) should be [1]
Pass CSS Animations: property <opacity> from [unset] to [0.2] at (0.3) should be [0.76]
Pass CSS Animations: property <opacity> from [unset] to [0.2] at (0.6) should be [0.52]
Pass CSS Animations: property <opacity> from [unset] to [0.2] at (1) should be [0.2]
Pass CSS Animations: property <opacity> from [unset] to [0.2] at (1.5) should be [0]
Pass Web Animations: property <opacity> from [unset] to [0.2] at (-0.3) should be [1]
Pass Web Animations: property <opacity> from [unset] to [0.2] at (0) should be [1]
Pass Web Animations: property <opacity> from [unset] to [0.2] at (0.3) should be [0.76]
Pass Web Animations: property <opacity> from [unset] to [0.2] at (0.6) should be [0.52]
Pass Web Animations: property <opacity> from [unset] to [0.2] at (1) should be [0.2]
Pass Web Animations: property <opacity> from [unset] to [0.2] at (1.5) should be [0]
Pass CSS Transitions: property <opacity> from [0] to [1] at (-0.3) should be [0]
Pass CSS Transitions: property <opacity> from [0] to [1] at (0) should be [0]
Pass CSS Transitions: property <opacity> from [0] to [1] at (0.3) should be [0.3]
Pass CSS Transitions: property <opacity> from [0] to [1] at (0.6) should be [0.6]
Pass CSS Transitions: property <opacity> from [0] to [1] at (1) should be [1]
Pass CSS Transitions: property <opacity> from [0] to [1] at (1.5) should be [1]
Pass CSS Transitions with transition: all: property <opacity> from [0] to [1] at (-0.3) should be [0]
Pass CSS Transitions with transition: all: property <opacity> from [0] to [1] at (0) should be [0]
Pass CSS Transitions with transition: all: property <opacity> from [0] to [1] at (0.3) should be [0.3]
Pass CSS Transitions with transition: all: property <opacity> from [0] to [1] at (0.6) should be [0.6]
Pass CSS Transitions with transition: all: property <opacity> from [0] to [1] at (1) should be [1]
Pass CSS Transitions with transition: all: property <opacity> from [0] to [1] at (1.5) should be [1]
Pass CSS Animations: property <opacity> from [0] to [1] at (-0.3) should be [0]
Pass CSS Animations: property <opacity> from [0] to [1] at (0) should be [0]
Pass CSS Animations: property <opacity> from [0] to [1] at (0.3) should be [0.3]
Pass CSS Animations: property <opacity> from [0] to [1] at (0.6) should be [0.6]
Pass CSS Animations: property <opacity> from [0] to [1] at (1) should be [1]
Pass CSS Animations: property <opacity> from [0] to [1] at (1.5) should be [1]
Pass Web Animations: property <opacity> from [0] to [1] at (-0.3) should be [0]
Pass Web Animations: property <opacity> from [0] to [1] at (0) should be [0]
Pass Web Animations: property <opacity> from [0] to [1] at (0.3) should be [0.3]
Pass Web Animations: property <opacity> from [0] to [1] at (0.6) should be [0.6]
Pass Web Animations: property <opacity> from [0] to [1] at (1) should be [1]
Pass Web Animations: property <opacity> from [0] to [1] at (1.5) should be [1]

View File

@ -0,0 +1,96 @@
<!DOCTYPE html>
<meta charset="UTF-8">
<title>opacity interpolation</title>
<link rel="help" href="https://drafts.csswg.org/css-color-3/#opacity">
<meta name="assert" content="opacity supports animation">
<script src="../../../resources/testharness.js"></script>
<script src="../../../resources/testharnessreport.js"></script>
<script src="../../../css/support/interpolation-testcommon.js"></script>
<style>
.parent {
opacity: 0.8;
}
.target {
width: 100px;
height: 100px;
background-color: black;
display: inline-block;
opacity: 0.1;
}
.expected {
background-color: green;
}
</style>
<body>
<script>
test_interpolation({
property: 'opacity',
from: neutralKeyframe,
to: '0.2',
}, [
{at: -0.3, expect: '0.07'},
{at: 0, expect: '0.1'},
{at: 0.3, expect: '0.13'},
{at: 0.6, expect: '0.16'},
{at: 1, expect: '0.2'},
{at: 1.5, expect: '0.25'},
]);
test_interpolation({
property: 'opacity',
from: 'initial',
to: '0.2',
}, [
{at: -0.3, expect: '1'},
{at: 0, expect: '1'},
{at: 0.3, expect: '0.76'},
{at: 0.6, expect: '0.52'},
{at: 1, expect: '0.2'},
{at: 1.5, expect: '0'},
]);
test_interpolation({
property: 'opacity',
from: 'inherit',
to: '0.2',
}, [
{at: -0.3, expect: '0.98'},
{at: 0, expect: '0.8'},
{at: 0.3, expect: '0.62'},
{at: 0.6, expect: '0.44'},
{at: 1, expect: '0.2'},
{at: 1.5, expect: '0'},
]);
test_interpolation({
property: 'opacity',
from: 'unset',
to: '0.2',
}, [
{at: -0.3, expect: '1'},
{at: 0, expect: '1'},
{at: 0.3, expect: '0.76'},
{at: 0.6, expect: '0.52'},
{at: 1, expect: '0.2'},
{at: 1.5, expect: '0'},
]);
test_interpolation({
property: 'opacity',
from: '0',
to: '1'
}, [
{at: -0.3, expect: '0'}, // CSS opacity is [0-1].
{at: 0, expect: '0'},
{at: 0.3, expect: '0.3'},
{at: 0.6, expect: '0.6'},
{at: 1, expect: '1'},
{at: 1.5, expect: '1'}
]);
</script>
</body>