| // 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 "third_party/blink/renderer/platform/graphics/paint/geometry_mapper.h" |
| |
| #include "third_party/blink/renderer/platform/geometry/layout_rect.h" |
| #include "third_party/blink/renderer/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); |
| 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, ()); |
| DEFINE_STATIC_LOCAL(TransformationMatrix, temp, ()); |
| |
| source = source->Unalias(); |
| destination = destination->Unalias(); |
| |
| if (source == destination) { |
| success = true; |
| return identity; |
| } |
| |
| if (source->Parent() && destination == source->Parent()->Unalias() && |
| // The result will be translate(origin)*matrix*translate(-origin) which |
| // equals to matrix if the origin is zero or if the matrix is just |
| // identity or 2d translation. |
| (source->Origin().IsZero() || source->IsIdentityOr2DTranslation())) { |
| success = true; |
| return source->Matrix(); |
| } |
| |
| const GeometryMapperTransformCache& source_cache = |
| source->GetTransformCache(); |
| const GeometryMapperTransformCache& destination_cache = |
| destination->GetTransformCache(); |
| |
| // Case 1a (fast path of case 1b): check if source and destination are under |
| // the same 2d translation root. |
| if (source_cache.root_of_2d_translation() == |
| destination_cache.root_of_2d_translation()) { |
| success = true; |
| if (source == destination_cache.root_of_2d_translation()) |
| return destination_cache.from_2d_translation_root(); |
| if (destination == source_cache.root_of_2d_translation()) |
| return source_cache.to_2d_translation_root(); |
| temp = destination_cache.from_2d_translation_root(); |
| temp.Translate(source_cache.to_2d_translation_root().E(), |
| source_cache.to_2d_translation_root().F()); |
| return temp; |
| } |
| |
| // Case 1b: 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. |
| // Screen transform data are updated lazily because they are rarely used. |
| source->UpdateScreenTransform(); |
| destination->UpdateScreenTransform(); |
| 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; |
| } |
| |
| bool GeometryMapper::LocalToAncestorVisualRect( |
| const PropertyTreeState& local_state, |
| const PropertyTreeState& ancestor_state, |
| FloatClipRect& mapping_rect, |
| OverlayScrollbarClipBehavior clip_behavior, |
| InclusiveIntersectOrNot inclusive_behavior) { |
| bool success = false; |
| bool result = LocalToAncestorVisualRectInternal(local_state, ancestor_state, |
| mapping_rect, clip_behavior, |
| inclusive_behavior, success); |
| DCHECK(success); |
| return result; |
| } |
| |
| bool GeometryMapper::PointVisibleInAncestorSpace( |
| const PropertyTreeState& local_state, |
| const PropertyTreeState& ancestor_state, |
| const FloatPoint& local_point) { |
| auto* ancestor_clip = ancestor_state.Clip()->Unalias(); |
| for (const auto* clip = local_state.Clip()->Unalias(); |
| clip && clip != ancestor_clip; clip = SafeUnalias(clip->Parent())) { |
| FloatPoint mapped_point = |
| SourceToDestinationProjection(local_state.Transform(), |
| clip->LocalTransformSpace()) |
| .MapPoint(local_point); |
| |
| if (!clip->ClipRect().IntersectsQuad( |
| FloatRect(mapped_point, FloatSize(1, 1)))) |
| return false; |
| |
| if (clip->ClipPath() && !clip->ClipPath()->Contains(mapped_point)) |
| return false; |
| } |
| |
| return true; |
| } |
| |
| bool GeometryMapper::LocalToAncestorVisualRectInternal( |
| const PropertyTreeState& local_state, |
| const PropertyTreeState& ancestor_state, |
| FloatClipRect& rect_to_map, |
| OverlayScrollbarClipBehavior clip_behavior, |
| InclusiveIntersectOrNot inclusive_behavior, |
| bool& success) { |
| if (local_state == ancestor_state) { |
| success = true; |
| return true; |
| } |
| |
| if (SafeUnalias(local_state.Effect()) != |
| SafeUnalias(ancestor_state.Effect())) { |
| return SlowLocalToAncestorVisualRectWithEffects( |
| local_state, ancestor_state, rect_to_map, clip_behavior, |
| inclusive_behavior, success); |
| } |
| |
| 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 false; |
| } |
| rect_to_map.Map(transform_matrix); |
| |
| FloatClipRect clip_rect = LocalToAncestorClipRectInternal( |
| local_state.Clip(), ancestor_state.Clip(), ancestor_state.Transform(), |
| clip_behavior, inclusive_behavior, success); |
| if (success) { |
| // This is where we propagate the roundedness and tightness of |clip_rect| |
| // to |rect_to_map|. |
| if (inclusive_behavior == kInclusiveIntersect) |
| return rect_to_map.InclusiveIntersect(clip_rect); |
| rect_to_map.Intersect(clip_rect); |
| return !rect_to_map.Rect().IsEmpty(); |
| } |
| |
| if (!RuntimeEnabledFeatures::CompositeAfterPaintEnabled()) { |
| // 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 web 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(); |
| } |
| return !rect_to_map.Rect().IsEmpty(); |
| } |
| |
| bool GeometryMapper::SlowLocalToAncestorVisualRectWithEffects( |
| const PropertyTreeState& local_state, |
| const PropertyTreeState& ancestor_state, |
| FloatClipRect& mapping_rect, |
| OverlayScrollbarClipBehavior clip_behavior, |
| InclusiveIntersectOrNot inclusive_behavior, |
| bool& success) { |
| PropertyTreeState last_transform_and_clip_state(local_state.Transform(), |
| local_state.Clip(), nullptr); |
| |
| auto* ancestor_effect = ancestor_state.Effect()->Unalias(); |
| for (const auto* effect = local_state.Effect()->Unalias(); |
| effect && effect != ancestor_effect; |
| effect = SafeUnalias(effect->Parent())) { |
| if (!effect->HasFilterThatMovesPixels()) |
| continue; |
| |
| DCHECK(effect->OutputClip()); |
| PropertyTreeState transform_and_clip_state(effect->LocalTransformSpace(), |
| effect->OutputClip(), nullptr); |
| bool intersects = LocalToAncestorVisualRectInternal( |
| last_transform_and_clip_state, transform_and_clip_state, mapping_rect, |
| clip_behavior, inclusive_behavior, success); |
| if (!success || !intersects) { |
| success = true; |
| mapping_rect = FloatClipRect(FloatRect()); |
| return false; |
| } |
| |
| 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, inclusive_behavior, success); |
| |
| // Many effects (e.g. filters, clip-paths) can make a clip rect not tight. |
| mapping_rect.ClearIsTight(); |
| return !mapping_rect.Rect().IsEmpty(); |
| } |
| |
| FloatClipRect GeometryMapper::LocalToAncestorClipRect( |
| const PropertyTreeState& local_state, |
| const PropertyTreeState& ancestor_state, |
| OverlayScrollbarClipBehavior clip_behavior) { |
| if (local_state.Clip()->Unalias() == ancestor_state.Clip()->Unalias()) |
| return FloatClipRect(); |
| |
| bool success = false; |
| auto result = LocalToAncestorClipRectInternal( |
| local_state.Clip(), ancestor_state.Clip(), ancestor_state.Transform(), |
| clip_behavior, kNonInclusiveIntersect, success); |
| DCHECK(success); |
| |
| // Many effects (e.g. filters, clip-paths) can make a clip rect not tight. |
| if (SafeUnalias(local_state.Effect()) != SafeUnalias(ancestor_state.Effect())) |
| result.ClearIsTight(); |
| |
| return result; |
| } |
| |
| static FloatClipRect GetClipRect(const ClipPaintPropertyNode* clip_node, |
| OverlayScrollbarClipBehavior clip_behavior) { |
| clip_node = clip_node->Unalias(); |
| FloatClipRect clip_rect( |
| UNLIKELY(clip_behavior == kExcludeOverlayScrollbarSizeForHitTesting) |
| ? clip_node->ClipRectExcludingOverlayScrollbars() |
| : clip_node->ClipRect()); |
| if (clip_node->ClipPath()) |
| clip_rect.ClearIsTight(); |
| return clip_rect; |
| } |
| |
| FloatClipRect GeometryMapper::LocalToAncestorClipRectInternal( |
| const ClipPaintPropertyNode* descendant, |
| const ClipPaintPropertyNode* ancestor_clip, |
| const TransformPaintPropertyNode* ancestor_transform, |
| OverlayScrollbarClipBehavior clip_behavior, |
| InclusiveIntersectOrNot inclusive_behavior, |
| bool& success) { |
| descendant = descendant->Unalias(); |
| ancestor_clip = ancestor_clip->Unalias(); |
| if (descendant == ancestor_clip) { |
| success = true; |
| return FloatClipRect(); |
| } |
| ancestor_transform = ancestor_transform->Unalias(); |
| if (SafeUnalias(descendant->Parent()) == ancestor_clip && |
| descendant->LocalTransformSpace() == ancestor_transform) { |
| success = true; |
| return GetClipRect(descendant, clip_behavior); |
| } |
| |
| FloatClipRect clip; |
| 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) { |
| const FloatClipRect* cached_clip = nullptr; |
| // Inclusive intersected clips are not cached at present. |
| if (inclusive_behavior != kInclusiveIntersect) |
| cached_clip = clip_node->GetClipCache().GetCachedClip(clip_and_transform); |
| |
| if (cached_clip) { |
| clip = *cached_clip; |
| break; |
| } |
| |
| intermediate_nodes.push_back(clip_node); |
| clip_node = SafeUnalias(clip_node->Parent()); |
| } |
| if (!clip_node) { |
| success = false; |
| if (!RuntimeEnabledFeatures::CompositeAfterPaintEnabled()) { |
| // 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(GetClipRect((*it), clip_behavior)); |
| mapped_rect.Map(transform_matrix); |
| if (inclusive_behavior == kInclusiveIntersect) { |
| clip.InclusiveIntersect(mapped_rect); |
| } else { |
| clip.Intersect(mapped_rect); |
| // Inclusive intersected clips are not cached at present. |
| (*it)->GetClipCache().SetCachedClip(clip_and_transform, clip); |
| } |
| } |
| // Inclusive intersected clips are not cached at present. |
| DCHECK(inclusive_behavior == kInclusiveIntersect || |
| *descendant->GetClipCache().GetCachedClip(clip_and_transform) == clip); |
| success = true; |
| return clip; |
| } |
| |
| void GeometryMapper::ClearCache() { |
| GeometryMapperTransformCache::ClearCache(); |
| GeometryMapperClipCache::ClearCache(); |
| } |
| |
| } // namespace blink |