blob: 82b4f805b4e9dc3dfc286989c2483a9bcb44c56a [file] [log] [blame]
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "platform/graphics/paint/GeometryMapper.h"
#include "platform/geometry/LayoutRect.h"
#include "platform/runtime_enabled_features.h"
namespace blink {
const TransformationMatrix& GeometryMapper::SourceToDestinationProjection(
const TransformPaintPropertyNode* source,
const TransformPaintPropertyNode* destination) {
DCHECK(source && destination);
bool success = false;
const auto& result =
SourceToDestinationProjectionInternal(source, destination, success);
DCHECK(success);
return result;
}
// Returns flatten(destination_to_screen)^-1 * flatten(source_to_screen)
//
// In case that source and destination are coplanar in tree hierarchy [1],
// computes destination_to_plane_root ^ -1 * source_to_plane_root.
// It can be proved that [2] the result will be the same (except numerical
// errors) when the plane root has invertible screen projection, and this
// offers fallback definition when plane root is singular. For example:
// <div style="transform:rotateY(90deg); overflow:scroll;">
// <div id="A" style="opacity:0.5;">
// <div id="B" style="position:absolute;"></div>
// </div>
// </div>
// Both A and B have non-invertible screen projection, nevertheless it is
// useful to define projection between A and B. Say, the transform may be
// animated in compositor thus become visible.
// As SPv1 treats 3D transforms as compositing trigger, that implies mappings
// within the same compositing layer can only contain 2D transforms, thus
// intra-composited-layer queries are guaranteed to be handled correctly.
//
// [1] As defined by that all local transforms between source and some common
// ancestor 'plane root' and all local transforms between the destination
// and the plane root being flat.
// [2] destination_to_screen = plane_root_to_screen * destination_to_plane_root
// source_to_screen = plane_root_to_screen * source_to_plane_root
// output = flatten(destination_to_screen)^-1 * flatten(source_to_screen)
// = flatten(plane_root_to_screen * destination_to_plane_root)^-1 *
// flatten(plane_root_to_screen * source_to_plane_root)
// Because both destination_to_plane_root and source_to_plane_root are
// already flat,
// = flatten(plane_root_to_screen * flatten(destination_to_plane_root))^-1 *
// flatten(plane_root_to_screen * flatten(source_to_plane_root))
// By flatten lemma [3] flatten(A * flatten(B)) = flatten(A) * flatten(B),
// = flatten(destination_to_plane_root)^-1 *
// flatten(plane_root_to_screen)^-1 *
// flatten(plane_root_to_screen) * flatten(source_to_plane_root)
// If flatten(plane_root_to_screen) is invertible, they cancel out:
// = flatten(destination_to_plane_root)^-1 * flatten(source_to_plane_root)
// = destination_to_plane_root^-1 * source_to_plane_root
// [3] Flatten lemma: https://goo.gl/DNKyOc
const TransformationMatrix&
GeometryMapper::SourceToDestinationProjectionInternal(
const TransformPaintPropertyNode* source,
const TransformPaintPropertyNode* destination,
bool& success) {
DCHECK(source && destination);
DEFINE_STATIC_LOCAL(TransformationMatrix, identity, (TransformationMatrix()));
DEFINE_STATIC_LOCAL(TransformationMatrix, temp, (TransformationMatrix()));
if (source == destination) {
success = true;
return identity;
}
const GeometryMapperTransformCache& source_cache =
source->GetTransformCache();
const GeometryMapperTransformCache& destination_cache =
destination->GetTransformCache();
// Case 1: Check if source and destination are known to be coplanar.
// Even if destination may have invertible screen projection,
// this formula is likely to be numerically more stable.
if (source_cache.plane_root() == destination_cache.plane_root()) {
success = true;
if (source == destination_cache.plane_root())
return destination_cache.from_plane_root();
if (destination == source_cache.plane_root())
return source_cache.to_plane_root();
temp = destination_cache.from_plane_root();
temp.Multiply(source_cache.to_plane_root());
return temp;
}
// Case 2: Check if we can fallback to the canonical definition of
// flatten(destination_to_screen)^-1 * flatten(source_to_screen)
// If flatten(destination_to_screen)^-1 is invalid, we are out of luck.
if (!destination_cache.projection_from_screen_is_valid()) {
success = false;
return identity;
}
// Case 3: Compute:
// flatten(destination_to_screen)^-1 * flatten(source_to_screen)
const auto* root = TransformPaintPropertyNode::Root();
success = true;
if (source == root)
return destination_cache.projection_from_screen();
if (destination == root) {
temp = source_cache.to_screen();
} else {
temp = destination_cache.projection_from_screen();
temp.Multiply(source_cache.to_screen());
}
temp.FlattenTo2d();
return temp;
}
void GeometryMapper::SourceToDestinationRect(
const TransformPaintPropertyNode* source_transform_node,
const TransformPaintPropertyNode* destination_transform_node,
FloatRect& mapping_rect) {
bool success = false;
const TransformationMatrix& source_to_destination =
SourceToDestinationProjectionInternal(
source_transform_node, destination_transform_node, success);
mapping_rect =
success ? source_to_destination.MapRect(mapping_rect) : FloatRect();
}
void GeometryMapper::LocalToAncestorVisualRect(
const PropertyTreeState& local_state,
const PropertyTreeState& ancestor_state,
FloatClipRect& mapping_rect,
OverlayScrollbarClipBehavior clip_behavior) {
bool success = false;
LocalToAncestorVisualRectInternal(local_state, ancestor_state, mapping_rect,
clip_behavior, success);
DCHECK(success);
}
void GeometryMapper::LocalToAncestorVisualRectInternal(
const PropertyTreeState& local_state,
const PropertyTreeState& ancestor_state,
FloatClipRect& rect_to_map,
OverlayScrollbarClipBehavior clip_behavior,
bool& success) {
if (local_state == ancestor_state) {
success = true;
return;
}
if (local_state.Effect() != ancestor_state.Effect()) {
SlowLocalToAncestorVisualRectWithEffects(
local_state, ancestor_state, rect_to_map, clip_behavior, success);
return;
}
const auto& transform_matrix = SourceToDestinationProjectionInternal(
local_state.Transform(), ancestor_state.Transform(), success);
if (!success) {
// A failure implies either source-to-plane or destination-to-plane being
// singular. A notable example of singular source-to-plane from valid CSS:
// <div id="plane" style="transform:rotateY(180deg)">
// <div style="overflow:overflow">
// <div id="ancestor" style="opacity:0.5;">
// <div id="local" style="position:absolute; transform:scaleX(0);">
// </div>
// </div>
// </div>
// </div>
// Either way, the element won't be renderable thus returning empty rect.
success = true;
rect_to_map = FloatClipRect(FloatRect());
return;
}
rect_to_map.Map(transform_matrix);
FloatClipRect clip_rect = LocalToAncestorClipRectInternal(
local_state.Clip(), ancestor_state.Clip(), ancestor_state.Transform(),
clip_behavior, success);
if (success) {
// This is where we propagate the roundedness and tightness of |clip_rect|
// to |rect_to_map|.
rect_to_map.Intersect(clip_rect);
return;
}
if (!RuntimeEnabledFeatures::SlimmingPaintV2Enabled()) {
// On SPv1* we may fail when the paint invalidation container creates an
// overflow clip (in ancestor_state) which is not in localState of an
// out-of-flow positioned descendant. See crbug.com/513108 and layout test
// compositing/overflow/handle-non-ancestor-clip-parent.html (run with
// --enable-prefer-compositing-to-lcd-text) for details.
// Ignore it for SPv1* for now.
success = true;
rect_to_map.ClearIsTight();
}
}
void GeometryMapper::SlowLocalToAncestorVisualRectWithEffects(
const PropertyTreeState& local_state,
const PropertyTreeState& ancestor_state,
FloatClipRect& mapping_rect,
OverlayScrollbarClipBehavior clip_behavior,
bool& success) {
PropertyTreeState last_transform_and_clip_state(local_state.Transform(),
local_state.Clip(), nullptr);
for (const auto* effect = local_state.Effect();
effect && effect != ancestor_state.Effect(); effect = effect->Parent()) {
if (!effect->HasFilterThatMovesPixels())
continue;
DCHECK(effect->OutputClip());
PropertyTreeState transform_and_clip_state(effect->LocalTransformSpace(),
effect->OutputClip(), nullptr);
LocalToAncestorVisualRectInternal(last_transform_and_clip_state,
transform_and_clip_state, mapping_rect,
clip_behavior, success);
if (!success) {
success = true;
mapping_rect = FloatClipRect(FloatRect());
return;
}
mapping_rect = FloatClipRect(effect->MapRect(mapping_rect.Rect()));
last_transform_and_clip_state = transform_and_clip_state;
}
PropertyTreeState final_transform_and_clip_state(
ancestor_state.Transform(), ancestor_state.Clip(), nullptr);
LocalToAncestorVisualRectInternal(last_transform_and_clip_state,
final_transform_and_clip_state,
mapping_rect, clip_behavior, success);
// Many effects (e.g. filters, clip-paths) can make a clip rect not tight.
mapping_rect.ClearIsTight();
}
FloatClipRect GeometryMapper::LocalToAncestorClipRect(
const PropertyTreeState& local_state,
const PropertyTreeState& ancestor_state,
OverlayScrollbarClipBehavior clip_behavior) {
if (local_state.Clip() == ancestor_state.Clip())
return FloatClipRect();
bool success = false;
auto result = LocalToAncestorClipRectInternal(
local_state.Clip(), ancestor_state.Clip(), ancestor_state.Transform(),
clip_behavior, success);
DCHECK(success);
return result;
}
FloatClipRect GeometryMapper::LocalToAncestorClipRectInternal(
const ClipPaintPropertyNode* descendant,
const ClipPaintPropertyNode* ancestor_clip,
const TransformPaintPropertyNode* ancestor_transform,
OverlayScrollbarClipBehavior clip_behavior,
bool& success) {
FloatClipRect clip;
if (descendant == ancestor_clip) {
success = true;
return FloatClipRect();
}
const ClipPaintPropertyNode* clip_node = descendant;
Vector<const ClipPaintPropertyNode*> intermediate_nodes;
GeometryMapperClipCache::ClipAndTransform clip_and_transform(
ancestor_clip, ancestor_transform, clip_behavior);
// Iterate over the path from localState.clip to ancestor_state.clip. Stop if
// we've found a memoized (precomputed) clip for any particular node.
while (clip_node && clip_node != ancestor_clip) {
if (const FloatClipRect* cached_clip =
clip_node->GetClipCache().GetCachedClip(clip_and_transform)) {
clip = *cached_clip;
break;
}
intermediate_nodes.push_back(clip_node);
clip_node = clip_node->Parent();
}
if (!clip_node) {
success = false;
if (!RuntimeEnabledFeatures::SlimmingPaintV2Enabled()) {
// On SPv1 we may fail when the paint invalidation container creates an
// overflow clip (in ancestor_state) which is not in localState of an
// out-of-flow positioned descendant. See crbug.com/513108 and layout
// test compositing/overflow/handle-non-ancestor-clip-parent.html (run
// with --enable-prefer-compositing-to-lcd-text) for details.
// Ignore it for SPv1 for now.
success = true;
}
FloatClipRect loose_infinite;
loose_infinite.ClearIsTight();
return loose_infinite;
}
// Iterate down from the top intermediate node found in the previous loop,
// computing and memoizing clip rects as we go.
for (auto it = intermediate_nodes.rbegin(); it != intermediate_nodes.rend();
++it) {
const TransformationMatrix& transform_matrix =
SourceToDestinationProjectionInternal((*it)->LocalTransformSpace(),
ancestor_transform, success);
if (!success) {
success = true;
return FloatClipRect(FloatRect());
}
// This is where we generate the roundedness and tightness of clip rect
// from clip and transform properties, and propagate them to |clip|.
FloatClipRect mapped_rect(clip_behavior ==
kExcludeOverlayScrollbarSizeForHitTesting
? (*it)->ClipRectExcludingOverlayScrollbars()
: (*it)->ClipRect());
mapped_rect.Map(transform_matrix);
clip.Intersect(mapped_rect);
(*it)->GetClipCache().SetCachedClip(clip_and_transform, clip);
}
DCHECK(*descendant->GetClipCache().GetCachedClip(clip_and_transform) == clip);
success = true;
return clip;
}
void GeometryMapper::ClearCache() {
GeometryMapperTransformCache::ClearCache();
GeometryMapperClipCache::ClearCache();
}
} // namespace blink