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/. */
7 #include "mozilla/MotionPathUtils.h"
9 #include "gfxPlatform.h"
10 #include "mozilla/dom/SVGPathData.h"
11 #include "mozilla/gfx/2D.h"
12 #include "mozilla/gfx/Matrix.h"
13 #include "mozilla/layers/LayersMessages.h"
14 #include "mozilla/RefPtr.h"
16 #include "nsStyleTransformMatrix.h"
22 using nsStyleTransformMatrix::TransformReferenceBox
;
24 RayReferenceData::RayReferenceData(const nsIFrame
* aFrame
) {
25 // We use GetContainingBlock() for now. TYLin said this function is buggy in
26 // modern CSS layout, but is ok for most cases.
27 // FIXME: Bug 1581237: This is still not clear that which box we should use
28 // for calculating the path length. We may need to update this.
29 // https://github.com/w3c/fxtf-drafts/issues/369
30 // FIXME: Bug 1579294: SVG layout may get a |container| with empty mRect
31 // (e.g. nsSVGOuterSVGAnonChildFrame), which makes the path length zero.
32 const nsIFrame
* container
= aFrame
->GetContainingBlock();
34 // If there is no parent frame, it's impossible to calculate the path
35 // length, so does the path.
39 // The initial position is (0, 0) in |aFrame|, and we have to transform it
40 // into the space of |container|, so use GetOffsetsTo() to get the delta
42 // FIXME: Bug 1559232: The initial position will be adjusted after
43 // supporting `offset-position`.
44 mInitialPosition
= CSSPoint::FromAppUnits(aFrame
->GetOffsetTo(container
));
45 // FIXME: We need a better definition for containing box in the spec. For now,
46 // we use border box for calculation.
47 // https://github.com/w3c/fxtf-drafts/issues/369
48 mContainingBlockRect
=
49 CSSRect::FromAppUnits(container
->GetRectRelativeToSelf());
52 // The distance is measured between the initial position and the intersection of
53 // the ray with the box
54 // https://drafts.fxtf.org/motion-1/#size-sides
55 static CSSCoord
ComputeSides(const CSSPoint
& aInitialPosition
,
56 const CSSSize
& aContainerSize
,
57 const StyleAngle
& aAngle
) {
58 // Given an acute angle |theta| (i.e. |t|) of a right-angled triangle, the
59 // hypotenuse |h| is the side that connects the two acute angles. The side
60 // |b| adjacent to |theta| is the side of the triangle that connects |theta|
61 // to the right angle.
63 // e.g. if the angle |t| is 0 ~ 90 degrees, and b * tan(theta) <= b',
66 // (0, 0) #--------*-----*--# (aContainerSize.width, 0)
72 // (aInitialPosition) *---b'---* (aContainerSize.width, aInitialPosition.y)
78 // #-----------------# (aContainerSize.width,
79 // (0, aContainerSize.height) aContainerSize.height)
80 double theta
= aAngle
.ToRadians();
81 double sint
= std::sin(theta
);
82 double cost
= std::cos(theta
);
84 double b
= cost
>= 0 ? aInitialPosition
.y
85 : aContainerSize
.height
- aInitialPosition
.y
;
86 double bPrime
= sint
>= 0 ? aContainerSize
.width
- aInitialPosition
.x
88 sint
= std::fabs(sint
);
89 cost
= std::fabs(cost
);
91 // If |b * tan(theta)| is larger than |bPrime|, the intersection is
92 // on the other side, and |b'| is the opposite side of angle |theta| in this
95 // e.g. If b * tan(theta) > b', h = b' / sin(theta):
103 if (b
* sint
> bPrime
* cost
) {
104 return bPrime
/ sint
;
109 static CSSCoord
ComputeRayPathLength(const StyleRaySize aRaySizeType
,
110 const StyleAngle
& aAngle
,
111 const RayReferenceData
& aRayData
) {
112 if (aRaySizeType
== StyleRaySize::Sides
) {
113 // If the initial position is not within the box, the distance is 0.
114 if (!aRayData
.mContainingBlockRect
.Contains(aRayData
.mInitialPosition
)) {
118 return ComputeSides(aRayData
.mInitialPosition
,
119 aRayData
.mContainingBlockRect
.Size(), aAngle
);
122 // left: the length between the initial point and the left side.
123 // right: the length between the initial point and the right side.
124 // top: the length between the initial point and the top side.
125 // bottom: the lenght between the initial point and the bottom side.
126 CSSCoord left
= std::abs(aRayData
.mInitialPosition
.x
);
127 CSSCoord right
= std::abs(aRayData
.mContainingBlockRect
.width
-
128 aRayData
.mInitialPosition
.x
);
129 CSSCoord top
= std::abs(aRayData
.mInitialPosition
.y
);
130 CSSCoord bottom
= std::abs(aRayData
.mContainingBlockRect
.height
-
131 aRayData
.mInitialPosition
.y
);
133 switch (aRaySizeType
) {
134 case StyleRaySize::ClosestSide
:
135 return std::min({left
, right
, top
, bottom
});
137 case StyleRaySize::FarthestSide
:
138 return std::max({left
, right
, top
, bottom
});
140 case StyleRaySize::ClosestCorner
:
141 case StyleRaySize::FarthestCorner
: {
144 if (aRaySizeType
== StyleRaySize::ClosestCorner
) {
145 h
= std::min(left
, right
);
146 v
= std::min(top
, bottom
);
148 h
= std::max(left
, right
);
149 v
= std::max(top
, bottom
);
151 return sqrt(h
.value
* h
.value
+ v
.value
* v
.value
);
154 MOZ_ASSERT_UNREACHABLE("Unsupported ray size");
160 static void ApplyRotationAndMoveRayToXAxis(
161 const StyleOffsetRotate
& aOffsetRotate
, const StyleAngle
& aRayAngle
,
162 AutoTArray
<gfx::Point
, 4>& aVertices
) {
163 const StyleAngle directionAngle
= aRayAngle
- StyleAngle
{90.0f
};
164 // Get the final rotation which includes the direction angle and
166 const StyleAngle rotateAngle
=
167 (aOffsetRotate
.auto_
? directionAngle
: StyleAngle
{0.0f
}) +
169 // This is the rotation to rotate ray to positive x-axis (i.e. 90deg).
170 const StyleAngle rayToXAxis
= StyleAngle
{90.0} - aRayAngle
;
173 m
.PreRotate((rotateAngle
+ rayToXAxis
).ToRadians());
174 for (gfx::Point
& p
: aVertices
) {
175 p
= m
.TransformPoint(p
);
179 class RayPointComparator
{
181 bool Equals(const gfx::Point
& a
, const gfx::Point
& b
) const {
182 return std::fabs(a
.y
) == std::fabs(b
.y
);
185 bool LessThan(const gfx::Point
& a
, const gfx::Point
& b
) const {
186 return std::fabs(a
.y
) > std::fabs(b
.y
);
189 // Note: the calculation of contain doesn't take other transform-like properties
190 // into account. The spec doesn't mention the co-operation for this, so for now,
191 // we assume we only need to take motion-path into account.
192 static CSSCoord
ComputeRayUsedDistance(const RayFunction
& aRay
,
193 const LengthPercentage
& aDistance
,
194 const StyleOffsetRotate
& aRotate
,
195 const StylePositionOrAuto
& aAnchor
,
196 const CSSPoint
& aTransformOrigin
,
197 TransformReferenceBox
& aRefBox
,
198 const CSSCoord
& aPathLength
) {
199 CSSCoord usedDistance
= aDistance
.ResolveToCSSPixels(aPathLength
);
204 // We have to simulate the 4 vertices to check if any of them is outside the
205 // path circle. Here, we create a 2D Cartesian coordinate system and its
206 // origin is at the anchor point of the box. And then apply the rotation on
207 // these 4 vertices, calculate the range of |usedDistance| which makes the box
208 // entirely contained within the path.
210 // "Contained within the path" means the rectangle is inside a circle whose
211 // radius is |aPathLength|.
212 CSSPoint usedAnchor
= aTransformOrigin
;
214 CSSPixel::FromAppUnits(nsSize(aRefBox
.Width(), aRefBox
.Height()));
215 if (!aAnchor
.IsAuto()) {
216 const StylePosition
& anchor
= aAnchor
.AsPosition();
217 usedAnchor
.x
= anchor
.horizontal
.ResolveToCSSPixels(size
.width
);
218 usedAnchor
.y
= anchor
.vertical
.ResolveToCSSPixels(size
.height
);
220 AutoTArray
<gfx::Point
, 4> vertices
= {
221 {-usedAnchor
.x
, -usedAnchor
.y
},
222 {size
.width
- usedAnchor
.x
, -usedAnchor
.y
},
223 {size
.width
- usedAnchor
.x
, size
.height
- usedAnchor
.y
},
224 {-usedAnchor
.x
, size
.height
- usedAnchor
.y
}};
226 ApplyRotationAndMoveRayToXAxis(aRotate
, aRay
.angle
, vertices
);
228 // We have to check if all 4 vertices are inside the circle with radius |r|.
229 // Assume the position of the vertex is (x, y), and the box is moved by
230 // |usedDistance| along the path:
232 // (usedDistance + x)^2 + y^2 <= r^2
233 // ==> (usedDistance + x)^2 <= r^2 - y^2 = d
234 // ==> -x - sqrt(d) <= used distance <= -x + sqrt(d)
236 // Note: |usedDistance| is added into |x| because we convert the ray function
237 // to 90deg, x-axis):
238 float upperMin
= std::numeric_limits
<float>::max();
239 float lowerMax
= std::numeric_limits
<float>::min();
240 bool shouldIncreasePathLength
= false;
241 for (const gfx::Point
& p
: vertices
) {
242 float d
= aPathLength
.value
* aPathLength
.value
- p
.y
* p
.y
;
244 // Impossible to make the box inside the path circle. Need to increase
246 shouldIncreasePathLength
= true;
249 float sqrtD
= sqrt(d
);
250 upperMin
= std::min(upperMin
, -p
.x
+ sqrtD
);
251 lowerMax
= std::max(lowerMax
, -p
.x
- sqrtD
);
254 if (!shouldIncreasePathLength
) {
255 return std::max(lowerMax
, std::min(upperMin
, (float)usedDistance
));
258 // Sort by the absolute value of y, so the first vertex of the each pair of
259 // vertices we check has a larger y value. (i.e. |yi| is always larger than or
261 vertices
.Sort(RayPointComparator());
263 // Assume we set |usedDistance| to |-vertices[0].x|, so the current radius is
264 // fabs(vertices[0].y). This is a possible solution.
265 double radius
= std::fabs(vertices
[0].y
);
266 usedDistance
= -vertices
[0].x
;
267 const double epsilon
= 1e-5;
269 for (size_t i
= 0; i
< 3; ++i
) {
270 for (size_t j
= i
+ 1; j
< 4; ++j
) {
271 double xi
= vertices
[i
].x
;
272 double yi
= vertices
[i
].y
;
273 double xj
= vertices
[j
].x
;
274 double yj
= vertices
[j
].y
;
277 // Check if any path that enclosed vertices[i] would also enclose
280 // For example, the initial setup:
286 // ----*-----------*----------*---
287 // (anchor point) | (0, 0)
289 // Assuming (0, yi) is on the path and (xj - xi, yj) is inside the path
290 // circle, we should use the inequality to check this:
291 // (xj - xi)^2 + yj^2 <= yi^2
293 // After the first iterations, the updated inequality is:
294 // (dx + d)^2 + yj^2 <= yi^2 + d^2
295 // ==> dx^2 + 2dx*d + yj^2 <= yi^2
296 // ==> dx^2 + yj^2 <= yi^2 - 2dx*d <= yi^2
297 // , |d| is the difference (or offset) between the old |usedDistance| and
298 // new |usedDistance|.
300 // Note: `2dx * d` must be positive because
301 // 1. if |xj| is larger than |xi|, only negative |d| could be used to get
302 // a new path length which encloses both vertices.
303 // 2. if |xj| is smaller than |xi|, only positive |d| could be used to get
304 // a new path length which encloses both vertices.
305 if (dx
* dx
+ yj
* yj
<= yi
* yi
+ epsilon
) {
309 // We have to find a new usedDistance which let both vertices[i] and
310 // vertices[j] be on the path.
311 // (usedDistance + xi)^2 + yi^2 = (usedDistance + xj)^2 + yj^2
313 // ==> usedDistance = (xj^2 + yj^2 - xi^2 - yi^2) / 2(xi-xj)
315 // Note: it's impossible to have a "divide by zero" problem here.
316 // If |dx| is zero, the if-condition above should always be true and so
317 // we skip the calculation.
318 double newUsedDistance
=
319 (xj
* xj
+ yj
* yj
- xi
* xi
- yi
* yi
) / dx
/ 2.0;
320 // Then, move vertices[i] and vertices[j] by |newUsedDistance|.
321 xi
+= newUsedDistance
; // or xj += newUsedDistance; if we use |xj| to get
323 double newRadius
= sqrt(xi
* xi
+ yi
* yi
);
324 if (newRadius
> radius
) {
325 // We have to increase the path length to make sure both vertices[i] and
326 // vertices[j] are contained by this new path length.
328 usedDistance
= (float)newUsedDistance
;
337 CSSPoint
MotionPathUtils::ComputeAnchorPointAdjustment(const nsIFrame
& aFrame
) {
338 if (!aFrame
.HasAnyStateBits(NS_FRAME_SVG_LAYOUT
)) {
342 auto transformBox
= aFrame
.StyleDisplay()->mTransformBox
;
343 if (transformBox
== StyleGeometryBox::ViewBox
||
344 transformBox
== StyleGeometryBox::BorderBox
) {
348 if (aFrame
.IsFrameOfType(nsIFrame::eSVGContainer
)) {
349 nsRect boxRect
= nsLayoutUtils::ComputeGeometryBox(
350 const_cast<nsIFrame
*>(&aFrame
), StyleGeometryBox::FillBox
);
351 return CSSPoint::FromAppUnits(boxRect
.TopLeft());
353 return CSSPoint::FromAppUnits(aFrame
.GetPosition());
357 Maybe
<ResolvedMotionPathData
> MotionPathUtils::ResolveMotionPath(
358 const OffsetPathData
& aPath
, const LengthPercentage
& aDistance
,
359 const StyleOffsetRotate
& aRotate
, const StylePositionOrAuto
& aAnchor
,
360 const CSSPoint
& aTransformOrigin
, TransformReferenceBox
& aRefBox
,
361 const CSSPoint
& aAnchorPointAdjustment
) {
362 if (aPath
.IsNone()) {
366 // Compute the point and angle for creating the equivalent translate and
368 double directionAngle
= 0.0;
370 if (aPath
.IsPath()) {
371 const auto& path
= aPath
.AsPath();
372 if (!path
.mGfxPath
) {
373 // Empty gfx::Path means it is path('') (i.e. empty path string).
377 // Per the spec, we have to convert offset distance to pixels, with 100%
378 // being converted to total length. So here |gfxPath| is built with CSS
379 // pixel, and we calculate |pathLength| and |computedDistance| with CSS
381 gfx::Float pathLength
= path
.mGfxPath
->ComputeLength();
382 gfx::Float usedDistance
=
383 aDistance
.ResolveToCSSPixels(CSSCoord(pathLength
));
384 if (path
.mIsClosedIntervals
) {
385 // Per the spec, let used offset distance be equal to offset distance
386 // modulus the total length of the path. If the total length of the path
387 // is 0, used offset distance is also 0.
388 usedDistance
= pathLength
> 0.0 ? fmod(usedDistance
, pathLength
) : 0.0;
389 // We make sure |usedDistance| is 0.0 or a positive value.
390 // https://github.com/w3c/fxtf-drafts/issues/339
391 if (usedDistance
< 0.0) {
392 usedDistance
+= pathLength
;
395 // Per the spec, for unclosed interval, let used offset distance be equal
396 // to offset distance clamped by 0 and the total length of the path.
397 usedDistance
= clamped(usedDistance
, 0.0f
, pathLength
);
400 point
= path
.mGfxPath
->ComputePointAtLength(usedDistance
, &tangent
);
401 directionAngle
= (double)atan2(tangent
.y
, tangent
.x
); // In Radian.
402 } else if (aPath
.IsRay()) {
403 const auto& ray
= aPath
.AsRay();
404 MOZ_ASSERT(ray
.mRay
);
406 CSSCoord pathLength
=
407 ComputeRayPathLength(ray
.mRay
->size
, ray
.mRay
->angle
, ray
.mData
);
408 CSSCoord usedDistance
=
409 ComputeRayUsedDistance(*ray
.mRay
, aDistance
, aRotate
, aAnchor
,
410 aTransformOrigin
, aRefBox
, pathLength
);
412 // 0deg pointing up and positive angles representing clockwise rotation.
414 StyleAngle
{ray
.mRay
->angle
.ToDegrees() - 90.0f
}.ToRadians();
416 point
.x
= usedDistance
* cos(directionAngle
);
417 point
.y
= usedDistance
* sin(directionAngle
);
419 MOZ_ASSERT_UNREACHABLE("Unsupported offset-path value");
423 // If |rotate.auto_| is true, the element should be rotated by the angle of
424 // the direction (i.e. directional tangent vector) of the offset-path, and the
425 // computed value of <angle> is added to this.
426 // Otherwise, the element has a constant clockwise rotation transformation
427 // applied to it by the specified rotation angle. (i.e. Don't need to
428 // consider the direction of the path.)
429 gfx::Float angle
= static_cast<gfx::Float
>(
430 (aRotate
.auto_
? directionAngle
: 0.0) + aRotate
.angle
.ToRadians());
432 // Compute the offset for motion path translate.
433 // Bug 1559232: the translate parameters will be adjusted more after we
434 // support offset-position.
435 // Per the spec, the default offset-anchor is `auto`, so initialize the anchor
436 // point to transform-origin.
437 CSSPoint
anchorPoint(aTransformOrigin
);
439 if (!aAnchor
.IsAuto()) {
440 const auto& pos
= aAnchor
.AsPosition();
441 anchorPoint
= nsStyleTransformMatrix::Convert2DPosition(
442 pos
.horizontal
, pos
.vertical
, aRefBox
);
443 // We need this value to shift the origin from transform-origin to
444 // offset-anchor (and vice versa).
445 // See nsStyleTransformMatrix::ReadTransform for more details.
446 shift
= (anchorPoint
- aTransformOrigin
).ToUnknownPoint();
449 anchorPoint
+= aAnchorPointAdjustment
;
451 return Some(ResolvedMotionPathData
{point
- anchorPoint
.ToUnknownPoint(),
455 static OffsetPathData
GenerateOffsetPathData(const nsIFrame
* aFrame
) {
456 const StyleOffsetPath
& path
= aFrame
->StyleDisplay()->mOffsetPath
;
458 case StyleOffsetPath::Tag::Path
: {
459 const StyleSVGPathData
& pathData
= path
.AsPath();
460 RefPtr
<gfx::Path
> gfxPath
=
461 aFrame
->GetProperty(nsIFrame::OffsetPathCache());
463 gfxPath
|| pathData
._0
.IsEmpty(),
464 "Should have a valid cached gfx::Path or an empty path string");
465 return OffsetPathData::Path(pathData
, gfxPath
.forget());
467 case StyleOffsetPath::Tag::Ray
:
468 return OffsetPathData::Ray(path
.AsRay(), RayReferenceData(aFrame
));
469 case StyleOffsetPath::Tag::None
:
470 return OffsetPathData::None();
472 MOZ_ASSERT_UNREACHABLE("Unknown offset-path");
473 return OffsetPathData::None();
478 Maybe
<ResolvedMotionPathData
> MotionPathUtils::ResolveMotionPath(
479 const nsIFrame
* aFrame
, TransformReferenceBox
& aRefBox
) {
482 const nsStyleDisplay
* display
= aFrame
->StyleDisplay();
484 // FIXME: It's possible to refactor the calculation of transform-origin, so we
485 // could calculate from the caller, and reuse the value in nsDisplayList.cpp.
486 CSSPoint transformOrigin
= nsStyleTransformMatrix::Convert2DPosition(
487 display
->mTransformOrigin
.horizontal
, display
->mTransformOrigin
.vertical
,
490 return ResolveMotionPath(GenerateOffsetPathData(aFrame
),
491 display
->mOffsetDistance
, display
->mOffsetRotate
,
492 display
->mOffsetAnchor
, transformOrigin
, aRefBox
,
493 ComputeAnchorPointAdjustment(*aFrame
));
496 static OffsetPathData
GenerateOffsetPathData(
497 const StyleOffsetPath
& aPath
, const RayReferenceData
& aRayReferenceData
,
498 gfx::Path
* aCachedMotionPath
) {
500 case StyleOffsetPath::Tag::Path
: {
501 const StyleSVGPathData
& pathData
= aPath
.AsPath();
502 // If aCachedMotionPath is valid, we have a fixed path.
503 // This means we have pre-built it already and no need to update.
504 RefPtr
<gfx::Path
> path
= aCachedMotionPath
;
506 RefPtr
<gfx::PathBuilder
> builder
=
507 MotionPathUtils::GetCompositorPathBuilder();
508 path
= MotionPathUtils::BuildPath(pathData
, builder
);
510 return OffsetPathData::Path(pathData
, path
.forget());
512 case StyleOffsetPath::Tag::Ray
:
513 return OffsetPathData::Ray(aPath
.AsRay(), aRayReferenceData
);
514 case StyleOffsetPath::Tag::None
:
516 return OffsetPathData::None();
521 Maybe
<ResolvedMotionPathData
> MotionPathUtils::ResolveMotionPath(
522 const StyleOffsetPath
* aPath
, const StyleLengthPercentage
* aDistance
,
523 const StyleOffsetRotate
* aRotate
, const StylePositionOrAuto
* aAnchor
,
524 const Maybe
<layers::MotionPathData
>& aMotionPathData
,
525 TransformReferenceBox
& aRefBox
, gfx::Path
* aCachedMotionPath
) {
530 MOZ_ASSERT(aMotionPathData
);
532 auto zeroOffsetDistance
= LengthPercentage::Zero();
533 auto autoOffsetRotate
= StyleOffsetRotate
{true, StyleAngle::Zero()};
534 auto autoOffsetAnchor
= StylePositionOrAuto::Auto();
535 return ResolveMotionPath(
536 GenerateOffsetPathData(*aPath
, aMotionPathData
->rayReferenceData(),
538 aDistance
? *aDistance
: zeroOffsetDistance
,
539 aRotate
? *aRotate
: autoOffsetRotate
,
540 aAnchor
? *aAnchor
: autoOffsetAnchor
, aMotionPathData
->origin(), aRefBox
,
541 aMotionPathData
->anchorAdjustment());
545 StyleSVGPathData
MotionPathUtils::NormalizeSVGPathData(
546 const StyleSVGPathData
& aPath
) {
548 Servo_SVGPathData_Normalize(&aPath
, &n
);
553 already_AddRefed
<gfx::Path
> MotionPathUtils::BuildPath(
554 const StyleSVGPathData
& aPath
, gfx::PathBuilder
* aPathBuilder
) {
559 const Span
<const StylePathCommand
>& path
= aPath
._0
.AsSpan();
560 return SVGPathData::BuildPath(path
, aPathBuilder
, StyleStrokeLinecap::Butt
,
565 already_AddRefed
<gfx::PathBuilder
> MotionPathUtils::GetCompositorPathBuilder() {
566 // FIXME: Perhaps we need a PathBuilder which is independent on the backend.
567 RefPtr
<gfx::PathBuilder
> builder
=
568 gfxPlatform::Initialized()
569 ? gfxPlatform::GetPlatform()
570 ->ScreenReferenceDrawTarget()
571 ->CreatePathBuilder(gfx::FillRule::FILL_WINDING
)
572 : gfx::Factory::CreateSimplePathBuilder();
573 return builder
.forget();
576 } // namespace mozilla