Skip to content

Commit

Permalink
[CSS Math Functions] Correctly serialize asin/acos/atan/atan2 root nodes
Browse files Browse the repository at this point in the history
https://bugs.webkit.org/show_bug.cgi?id=259019
rdar://111960627

Reviewed by Darin Adler.

From the spec https://drafts.csswg.org/css-values-4/#calc-simplification:

> If root is an operator node that’s not one of the calc-operator nodes, and all of its calculation children are numeric values with enough information to compute the operation root represents, return the result of running root’s operation using its children, expressed in the result’s canonical unit.

We now always try to resolve the top-level asin/acos/atan/atan2, e.g. acos(0) now gives calc(0deg) instead of acos(0).
This is consistent with calc(acos(0)) being serialized as calc(0deg).

* LayoutTests/imported/w3c/web-platform-tests/css/css-values/acos-asin-atan-atan2-serialize-expected.txt:
* LayoutTests/imported/w3c/web-platform-tests/css/css-values/acos-asin-atan-atan2-serialize.html:
* Source/WebCore/css/calc/CSSCalcOperationNode.h:

Canonical link: https://commits.webkit.org/265885@main
  • Loading branch information
nt1m committed Jul 9, 2023
1 parent 7e5881c commit 60b5513
Show file tree
Hide file tree
Showing 3 changed files with 113 additions and 48 deletions.
Original file line number Diff line number Diff line change
@@ -1,12 +1,66 @@

FAIL 'rotate(acos(0))' as a specified value should serialize as 'rotate(calc(90deg))'. assert_equals: 'rotate(acos(0))' and 'rotate(calc(90deg))' should serialize the same in specified values. expected "rotate(calc(90deg))" but got "rotate(acos(0))"
PASS 'rotate(acos(0))' as a computed value should serialize as 'matrix(0, 1, -1, 0, 0, 0)'.
FAIL 'rotate(asin(1))' as a specified value should serialize as 'rotate(calc(90deg))'. assert_equals: 'rotate(asin(1))' and 'rotate(calc(90deg))' should serialize the same in specified values. expected "rotate(calc(90deg))" but got "rotate(asin(1))"
PASS 'rotate(asin(1))' as a computed value should serialize as 'matrix(0, 1, -1, 0, 0, 0)'.
PASS 'rotate(acos(1))' as a specified value should serialize as 'rotate(calc(0deg))'.
PASS 'rotate(calc(acos(1)))' as a specified value should serialize as 'rotate(calc(0deg))'.
PASS 'rotate(acos(-1))' as a specified value should serialize as 'rotate(calc(180deg))'.
PASS 'rotate(calc(acos(-1)))' as a specified value should serialize as 'rotate(calc(180deg))'.
PASS 'rotate(acos(-1.5))' as a specified value should serialize as 'rotate(calc(NaN * 1deg))'.
PASS 'rotate(calc(acos(-1.5)))' as a specified value should serialize as 'rotate(calc(NaN * 1deg))'.
PASS 'rotate(acos(1.5))' as a specified value should serialize as 'rotate(calc(NaN * 1deg))'.
PASS 'rotate(calc(acos(1.5)))' as a specified value should serialize as 'rotate(calc(NaN * 1deg))'.
PASS 'rotate(acos(2))' as a specified value should serialize as 'rotate(calc(NaN * 1deg))'.
PASS 'rotate(calc(acos(2)))' as a specified value should serialize as 'rotate(calc(NaN * 1deg))'.
PASS 'rotate(acos(0.5))' as a specified value should serialize as 'rotate(calc(60deg))'.
PASS 'rotate(calc(acos(0.5)))' as a specified value should serialize as 'rotate(calc(60deg))'.
PASS 'rotate(acos(1 - 0.5))' as a specified value should serialize as 'rotate(calc(60deg))'.
PASS 'rotate(calc(acos(1 - 0.5)))' as a specified value should serialize as 'rotate(calc(60deg))'.
PASS 'rotate(acos(0))' as a specified value should serialize as 'rotate(calc(90deg))'.
PASS 'rotate(calc(acos(0)))' as a specified value should serialize as 'rotate(calc(90deg))'.
PASS 'rotate(asin(1))' as a specified value should serialize as 'rotate(calc(90deg))'.
PASS 'rotate(calc(asin(1)))' as a specified value should serialize as 'rotate(calc(90deg))'.
PASS 'rotate(asin(-1))' as a specified value should serialize as 'rotate(calc(-90deg))'.
PASS 'rotate(calc(asin(-1)))' as a specified value should serialize as 'rotate(calc(-90deg))'.
PASS 'rotate(asin(-1.5))' as a specified value should serialize as 'rotate(calc(NaN * 1deg))'.
PASS 'rotate(calc(asin(-1.5)))' as a specified value should serialize as 'rotate(calc(NaN * 1deg))'.
PASS 'rotate(asin(1.5))' as a specified value should serialize as 'rotate(calc(NaN * 1deg))'.
PASS 'rotate(calc(asin(1.5)))' as a specified value should serialize as 'rotate(calc(NaN * 1deg))'.
PASS 'rotate(asin(2))' as a specified value should serialize as 'rotate(calc(NaN * 1deg))'.
PASS 'rotate(calc(asin(2)))' as a specified value should serialize as 'rotate(calc(NaN * 1deg))'.
PASS 'rotate(asin(0.5))' as a specified value should serialize as 'rotate(calc(30deg))'.
PASS 'rotate(calc(asin(0.5)))' as a specified value should serialize as 'rotate(calc(30deg))'.
PASS 'rotate(asin(1 - 0.5))' as a specified value should serialize as 'rotate(calc(30deg))'.
PASS 'rotate(calc(asin(1 - 0.5)))' as a specified value should serialize as 'rotate(calc(30deg))'.
PASS 'rotate(asin(0))' as a specified value should serialize as 'rotate(calc(0deg))'.
PASS 'rotate(calc(asin(0)))' as a specified value should serialize as 'rotate(calc(0deg))'.
PASS 'rotate(acos(pi - pi))' as a specified value should serialize as 'rotate(calc(90deg))'.
PASS 'rotate(calc(acos(pi - pi)))' as a specified value should serialize as 'rotate(calc(90deg))'.
PASS 'rotate(calc(acos(pi - pi)))' as a computed value should serialize as 'matrix(0, 1, -1, 0, 0, 0)'.
PASS 'rotate(asin(pi - pi + 1))' as a specified value should serialize as 'rotate(calc(90deg))'.
PASS 'rotate(calc(asin(pi - pi + 1)))' as a specified value should serialize as 'rotate(calc(90deg))'.
PASS 'rotate(calc(asin(pi - pi + 1)))' as a computed value should serialize as 'matrix(0, 1, -1, 0, 0, 0)'.
PASS 'rotate(atan(1))' as a specified value should serialize as 'rotate(calc(45deg))'.
PASS 'rotate(calc(atan(1)))' as a specified value should serialize as 'rotate(calc(45deg))'.
FAIL 'rotate(atan(0.5))' as a specified value should serialize as 'rotate(calc(26.5651deg))'. assert_equals: 'rotate(atan(0.5))' and 'rotate(calc(26.5651deg))' should serialize the same in specified values. expected "rotate(calc(26.5651deg))" but got "rotate(calc(26.565051deg))"
FAIL 'rotate(calc(atan(0.5)))' as a specified value should serialize as 'rotate(calc(26.5651deg))'. assert_equals: 'rotate(calc(atan(0.5)))' and 'rotate(calc(26.5651deg))' should serialize the same in specified values. expected "rotate(calc(26.5651deg))" but got "rotate(calc(26.565051deg))"
PASS 'rotate(atan(0.577350269))' as a specified value should serialize as 'rotate(calc(30deg))'.
PASS 'rotate(calc(atan(0.577350269)))' as a specified value should serialize as 'rotate(calc(30deg))'.
PASS 'rotate(atan(0))' as a specified value should serialize as 'rotate(calc(0deg))'.
PASS 'rotate(calc(atan(0)))' as a specified value should serialize as 'rotate(calc(0deg))'.
PASS 'rotate(atan(infinity))' as a specified value should serialize as 'rotate(calc(90deg))'.
PASS 'rotate(calc(atan(infinity)))' as a specified value should serialize as 'rotate(calc(90deg))'.
PASS 'rotate(calc(atan(infinity)))' as a computed value should serialize as 'matrix(0, 1, -1, 0, 0, 0)'.
PASS 'rotate(atan2(37.320508075, 10))' as a specified value should serialize as 'rotate(calc(75deg))'.
PASS 'rotate(calc(atan2(37.320508075, 10)))' as a specified value should serialize as 'rotate(calc(75deg))'.
PASS 'rotate(atan2(1s, 1000ms))' as a specified value should serialize as 'rotate(calc(45deg))'.
PASS 'rotate(calc(atan2(1s, 1000ms)))' as a specified value should serialize as 'rotate(calc(45deg))'.
PASS 'rotate(atan2(infinity, infinity))' as a specified value should serialize as 'rotate(calc(45deg))'.
PASS 'rotate(calc(atan2(infinity, infinity)))' as a specified value should serialize as 'rotate(calc(45deg))'.
PASS 'rotate(atan2(-infinity, -infinity))' as a specified value should serialize as 'rotate(calc(-135deg))'.
PASS 'rotate(calc(atan2(-infinity, -infinity)))' as a specified value should serialize as 'rotate(calc(-135deg))'.
PASS 'rotate(atan2(infinity, 10))' as a specified value should serialize as 'rotate(calc(90deg))'.
PASS 'rotate(calc(atan2(infinity, 10)))' as a specified value should serialize as 'rotate(calc(90deg))'.
PASS 'rotate(atan2(10, infinity))' as a specified value should serialize as 'rotate(calc(0deg))'.
PASS 'rotate(calc(atan2(10, infinity)))' as a specified value should serialize as 'rotate(calc(0deg))'.
PASS 'rotate(atan2(NaN, 10))' as a specified value should serialize as 'rotate(calc(NaN * 1deg))'.
PASS 'rotate(calc(atan2(NaN, 10)))' as a specified value should serialize as 'rotate(calc(NaN * 1deg))'.
PASS 'rotate(atan2(10, NaN))' as a specified value should serialize as 'rotate(calc(NaN * 1deg))'.
PASS 'rotate(calc(atan2(10, NaN)))' as a specified value should serialize as 'rotate(calc(NaN * 1deg))'.
PASS 'rotate(atan2(NaN, NaN))' as a specified value should serialize as 'rotate(calc(NaN * 1deg))'.
PASS 'rotate(calc(atan2(NaN, NaN)))' as a specified value should serialize as 'rotate(calc(NaN * 1deg))'.

Original file line number Diff line number Diff line change
@@ -1,51 +1,62 @@
<!DOCTYPE html>
<link rel="help" href="https://drafts.csswg.org/css-values-4/#comp-func">
<link rel="help" href="https://drafts.csswg.org/css-values-4/#trig-funcs">
<link rel="help" href="https://drafts.csswg.org/css-values-4/#angles">
<link rel="help" href="https://drafts.csswg.org/css-values-4/#calc-serialize">
<link rel="author" title="Apple Inc">
<link rel="author" title="Seokho Song" href="seokho@chromium.org">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="../support/serialize-testcommon.js"></script>
<div id=target></div>
<script>
function test_serialization(t,s,c,u, {prop="transform"}={}) {
t = `rotate(${t})`;
test_specified_serialization(prop, t, `rotate(${s})`);
test_computed_serialization(prop, t, c);
if(u) test_used_serialization(prop, t, u);
}

// Browsers aren't perfectly interoperable about how a 90deg rotation is serialized,
// but that's not the focus of this test,
// so just capture *whatever* the browser does and expect that.
const rotateMatrix = (()=>{
const el = document.querySelector("#target");
el.style.transform = "rotate(90deg)";
const ret = getComputedStyle(el).transform;
el.removeAttribute('style');
return ret;
})();

test_serialization(
'acos(0)',
'calc(90deg)',
rotateMatrix);
test_serialization(
'asin(1)',
'calc(90deg)',
rotateMatrix);
function test_serialization(specified, expected, {prop="transform"}={}) {

test_serialization(
'calc(acos(pi - pi))',
'calc(90deg)',
rotateMatrix);
test_serialization(
'calc(asin(pi - pi + 1))',
'calc(90deg)',
rotateMatrix);
// We only test the specified serialization,
// and not the computed or used serialization,
// since we'd need to do that by retrieving the rotation matrix,
// and that isn't perfectly interoperable in corner cases.
// Plus the point of this test is to check the trig functions themselves.
test_specified_serialization(prop, `rotate(${specified})`, `rotate(${expected})`)
}
//TEST CASE | EXPECTED
var test_map = {
"acos(1)" :"calc(0deg)",
"acos(-1)" :"calc(180deg)",
"acos(-1.5)" :"calc(NaN * 1deg)",
"acos(1.5)" :"calc(NaN * 1deg)",
"acos(2)" :"calc(NaN * 1deg)",
"acos(0.5)" :"calc(60deg)",
"acos(1 - 0.5)" :"calc(60deg)",
"acos(0)" :"calc(90deg)",
"asin(1)" :"calc(90deg)",
"asin(-1)" :"calc(-90deg)",
"asin(-1.5)" :"calc(NaN * 1deg)",
"asin(1.5)" :"calc(NaN * 1deg)",
"asin(2)" :"calc(NaN * 1deg)",
"asin(0.5)" :"calc(30deg)",
"asin(1 - 0.5)" :"calc(30deg)",
"asin(0)" :"calc(0deg)",
"acos(pi - pi)" :"calc(90deg)",
"asin(pi - pi + 1)" :"calc(90deg)",
"atan(1)" :"calc(45deg)",
"atan(0.5)" :"calc(26.5651deg)",
"atan(0.577350269)" :"calc(30deg)",
"atan(0)" :"calc(0deg)",
"atan(infinity)" :"calc(90deg)",
"atan2(37.320508075, 10)" :"calc(75deg)",
"atan2(1s, 1000ms)" :"calc(45deg)",
"atan2(infinity, infinity)" :"calc(45deg)",
"atan2(-infinity, -infinity)" :"calc(-135deg)",
"atan2(infinity, 10)" :"calc(90deg)",
"atan2(10, infinity)" :"calc(0deg)",
"atan2(NaN, 10)" :"calc(NaN * 1deg)",
"atan2(10, NaN)" :"calc(NaN * 1deg)",
"atan2(NaN, NaN)" :"calc(NaN * 1deg)",
};

test_serialization(
'calc(atan(infinity))',
'calc(90deg)',
rotateMatrix);
</script>
for (var exp in test_map) {
test_serialization(exp, test_map[exp]);
test_serialization(`calc(${exp})`, test_map[exp]);
}
</script>
4 changes: 2 additions & 2 deletions Source/WebCore/css/calc/CSSCalcOperationNode.h
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ class CSSCalcOperationNode final : public CSSCalcExpressionNode {
bool isHypotNode() const { return m_operator == CalcOperator::Hypot; }
bool isSqrtNode() const { return m_operator == CalcOperator::Sqrt; }
bool isPowOrSqrtNode() const { return m_operator == CalcOperator::Pow || isSqrtNode(); }
bool shouldPreserveFunction() const { return isInverseTrigNode() || isAtan2Node() || isSteppedNode() || isRoundOperation() || isClampNode(); }
bool shouldPreserveFunction() const { return isSteppedNode() || isRoundOperation() || isClampNode(); }
bool isClampNode() const { return m_operator == CalcOperator::Clamp; }

void hoistChildrenWithOperator(CalcOperator);
Expand Down Expand Up @@ -145,7 +145,7 @@ class CSSCalcOperationNode final : public CSSCalcExpressionNode {
}

void makeTopLevelCalc();
bool shouldNotPreserveFunction() const { return isSignNode() || isMinOrMaxNode() || isExpNode() || isHypotNode() || isPowOrSqrtNode() || isTrigNode(); }
bool shouldNotPreserveFunction() const { return isSignNode() || isMinOrMaxNode() || isExpNode() || isHypotNode() || isPowOrSqrtNode() || isInverseTrigNode() || isAtan2Node() || isTrigNode(); }
static double evaluateOperator(CalcOperator, const Vector<double>&);
static Ref<CSSCalcExpressionNode> simplifyNode(Ref<CSSCalcExpressionNode>&&, int depth);
static Ref<CSSCalcExpressionNode> simplifyRecursive(Ref<CSSCalcExpressionNode>&&, int depth);
Expand Down

0 comments on commit 60b5513

Please sign in to comment.