LibWeb: Implement CSS perspective property

This commit is contained in:
Psychpsyo 2025-11-10 15:10:38 +01:00 committed by Jelle Raaijmakers
parent e44a97e508
commit eb21ea890c
Notes: github-actions[bot] 2025-11-11 23:42:45 +00:00
13 changed files with 108 additions and 2 deletions

View file

@ -714,6 +714,20 @@ TransformBox ComputedProperties::transform_box() const
return keyword_to_transform_box(value.to_keyword()).release_value();
}
Optional<CSSPixels> ComputedProperties::perspective() const
{
auto const& value = property(PropertyID::Perspective);
if (value.is_keyword() && value.to_keyword() == Keyword::None)
return {};
if (value.is_length())
return value.as_length().length().absolute_length_to_px();
if (value.is_calculated())
return value.as_calculated().resolve_length({ .length_resolution_context = {} })->absolute_length_to_px();
VERIFY_NOT_REACHED();
}
TransformOrigin ComputedProperties::transform_origin() const
{
auto length_percentage_with_keywords_resolved = [](StyleValue const& value) -> LengthPercentage {

View file

@ -210,6 +210,7 @@ public:
Optional<Transformation> rotate() const;
Optional<Transformation> translate() const;
Optional<Transformation> scale() const;
Optional<CSSPixels> perspective() const;
MaskType mask_type() const;
float stop_opacity() const;

View file

@ -637,6 +637,7 @@ public:
Optional<Transformation> const& rotate() const { return m_noninherited.rotate; }
Optional<Transformation> const& translate() const { return m_noninherited.translate; }
Optional<Transformation> const& scale() const { return m_noninherited.scale; }
Optional<CSSPixels> const& perspective() const { return m_noninherited.perspective; }
Gfx::FontCascadeList const& font_list() const { return *m_inherited.font_list; }
CSSPixels font_size() const { return m_inherited.font_size; }
@ -838,6 +839,7 @@ protected:
Optional<Transformation> rotate;
Optional<Transformation> translate;
Optional<Transformation> scale;
Optional<CSSPixels> perspective;
Optional<MaskReference> mask;
MaskType mask_type { InitialValues::mask_type() };
@ -990,6 +992,7 @@ public:
void set_box_shadow(Vector<ShadowData>&& value) { m_noninherited.box_shadow = move(value); }
void set_rotate(Transformation value) { m_noninherited.rotate = move(value); }
void set_scale(Transformation value) { m_noninherited.scale = move(value); }
void set_perspective(Optional<CSSPixels> value) { m_noninherited.perspective = move(value); }
void set_transformations(Vector<Transformation> value) { m_noninherited.transformations = move(value); }
void set_transform_box(TransformBox value) { m_noninherited.transform_box = value; }
void set_transform_origin(TransformOrigin value) { m_noninherited.transform_origin = move(value); }

View file

@ -3215,6 +3215,17 @@
"paint-order"
]
},
"perspective": {
"animation-type": "by-computed-value",
"inherited": false,
"initial": "none",
"valid-identifiers": [
"none"
],
"valid-types": [
"length [0,∞]"
]
},
"place-content": {
"inherited": false,
"initial": "normal",

View file

@ -98,6 +98,12 @@ bool Node::can_contain_boxes_with_position_absolute() const
if (computed_values().scale().has_value())
return true;
// https://drafts.csswg.org/css-transforms-2/#propdef-perspective
// The use of this property with any value other than 'none' establishes a stacking context. It also establishes
// a containing block for all descendants, just like the 'transform' property does.
if (computed_values().perspective().has_value())
return true;
// https://drafts.csswg.org/css-contain-2/#containment-types
// 4. The layout containment box establishes an absolute positioning containing block and a fixed positioning
// containing block.
@ -284,6 +290,11 @@ bool Node::establishes_stacking_context() const
if (computed_values.view_transition_name().has_value() || will_change_property(CSS::PropertyID::ViewTransitionName))
return true;
// https://drafts.csswg.org/css-transforms-2/#propdef-perspective
// The use of this property with any value other than 'none' establishes a stacking context.
if (computed_values.perspective().has_value() || will_change_property(CSS::PropertyID::Perspective))
return true;
return computed_values.opacity() < 1.0f || will_change_property(CSS::PropertyID::Opacity);
}
@ -682,6 +693,7 @@ void NodeWithStyle::apply_style(CSS::ComputedProperties const& computed_style)
computed_values.set_transformations(computed_style.transformations());
computed_values.set_transform_box(computed_style.transform_box());
computed_values.set_transform_origin(computed_style.transform_origin());
computed_values.set_perspective(computed_style.perspective());
auto const& transition_delay_property = computed_style.property(CSS::PropertyID::TransitionDelay);
if (transition_delay_property.is_time()) {

View file

@ -1535,6 +1535,35 @@ void PaintableBox::resolve_paint_properties()
matrix = matrix * transform.to_matrix(*this).release_value();
set_transform(matrix);
// https://drafts.csswg.org/css-transforms-2/#perspective
auto const& perspective = computed_values.perspective();
if (perspective.has_value()) {
// The perspective matrix is computed as follows:
// 1. Start with the identity matrix.
m_perspective_matrix = Gfx::FloatMatrix4x4::identity();
// 2. Translate by the computed X and Y values of 'perspective-origin'
// FIXME: Implement this.
// 3. Multiply by the matrix that would be obtained from the 'perspective()' transform function, where the
// length is provided by the value of the perspective property
auto perspective_value = perspective.value();
// https://drafts.csswg.org/css-transforms-2/#perspective-property
// As very small <length> values can produce bizarre rendering results and stress the numerical accuracy
// of transform calculations, values less than '1px' must be treated as '1px' for rendering purposes. (This
// clamping does not affect the underlying value, so 'perspective: 0;' in a stylesheet will still serialize back
// as '0'.)
if (perspective_value < 1)
perspective_value = 1;
m_perspective_matrix = m_perspective_matrix * CSS::Transformation(CSS::TransformFunction::Perspective, Vector<CSS::TransformValue>({ CSS::TransformValue(CSS::Length::make_px(perspective_value)) })).to_matrix(*this).release_value();
// 4. Translate by the negated computed X and Y values of 'perspective-origin'
// FIXME: Implement this.
}
auto const& transform_origin = computed_values.transform_origin();
auto reference_box = transform_box_rect();
auto x = reference_box.left() + transform_origin.x.to_px(layout_node, reference_box.width());

View file

@ -201,6 +201,9 @@ public:
void set_transform(Gfx::FloatMatrix4x4 transform) { m_transform = transform; }
Gfx::FloatMatrix4x4 const& transform() const { return m_transform; }
void set_perspective_matrix(Gfx::FloatMatrix4x4 perspective_matrix) { m_perspective_matrix = perspective_matrix; }
Gfx::FloatMatrix4x4 const& perspective_matrix() const { return m_perspective_matrix; }
void set_transform_origin(CSSPixelPoint transform_origin) { m_transform_origin = transform_origin; }
CSSPixelPoint const& transform_origin() const { return m_transform_origin; }
@ -328,6 +331,7 @@ private:
BorderRadiiData m_border_radii_data;
Vector<ShadowData> m_box_shadow_data;
Gfx::FloatMatrix4x4 m_transform { Gfx::FloatMatrix4x4::identity() };
Gfx::FloatMatrix4x4 m_perspective_matrix { Gfx::FloatMatrix4x4::identity() };
CSSPixelPoint m_transform_origin;
Optional<BordersData> m_outline_data;

View file

@ -306,6 +306,13 @@ void StackingContext::paint(DisplayListRecordingContext& context) const
auto source_paintable_rect = context.enclosing_device_rect(paintable_box().absolute_paint_rect()).to_type<int>();
auto transform_matrix = paintable_box().transform();
// https://drafts.csswg.org/css-transforms-2/#perspective
// Second, the 'perspective' and 'perspective-origin' properties can be applied to an element to influence the
// rendering of its 3d-transformed children, giving them a shared perspective that provides the impression of
// them living in the same three-dimensional scene.
if (auto const* parent = as_if<PaintableBox>(paintable_box().parent()))
transform_matrix = parent->perspective_matrix() * transform_matrix;
auto transform_origin = paintable_box().transform_origin().to_type<float>();
Gfx::CompositingAndBlendingOperator compositing_and_blending_operator = mix_blend_mode_to_compositing_and_blending_operator(paintable_box().computed_values().mix_blend_mode());

View file

@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<head>
<title>CSS Test (Transforms): rotatex() and 'perspective'</title>
<link rel="author" title="Matt Woodrow" href="mailto:mwoodrow@mozilla.com">
<link rel="author" title="Aryeh Gregor" href="mailto:ayg@aryeh.name">
<link rel="help" href="http://www.w3.org/TR/css-transforms-2/#three-d-transform-functions">
<link rel="help" href="http://www.w3.org/TR/css-transforms-2/#funcdef-rotatex">
<link rel="help" href="http://www.w3.org/TR/css-transforms-2/#perspective-property">
<meta name="assert" content="This tests that 'perspective' has some effect
when combined with rotatex() (i.e., is not equivalent to simply omitting
the perspective).">
<link rel="mismatch" href="../../../../expected/wpt-import/css/css-transforms/transform3d-rotatex-ref.html">
</head>
<body>
<div style="perspective: 1000px;">
<div style="transform: rotatex(45deg); width: 100px; height: 100px;
background: lime"></div>
</div>
</body>
</html>

View file

@ -244,6 +244,7 @@ All properties associated with getComputedStyle(document.body):
"padding-left",
"padding-right",
"padding-top",
"perspective",
"position",
"position-anchor",
"position-area",

View file

@ -663,6 +663,7 @@ All supported properties and their default values exposed from CSSStylePropertie
'padding-top': '0px'
'paintOrder': 'normal'
'paint-order': 'normal'
'perspective': 'none'
'placeContent': 'normal'
'place-content': 'normal'
'placeItems': 'normal legacy'

View file

@ -242,6 +242,7 @@ padding-inline-start: 0px
padding-left: 0px
padding-right: 0px
padding-top: 0px
perspective: none
position: static
position-anchor: auto
position-area: none

View file

@ -1,8 +1,8 @@
Harness status: OK
Found 269 tests
Found 270 tests
263 Pass
264 Pass
6 Fail
Pass accent-color
Pass border-collapse
@ -231,6 +231,7 @@ Pass padding-inline-start
Pass padding-left
Pass padding-right
Pass padding-top
Pass perspective
Pass position
Pass r
Pass right