LibWeb: Implement CSS transforms on stacking contexts

Since there is currently no easy way to handle rotations and skews
with LibGfx this only implements translation and scaling by first
constructing a general 4x4 transformation matrix like outlined in
the css-transforms-1 specification. This is then downgraded to a
Gfx::AffineTransform in order to transform the destination rectangle
used with draw_scaled_bitmap()

While rotation would be nice this already looks pretty good :^)
This commit is contained in:
Simon Wanner 2022-03-18 01:29:20 +01:00 committed by Andreas Kling
parent 7c79fc209f
commit a2331e8dd3
Notes: sideshowbarker 2024-07-17 21:11:12 +09:00
4 changed files with 121 additions and 42 deletions

View file

@ -73,44 +73,6 @@ void BlockFormattingContext::parent_context_did_dimension_child_root_box()
// We can also layout absolutely positioned boxes within this BFC.
for (auto& box : m_absolutely_positioned_boxes)
layout_absolutely_positioned_element(box);
// FIXME: Transforms should be a painting concept, not a layout concept.
apply_transformations_to_children(root());
}
void BlockFormattingContext::apply_transformations_to_children(Box const& box)
{
box.for_each_child_of_type<Box>([&](auto& child_box) {
float transform_y_offset = 0.0f;
if (!child_box.computed_values().transformations().is_empty()) {
// FIXME: All transformations can be interpreted as successive 3D-matrix operations on the box, we don't do that yet.
// https://drafts.csswg.org/css-transforms/#serialization-of-the-computed-value
for (auto transformation : child_box.computed_values().transformations()) {
switch (transformation.function) {
case CSS::TransformFunction::TranslateY:
if (transformation.values.size() != 1)
continue;
transformation.values.first().visit(
[&](CSS::Length& value) {
transform_y_offset += value.to_px(child_box);
},
[&](float value) {
transform_y_offset += value;
},
[&](auto&) {
dbgln("FIXME: Implement unsupported transformation function value type!");
});
break;
default:
dbgln("FIXME: Implement missing transform function!");
}
}
}
auto& child_box_state = m_state.get_mutable(child_box);
auto untransformed_offset = child_box_state.offset;
child_box_state.offset = Gfx::FloatPoint { untransformed_offset.x(), untransformed_offset.y() + transform_y_offset };
});
}
void BlockFormattingContext::compute_width(Box const& box, LayoutMode layout_mode)

View file

@ -60,8 +60,6 @@ private:
void layout_floating_box(Box const& child, BlockContainer const& containing_block, LayoutMode);
void apply_transformations_to_children(Box const&);
void layout_list_item_marker(ListItemBox const&);
enum class FloatSide {

View file

@ -4,9 +4,13 @@
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/Debug.h>
#include <AK/QuickSort.h>
#include <AK/StringBuilder.h>
#include <LibGfx/AffineTransform.h>
#include <LibGfx/Matrix4x4.h>
#include <LibGfx/Painter.h>
#include <LibGfx/Rect.h>
#include <LibWeb/Layout/Box.h>
#include <LibWeb/Layout/InitialContainingBlock.h>
#include <LibWeb/Layout/ReplacedBox.h>
@ -137,6 +141,104 @@ void StackingContext::paint_internal(PaintContext& context) const
paint_descendants(context, m_box, StackingContextPaintPhase::FocusAndOverlay);
}
Gfx::FloatMatrix4x4 StackingContext::get_transformation_matrix(CSS::Transformation const& transformation) const
{
Vector<float> float_values;
for (auto const& value : transformation.values) {
value.visit(
[&](CSS::Length const& value) {
float_values.append(value.to_px(m_box));
},
[&](float value) {
float_values.append(value);
});
}
switch (transformation.function) {
case CSS::TransformFunction::Matrix:
if (float_values.size() == 6)
return Gfx::FloatMatrix4x4(float_values[0], float_values[2], 0, float_values[4],
float_values[1], float_values[3], 0, float_values[5],
0, 0, 1, 0,
0, 0, 0, 1);
break;
case CSS::TransformFunction::Translate:
if (float_values.size() == 1)
return Gfx::FloatMatrix4x4(1, 0, 0, float_values[0],
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1);
if (float_values.size() == 2)
return Gfx::FloatMatrix4x4(1, 0, 0, float_values[0],
0, 1, 0, float_values[1],
0, 0, 1, 0,
0, 0, 0, 1);
break;
case CSS::TransformFunction::TranslateX:
if (float_values.size() == 1)
return Gfx::FloatMatrix4x4(1, 0, 0, float_values[0],
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1);
break;
case CSS::TransformFunction::TranslateY:
if (float_values.size() == 1)
return Gfx::FloatMatrix4x4(1, 0, 0, 0,
0, 1, 0, float_values[0],
0, 0, 1, 0,
0, 0, 0, 1);
break;
case CSS::TransformFunction::Scale:
if (float_values.size() == 1)
return Gfx::FloatMatrix4x4(float_values[0], 0, 0, 0,
0, float_values[0], 0, 0,
0, 0, 1, 0,
0, 0, 0, 1);
if (float_values.size() == 2)
return Gfx::FloatMatrix4x4(float_values[0], 0, 0, 0,
0, float_values[1], 0, 0,
0, 0, 1, 0,
0, 0, 0, 1);
break;
case CSS::TransformFunction::ScaleX:
if (float_values.size() == 1)
return Gfx::FloatMatrix4x4(float_values[0], 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1);
break;
case CSS::TransformFunction::ScaleY:
if (float_values.size() == 1)
return Gfx::FloatMatrix4x4(1, 0, 0, 0,
0, float_values[0], 0, 0,
0, 0, 1, 0,
0, 0, 0, 1);
break;
default:
dbgln_if(LIBWEB_CSS_DEBUG, "FIXME: Unhandled transformation function {}", CSS::TransformationStyleValue::create(transformation.function, {})->to_string());
}
return Gfx::FloatMatrix4x4::identity();
}
Gfx::FloatMatrix4x4 StackingContext::combine_transformations(Vector<CSS::Transformation> const& transformations) const
{
auto matrix = Gfx::FloatMatrix4x4::identity();
for (auto const& transform : transformations)
matrix = matrix * get_transformation_matrix(transform);
return matrix;
}
// FIXME: This extracts the affine 2D part of the full transformation matrix.
// Use the whole matrix when we get better transformation support in LibGfx or use LibGL for drawing the bitmap
Gfx::AffineTransform StackingContext::combine_transformations_2d(Vector<CSS::Transformation> const& transformations) const
{
auto matrix = combine_transformations(transformations);
auto* m = matrix.elements();
return Gfx::AffineTransform(m[0][0], m[1][0], m[0][1], m[1][1], m[0][3], m[1][3]);
}
void StackingContext::paint(PaintContext& context) const
{
Gfx::PainterStateSaver saver(context.painter());
@ -148,7 +250,9 @@ void StackingContext::paint(PaintContext& context) const
if (opacity == 0.0f)
return;
if (opacity < 1.0f) {
auto affine_transform = combine_transformations_2d(m_box.computed_values().transformations());
if (opacity < 1.0f || !affine_transform.is_identity()) {
auto bitmap_or_error = Gfx::Bitmap::try_create(Gfx::BitmapFormat::BGRA8888, context.painter().target()->size());
if (bitmap_or_error.is_error())
return;
@ -156,7 +260,13 @@ void StackingContext::paint(PaintContext& context) const
Gfx::Painter painter(bitmap);
PaintContext paint_context(painter, context.palette(), context.scroll_offset());
paint_internal(paint_context);
context.painter().blit(Gfx::IntPoint(m_box.paint_box()->absolute_position()), bitmap, Gfx::IntRect(m_box.paint_box()->absolute_rect()), opacity);
// FIXME: Use the transform origin specified in CSS or SVG
auto transform_origin = m_box.paint_box()->absolute_position();
auto source_rect = m_box.paint_box()->absolute_rect().translated(-transform_origin);
auto transformed_destination_rect = affine_transform.map(source_rect).translated(transform_origin);
source_rect.translate_by(transform_origin);
context.painter().draw_scaled_bitmap(Gfx::rounded_int_rect(transformed_destination_rect), *bitmap, source_rect, opacity, Gfx::Painter::ScalingMode::BilinearBlend);
} else {
paint_internal(context);
}
@ -252,6 +362,11 @@ void StackingContext::dump(int indent) const
else
builder.append("auto");
builder.append(')');
auto affine_transform = combine_transformations_2d(m_box.computed_values().transformations());
if (!affine_transform.is_identity()) {
builder.appendff(", transform: {}", affine_transform);
}
dbgln("{}", builder.string_view());
for (auto& child : m_children)
child->dump(indent + 1);

View file

@ -7,6 +7,7 @@
#pragma once
#include <AK/Vector.h>
#include <LibGfx/Matrix4x4.h>
#include <LibWeb/Layout/Node.h>
#include <LibWeb/Painting/Paintable.h>
@ -41,6 +42,9 @@ private:
Vector<StackingContext*> m_children;
void paint_internal(PaintContext&) const;
Gfx::FloatMatrix4x4 get_transformation_matrix(CSS::Transformation const& transformation) const;
Gfx::FloatMatrix4x4 combine_transformations(Vector<CSS::Transformation> const& transformations) const;
Gfx::AffineTransform combine_transformations_2d(Vector<CSS::Transformation> const& transformations) const;
};
}