MotionPathUtils.cpp (33425B)
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */ 3 /* This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 #include "mozilla/MotionPathUtils.h" 8 9 #include <math.h> 10 11 #include "gfxPlatform.h" 12 #include "mozilla/RefPtr.h" 13 #include "mozilla/SVGObserverUtils.h" 14 #include "mozilla/ShapeUtils.h" 15 #include "mozilla/dom/SVGGeometryElement.h" 16 #include "mozilla/dom/SVGPathData.h" 17 #include "mozilla/dom/SVGViewportElement.h" 18 #include "mozilla/gfx/2D.h" 19 #include "mozilla/gfx/Matrix.h" 20 #include "mozilla/layers/LayersMessages.h" 21 #include "nsIFrame.h" 22 #include "nsLayoutUtils.h" 23 #include "nsStyleTransformMatrix.h" 24 25 namespace mozilla { 26 27 using nsStyleTransformMatrix::TransformReferenceBox; 28 29 /* static */ 30 CSSPoint MotionPathUtils::ComputeAnchorPointAdjustment(const nsIFrame& aFrame) { 31 if (!aFrame.HasAnyStateBits(NS_FRAME_SVG_LAYOUT)) { 32 return {}; 33 } 34 35 auto transformBox = aFrame.StyleDisplay()->mTransformBox; 36 if (transformBox == StyleTransformBox::ViewBox || 37 transformBox == StyleTransformBox::BorderBox) { 38 return {}; 39 } 40 41 if (aFrame.IsSVGContainerFrame()) { 42 nsRect boxRect = nsLayoutUtils::ComputeSVGReferenceRect( 43 const_cast<nsIFrame*>(&aFrame), StyleGeometryBox::FillBox); 44 return CSSPoint::FromAppUnits(boxRect.TopLeft()); 45 } 46 return CSSPoint::FromAppUnits(aFrame.GetPosition()); 47 } 48 49 // Convert the StyleCoordBox into the StyleGeometryBox in CSS layout. 50 // https://drafts.csswg.org/css-box-4/#keywords 51 static StyleGeometryBox CoordBoxToGeometryBoxInCSSLayout( 52 StyleCoordBox aCoordBox) { 53 switch (aCoordBox) { 54 case StyleCoordBox::ContentBox: 55 return StyleGeometryBox::ContentBox; 56 case StyleCoordBox::PaddingBox: 57 return StyleGeometryBox::PaddingBox; 58 case StyleCoordBox::BorderBox: 59 return StyleGeometryBox::BorderBox; 60 case StyleCoordBox::FillBox: 61 return StyleGeometryBox::ContentBox; 62 case StyleCoordBox::StrokeBox: 63 case StyleCoordBox::ViewBox: 64 return StyleGeometryBox::BorderBox; 65 } 66 MOZ_ASSERT_UNREACHABLE("Unknown coord-box type"); 67 return StyleGeometryBox::BorderBox; 68 } 69 70 /* static */ 71 const nsIFrame* MotionPathUtils::GetOffsetPathReferenceBox( 72 const nsIFrame* aFrame, nsRect& aOutputRect) { 73 const StyleOffsetPath& offsetPath = aFrame->StyleDisplay()->mOffsetPath; 74 if (offsetPath.IsNone()) { 75 return nullptr; 76 } 77 78 if (aFrame->HasAnyStateBits(NS_FRAME_SVG_LAYOUT)) { 79 MOZ_ASSERT(aFrame->GetContent()->IsSVGElement()); 80 auto* viewportElement = 81 dom::SVGElement::FromNode(aFrame->GetContent())->GetCtx(); 82 aOutputRect = nsLayoutUtils::ComputeSVGOriginBox(viewportElement); 83 return viewportElement ? viewportElement->GetPrimaryFrame() : nullptr; 84 } 85 86 const nsIFrame* containingBlock = aFrame->GetContainingBlock(); 87 const StyleCoordBox coordBox = offsetPath.IsCoordBox() 88 ? offsetPath.AsCoordBox() 89 : offsetPath.AsOffsetPath().coord_box; 90 aOutputRect = nsLayoutUtils::ComputeHTMLReferenceRect( 91 containingBlock, CoordBoxToGeometryBoxInCSSLayout(coordBox)); 92 return containingBlock; 93 } 94 95 /* static */ 96 CSSCoord MotionPathUtils::GetRayContainReferenceSize(nsIFrame* aFrame) { 97 // We use the border-box size to calculate the reduced path length when using 98 // "contain" keyword. 99 // https://drafts.fxtf.org/motion-1/#valdef-ray-contain 100 // 101 // Note: Per the spec, border-box is treated as stroke-box in the SVG context, 102 // https://drafts.csswg.org/css-box-4/#valdef-box-border-box 103 104 // To calculate stroke bounds for an element with `non-scaling-stroke` we 105 // need to resolve its transform to its outer-svg, but to resolve that 106 // transform when it has `transform-box:stroke-box` (or `border-box`) 107 // may require its stroke bounds. There's no ideal way to break this 108 // cyclical dependency, but we break it by using the FillBox. 109 // https://github.com/w3c/csswg-drafts/issues/9640 110 111 const auto size = CSSSize::FromAppUnits( 112 (aFrame->HasAnyStateBits(NS_FRAME_SVG_LAYOUT) 113 ? nsLayoutUtils::ComputeSVGReferenceRect( 114 aFrame, 115 aFrame->StyleSVGReset()->HasNonScalingStroke() 116 ? StyleGeometryBox::FillBox 117 : StyleGeometryBox::StrokeBox, 118 nsLayoutUtils::MayHaveNonScalingStrokeCyclicDependency::Yes) 119 : nsLayoutUtils::ComputeHTMLReferenceRect( 120 aFrame, StyleGeometryBox::BorderBox)) 121 .Size()); 122 return std::max(size.width, size.height); 123 } 124 125 /* static */ 126 nsTArray<nscoord> MotionPathUtils::ComputeBorderRadii( 127 const StyleBorderRadius& aBorderRadius, const nsRect& aCoordBox) { 128 const nsRect insetRect = ShapeUtils::ComputeInsetRect( 129 StyleRect<LengthPercentage>::WithAllSides(LengthPercentage::Zero()), 130 aCoordBox); 131 nsTArray<nscoord> result; 132 nsRectCornerRadii radii; 133 if (ShapeUtils::ComputeRectRadii(aBorderRadius, aCoordBox, insetRect, 134 radii)) { 135 result.SetCapacity(8); 136 for (auto hc : AllPhysicalHalfCorners()) { 137 result.AppendElement(radii[hc]); 138 } 139 } 140 return result; 141 } 142 143 // The distance is measured between the origin and the intersection of the ray 144 // with the reference box of the containing block. 145 // Note: |aOrigin| and |aContaingBlock| should be in the same coordinate system 146 // (i.e. the nsIFrame::mRect of the containing block). 147 // https://drafts.fxtf.org/motion-1/#size-sides 148 static CSSCoord ComputeSides(const CSSPoint& aOrigin, 149 const CSSRect& aContainingBlock, 150 const StyleAngle& aAngle) { 151 const CSSPoint& topLeft = aContainingBlock.TopLeft(); 152 // Given an acute angle |theta| (i.e. |t|) of a right-angled triangle, the 153 // hypotenuse |h| is the side that connects the two acute angles. The side 154 // |b| adjacent to |theta| is the side of the triangle that connects |theta| 155 // to the right angle. 156 // 157 // e.g. if the angle |t| is 0 ~ 90 degrees, and b * tan(theta) <= b', 158 // h = b / cos(t): 159 // b*tan(t) 160 // (topLeft) #--------*-----*--# (aContainingBlock.XMost(), topLeft.y) 161 // | | / | 162 // | | / | 163 // | b h | 164 // | |t/ | 165 // | |/ | 166 // (aOrigin) *---b'---* (aContainingBlock.XMost(), aOrigin.y) 167 // | | | 168 // | | | 169 // | | | 170 // | | | 171 // | | | 172 // #-----------------# (aContainingBlock.XMost(), 173 // (topLeft.x, aContainingBlock.YMost()) 174 // aContainingBlock.YMost()) 175 const double theta = aAngle.ToRadians(); 176 double sint = std::sin(theta); 177 double cost = std::cos(theta); 178 179 const double b = cost >= 0 ? aOrigin.y.value - topLeft.y 180 : aContainingBlock.YMost() - aOrigin.y.value; 181 const double bPrime = sint >= 0 ? aContainingBlock.XMost() - aOrigin.x.value 182 : aOrigin.x.value - topLeft.x; 183 sint = std::fabs(sint); 184 cost = std::fabs(cost); 185 186 // The trigonometric formula here doesn't work well if |theta| is 0deg or 187 // 90deg, so we handle these edge cases first. 188 if (sint < std::numeric_limits<double>::epsilon()) { 189 // For 0deg (or 180deg), we use |b| directly. 190 return static_cast<float>(b); 191 } 192 193 if (cost < std::numeric_limits<double>::epsilon()) { 194 // For 90deg (or 270deg), we use |bPrime| directly. This can also avoid 0/0 195 // if both |b| and |cost| are 0.0. (i.e. b / cost). 196 return static_cast<float>(bPrime); 197 } 198 199 // Note: The following formula works well only when 0 < theta < 90deg. So we 200 // handle 0deg and 90deg above first. 201 // 202 // If |b * tan(theta)| is larger than |bPrime|, the intersection is 203 // on the other side, and |b'| is the opposite side of angle |theta| in this 204 // case. 205 // 206 // e.g. If b * tan(theta) > b', h = b' / sin(theta): 207 // *----* 208 // | | 209 // | /| 210 // b /t| 211 // |t/ | 212 // |/ | 213 // *-b'-* 214 if (b * sint > bPrime * cost) { 215 return bPrime / sint; 216 } 217 return b / cost; 218 } 219 220 // Compute the position of "at <position>" together with offset starting 221 // position (i.e. offset-position). 222 static nsPoint ComputePosition(const StylePositionOrAuto& aAtPosition, 223 const StyleOffsetPosition& aOffsetPosition, 224 const nsRect& aCoordBox, 225 const nsPoint& aCurrentCoord) { 226 if (aAtPosition.IsPosition()) { 227 // Resolve this by using the <position> to position a 0x0 object area within 228 // the box’s containing block. 229 return ShapeUtils::ComputePosition(aAtPosition.AsPosition(), aCoordBox); 230 } 231 232 MOZ_ASSERT(aAtPosition.IsAuto(), "\"at <position>\" should be omitted"); 233 234 // Use the offset starting position of the element, given by offset-position. 235 // https://drafts.fxtf.org/motion-1/#valdef-ray-at-position 236 if (aOffsetPosition.IsPosition()) { 237 return ShapeUtils::ComputePosition(aOffsetPosition.AsPosition(), aCoordBox); 238 } 239 240 if (aOffsetPosition.IsNormal()) { 241 // If the element doesn’t have an offset starting position either, it 242 // behaves as at center. 243 const StylePosition& center = StylePosition::FromPercentage(0.5); 244 return ShapeUtils::ComputePosition(center, aCoordBox); 245 } 246 247 MOZ_ASSERT(aOffsetPosition.IsAuto()); 248 return aCurrentCoord; 249 } 250 251 static CSSCoord ComputeRayPathLength(const StyleRaySize aRaySizeType, 252 const StyleAngle& aAngle, 253 const CSSPoint& aOrigin, 254 const CSSRect& aContainingBlock) { 255 if (aRaySizeType == StyleRaySize::Sides) { 256 // If the initial position is not within the box, the distance is 0. 257 // 258 // Note: If the origin is at XMost() (and/or YMost()), we should consider it 259 // to be inside containing block (because we expect 100% x (or y) coordinate 260 // is still to be considered inside the containing block. 261 if (!aContainingBlock.ContainsInclusively(aOrigin)) { 262 return 0.0; 263 } 264 265 return ComputeSides(aOrigin, aContainingBlock, aAngle); 266 } 267 268 // left: the length between the origin and the left side. 269 // right: the length between the origin and the right side. 270 // top: the length between the origin and the top side. 271 // bottom: the lenght between the origin and the bottom side. 272 const CSSPoint& topLeft = aContainingBlock.TopLeft(); 273 const CSSCoord left = std::abs(aOrigin.x - topLeft.x); 274 const CSSCoord right = std::abs(aContainingBlock.XMost() - aOrigin.x); 275 const CSSCoord top = std::abs(aOrigin.y - topLeft.y); 276 const CSSCoord bottom = std::abs(aContainingBlock.YMost() - aOrigin.y); 277 278 switch (aRaySizeType) { 279 case StyleRaySize::ClosestSide: 280 return std::min({left, right, top, bottom}); 281 282 case StyleRaySize::FarthestSide: 283 return std::max({left, right, top, bottom}); 284 285 case StyleRaySize::ClosestCorner: 286 case StyleRaySize::FarthestCorner: { 287 CSSCoord h = 0; 288 CSSCoord v = 0; 289 if (aRaySizeType == StyleRaySize::ClosestCorner) { 290 h = std::min(left, right); 291 v = std::min(top, bottom); 292 } else { 293 h = std::max(left, right); 294 v = std::max(top, bottom); 295 } 296 return sqrt(h.value * h.value + v.value * v.value); 297 } 298 case StyleRaySize::Sides: 299 MOZ_ASSERT_UNREACHABLE("Unsupported ray size"); 300 } 301 302 return 0.0; 303 } 304 305 static CSSCoord ComputeRayUsedDistance( 306 const StyleRayFunction& aRay, const LengthPercentage& aDistance, 307 const CSSCoord& aPathLength, const CSSCoord& aRayContainReferenceLength) { 308 CSSCoord usedDistance = aDistance.ResolveToCSSPixels(aPathLength); 309 if (!aRay.contain) { 310 return usedDistance; 311 } 312 313 // The length of the offset path is reduced so that the element stays within 314 // the containing block even at offset-distance: 100%. Specifically, the 315 // path’s length is reduced by half the width or half the height of the 316 // element’s border box, whichever is larger, and floored at zero. 317 // https://drafts.fxtf.org/motion-1/#valdef-ray-contain 318 return std::max((usedDistance - aRayContainReferenceLength / 2.0f).value, 319 0.0f); 320 } 321 322 /* static */ 323 Maybe<ResolvedMotionPathData> MotionPathUtils::ResolveMotionPath( 324 const OffsetPathData& aPath, const LengthPercentage& aDistance, 325 const StyleOffsetRotate& aRotate, const StylePositionOrAuto& aAnchor, 326 const StyleOffsetPosition& aPosition, const CSSPoint& aTransformOrigin, 327 TransformReferenceBox& aRefBox, const CSSPoint& aAnchorPointAdjustment) { 328 if (aPath.IsNone()) { 329 return Nothing(); 330 } 331 332 // Compute the point and angle for creating the equivalent translate and 333 // rotate. 334 double directionAngle = 0.0; 335 gfx::Point point; 336 if (aPath.IsShape()) { 337 const auto& data = aPath.AsShape(); 338 RefPtr<gfx::Path> path = data.mGfxPath; 339 MOZ_ASSERT(path, "The empty path is not allowed"); 340 341 // Per the spec, we have to convert offset distance to pixels, with 100% 342 // being converted to total length. So here |gfxPath| is built with CSS 343 // pixel, and we calculate |pathLength| and |computedDistance| with CSS 344 // pixel as well. 345 gfx::Float pathLength = path->ComputeLength(); 346 gfx::Float usedDistance = 347 aDistance.ResolveToCSSPixels(CSSCoord(pathLength)); 348 if (data.mIsClosedLoop) { 349 // Per the spec, let used offset distance be equal to offset distance 350 // modulus the total length of the path. If the total length of the path 351 // is 0, used offset distance is also 0. 352 usedDistance = pathLength > 0.0 ? fmod(usedDistance, pathLength) : 0.0; 353 // We make sure |usedDistance| is 0.0 or a positive value. 354 if (usedDistance < 0.0) { 355 usedDistance += pathLength; 356 } 357 } else { 358 // Per the spec, for unclosed interval, let used offset distance be equal 359 // to offset distance clamped by 0 and the total length of the path. 360 usedDistance = std::clamp(usedDistance, 0.0f, pathLength); 361 } 362 gfx::Point tangent; 363 point = path->ComputePointAtLength(usedDistance, &tangent); 364 // Basically, |point| should be a relative distance between the current 365 // position and the target position. The built |path| is in the coordinate 366 // system of its containing block. Therefore, we have to take the current 367 // position of this box into account to offset the translation so it's final 368 // position is not affected by other boxes in the same containing block. 369 point -= NSPointToPoint(data.mCurrentPosition, AppUnitsPerCSSPixel()); 370 // If the path length is 0, it's unlikely to get a valid tangent angle, e.g. 371 // it may be (0, 0). And so we may get an undefined value from atan2(). 372 // Therefore, we use 0rad as the default behavior. 373 directionAngle = 374 pathLength < std::numeric_limits<gfx::Float>::epsilon() 375 ? 0.0 376 : atan2((double)tangent.y, (double)tangent.x); // in Radian. 377 } else if (aPath.IsRay()) { 378 const auto& ray = aPath.AsRay(); 379 MOZ_ASSERT(ray.mRay); 380 381 // Compute the origin, where the ray’s line begins (the 0% position). 382 // https://drafts.fxtf.org/motion-1/#ray-origin 383 const CSSPoint origin = CSSPoint::FromAppUnits(ComputePosition( 384 ray.mRay->position, aPosition, ray.mCoordBox, ray.mCurrentPosition)); 385 const CSSCoord pathLength = 386 ComputeRayPathLength(ray.mRay->size, ray.mRay->angle, origin, 387 CSSRect::FromAppUnits(ray.mCoordBox)); 388 const CSSCoord usedDistance = ComputeRayUsedDistance( 389 *ray.mRay, aDistance, pathLength, ray.mContainReferenceLength); 390 391 // 0deg pointing up and positive angles representing clockwise rotation. 392 directionAngle = 393 StyleAngle{ray.mRay->angle.ToDegrees() - 90.0f}.ToRadians(); 394 395 // The vector from the current position of this box to the origin of this 396 // polar coordinate system. 397 const gfx::Point vectorToOrigin = 398 (origin - CSSPoint::FromAppUnits(ray.mCurrentPosition)) 399 .ToUnknownPoint(); 400 // |vectorToOrigin| + The vector from the origin to this polar coordinate, 401 // (|usedDistance|, |directionAngle|), i.e. the vector from the current 402 // position to this polar coordinate. 403 point = 404 vectorToOrigin + 405 gfx::Point(usedDistance * static_cast<gfx::Float>(cos(directionAngle)), 406 usedDistance * static_cast<gfx::Float>(sin(directionAngle))); 407 } else { 408 MOZ_ASSERT_UNREACHABLE("Unsupported offset-path value"); 409 return Nothing(); 410 } 411 412 // If |rotate.auto_| is true, the element should be rotated by the angle of 413 // the direction (i.e. directional tangent vector) of the offset-path, and the 414 // computed value of <angle> is added to this. 415 // Otherwise, the element has a constant clockwise rotation transformation 416 // applied to it by the specified rotation angle. (i.e. Don't need to 417 // consider the direction of the path.) 418 gfx::Float angle = static_cast<gfx::Float>( 419 (aRotate.auto_ ? directionAngle : 0.0) + aRotate.angle.ToRadians()); 420 421 // Compute the offset for motion path translate. 422 // Bug 1559232: the translate parameters will be adjusted more after we 423 // support offset-position. 424 // Per the spec, the default offset-anchor is `auto`, so initialize the anchor 425 // point to transform-origin. 426 CSSPoint anchorPoint(aTransformOrigin); 427 gfx::Point shift; 428 if (!aAnchor.IsAuto()) { 429 const auto& pos = aAnchor.AsPosition(); 430 anchorPoint = nsStyleTransformMatrix::Convert2DPosition( 431 pos.horizontal, pos.vertical, aRefBox); 432 // We need this value to shift the origin from transform-origin to 433 // offset-anchor (and vice versa). 434 // See nsStyleTransformMatrix::ReadTransform for more details. 435 shift = (anchorPoint - aTransformOrigin).ToUnknownPoint(); 436 } 437 438 anchorPoint += aAnchorPointAdjustment; 439 440 return Some(ResolvedMotionPathData{point - anchorPoint.ToUnknownPoint(), 441 angle, shift}); 442 } 443 444 static inline bool IsClosedLoop(const StyleSVGPathData& aPathData) { 445 return !aPathData._0.AsSpan().empty() && 446 aPathData._0.AsSpan().rbegin()->IsClose(); 447 } 448 449 // Create a path for "inset(0 round X)", where X is the value of border-radius 450 // on the element that establishes the containing block for this element. 451 static already_AddRefed<gfx::Path> BuildSimpleInsetPath( 452 const StyleBorderRadius& aBorderRadius, const nsRect& aCoordBox, 453 gfx::PathBuilder* aPathBuilder) { 454 if (!aPathBuilder) { 455 return nullptr; 456 } 457 458 const nsRect insetRect = ShapeUtils::ComputeInsetRect( 459 StyleRect<LengthPercentage>::WithAllSides(LengthPercentage::Zero()), 460 aCoordBox); 461 nsRectCornerRadii radii; 462 const bool hasRadii = 463 ShapeUtils::ComputeRectRadii(aBorderRadius, aCoordBox, insetRect, radii); 464 return ShapeUtils::BuildRectPath(insetRect, hasRadii ? &radii : nullptr, 465 aCoordBox, AppUnitsPerCSSPixel(), 466 aPathBuilder); 467 } 468 469 // Create a path for `path("m 0 0")`, which is the default URL path if we cannot 470 // resolve a SVG shape element. 471 // https://drafts.fxtf.org/motion-1/#valdef-offset-path-url 472 static already_AddRefed<gfx::Path> BuildDefaultPathForURL( 473 gfx::PathBuilder* aBuilder) { 474 if (!aBuilder) { 475 return nullptr; 476 } 477 478 using CommandEndPoint = 479 StyleCommandEndPoint<StyleShapePosition<StyleCSSFloat>, StyleCSSFloat>; 480 Array<const StylePathCommand, 1> array( 481 StylePathCommand::Move(CommandEndPoint::ByCoordinate({0.0, 0.0}))); 482 return SVGPathData::BuildPath(array, aBuilder, StyleStrokeLinecap::Butt, 0.0); 483 } 484 485 // Generate data for motion path on the main thread. 486 static OffsetPathData GenerateOffsetPathData(const nsIFrame* aFrame) { 487 const StyleOffsetPath& offsetPath = aFrame->StyleDisplay()->mOffsetPath; 488 if (offsetPath.IsNone()) { 489 return OffsetPathData::None(); 490 } 491 492 // Handle ray(). 493 if (offsetPath.IsRay()) { 494 nsRect coordBox; 495 const nsIFrame* containingBlockFrame = 496 MotionPathUtils::GetOffsetPathReferenceBox(aFrame, coordBox); 497 return !containingBlockFrame 498 ? OffsetPathData::None() 499 : OffsetPathData::Ray( 500 offsetPath.AsRay(), std::move(coordBox), 501 aFrame->GetOffsetTo(containingBlockFrame), 502 MotionPathUtils::GetRayContainReferenceSize( 503 const_cast<nsIFrame*>(aFrame))); 504 } 505 506 // Handle path(). We cache it so we handle it separately. 507 // FIXME: Bug 1837042, cache gfx::Path for shapes other than path(). Once we 508 // cache all basic shapes, we can merge this branch into other basic shapes. 509 if (offsetPath.IsPath()) { 510 const StyleSVGPathData& pathData = offsetPath.AsSVGPathData(); 511 RefPtr<gfx::Path> gfxPath = 512 aFrame->GetProperty(nsIFrame::OffsetPathCache()); 513 MOZ_ASSERT(gfxPath || pathData._0.IsEmpty(), 514 "Should have a valid cached gfx::Path or an empty path string"); 515 // FIXME: Bug 1836847. Once we support "at <position>" for path(), we have 516 // to give it the current box position. 517 return OffsetPathData::Shape(gfxPath.forget(), {}, IsClosedLoop(pathData)); 518 } 519 520 nsRect coordBox; 521 const nsIFrame* containingFrame = 522 MotionPathUtils::GetOffsetPathReferenceBox(aFrame, coordBox); 523 if (!containingFrame || coordBox.IsEmpty()) { 524 return OffsetPathData::None(); 525 } 526 nsPoint currentPosition = aFrame->GetOffsetTo(containingFrame); 527 RefPtr<gfx::PathBuilder> builder = MotionPathUtils::GetPathBuilder(); 528 529 if (offsetPath.IsUrl()) { 530 dom::SVGGeometryElement* element = 531 SVGObserverUtils::GetAndObserveGeometry(const_cast<nsIFrame*>(aFrame)); 532 if (!element) { 533 // Note: This behaves as path("m 0 0") (a <basic-shape>). 534 RefPtr<gfx::Path> path = BuildDefaultPathForURL(builder); 535 // FIXME: Bug 1836847. Once we support "at <position>" for path(), we have 536 // to give it the current box position. 537 return path ? OffsetPathData::Shape(path.forget(), {}, false) 538 : OffsetPathData::None(); 539 } 540 541 // We just need this path to calculate the specific point and direction 542 // angle, so use measuring function and get the benefit of caching the path 543 // in the SVG shape element. 544 RefPtr<gfx::Path> path = element->GetOrBuildPathForMeasuring(); 545 546 // The built |path| from SVG shape element doesn't take |coordBox| into 547 // account. It uses the SVG viewport as its coordinate system. So after 548 // mapping it into the CSS layout, we should use |coordBox| as its viewport 549 // and user coordinate system. |currentPosition| is based on the border-box 550 // of the containing block. Therefore, we have to apply an extra translation 551 // to put it at the correct position based on |coordBox|. 552 // 553 // Note: we reuse |OffsetPathData::ShapeData::mCurrentPosition| to include 554 // this extra translation, so we don't have to add an extra field. 555 nsPoint positionInCoordBox = currentPosition - coordBox.TopLeft(); 556 return path ? OffsetPathData::Shape(path.forget(), 557 std::move(positionInCoordBox), 558 element->IsClosedLoop()) 559 : OffsetPathData::None(); 560 } 561 562 // The rest part is to handle "<basic-shape> || <coord-box>". 563 MOZ_ASSERT(offsetPath.IsBasicShapeOrCoordBox()); 564 565 const nsStyleDisplay* disp = aFrame->StyleDisplay(); 566 RefPtr<gfx::Path> path = 567 disp->mOffsetPath.IsCoordBox() 568 ? BuildSimpleInsetPath(containingFrame->StyleBorder()->mBorderRadius, 569 coordBox, builder) 570 : MotionPathUtils::BuildPath( 571 disp->mOffsetPath.AsOffsetPath().path->AsShape(), 572 disp->mOffsetPosition, coordBox, currentPosition, builder); 573 return path ? OffsetPathData::Shape(path.forget(), std::move(currentPosition), 574 true) 575 : OffsetPathData::None(); 576 } 577 578 /* static*/ 579 Maybe<ResolvedMotionPathData> MotionPathUtils::ResolveMotionPath( 580 const nsIFrame* aFrame, TransformReferenceBox& aRefBox) { 581 MOZ_ASSERT(aFrame); 582 583 const nsStyleDisplay* display = aFrame->StyleDisplay(); 584 585 // FIXME: It's possible to refactor the calculation of transform-origin, so we 586 // could calculate from the caller, and reuse the value in nsDisplayList.cpp. 587 CSSPoint transformOrigin = nsStyleTransformMatrix::Convert2DPosition( 588 display->mTransformOrigin.horizontal, display->mTransformOrigin.vertical, 589 aRefBox); 590 591 return ResolveMotionPath( 592 GenerateOffsetPathData(aFrame), display->mOffsetDistance, 593 display->mOffsetRotate, display->mOffsetAnchor, display->mOffsetPosition, 594 transformOrigin, aRefBox, ComputeAnchorPointAdjustment(*aFrame)); 595 } 596 597 // Generate data for motion path on the compositor thread. 598 static OffsetPathData GenerateOffsetPathData( 599 const StyleOffsetPath& aOffsetPath, 600 const StyleOffsetPosition& aOffsetPosition, 601 const layers::MotionPathData& aMotionPathData, 602 gfx::Path* aCachedMotionPath) { 603 if (aOffsetPath.IsNone()) { 604 return OffsetPathData::None(); 605 } 606 607 // Handle ray(). 608 if (aOffsetPath.IsRay()) { 609 return aMotionPathData.coordBox().IsEmpty() 610 ? OffsetPathData::None() 611 : OffsetPathData::Ray( 612 aOffsetPath.AsRay(), aMotionPathData.coordBox(), 613 aMotionPathData.currentPosition(), 614 aMotionPathData.rayContainReferenceLength()); 615 } 616 617 // Handle path(). 618 // FIXME: Bug 1837042, cache gfx::Path for shapes other than path(). 619 if (aOffsetPath.IsPath()) { 620 const StyleSVGPathData& pathData = aOffsetPath.AsSVGPathData(); 621 // If aCachedMotionPath is valid, we have a fixed path. 622 // This means we have pre-built it already and no need to update. 623 RefPtr<gfx::Path> path = aCachedMotionPath; 624 if (!path) { 625 RefPtr<gfx::PathBuilder> builder = 626 MotionPathUtils::GetCompositorPathBuilder(); 627 path = MotionPathUtils::BuildSVGPath(pathData, builder); 628 } 629 // FIXME: Bug 1836847. Once we support "at <position>" for path(), we have 630 // to give it the current box position. 631 return OffsetPathData::Shape(path.forget(), {}, IsClosedLoop(pathData)); 632 } 633 634 // The rest part is to handle "<basic-shape> || <coord-box>". 635 MOZ_ASSERT(aOffsetPath.IsBasicShapeOrCoordBox()); 636 637 const nsRect& coordBox = aMotionPathData.coordBox(); 638 if (coordBox.IsEmpty()) { 639 return OffsetPathData::None(); 640 } 641 642 RefPtr<gfx::PathBuilder> builder = 643 MotionPathUtils::GetCompositorPathBuilder(); 644 if (!builder) { 645 return OffsetPathData::None(); 646 } 647 648 RefPtr<gfx::Path> path; 649 if (aOffsetPath.IsCoordBox()) { 650 const nsRect insetRect = ShapeUtils::ComputeInsetRect( 651 StyleRect<LengthPercentage>::WithAllSides(LengthPercentage::Zero()), 652 coordBox); 653 const nsTArray<nscoord>& radii = aMotionPathData.coordBoxInsetRadii(); 654 nsRectCornerRadii rectRadii; 655 if (!radii.IsEmpty()) { 656 for (auto hc : AllPhysicalHalfCorners()) { 657 rectRadii[hc] = radii[hc]; 658 } 659 } 660 path = ShapeUtils::BuildRectPath(insetRect, 661 radii.IsEmpty() ? nullptr : &rectRadii, 662 coordBox, AppUnitsPerCSSPixel(), builder); 663 } else { 664 path = MotionPathUtils::BuildPath( 665 aOffsetPath.AsOffsetPath().path->AsShape(), aOffsetPosition, coordBox, 666 aMotionPathData.currentPosition(), builder); 667 } 668 669 return path ? OffsetPathData::Shape( 670 path.forget(), nsPoint(aMotionPathData.currentPosition()), 671 true) 672 : OffsetPathData::None(); 673 } 674 675 /* static */ 676 Maybe<ResolvedMotionPathData> MotionPathUtils::ResolveMotionPath( 677 const StyleOffsetPath* aPath, const StyleLengthPercentage* aDistance, 678 const StyleOffsetRotate* aRotate, const StylePositionOrAuto* aAnchor, 679 const StyleOffsetPosition* aPosition, 680 const Maybe<layers::MotionPathData>& aMotionPathData, 681 TransformReferenceBox& aRefBox, gfx::Path* aCachedMotionPath) { 682 if (!aPath) { 683 return Nothing(); 684 } 685 686 MOZ_ASSERT(aMotionPathData); 687 688 auto zeroOffsetDistance = LengthPercentage::Zero(); 689 auto autoOffsetRotate = StyleOffsetRotate{true, StyleAngle::Zero()}; 690 auto autoOffsetAnchor = StylePositionOrAuto::Auto(); 691 auto autoOffsetPosition = StyleOffsetPosition::Auto(); 692 return ResolveMotionPath( 693 GenerateOffsetPathData(*aPath, 694 aPosition ? *aPosition : autoOffsetPosition, 695 *aMotionPathData, aCachedMotionPath), 696 aDistance ? *aDistance : zeroOffsetDistance, 697 aRotate ? *aRotate : autoOffsetRotate, 698 aAnchor ? *aAnchor : autoOffsetAnchor, 699 aPosition ? *aPosition : autoOffsetPosition, aMotionPathData->origin(), 700 aRefBox, aMotionPathData->anchorAdjustment()); 701 } 702 703 /* static */ 704 already_AddRefed<gfx::Path> MotionPathUtils::BuildSVGPath( 705 const StyleSVGPathData& aPath, gfx::PathBuilder* aPathBuilder) { 706 if (!aPathBuilder) { 707 return nullptr; 708 } 709 710 const Span<const StylePathCommand>& path = aPath._0.AsSpan(); 711 return SVGPathData::BuildPath(path, aPathBuilder, StyleStrokeLinecap::Butt, 712 0.0); 713 } 714 715 static already_AddRefed<gfx::Path> BuildShape( 716 const Span<const StyleShapeCommand>& aShape, gfx::PathBuilder* aPathBuilder, 717 const nsRect& aCoordBox) { 718 if (!aPathBuilder) { 719 return nullptr; 720 } 721 722 // For motion path, we always use CSSPixel unit to compute the offset 723 // transform (i.e. motion path transform). 724 const auto rect = CSSRect::FromAppUnits(aCoordBox); 725 return SVGPathData::BuildPath(aShape, aPathBuilder, StyleStrokeLinecap::Butt, 726 0.0, rect.Size(), 727 rect.TopLeft().ToUnknownPoint()); 728 } 729 730 /* static */ 731 already_AddRefed<gfx::Path> MotionPathUtils::BuildPath( 732 const StyleBasicShape& aBasicShape, 733 const StyleOffsetPosition& aOffsetPosition, const nsRect& aCoordBox, 734 const nsPoint& aCurrentPosition, gfx::PathBuilder* aPathBuilder) { 735 if (!aPathBuilder) { 736 return nullptr; 737 } 738 739 switch (aBasicShape.tag) { 740 case StyleBasicShape::Tag::Circle: { 741 const nsPoint center = 742 ComputePosition(aBasicShape.AsCircle().position, aOffsetPosition, 743 aCoordBox, aCurrentPosition); 744 return ShapeUtils::BuildCirclePath(aBasicShape, aCoordBox, center, 745 AppUnitsPerCSSPixel(), aPathBuilder); 746 } 747 case StyleBasicShape::Tag::Ellipse: { 748 const nsPoint center = 749 ComputePosition(aBasicShape.AsEllipse().position, aOffsetPosition, 750 aCoordBox, aCurrentPosition); 751 return ShapeUtils::BuildEllipsePath(aBasicShape, aCoordBox, center, 752 AppUnitsPerCSSPixel(), aPathBuilder); 753 } 754 case StyleBasicShape::Tag::Rect: 755 return ShapeUtils::BuildInsetPath(aBasicShape, aCoordBox, 756 AppUnitsPerCSSPixel(), aPathBuilder); 757 case StyleBasicShape::Tag::Polygon: 758 return ShapeUtils::BuildPolygonPath(aBasicShape, aCoordBox, 759 AppUnitsPerCSSPixel(), aPathBuilder); 760 case StyleBasicShape::Tag::PathOrShape: { 761 // FIXME: Bug 1836847. Once we support "at <position>" for path(), we have 762 // to also check its containing block as well. For now, we are still 763 // building its gfx::Path directly by its SVGPathData without other 764 // reference. https://github.com/w3c/fxtf-drafts/issues/504 765 const auto& pathOrShape = aBasicShape.AsPathOrShape(); 766 if (pathOrShape.IsPath()) { 767 return BuildSVGPath(pathOrShape.AsPath().path, aPathBuilder); 768 } 769 770 // Note that shape() always defines the initial position, i.e. "from x y", 771 // by its first move command, so |aOffsetPosition|, i.e. offset-position 772 // property, is ignored. 773 return BuildShape(pathOrShape.AsShape().commands.AsSpan(), aPathBuilder, 774 aCoordBox); 775 } 776 } 777 778 return nullptr; 779 } 780 781 /* static */ 782 already_AddRefed<gfx::PathBuilder> MotionPathUtils::GetPathBuilder() { 783 // Here we only need to build a valid path for motion path, so 784 // using the default values of stroke-width, stoke-linecap, and fill-rule 785 // is fine for now because what we want is to get the point and its normal 786 // vector along the path, instead of rendering it. 787 RefPtr<gfx::PathBuilder> builder = 788 gfxPlatform::GetPlatform() 789 ->ScreenReferenceDrawTarget() 790 ->CreatePathBuilder(gfx::FillRule::FILL_WINDING); 791 return builder.forget(); 792 } 793 794 /* static */ 795 already_AddRefed<gfx::PathBuilder> MotionPathUtils::GetCompositorPathBuilder() { 796 // FIXME: Perhaps we need a PathBuilder which is independent on the backend. 797 RefPtr<gfx::PathBuilder> builder = 798 gfxPlatform::Initialized() 799 ? gfxPlatform::GetPlatform() 800 ->ScreenReferenceDrawTarget() 801 ->CreatePathBuilder(gfx::FillRule::FILL_WINDING) 802 : gfx::Factory::CreateSimplePathBuilder(); 803 return builder.forget(); 804 } 805 806 } // namespace mozilla