diff --git a/Libraries/LibWeb/CSS/ComputedProperties.cpp b/Libraries/LibWeb/CSS/ComputedProperties.cpp index 1a2e312ad15..bcba3642f69 100644 --- a/Libraries/LibWeb/CSS/ComputedProperties.cpp +++ b/Libraries/LibWeb/CSS/ComputedProperties.cpp @@ -751,6 +751,11 @@ Optional ComputedProperties::perspective() const VERIFY_NOT_REACHED(); } +Position ComputedProperties::perspective_origin() const +{ + return position_value(PropertyID::PerspectiveOrigin); +} + TransformOrigin ComputedProperties::transform_origin() const { auto length_percentage_with_keywords_resolved = [](StyleValue const& value) -> LengthPercentage { diff --git a/Libraries/LibWeb/CSS/ComputedProperties.h b/Libraries/LibWeb/CSS/ComputedProperties.h index 3253377a88f..6fd6bd5d130 100644 --- a/Libraries/LibWeb/CSS/ComputedProperties.h +++ b/Libraries/LibWeb/CSS/ComputedProperties.h @@ -213,6 +213,7 @@ public: Optional translate() const; Optional scale() const; Optional perspective() const; + Position perspective_origin() const; MaskType mask_type() const; float stop_opacity() const; diff --git a/Libraries/LibWeb/CSS/ComputedValues.h b/Libraries/LibWeb/CSS/ComputedValues.h index c6bee9109cb..2b38b5c71b2 100644 --- a/Libraries/LibWeb/CSS/ComputedValues.h +++ b/Libraries/LibWeb/CSS/ComputedValues.h @@ -68,6 +68,17 @@ struct Position { LengthPercentage offset_x { Percentage(50) }; PositionEdge edge_y { PositionEdge::Top }; LengthPercentage offset_y { Percentage(50) }; + + CSSPixelPoint resolved(Layout::Node const& node, CSSPixelRect const& rect) const + { + CSSPixels x = offset_x.to_px(node, rect.width()); + CSSPixels y = offset_y.to_px(node, rect.height()); + if (edge_x == PositionEdge::Right) + x = rect.width() - x; + if (edge_y == PositionEdge::Bottom) + y = rect.height() - y; + return CSSPixelPoint { rect.x() + x, rect.y() + y }; + } }; // https://drafts.csswg.org/css-contain-2/#containment-types @@ -645,6 +656,7 @@ public: Optional const& translate() const { return m_noninherited.translate; } Optional const& scale() const { return m_noninherited.scale; } Optional const& perspective() const { return m_noninherited.perspective; } + Position const& perspective_origin() const { return m_noninherited.perspective_origin; } Gfx::FontCascadeList const& font_list() const { return *m_inherited.font_list; } CSSPixels font_size() const { return m_inherited.font_size; } @@ -848,6 +860,7 @@ protected: Optional translate; Optional scale; Optional perspective; + Position perspective_origin; Optional mask; MaskType mask_type { InitialValues::mask_type() }; @@ -1001,6 +1014,7 @@ public: void set_rotate(Transformation value) { m_noninherited.rotate = move(value); } void set_scale(Transformation value) { m_noninherited.scale = move(value); } void set_perspective(Optional value) { m_noninherited.perspective = move(value); } + void set_perspective_origin(Position value) { m_noninherited.perspective_origin = move(value); } void set_transformations(Vector 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); } diff --git a/Libraries/LibWeb/CSS/Properties.json b/Libraries/LibWeb/CSS/Properties.json index 3c12b9d6059..44003355cd8 100644 --- a/Libraries/LibWeb/CSS/Properties.json +++ b/Libraries/LibWeb/CSS/Properties.json @@ -3226,6 +3226,16 @@ "length [0,∞]" ] }, + "perspective-origin": { + "animation-type": "by-computed-value", + "affects-layout": false, + "inherited": false, + "initial": "50% 50%", + "valid-types": [ + "position" + ], + "percentages-resolve-to": "length" + }, "place-content": { "inherited": false, "initial": "normal", diff --git a/Libraries/LibWeb/Layout/Node.cpp b/Libraries/LibWeb/Layout/Node.cpp index 7fb47a268fd..65937399899 100644 --- a/Libraries/LibWeb/Layout/Node.cpp +++ b/Libraries/LibWeb/Layout/Node.cpp @@ -698,8 +698,9 @@ 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()); computed_values.set_transform_style(computed_style.transform_style()); + computed_values.set_perspective(computed_style.perspective()); + computed_values.set_perspective_origin(computed_style.perspective_origin()); auto const& transition_delay_property = computed_style.property(CSS::PropertyID::TransitionDelay); if (transition_delay_property.is_time()) { diff --git a/Libraries/LibWeb/Painting/DisplayListPlayerSkia.cpp b/Libraries/LibWeb/Painting/DisplayListPlayerSkia.cpp index 010635553b9..7828285a534 100644 --- a/Libraries/LibWeb/Painting/DisplayListPlayerSkia.cpp +++ b/Libraries/LibWeb/Painting/DisplayListPlayerSkia.cpp @@ -230,6 +230,8 @@ void DisplayListPlayerSkia::push_stacking_context(PushStackingContext const& com auto new_transform = Gfx::translation_matrix(Vector3(command.transform.origin.x(), command.transform.origin.y(), 0)); new_transform = new_transform * command.transform.matrix; new_transform = new_transform * Gfx::translation_matrix(Vector3(-command.transform.origin.x(), -command.transform.origin.y(), 0)); + if (command.transform.parent_perspective_matrix.has_value()) + new_transform = command.transform.parent_perspective_matrix.value() * new_transform; auto matrix = to_skia_matrix4x4(new_transform); surface().canvas().save(); diff --git a/Libraries/LibWeb/Painting/DisplayListRecorder.cpp b/Libraries/LibWeb/Painting/DisplayListRecorder.cpp index ed012c7649c..a160d56eb25 100644 --- a/Libraries/LibWeb/Painting/DisplayListRecorder.cpp +++ b/Libraries/LibWeb/Painting/DisplayListRecorder.cpp @@ -12,13 +12,14 @@ namespace Web::Painting { -StackingContextTransform::StackingContextTransform(Gfx::FloatPoint origin, Gfx::FloatMatrix4x4 matrix, float scale) +StackingContextTransform::StackingContextTransform(Gfx::FloatPoint origin, Gfx::FloatMatrix4x4 matrix, Optional parent_perspective_matrix, float scale) { this->origin = origin.scaled(scale); matrix[0, 3] *= scale; matrix[1, 3] *= scale; matrix[2, 3] *= scale; this->matrix = matrix; + this->parent_perspective_matrix = parent_perspective_matrix; } DisplayListRecorder::DisplayListRecorder(DisplayList& command_list) diff --git a/Libraries/LibWeb/Painting/DisplayListRecorder.h b/Libraries/LibWeb/Painting/DisplayListRecorder.h index 09c360896b7..e9679aa3772 100644 --- a/Libraries/LibWeb/Painting/DisplayListRecorder.h +++ b/Libraries/LibWeb/Painting/DisplayListRecorder.h @@ -33,8 +33,9 @@ namespace Web::Painting { struct StackingContextTransform { Gfx::FloatPoint origin; Gfx::FloatMatrix4x4 matrix; + Optional parent_perspective_matrix; - StackingContextTransform(Gfx::FloatPoint origin, Gfx::FloatMatrix4x4 matrix, float scale); + StackingContextTransform(Gfx::FloatPoint origin, Gfx::FloatMatrix4x4 matrix, Optional parent_perspective_matrix, float scale); [[nodiscard]] bool is_identity() const { return matrix.is_identity(); } }; diff --git a/Libraries/LibWeb/Painting/Paintable.cpp b/Libraries/LibWeb/Painting/Paintable.cpp index 07bdaea7f4a..0ab9bb27f06 100644 --- a/Libraries/LibWeb/Painting/Paintable.cpp +++ b/Libraries/LibWeb/Painting/Paintable.cpp @@ -182,12 +182,15 @@ void Paintable::paint_inspector_overlay(DisplayListRecordingContext& context) co auto to_device_pixels_scale = float(context.device_pixels_per_css_pixel()); auto transform_matrix = box->transform(); auto transform_origin = box->transform_origin().to_type(); + Optional parent_perspective_matrix; + if (auto const* parent = as_if(box->parent())) + parent_perspective_matrix = parent->perspective_matrix(); // We only want the transform here, everything else undesirable for the inspector overlay DisplayListRecorder::PushStackingContextParams push_stacking_context_params { .opacity = 1.0, .compositing_and_blending_operator = Gfx::CompositingAndBlendingOperator::Normal, .isolate = false, - .transform = StackingContextTransform(transform_origin, transform_matrix, to_device_pixels_scale), + .transform = StackingContextTransform(transform_origin, transform_matrix, parent_perspective_matrix, to_device_pixels_scale), }; context.display_list_recorder().push_stacking_context(push_stacking_context_params); } diff --git a/Libraries/LibWeb/Painting/PaintableBox.cpp b/Libraries/LibWeb/Painting/PaintableBox.cpp index da6b90e9bc1..fbea54a318f 100644 --- a/Libraries/LibWeb/Painting/PaintableBox.cpp +++ b/Libraries/LibWeb/Painting/PaintableBox.cpp @@ -1553,41 +1553,33 @@ 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 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::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_reference_box(); auto x = reference_box.left() + transform_origin.x.to_px(layout_node, reference_box.width()); auto y = reference_box.top() + transform_origin.y.to_px(layout_node, reference_box.height()); set_transform_origin({ x, y }); - set_transform_origin({ x, y }); + + // https://drafts.csswg.org/css-transforms-2/#perspective-matrix + if (auto perspective = computed_values.perspective(); perspective.has_value()) { + // The perspective matrix is computed as follows: + + // 1. Start with the identity matrix. + // 2. Translate by the computed X and Y values of 'perspective-origin' + // https://drafts.csswg.org/css-transforms-2/#perspective-origin-property + // Percentages: refer to the size of the reference box + auto perspective_origin = computed_values.perspective_origin().resolved(layout_node, reference_box).to_type(); + auto computed_x = perspective_origin.x(); + auto computed_y = perspective_origin.y(); + m_perspective_matrix = Gfx::translation_matrix(Vector3(computed_x, computed_y, 0)); + + // 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 + // NB: Length values less than 1px being clamped to 1px is handled by the perspective() function already. + m_perspective_matrix = m_perspective_matrix.value() * CSS::Transformation(CSS::TransformFunction::Perspective, Vector({ CSS::TransformValue(CSS::Length::make_px(perspective.value())) })).to_matrix({}).release_value(); + + // 4. Translate by the negated computed X and Y values of 'perspective-origin' + m_perspective_matrix = m_perspective_matrix.value() * Gfx::translation_matrix(Vector3(-computed_x, -computed_y, 0)); + } // Outlines auto outline_data = borders_data_for_outline(layout_node, computed_values.outline_color(), computed_values.outline_style(), computed_values.outline_width()); diff --git a/Libraries/LibWeb/Painting/PaintableBox.h b/Libraries/LibWeb/Painting/PaintableBox.h index 73ed65d14b9..7f262d836d3 100644 --- a/Libraries/LibWeb/Painting/PaintableBox.h +++ b/Libraries/LibWeb/Painting/PaintableBox.h @@ -201,8 +201,8 @@ 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_perspective_matrix(Optional perspective_matrix) { m_perspective_matrix = perspective_matrix; } + Optional 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; } @@ -331,8 +331,8 @@ private: BorderRadiiData m_border_radii_data; Vector m_box_shadow_data; Gfx::FloatMatrix4x4 m_transform { Gfx::FloatMatrix4x4::identity() }; - Gfx::FloatMatrix4x4 m_perspective_matrix { Gfx::FloatMatrix4x4::identity() }; CSSPixelPoint m_transform_origin; + Optional m_perspective_matrix {}; Optional m_outline_data; CSSPixels m_outline_offset { 0 }; diff --git a/Libraries/LibWeb/Painting/StackingContext.cpp b/Libraries/LibWeb/Painting/StackingContext.cpp index c00569accd1..1f608044d19 100644 --- a/Libraries/LibWeb/Painting/StackingContext.cpp +++ b/Libraries/LibWeb/Painting/StackingContext.cpp @@ -310,8 +310,9 @@ void StackingContext::paint(DisplayListRecordingContext& context) const // 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. + Optional parent_perspective_matrix; if (auto const* parent = as_if(paintable_box().parent())) - transform_matrix = parent->perspective_matrix() * transform_matrix; + parent_perspective_matrix = parent->perspective_matrix(); auto transform_origin = paintable_box().transform_origin().to_type(); @@ -321,7 +322,7 @@ void StackingContext::paint(DisplayListRecordingContext& context) const .opacity = opacity, .compositing_and_blending_operator = compositing_and_blending_operator, .isolate = paintable_box().computed_values().isolation() == CSS::Isolation::Isolate, - .transform = StackingContextTransform(transform_origin, transform_matrix, to_device_pixels_scale), + .transform = StackingContextTransform(transform_origin, transform_matrix, parent_perspective_matrix, to_device_pixels_scale), }; auto const& computed_values = paintable_box().computed_values(); diff --git a/Tests/LibWeb/Ref/expected/wpt-import/css/css-transforms/reference/perspective-origin-reftest.html b/Tests/LibWeb/Ref/expected/wpt-import/css/css-transforms/reference/perspective-origin-reftest.html new file mode 100644 index 00000000000..c39661796a4 --- /dev/null +++ b/Tests/LibWeb/Ref/expected/wpt-import/css/css-transforms/reference/perspective-origin-reftest.html @@ -0,0 +1,30 @@ + + + + Reference File + + + + +

The test passes if there is a green square and no red.

+
+
+
+ + diff --git a/Tests/LibWeb/Ref/expected/wpt-import/css/css-transforms/reference/perspective-reftest.html b/Tests/LibWeb/Ref/expected/wpt-import/css/css-transforms/reference/perspective-reftest.html new file mode 100644 index 00000000000..c39661796a4 --- /dev/null +++ b/Tests/LibWeb/Ref/expected/wpt-import/css/css-transforms/reference/perspective-reftest.html @@ -0,0 +1,30 @@ + + + + Reference File + + + + +

The test passes if there is a green square and no red.

+
+
+
+ + diff --git a/Tests/LibWeb/Ref/expected/wpt-import/css/css-transforms/transform3d-perspective-origin-ref.html b/Tests/LibWeb/Ref/expected/wpt-import/css/css-transforms/transform3d-perspective-origin-ref.html new file mode 100644 index 00000000000..e174d7596f7 --- /dev/null +++ b/Tests/LibWeb/Ref/expected/wpt-import/css/css-transforms/transform3d-perspective-origin-ref.html @@ -0,0 +1,16 @@ + + + + CSS Reftest Reference + + + + + +
+
+
+
+ + diff --git a/Tests/LibWeb/Ref/input/wpt-import/css/css-transforms/perspective-origin-001.html b/Tests/LibWeb/Ref/input/wpt-import/css/css-transforms/perspective-origin-001.html new file mode 100644 index 00000000000..9fd7caf1b55 --- /dev/null +++ b/Tests/LibWeb/Ref/input/wpt-import/css/css-transforms/perspective-origin-001.html @@ -0,0 +1,39 @@ + + +CSS Transforms Test: perspective-origin - 0px center('center' computes to '50%' in vertical position) + + + + + + + +

Test passes if there is a filled green square and no red.

+
+
+ diff --git a/Tests/LibWeb/Ref/input/wpt-import/css/css-transforms/perspective-origin-002.html b/Tests/LibWeb/Ref/input/wpt-import/css/css-transforms/perspective-origin-002.html new file mode 100644 index 00000000000..f5843f0397a --- /dev/null +++ b/Tests/LibWeb/Ref/input/wpt-import/css/css-transforms/perspective-origin-002.html @@ -0,0 +1,39 @@ + + +CSS Transforms Test: perspective-origin - center 0px('center' computes to '50%' in horizontal position) + + + + + + + +

Test passes if there is a filled green square and no red.

+
+
+ diff --git a/Tests/LibWeb/Ref/input/wpt-import/css/css-transforms/perspective-origin-003.html b/Tests/LibWeb/Ref/input/wpt-import/css/css-transforms/perspective-origin-003.html new file mode 100644 index 00000000000..6bda138d75b --- /dev/null +++ b/Tests/LibWeb/Ref/input/wpt-import/css/css-transforms/perspective-origin-003.html @@ -0,0 +1,40 @@ + + +CSS Transforms Test: perspective-origin - 50% bottom('bottom' computes to '100%' in vertical position) + + + + + + + +

Test passes if there is a filled green square and no red.

+
+
+ diff --git a/Tests/LibWeb/Ref/input/wpt-import/css/css-transforms/perspective-origin-004.html b/Tests/LibWeb/Ref/input/wpt-import/css/css-transforms/perspective-origin-004.html new file mode 100644 index 00000000000..c049c072952 --- /dev/null +++ b/Tests/LibWeb/Ref/input/wpt-import/css/css-transforms/perspective-origin-004.html @@ -0,0 +1,39 @@ + + +CSS Transforms Test: perspective-origin - 50% top('top' computes to '0%' in vertical position) + + + + + + + +

Test passes if there is a filled green square and no red.

+
+
+ diff --git a/Tests/LibWeb/Ref/input/wpt-import/css/css-transforms/perspective-origin-005.html b/Tests/LibWeb/Ref/input/wpt-import/css/css-transforms/perspective-origin-005.html new file mode 100644 index 00000000000..4e79e6b74ab --- /dev/null +++ b/Tests/LibWeb/Ref/input/wpt-import/css/css-transforms/perspective-origin-005.html @@ -0,0 +1,39 @@ + + +CSS Transforms Test: perspective-origin - left 50%('left' computes to '0%' in horizontal position) + + + + + + + +

Test passes if there is a filled green square and no red.

+
+
+ diff --git a/Tests/LibWeb/Ref/input/wpt-import/css/css-transforms/perspective-origin-006.html b/Tests/LibWeb/Ref/input/wpt-import/css/css-transforms/perspective-origin-006.html new file mode 100644 index 00000000000..48b909cf2ee --- /dev/null +++ b/Tests/LibWeb/Ref/input/wpt-import/css/css-transforms/perspective-origin-006.html @@ -0,0 +1,40 @@ + + +CSS Transforms Test: perspective-origin - right 50%('right' computes to '100%' in horizontal position) + + + + + + + +

Test passes if there is a filled green square and no red.

+
+
+ diff --git a/Tests/LibWeb/Ref/input/wpt-import/css/css-transforms/perspective-origin-scroll.html b/Tests/LibWeb/Ref/input/wpt-import/css/css-transforms/perspective-origin-scroll.html new file mode 100644 index 00000000000..fcdd300de9f --- /dev/null +++ b/Tests/LibWeb/Ref/input/wpt-import/css/css-transforms/perspective-origin-scroll.html @@ -0,0 +1,40 @@ + + +The 'perspective-origin's position must not move with the children of a scroll container if it is on one. + + + + + + +

Test passes if there is a filled green square and no red.

+
+
+
+
+ diff --git a/Tests/LibWeb/Ref/input/wpt-import/css/css-transforms/perspective-origin-x.html b/Tests/LibWeb/Ref/input/wpt-import/css/css-transforms/perspective-origin-x.html new file mode 100644 index 00000000000..de7d45681a2 --- /dev/null +++ b/Tests/LibWeb/Ref/input/wpt-import/css/css-transforms/perspective-origin-x.html @@ -0,0 +1,49 @@ + + + + CSS Transforms Test: perspective property + + + + + + + + + + +

The test passes if there is a green square and no red.

+
+
+
+
+ + \ No newline at end of file diff --git a/Tests/LibWeb/Ref/input/wpt-import/css/css-transforms/perspective-origin-xy.html b/Tests/LibWeb/Ref/input/wpt-import/css/css-transforms/perspective-origin-xy.html new file mode 100644 index 00000000000..6fc1aec4fb3 --- /dev/null +++ b/Tests/LibWeb/Ref/input/wpt-import/css/css-transforms/perspective-origin-xy.html @@ -0,0 +1,48 @@ + + + + CSS Transforms Test: perspective property + + + + + + + + + +

The test passes if there is a green square and no red.

+
+
+
+
+ + \ No newline at end of file diff --git a/Tests/LibWeb/Ref/input/wpt-import/css/css-transforms/transform3d-perspective-origin-001.html b/Tests/LibWeb/Ref/input/wpt-import/css/css-transforms/transform3d-perspective-origin-001.html new file mode 100644 index 00000000000..f2a5b446c1b --- /dev/null +++ b/Tests/LibWeb/Ref/input/wpt-import/css/css-transforms/transform3d-perspective-origin-001.html @@ -0,0 +1,20 @@ + + + + CSS Test (Transforms): perspective-origin + + + + + + + +
+
+
+
+ + diff --git a/Tests/LibWeb/Screenshot/images/image-transformation-anti-aliasing-ref.png b/Tests/LibWeb/Screenshot/images/image-transformation-anti-aliasing-ref.png index 7cd5b99f807..cc6270533a1 100644 Binary files a/Tests/LibWeb/Screenshot/images/image-transformation-anti-aliasing-ref.png and b/Tests/LibWeb/Screenshot/images/image-transformation-anti-aliasing-ref.png differ diff --git a/Tests/LibWeb/Screenshot/input/image-transformation-anti-aliasing.html b/Tests/LibWeb/Screenshot/input/image-transformation-anti-aliasing.html index e3576d50b25..ecc38bb9ce6 100644 --- a/Tests/LibWeb/Screenshot/input/image-transformation-anti-aliasing.html +++ b/Tests/LibWeb/Screenshot/input/image-transformation-anti-aliasing.html @@ -1,6 +1,6 @@ - +