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:
parent
7c79fc209f
commit
a2331e8dd3
Notes:
sideshowbarker
2024-07-17 21:11:12 +09:00
Author: https://github.com/skyrising Commit: https://github.com/SerenityOS/serenity/commit/a2331e8dd3 Pull-request: https://github.com/SerenityOS/serenity/pull/13093 Reviewed-by: https://github.com/AtkinsSJ
4 changed files with 121 additions and 42 deletions
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue