Skip to content
Permalink
Browse files
Perspective should not be affected by transform-origin
https://bugs.webkit.org/show_bug.cgi?id=211787
<rdar://problem/63143806>

Patch by Nikolas Zimmermann <nzimmermann@igalia.com> on 2022-05-21
Reviewed by Simon Fraser.

Fix a number of issues related to perspective handling:
- 'perspective-origin' always used the border-box as reference box, when resolving
  length percentages, ignoring the choice of 'transform-box'. Fix that.

- Proper 'transform-box' awareness throghout RenderLayerBacking (few places with issue, e.g. perspectiveOrigin()
  affecting repaint & coverage rects)

- The chosen perspective transformation was not invariant under 'transform-origin' / 'transform-box'
  changes of the element A, that defines the perspective. However the perspective set on element A
  should only affect the rendering of its descendants: the choice of the 'transform-origin' / 'transform-box'
  of element A should have no effect on the perspective established for the children.

- Assure that 'transform-box' changes trigger GraphicLayer geometry updates: this fully fixes 'transform-box'
  support for composited elements, and brings its state on-par with non-composited elements (both support all kind of
  transform-box / transform-origin combinations on regular layers, clipped layers, scrolled layers).

This fixes the (not yet upstreamed) test web-platform-tests/css/css-transforms/animation/transform-box-will-change-transform-layer.html.

Prepared a new WPT test (see above) for upstreaming.

* Source/WebCore/animation/KeyframeEffect.cpp:
(WebCore::KeyframeEffect::computeTransformedExtentViaTransformList const):
* Source/WebCore/rendering/RenderLayer.cpp:
(WebCore::RenderLayer::perspectiveTransform const):
(WebCore::RenderLayer::perspectiveOrigin const):
* Source/WebCore/rendering/RenderLayer.h:
* Source/WebCore/rendering/RenderLayerBacking.cpp:
(WebCore::RenderLayerBacking::updateTransform):
(WebCore::RenderLayerBacking::updateChildrenTransformAndAnchorPoint):
(WebCore::RenderLayerBacking::computeTransformOriginForPainting const): Deleted.
* Source/WebCore/rendering/RenderLayerBacking.h:
* Source/WebCore/rendering/RenderLayerCompositor.cpp:
(WebCore::recompositeChangeRequiresGeometryUpdate):
* Source/WebCore/rendering/RenderLayerModelObject.cpp:
(WebCore::RenderLayerModelObject::applySVGTransform const):
* Source/WebCore/rendering/style/RenderStyle.cpp:
(WebCore::RenderStyle::computePerspectiveOrigin const):
(WebCore::RenderStyle::applyPerspective const):
(WebCore::RenderStyle::computeTransformOrigin const):
(WebCore::RenderStyle::applyTransformOrigin const):
(WebCore::RenderStyle::unapplyTransformOrigin const):
(WebCore::RenderStyle::applyTransform const):
(WebCore::RenderStyle::applyMotionPathTransform const):
* Source/WebCore/rendering/style/RenderStyle.h:
* Source/WebCore/svg/SVGGraphicsElement.cpp:
(WebCore::SVGGraphicsElement::animatedLocalTransform const):
* LayoutTests/compositing/tiling/coverage-adjustment-secondary-quad-mapping-expected.txt:
* LayoutTests/compositing/tiling/perspective-on-scroller-tile-coverage-expected.txt:
* LayoutTests/platform/glib/TestExpectations:
* LayoutTests/platform/ios-wk2/compositing/tiling/coverage-adjustment-secondary-quad-mapping-expected.txt:
* LayoutTests/platform/ios/TestExpectations:
* LayoutTests/platform/mac-wk1/compositing/tiling/perspective-on-scroller-tile-coverage-expected.txt:
* LayoutTests/platform/mac/TestExpectations:
* LayoutTests/platform/win/TestExpectations:

Canonical link: https://commits.webkit.org/250841@main
git-svn-id: https://svn.webkit.org/repository/webkit/trunk@294615 268f45cc-cd09-0410-ab3c-d52691b4dbfc
  • Loading branch information
Nikolas Zimmermann authored and webkit-commit-queue committed May 21, 2022
1 parent 9ebbc11 commit 0d63a642cb91f26934728bacf945d33a6eeb9ecf
Showing 18 changed files with 130 additions and 92 deletions.
@@ -24,9 +24,10 @@
(GraphicsLayer
(offsetFromRenderer width=1 height=1)
(position 1.00 1.00)
(anchor 0.51 0.50)
(bounds 583.00 578.00)
(backingStoreAttached 1)
(childrenTransform [1.00 0.00 0.00 0.00] [0.00 1.00 0.00 0.00] [-307.50 -290.00 1.00 -1.00] [0.00 0.00 0.00 1.00])
(childrenTransform [1.00 0.00 0.00 0.00] [0.00 1.00 0.00 0.00] [-300.00 -290.00 1.00 -1.00] [0.00 0.00 0.00 1.00])
(children 1
(GraphicsLayer
(position 0.00 -400.00)
@@ -29,8 +29,9 @@
(children 2
(GraphicsLayer
(bounds origin 0.00 900.00)
(anchor 0.51 0.50)
(bounds 585.00 500.00)
(childrenTransform [1.00 0.00 0.00 0.00] [0.00 1.00 0.00 0.00] [2.93 2.50 1.00 -0.01] [0.00 0.00 0.00 1.00])
(childrenTransform [1.00 0.00 0.00 0.00] [0.00 1.00 0.00 0.00] [3.00 2.50 1.00 -0.01] [0.00 0.00 0.00 1.00])
(visible rect 0.00, 900.00 585.00 x 500.00)
(coverage rect 0.00, 900.00 585.00 x 500.00)
(intersects coverage rect 1)
@@ -663,9 +663,6 @@ webkit.org/b/230277 imported/w3c/web-platform-tests/css/css-transforms/perspecti
webkit.org/b/230277 imported/w3c/web-platform-tests/css/css-transforms/dynamic-fixed-pos-cb-change.html [ ImageOnlyFailure ]
webkit.org/b/230277 imported/w3c/web-platform-tests/css/css-transforms/perspective-transforms-equivalence.html [ ImageOnlyFailure ]

# 'transform-box' support is broken for composited elements.
webkit.org/b/237553 imported/w3c/web-platform-tests/css/css-transforms/animation/transform-box-will-change-transform-layer.html [ ImageOnlyFailure ]

webkit.org/b/237502 fast/dom/Range/getClientRects.html [ Failure ]
webkit.org/b/237502 fast/multicol/newmulticol/hide-box-vertical-lr.html [ ImageOnlyFailure ]
webkit.org/b/237502 imported/blink/fast/multicol/vertical-lr/float-big-line.html [ ImageOnlyFailure ]
@@ -23,9 +23,10 @@
(GraphicsLayer
(offsetFromRenderer width=1 height=1)
(position 1.00 1.00)
(anchor 0.51 0.50)
(bounds 583.00 578.00)
(backingStoreAttached 1)
(childrenTransform [1.00 0.00 0.00 0.00] [0.00 1.00 0.00 0.00] [-307.50 -290.00 1.00 -1.00] [0.00 0.00 0.00 1.00])
(childrenTransform [1.00 0.00 0.00 0.00] [0.00 1.00 0.00 0.00] [-300.00 -290.00 1.00 -1.00] [0.00 0.00 0.00 1.00])
(children 1
(GraphicsLayer
(position 0.00 -400.00)
@@ -3023,9 +3023,6 @@ fast/canvas/set-colors.html [ Failure ]

webkit.org/b/202958 css3/filters/svg-blur-filter-clipped.html [ ImageOnlyFailure ]

# 'transform-box' support is broken for composited elements.
webkit.org/b/237553 imported/w3c/web-platform-tests/css/css-transforms/animation/transform-box-will-change-transform-layer.html [ ImageOnlyFailure ]

webkit.org/b/203305 imported/w3c/web-platform-tests/css/css-transitions/properties-value-001.html [ Pass Failure ]
webkit.org/b/203305 [ Debug ] imported/w3c/web-platform-tests/css/css-transitions/properties-value-inherit-001.html [ Pass Failure ]
webkit.org/b/203356 [ Debug ] imported/w3c/web-platform-tests/css/css-transitions/properties-value-003.html [ Pass Failure ]
@@ -24,8 +24,9 @@
(contentsScale 1.00)
(children 1
(GraphicsLayer
(anchor 0.51 0.50)
(bounds 585.00 500.00)
(childrenTransform [1.00 0.00 0.00 0.00] [0.00 1.00 0.00 0.00] [2.93 2.50 1.00 -0.01] [0.00 0.00 0.00 1.00])
(childrenTransform [1.00 0.00 0.00 0.00] [0.00 1.00 0.00 0.00] [3.00 2.50 1.00 -0.01] [0.00 0.00 0.00 1.00])
(visible rect 0.00, 0.00 585.00 x 500.00)
(coverage rect 0.00, 0.00 585.00 x 500.00)
(intersects coverage rect 1)
@@ -2197,9 +2197,6 @@ webkit.org/b/228176 [ BigSur Monterey ] fast/text/variable-system-font-2.html [

webkit.org/b/230327 imported/w3c/web-platform-tests/css/css-transforms/crashtests/transform-marquee-resize-div-image-001.html [ Pass Failure ]

# 'transform-box' support is broken for composited elements.
webkit.org/b/237553 imported/w3c/web-platform-tests/css/css-transforms/animation/transform-box-will-change-transform-layer.html [ ImageOnlyFailure ]

# rdar://83591040
[ Monterey+ ] editing/mac/dictionary-lookup/dictionary-lookup-input.html [ Crash ]
[ Monterey+ ] editing/mac/dictionary-lookup/dictionary-lookup-inside-selection.html [ Crash ]
@@ -953,9 +953,6 @@ webkit.org/b/149245 imported/w3c/web-platform-tests/css/css-multicol/multicol-sh
webkit.org/b/149245 imported/w3c/web-platform-tests/css/css-multicol/multicol-span-all-block-sibling-003.xht [ Skip ]
webkit.org/b/149245 imported/w3c/web-platform-tests/css/css-multicol/multicol-span-all-margin-nested-firstchild-001.xht [ Skip ]

# 'transform-box' support is broken for composited elements.
webkit.org/b/237553 imported/w3c/web-platform-tests/css/css-transforms/animation/transform-box-will-change-transform-layer.html [ ImageOnlyFailure ]

# TODO EXIF-resolution is not supported
imported/w3c/web-platform-tests/density-size-correction/ [ Skip ]

@@ -1992,7 +1992,7 @@ bool KeyframeEffect::computeTransformedExtentViaTransformList(const FloatRect& r

bool applyTransformOrigin = containsRotation(style.transform().operations()) || style.transform().affectedByTransformOrigin();
if (applyTransformOrigin) {
transformOrigin = rendererBox.location() + floatPointForLengthPoint(style.transformOriginXY(), rendererBox.size());
transformOrigin = style.computeTransformOrigin(rendererBox).xy();
// Ignore transformOriginZ because we'll bail if we encounter any 3D transforms.
floatBounds.moveBy(-transformOrigin);
}
@@ -1800,43 +1800,62 @@ bool RenderLayer::updateLayerPosition(OptionSet<UpdateLayerPositionsFlag>* flags
return positionOrOffsetChanged;
}

TransformationMatrix RenderLayer::perspectiveTransform(const LayoutRect& layerRect) const
TransformationMatrix RenderLayer::perspectiveTransform() const
{
// FIXME: [LBSE] Upstream transform support for RenderSVGModelObject derived renderers
if (!is<RenderBox>(renderer()))
return { };

auto& renderBox = downcast<RenderBox>(renderer());
if (!renderBox.hasTransformRelatedProperty())
if (!renderer().hasTransformRelatedProperty())
return { };

const auto& style = renderBox.style();
const auto& style = renderer().style();
if (!style.hasPerspective())
return { };

auto referenceBoxRect = snapRectToDevicePixelsIfNeeded(renderer().transformReferenceBoxRect(style), renderer());
auto snappedLayerRect = snapRectToDevicePixelsIfNeeded(layerRect, renderer());
auto transformReferenceBoxRect = snapRectToDevicePixelsIfNeeded(renderer().transformReferenceBoxRect(style), renderer());
auto perspectiveOrigin = style.computePerspectiveOrigin(transformReferenceBoxRect);

auto perspectiveOrigin = referenceBoxRect.location() - toFloatSize(snappedLayerRect.location()) + floatPointForLengthPoint(style.perspectiveOrigin(), referenceBoxRect.size());
// In the regular case of a non-clipped, non-scrolled GraphicsLayer, all transformations
// (via CSS 'transform' / 'perspective') are applied with respect to a predefined anchor point,
// which depends on the chosen CSS 'transform-box' / 'transform-origin' properties.
//
// A transformation given by the CSS 'transform' property is applied, by translating
// to the 'transform origin', applying the transformation, and translating back.
// When an element specifies a CSS 'perspective' property, the perspective transformation matrix
// that's computed here is propagated to the GraphicsLayer by calling setChildrenTransform().
//
// However the GraphicsLayer platform implementations (e.g. CA on macOS) apply the children transform,
// defined on the parent, with respect to the anchor point of the parent, when rendering child elements.
// This is wrong, as the perspective transformation (applied to a child of the element defining the
// 3d effect), must be independant of the chosen transform-origin (the parents transform origin
// must not affect its children).
//
// To circumvent this, explicitely remove the transform-origin dependency in the perspective matrix.
auto transformOrigin = transformOriginPixelSnappedIfNeeded();

TransformationMatrix transform;
style.unapplyTransformOrigin(transform, transformOrigin);
style.applyPerspective(transform, renderer(), perspectiveOrigin);
style.applyTransformOrigin(transform, transformOrigin);
return transform;
}

// A perspective origin of 0,0 makes the vanishing point in the center of the element.
// We want it to be in the top-left, so subtract half the height and width.
perspectiveOrigin -= snappedLayerRect.size() / 2.0f;
FloatPoint3D RenderLayer::transformOriginPixelSnappedIfNeeded() const
{
if (!renderer().hasTransformRelatedProperty())
return { };

TransformationMatrix t;
t.translate(perspectiveOrigin.x(), perspectiveOrigin.y());
t.applyPerspective(style.usedPerspective(renderer()));
t.translate(-perspectiveOrigin.x(), -perspectiveOrigin.y());
const auto& style = renderer().style();
auto referenceBoxRect = renderer().transformReferenceBoxRect(style);

return t;
auto origin = style.computeTransformOrigin(referenceBoxRect);
if (rendererNeedsPixelSnapping(renderer()))
origin.setXY(roundPointToDevicePixels(LayoutPoint(origin.xy()), renderer().document().deviceScaleFactor()));
return origin;
}

FloatPoint RenderLayer::perspectiveOrigin() const
{
if (!renderer().hasTransformRelatedProperty())
return { };
// FIXME: This uses the wrong, transform-box unaware, geometry.
return floatPointForLengthPoint(renderer().style().perspectiveOrigin(), rendererBorderBoxRect().size());
return floatPointForLengthPoint(renderer().style().perspectiveOrigin(), renderer().transformReferenceBoxRect(renderer().style()).size());
}

static inline bool isContainerForPositioned(RenderLayer& layer, PositionType position, bool establishesTopLayer)
@@ -739,11 +739,12 @@ class RenderLayer : public CanMakeWeakPtr<RenderLayer> {
TransformationMatrix currentTransform(OptionSet<RenderStyle::TransformOperationOption> = RenderStyle::allTransformOperations) const;
TransformationMatrix renderableTransform(OptionSet<PaintBehavior>) const;

// Get the perspective transform, which is applied to transformed sublayers.
// Returns true if the layer has a -webkit-perspective.
// Get the children transform (to apply a perspective on children), which is applied to transformed sublayers, but not this layer.
// Returns true if the layer has a perspective.
// Note that this transform has the perspective-origin baked in.
TransformationMatrix perspectiveTransform(const LayoutRect& layerRect) const;
TransformationMatrix perspectiveTransform() const;
FloatPoint perspectiveOrigin() const;
FloatPoint3D transformOriginPixelSnappedIfNeeded() const;
bool preserves3D() const { return renderer().style().preserves3D(); }
bool has3DTransform() const { return m_transform && !m_transform->isAffine(); }
bool hasTransformedAncestor() const { return m_hasTransformedAncestor; }
@@ -635,16 +635,11 @@ void RenderLayerBacking::updateOpacity(const RenderStyle& style)
m_graphicsLayer->setOpacity(compositingOpacity(style.opacity()));
}

void RenderLayerBacking::updateTransform(const RenderStyle&)
void RenderLayerBacking::updateTransform(const RenderStyle& style)
{
// FIXME: This could use m_owningLayer.transform(), but that currently has transform-origin
// baked into it, and we don't want that.
TransformationMatrix t;
if (m_owningLayer.hasTransform()) {
// FIXME: This uses the wrong, transform-box unaware, geometry.
renderer().applyTransform(t, renderer().style(), snapRectToDevicePixels(m_owningLayer.rendererBorderBoxRect(), deviceScaleFactor()), RenderStyle::individualTransformOperations);
makeMatrixRenderable(t, compositor().canRender3DTransforms());
}
if (m_owningLayer.hasTransform())
m_owningLayer.updateTransformFromStyle(t, style, RenderStyle::individualTransformOperations);

if (m_contentsContainmentLayer) {
m_contentsContainmentLayer->setTransform(t);
@@ -655,20 +650,25 @@ void RenderLayerBacking::updateTransform(const RenderStyle&)

void RenderLayerBacking::updateChildrenTransformAndAnchorPoint(const LayoutRect& primaryGraphicsLayerRect, LayoutSize offsetFromParentGraphicsLayer)
{
auto defaultAnchorPoint = FloatPoint3D { 0.5, 0.5, 0 };
if (!renderer().hasTransformRelatedProperty()) {
auto defaultAnchorPoint = FloatPoint3D { 0.5, 0.5, 0 };
m_graphicsLayer->setAnchorPoint(defaultAnchorPoint);
if (m_contentsContainmentLayer)
m_contentsContainmentLayer->setAnchorPoint(defaultAnchorPoint);

if (m_childContainmentLayer)
m_childContainmentLayer->setAnchorPoint(defaultAnchorPoint);

if (m_scrollContainerLayer)
m_scrollContainerLayer->setAnchorPoint(defaultAnchorPoint);

if (m_scrolledContentsLayer)
m_scrolledContentsLayer->setPreserves3D(false);
return;
}

const auto deviceScaleFactor = this->deviceScaleFactor();
auto borderBoxRect = m_owningLayer.rendererBorderBoxRect();
auto transformOrigin = computeTransformOriginForPainting(borderBoxRect);
auto transformOrigin = m_owningLayer.transformOriginPixelSnappedIfNeeded();
auto layerOffset = roundPointToDevicePixels(toLayoutPoint(offsetFromParentGraphicsLayer), deviceScaleFactor);
auto anchor = FloatPoint3D {
primaryGraphicsLayerRect.width() ? ((layerOffset.x() - primaryGraphicsLayerRect.x()) + transformOrigin.x()) / primaryGraphicsLayerRect.width() : 0.5f,
@@ -683,11 +683,14 @@ void RenderLayerBacking::updateChildrenTransformAndAnchorPoint(const LayoutRect&

auto removeChildrenTransformFromLayers = [&](GraphicsLayer* layerToIgnore = nullptr) {
auto* clippingLayer = this->clippingLayer();
if (clippingLayer && clippingLayer != layerToIgnore)
if (clippingLayer && clippingLayer != layerToIgnore) {
clippingLayer->setChildrenTransform({ });

clippingLayer->setAnchorPoint(defaultAnchorPoint);
}

if (m_scrollContainerLayer && m_scrollContainerLayer != layerToIgnore) {
m_scrollContainerLayer->setChildrenTransform({ });
m_scrollContainerLayer->setAnchorPoint(defaultAnchorPoint);
m_scrolledContentsLayer->setPreserves3D(false);
}

@@ -700,27 +703,34 @@ void RenderLayerBacking::updateChildrenTransformAndAnchorPoint(const LayoutRect&
return;
}

auto layerForChildrenTransform = [&] {
auto layerForChildrenTransform = [&]() -> std::tuple<GraphicsLayer*, FloatRect> {
if (m_scrollContainerLayer) {
ASSERT(is<RenderBox>(renderer())); // Scroll container layers are only created for RenderBox derived renderers.
return std::make_tuple(m_scrollContainerLayer.get(), scrollContainerLayerBox(downcast<RenderBox>(renderer())));
}
if (auto* layer = clippingLayer())
return std::make_tuple(layer, clippingLayerBox(renderer()));

return std::make_tuple(m_graphicsLayer.get(), borderBoxRect);
return std::make_tuple(m_graphicsLayer.get(), renderer().transformReferenceBoxRect());
};

auto [layerForPerspective, perspectiveRelativeBox] = layerForChildrenTransform();
// FIXME: perspectiveRelativeBox isn't quite right here. This needs work: webkit.org/b/211787.
auto perspectiveTransform = owningLayer().perspectiveTransform(perspectiveRelativeBox);

// If we have scrolling layers, we need the children transform on m_scrollContainerLayer to
// affect children of m_scrolledContentsLayer, so set setPreserves3D(true).
if (layerForPerspective == m_scrollContainerLayer)
m_scrolledContentsLayer->setPreserves3D(true);

layerForPerspective->setChildrenTransform(perspectiveTransform);
auto [layerForPerspective, layerForPerspectiveRect] = layerForChildrenTransform();
if (layerForPerspective != m_graphicsLayer) {
// If we have scrolling layers, we need the children transform on m_scrollContainerLayer to
// affect children of m_scrolledContentsLayer, so set setPreserves3D(true).
if (layerForPerspective == m_scrollContainerLayer)
m_scrolledContentsLayer->setPreserves3D(true);

auto perspectiveAnchorPoint = FloatPoint3D {
layerForPerspectiveRect.width() ? (transformOrigin.x() - layerForPerspectiveRect.x()) / layerForPerspectiveRect.width() : 0.5f,
layerForPerspectiveRect.height() ? (transformOrigin.y() - layerForPerspectiveRect.y()) / layerForPerspectiveRect.height() : 0.5f,
transformOrigin.z()
};

layerForPerspective->setAnchorPoint(perspectiveAnchorPoint);
}

layerForPerspective->setChildrenTransform(m_owningLayer.perspectiveTransform());
removeChildrenTransformFromLayers(layerForPerspective);
}

@@ -2980,17 +2990,6 @@ void RenderLayerBacking::updateImageContents(PaintedContentsInfo& contentsInfo)
image->startAnimation();
}

FloatPoint3D RenderLayerBacking::computeTransformOriginForPainting(const LayoutRect& borderBox) const
{
const RenderStyle& style = renderer().style();

FloatPoint3D origin;
origin.setXY(roundPointToDevicePixels(pointForLengthPoint(style.transformOriginXY(), borderBox.size()), deviceScaleFactor()));
origin.setZ(style.transformOriginZ());

return origin;
}

// Return the offset from the top-left of this compositing layer at which the renderer's contents are painted.
LayoutSize RenderLayerBacking::contentOffsetInCompositingLayer() const
{
@@ -331,9 +331,6 @@ class RenderLayerBacking final : public GraphicsLayerClient {
void setBackgroundLayerPaintsFixedRootBackground(bool);

LayoutSize contentOffsetInCompositingLayer() const;
// Result is transform origin in device pixels.
FloatPoint3D computeTransformOriginForPainting(const LayoutRect& borderBox) const;

LayoutSize offsetRelativeToRendererOriginForDescendantLayers() const;

void ensureClippingStackLayers(LayerAncestorClippingStack&);
@@ -1654,6 +1654,7 @@ static bool recompositeChangeRequiresGeometryUpdate(const RenderStyle& oldStyle,
|| oldStyle.translate() != newStyle.translate()
|| oldStyle.scale() != newStyle.scale()
|| oldStyle.rotate() != newStyle.rotate()
|| oldStyle.transformBox() != newStyle.transformBox()
|| oldStyle.transformOriginX() != newStyle.transformOriginX()
|| oldStyle.transformOriginY() != newStyle.transformOriginY()
|| oldStyle.transformOriginZ() != newStyle.transformOriginZ()
@@ -347,7 +347,9 @@ void RenderLayerModelObject::applySVGTransform(TransformationMatrix& transform,

FloatPoint3D originTranslate;
if (options.contains(RenderStyle::TransformOperationOption::TransformOrigin) && affectedByTransformOrigin)
originTranslate = style.applyTransformOrigin(transform, boundingBox);
originTranslate = style.computeTransformOrigin(boundingBox);

style.applyTransformOrigin(transform, originTranslate);

// CSS transforms take precedence over SVG transforms.
if (hasCSSTransform)

0 comments on commit 0d63a64

Please sign in to comment.