Bug 1480665 - Implement the contain flag for ray() function. r=birtles

Basically, the math calculation logic is from the github repo of the spec
author, Eric:
20b7403d85/ray.py (L311-L363)

Differential Revision: https://phabricator.services.mozilla.com/D42722

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Boris Chiou 2019-09-16 23:24:50 +00:00
Родитель 88285c24ba
Коммит 62b0e230d6
12 изменённых файлов: 474 добавлений и 2 удалений

Просмотреть файл

@ -10117,6 +10117,38 @@ static CSSCoord ComputeRayPathLength(const nsIFrame* aFrame,
return 0.0;
}
static void ApplyRotationAndMoveRayToXAxis(
const StyleOffsetRotate& aOffsetRotate, const StyleAngle& aRayAngle,
AutoTArray<gfx::Point, 4>& aVertices) {
const StyleAngle directionAngle = aRayAngle - StyleAngle{90.0f};
// Get the final rotation which includes the direction angle and
// offset-rotate.
const StyleAngle rotateAngle =
(aOffsetRotate.auto_ ? directionAngle : StyleAngle{0.0f}) +
aOffsetRotate.angle;
// This is the rotation to rotate ray to positive x-axis (i.e. 90deg).
const StyleAngle rayToXAxis = StyleAngle{90.0} - aRayAngle;
gfx::Matrix m;
m.PreRotate((rotateAngle + rayToXAxis).ToRadians());
for (gfx::Point& p : aVertices) {
p = m.TransformPoint(p);
}
}
class RayPointComparator {
public:
bool Equals(const gfx::Point& a, const gfx::Point& b) const {
return std::fabs(a.y) == std::fabs(b.y);
}
bool LessThan(const gfx::Point& a, const gfx::Point& b) const {
return std::fabs(a.y) > std::fabs(b.y);
}
};
// Note: the calculation of contain doesn't take other transform-like properties
// into account. The spec doesn't mention the co-operation for this, so for now,
// we assume we only need to take motion-path into account.
static CSSCoord ComputeRayUsedDistance(const nsStyleDisplay* aDisplay,
const nsSize& aSize,
const CSSCoord& aPathLength) {
@ -10124,9 +10156,141 @@ static CSSCoord ComputeRayUsedDistance(const nsStyleDisplay* aDisplay,
CSSCoord usedDistance =
aDisplay->mOffsetDistance.ResolveToCSSPixels(aPathLength);
if (!aDisplay->mOffsetPath.AsRay().contain) {
return usedDistance;
}
if (aDisplay->mOffsetPath.AsRay().contain) {
// TODO: In the next patch.
// We have to simulate the 4 vertices to check if any of them is outside the
// path circle. Here, we create a 2D Cartesian coordinate system and its
// origin is at the anchor point of the box. And then apply the rotation on
// these 4 vertices, calculate the range of |usedDistance| which makes the box
// entirely contained within the path.
// Note:
// "Contained within the path" means the rectangle is inside a circle whose
// radius is |aPathLength|.
const StylePositionOrAuto& anchor = aDisplay->mOffsetAnchor;
const StyleTransformOrigin& origin = aDisplay->mTransformOrigin;
const StyleLengthPercentage& anchorX =
anchor.IsAuto() ? origin.horizontal : anchor.AsPosition().horizontal;
const StyleLengthPercentage& anchorY =
anchor.IsAuto() ? origin.vertical : anchor.AsPosition().vertical;
const CSSCoord width = CSSPixel::FromAppUnits(aSize.width);
const CSSCoord height = CSSPixel::FromAppUnits(aSize.height);
const CSSPoint usedAnchor = {anchorX.ResolveToCSSPixels(width),
anchorY.ResolveToCSSPixels(height)};
AutoTArray<gfx::Point, 4> vertices = {
{-usedAnchor.x, -usedAnchor.y},
{width - usedAnchor.x, -usedAnchor.y},
{width - usedAnchor.x, height - usedAnchor.y},
{-usedAnchor.x, height - usedAnchor.y}};
ApplyRotationAndMoveRayToXAxis(aDisplay->mOffsetRotate,
aDisplay->mOffsetPath.AsRay().angle, vertices);
// We have to check if all 4 vertices are inside the circle with radius |r|.
// Assume the position of the vertex is (x, y), and the box is moved by
// |usedDistance| along the path:
//
// (usedDistance + x)^2 + y^2 <= r^2
// ==> (usedDistance + x)^2 <= r^2 - y^2 = d
// ==> -x - sqrt(d) <= used distance <= -x + sqrt(d)
//
// Note: |usedDistance| is added into |x| because we convert the ray function
// to 90deg, x-axis):
float upperMin = std::numeric_limits<float>::max();
float lowerMax = std::numeric_limits<float>::min();
bool shouldIncreasePathLength = false;
for (const gfx::Point& p : vertices) {
float d = aPathLength.value * aPathLength.value - p.y * p.y;
if (d < 0) {
// Impossible to make the box inside the path circle. Need to increase
// the path length.
shouldIncreasePathLength = true;
break;
}
float sqrtD = sqrt(d);
upperMin = std::min(upperMin, -p.x + sqrtD);
lowerMax = std::max(lowerMax, -p.x - sqrtD);
}
if (!shouldIncreasePathLength) {
return std::max(lowerMax, std::min(upperMin, (float)usedDistance));
}
// Sort by the absolute value of y, so the first vertex of the each pair of
// vertices we check has a larger y value. (i.e. |yi| is always larger than or
// equal to |yj|.)
vertices.Sort(RayPointComparator());
// Assume we set |usedDistance| to |-vertices[0].x|, so the current radius is
// fabs(vertices[0].y). This is a possible solution.
double radius = std::fabs(vertices[0].y);
usedDistance = -vertices[0].x;
const double epsilon = 1e-5;
for (size_t i = 0; i < 3; ++i) {
for (size_t j = i + 1; j < 4; ++j) {
double xi = vertices[i].x;
double yi = vertices[i].y;
double xj = vertices[j].x;
double yj = vertices[j].y;
double dx = xi - xj;
// Check if any path that enclosed vertices[i] would also enclose
// vertices[j].
//
// For example, the initial setup:
// * (0, yi)
// |
// r
// | * (xj - xi, yj)
// xi | dx
// ----*-----------*----------*---
// (anchor point) | (0, 0)
//
// Assuming (0, yi) is on the path and (xj - xi, yj) is inside the path
// circle, we should use the inequality to check this:
// (xj - xi)^2 + yj^2 <= yi^2
//
// After the first iterations, the updated inequality is:
// (dx + d)^2 + yj^2 <= yi^2 + d^2
// ==> dx^2 + 2dx*d + yj^2 <= yi^2
// ==> dx^2 + yj^2 <= yi^2 - 2dx*d <= yi^2
// , |d| is the difference (or offset) between the old |usedDistance| and
// new |usedDistance|.
//
// Note: `2dx * d` must be positive because
// 1. if |xj| is larger than |xi|, only negative |d| could be used to get
// a new path length which encloses both vertices.
// 2. if |xj| is smaller than |xi|, only positive |d| could be used to get
// a new path length which encloses both vertices.
if (dx * dx + yj * yj <= yi * yi + epsilon) {
continue;
}
// We have to find a new usedDistance which let both vertices[i] and
// vertices[j] be on the path.
// (usedDistance + xi)^2 + yi^2 = (usedDistance + xj)^2 + yj^2
// = radius^2
// ==> usedDistance = (xj^2 + yj^2 - xi^2 - yi^2) / 2(xi-xj)
//
// Note: it's impossible to have a "divide by zero" problem here.
// If |dx| is zero, the if-condition above should always be true and so
// we skip the calculation.
double newUsedDistance =
(xj * xj + yj * yj - xi * xi - yi * yi) / dx / 2.0;
// Then, move vertices[i] and vertices[j] by |newUsedDistance|.
xi += newUsedDistance; // or xj += newUsedDistance; if we use |xj| to get
// |newRadius|.
double newRadius = sqrt(xi * xi + yi * yi);
if (newRadius > radius) {
// We have to increase the path length to make sure both vertices[i] and
// vertices[j] are contained by this new path length.
radius = newRadius;
usedDistance = (float)newUsedDistance;
}
}
}
return usedDistance;

Просмотреть файл

@ -519,6 +519,12 @@ renaming_overrides_prefixing = true
inline static StyleAngle Zero();
inline float ToDegrees() const;
inline double ToRadians() const;
StyleAngle operator+(const StyleAngle& aAngle) const {
return StyleAngle{_0 + aAngle._0};
}
StyleAngle operator-(const StyleAngle& aAngle) const {
return StyleAngle{_0 - aAngle._0};
}
"""
"TextOverflow" = """

Просмотреть файл

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html>
<head>
<title>CSS Motion Path: ray paths with contain</title>
<style>
#container {
width: 300px;
height: 300px;
}
#target {
position: relative;
left: 150px;
top: 150px;
width: 100px;
height: 100px;
background-color: lime;
transform: rotate(0deg) translate(50px, -150px);
}
</style>
</head>
<body>
<div id="container">
<div id="target"></div>
</div>
</body>
</html>

Просмотреть файл

@ -0,0 +1,31 @@
<!DOCTYPE html>
<html>
<head>
<title>CSS Motion Path: ray paths with contain</title>
<link rel="help" href="https://drafts.fxtf.org/motion-1/#offset-path-property">
<link rel="match" href="offset-path-ray-contain-001-ref.html">
<meta name="assert" content="This tests that the element should be contained in ray() path circle.">
<style>
#container {
width: 300px;
height: 300px;
}
#target {
position: relative;
left: 150px;
top: 150px;
width: 100px;
height: 100px;
background-color: lime;
offset-path: ray(45deg closest-corner contain);
offset-rotate: 0deg;
offset-distance: 100%;
}
</style>
</head>
<body>
<div id="container">
<div id="target"></div>
</div>
</body>
</html>

Просмотреть файл

@ -0,0 +1,27 @@
<!DOCTYPE html>
<html>
<head>
<title>CSS Motion Path: ray paths with contain</title>
<style>
#container {
width: 300px;
height: 300px;
}
#target {
position: relative;
left: 100px;
top: 100px;
width: 100px;
height: 100px;
background-color: lime;
/* The vertical movement is about sqrt(150^2 - 50^2) - 50 */
transform: translateY(91.42px);
}
</style>
</head>
<body>
<div id="container">
<div id="target"></div>
</div>
</body>
</html>

Просмотреть файл

@ -0,0 +1,31 @@
<!DOCTYPE html>
<html>
<head>
<title>CSS Motion Path: ray paths with contain</title>
<link rel="help" href="https://drafts.fxtf.org/motion-1/#offset-path-property">
<link rel="match" href="offset-path-ray-contain-002-ref.html">
<meta name="assert" content="This tests that the element should be contained in ray() path circle.">
<style>
#container {
width: 300px;
height: 300px;
}
#target {
position: relative;
left: 150px;
top: 150px;
width: 100px;
height: 100px;
background-color: lime;
offset-path: ray(180deg closest-side contain);
offset-rotate: 0deg;
offset-distance: 100%;
}
</style>
</head>
<body>
<div id="container">
<div id="target"></div>
</div>
</body>
</html>

Просмотреть файл

@ -0,0 +1,27 @@
<!DOCTYPE html>
<html>
<head>
<title>CSS Motion Path: ray paths with contain</title>
<style>
#container {
width: 300px;
height: 300px;
}
#target {
position: relative;
left: 100px;
top: 100px;
width: 100px;
height: 100px;
background-color: lime;
/* The movement is about 150 - 50 * sqrt(2) */
transform: rotate(-45deg) translate(79.29px) rotate(45deg);
}
</style>
</head>
<body>
<div id="container">
<div id="target"></div>
</div>
</body>
</html>

Просмотреть файл

@ -0,0 +1,31 @@
<!DOCTYPE html>
<html>
<head>
<title>CSS Motion Path: ray paths with contain</title>
<link rel="help" href="https://drafts.fxtf.org/motion-1/#offset-path-property">
<link rel="match" href="offset-path-ray-contain-003-ref.html">
<meta name="assert" content="This tests that the element should be contained in ray() path circle.">
<style>
#container {
width: 300px;
height: 300px;
}
#target {
position: relative;
left: 150px;
top: 150px;
width: 100px;
height: 100px;
background-color: lime;
offset-path: ray(45deg closest-side contain);
offset-rotate: 0deg;
offset-distance: 100%;
}
</style>
</head>
<body>
<div id="container">
<div id="target"></div>
</div>
</body>
</html>

Просмотреть файл

@ -0,0 +1,27 @@
<!DOCTYPE html>
<html>
<head>
<title>CSS Motion Path: ray paths with contain</title>
<style>
#container {
width: 300px;
height: 300px;
}
#target {
position: relative;
left: 100px;
top: 100px;
width: 100px;
height: 100px;
background-color: lime;
/* The movement is about sqrt(150^2 - 50^2) - 50 */
transform: rotate(-45deg) translate(91.42px);
}
</style>
</head>
<body>
<div id="container">
<div id="target"></div>
</div>
</body>
</html>

Просмотреть файл

@ -0,0 +1,31 @@
<!DOCTYPE html>
<html>
<head>
<title>CSS Motion Path: ray paths with contain</title>
<link rel="help" href="https://drafts.fxtf.org/motion-1/#offset-path-property">
<link rel="match" href="offset-path-ray-contain-004-ref.html">
<meta name="assert" content="This tests that the element should be contained in ray() path circle.">
<style>
#container {
width: 300px;
height: 300px;
}
#target {
position: relative;
left: 150px;
top: 150px;
width: 100px;
height: 100px;
background-color: lime;
offset-path: ray(45deg closest-side contain);
offset-rotate: auto;
offset-distance: 100%;
}
</style>
</head>
<body>
<div id="container">
<div id="target"></div>
</div>
</body>
</html>

Просмотреть файл

@ -0,0 +1,36 @@
<!DOCTYPE html>
<html>
<head>
<title>CSS Motion Path: ray paths with contain</title>
<style>
#container {
width: 250px;
height: 250px;
transform: translate(100px);
}
#target {
position: relative;
left: 50px;
top: 50px;
width: 150px;
height: 25px;
background-color: lime;
/*
* The original path length is 50px, which is not enough to contain
* the element entirely, so it should be increased.
* "75px" is just the center of the element, which makes the path
* length increase minimally.
* Besides, -75px = (-150px * 2) + 225px, the used offset distance is
* -225px in this case.
* Note: offset-anchor is "200% -300%", and ray angle is -90deg.
*/
transform: translate(calc(-75px), calc(25px * 3));
}
</style>
</head>
<body>
<div id="container">
<div id="target"></div>
</div>
</body>
</html>

Просмотреть файл

@ -0,0 +1,35 @@
<!DOCTYPE html>
<html>
<head>
<title>CSS Motion Path: ray paths with contain</title>
<link rel="help" href="https://drafts.fxtf.org/motion-1/#offset-path-property">
<link rel="match" href="offset-path-ray-contain-005-ref.html">
<meta name="assert" content="This tests that ray() with contain and the path length increases.">
<style>
#container {
width: 250px;
height: 250px;
/* move container to right 100px to make sure we render the element
properly */
transform: translate(100px);
}
#target {
position: relative;
left: 50px;
top: 50px;
width: 150px;
height: 25px;
background-color: lime;
offset-path: ray(-90deg closest-side contain);
offset-anchor: 200% -300%;
offset-rotate: 0deg;
offset-distance: 0%;
}
</style>
</head>
<body>
<div id="container">
<div id="target"></div>
</div>
</body>
</html>