<!doctype html>
<meta charset=utf-8>
<script src='/resources/testharness.js'></script>
<script src='/resources/testharnessreport.js'></script>
<script src='../testcommon.js'></script>
<div id='log'></div>
<script type='text/javascript'>
'use strict';

// We don't have an official spec to define the distance between two transform
// lists, but we still need this for DevTools, so Gecko and Servo backend use
// the similar rules to define the distance. If there is a spec for it, we have
// to update this test file.

const EPSILON = 0.00001;

// |v| should be a unit vector (i.e. having length 1)
function getQuaternion(v, angle) {
  return [
    v[0] * Math.sin(angle / 2.0),
    v[1] * Math.sin(angle / 2.0),
    v[2] * Math.sin(angle / 2.0),
    Math.cos(angle / 2.0)
  ];
}

function computeRotateDistance(q1, q2) {
  const dot = q1.reduce((sum, e, i) => sum + e * q2[i], 0);
  return Math.acos(Math.min(Math.max(dot, -1.0), 1.0)) * 2.0;
}

function createMatrixFromArray(array) {
  return (array.length === 16 ? 'matrix3d' : 'matrix') + `(${array.join()})`;
}

function rotate3dToMatrix(x, y, z, radian) {
  var sc = Math.sin(radian / 2) * Math.cos(radian / 2);
  var sq = Math.sin(radian / 2) * Math.sin(radian / 2);

  // Normalize the vector.
  var length = Math.sqrt(x*x + y*y + z*z);
  x /= length;
  y /= length;
  z /= length;

  return [
    1 - 2 * (y*y + z*z) * sq,
    2 * (x * y * sq + z * sc),
    2 * (x * z * sq - y * sc),
    0,
    2 * (x * y * sq - z * sc),
    1 - 2 * (x*x + z*z) * sq,
    2 * (y * z * sq + x * sc),
    0,
    2 * (x * z * sq + y * sc),
    2 * (y * z * sq - x * sc),
    1 - 2 * (x*x + y*y) * sq,
    0,
    0,
    0,
    0,
    1
  ];
}

test(function(t) {
  var target = addDiv(t);
  var dist = getDistance(target, 'transform', 'none', 'none');
  assert_equals(dist, 0, 'distance of translate');
}, 'Test distance of none and none');

test(function(t) {
  var target = addDiv(t);
  var dist = getDistance(target, 'transform', 'translate(100px)', 'none');
  assert_equals(dist, 100, 'distance of translate');
}, 'Test distance of translate function and none');

test(function(t) {
  var target = addDiv(t);
  var dist =
    getDistance(target, 'transform', 'translate(100px)', 'translate(200px)');
  assert_equals(dist, 200 - 100, 'distance of translate');
}, 'Test distance of translate functions');

test(function(t) {
  var target = addDiv(t);
  var dist =
    getDistance(target, 'transform', 'translate3d(100px, 0, 50px)', 'none');
  assert_equals(dist, Math.sqrt(100 * 100 + 50 * 50),
                'distance of translate3d');
}, 'Test distance of translate3d function and none');

test(function(t) {
  var target = addDiv(t);
  var dist =
    getDistance(target, 'transform',
                'translate3d(100px, 0, 50px)',
                'translate3d(200px, 80px, 0)');
  assert_equals(dist, Math.sqrt(100 * 100 + 80 * 80 + 50 * 50),
                'distance of translate');
}, 'Test distance of translate3d functions');

test(function(t) {
  var target = addDiv(t);
  var dist = getDistance(target, 'transform', 'scale(1.5)', 'none');
  assert_equals(dist, Math.sqrt(0.5 * 0.5 + 0.5 * 0.5), 'distance of scale');
}, 'Test distance of scale function and none');

test(function(t) {
  var target = addDiv(t);
  var dist = getDistance(target, 'transform', 'scale(1.5)', 'scale(2.0)');
  assert_equals(dist, Math.sqrt(0.5 * 0.5 + 0.5 * 0.5), 'distance of scale');
}, 'Test distance of scale functions');

test(function(t) {
  var target = addDiv(t);
  var dist = getDistance(target, 'transform',
                         'scale3d(1.5, 1.5, 1.5)',
                         'none');
  assert_equals(dist,
                Math.sqrt(0.5 * 0.5 + 0.5 * 0.5 + 0.5 * 0.5),
                'distance of scale3d');
}, 'Test distance of scale3d function and none');

test(function(t) {
  var target = addDiv(t);
  var dist = getDistance(target, 'transform',
                         'scale3d(1.5, 1.5, 1.5)',
                         'scale3d(2.0, 2.0, 1.0)');
  assert_equals(dist,
                Math.sqrt(0.5 * 0.5 + 0.5 * 0.5 + 0.5 * 0.5),
                'distance of scale3d');
}, 'Test distance of scale3d functions');

test(function(t) {
  var target = addDiv(t);
  var dist =
    getDistance(target, 'transform', 'rotate(45deg)', 'rotate(90deg)');
  assert_approx_equals(dist, Math.PI / 2.0 - Math.PI / 4.0, EPSILON, 'distance of rotate');
}, 'Test distance of rotate functions');

test(function(t) {
  var target = addDiv(t);
  var dist =
    getDistance(target, 'transform', 'rotate(45deg)', 'none');
  assert_approx_equals(dist, Math.PI / 4.0, EPSILON, 'distance of rotate');
}, 'Test distance of rotate function and none');

test(function(t) {
  var target = addDiv(t);
  var dist = getDistance(target, 'transform',
                         'rotate3d(0, 1, 0, 90deg)',
                         'none');
  assert_approx_equals(dist, Math.PI / 2, EPSILON, 'distance of rotate3d');
}, 'Test distance of rotate3d function and none');

test(function(t) {
  var target = addDiv(t);
  var dist = getDistance(target, 'transform',
                         'rotate3d(0, 0, 1, 90deg)',
                         'rotate3d(1, 0, 0, 90deg)');
  let q1 = getQuaternion([0, 0, 1], Math.PI / 2.0);
  let q2 = getQuaternion([1, 0, 0], Math.PI / 2.0);
  assert_approx_equals(dist, computeRotateDistance(q1, q2), EPSILON, 'distance of rotate3d');
}, 'Test distance of rotate3d functions');

test(function(t) {
  var target = addDiv(t);
  var dist = getDistance(target, 'transform',
                         'rotate3d(0, 0, 1, 90deg)',
                         'rotate3d(0, 0, 0, 90deg)');
  assert_approx_equals(dist, Math.PI / 2, EPSILON, 'distance of rotate3d');
}, 'Test distance of rotate3d functions whose direction vector cannot be ' +
   'normalized');

test(function(t) {
  var target = addDiv(t);
  var dist = getDistance(target, 'transform', 'skew(1rad, 0.5rad)', 'none');
  assert_approx_equals(dist, Math.sqrt(1 * 1 + 0.5 * 0.5), EPSILON, 'distance of skew');
}, 'Test distance of skew function and none');

test(function(t) {
  var target = addDiv(t);
  var dist = getDistance(target, 'transform',
                         'skew(1rad, 0.5rad)',
                         'skew(-1rad, 0)');
  assert_approx_equals(dist, Math.sqrt(2 * 2 + 0.5 * 0.5), EPSILON, 'distance of skew');
}, 'Test distance of skew functions');

test(function(t) {
  var target = addDiv(t);
  var dist = getDistance(target, 'transform',
                         'perspective(128px)',
                         'none');
  assert_equals(dist, Infinity, 'distance of perspective');
}, 'Test distance of perspective function and none');

test(function(t) {
  var target = addDiv(t);
  // perspective(0) is treated as perspective(inf) because perspective length
  // should be greater than or equal to zero.
  var dist = getDistance(target, 'transform',
                         'perspective(128px)',
                         'perspective(0)');
  assert_equals(dist, 128, 'distance of perspective');
}, 'Test distance of perspective function and an invalid perspective');

test(function(t) {
  var target = addDiv(t);
  var dist = getDistance(target, 'transform',
                         'perspective(128px)',
                         'perspective(1024px)');
  assert_equals(dist, 1024 - 128, 'distance of perspective');
}, 'Test distance of perspective functions');

test(function(t) {
  var target = addDiv(t);
  var sin_30 = Math.sin(Math.PI / 6);
  var cos_30 = Math.cos(Math.PI / 6);
  // matrix => translate(100, 0) rotate(30deg).
  var matrix = createMatrixFromArray([ cos_30, sin_30,
                                      -sin_30, cos_30,
                                       100, 0 ]);
  var dist = getDistance(target, 'transform', matrix, 'none');
  assert_approx_equals(dist,
                       Math.sqrt(100 * 100 + (Math.PI / 6) * (Math.PI / 6)),
                       EPSILON,
                       'distance of matrix');
}, 'Test distance of matrix function and none');

test(function(t) {
  var target = addDiv(t);
  var sin_30 = Math.sin(Math.PI / 6);
  var cos_30 = Math.cos(Math.PI / 6);
  // matrix1 => translate(100, 0) rotate(30deg).
  var matrix1 = createMatrixFromArray([ cos_30, sin_30,
                                       -sin_30, cos_30,
                                        100, 0 ]);
  // matrix2 => translate(0, 100) scale(0.5).
  var matrix2 = createMatrixFromArray([ 0.5, 0, 0, 0.5, 0, 100 ]);
  var dist = getDistance(target, 'transform', matrix1, matrix2);
  assert_approx_equals(dist,
                       Math.sqrt(100 * 100 + 100 * 100 +          // translate
                                 (Math.PI / 6) * (Math.PI / 6) +  // rotate
                                 0.5 * 0.5 + 0.5 * 0.5),          // scale
                       EPSILON,
                       'distance of matrix');
}, 'Test distance of matrix functions');

test(function(t) {
  var target = addDiv(t);
  var matrix = createMatrixFromArray(rotate3dToMatrix(0, 1, 0, Math.PI / 6));
  var dist = getDistance(target, 'transform', matrix, 'none');
  assert_approx_equals(dist, Math.PI / 6, EPSILON, 'distance of matrix3d');
}, 'Test distance of matrix3d function and none');

test(function(t) {
  var target = addDiv(t);
  // matrix1 => rotate3d(0, 1, 0, 30deg).
  var matrix1 = createMatrixFromArray(rotate3dToMatrix(0, 1, 0, Math.PI / 6));
  // matrix1 => translate3d(100, 0, 0) scale3d(0.5, 0.5, 0.5).
  var matrix2 = createMatrixFromArray([ 0.5, 0, 0, 0,
                                        0, 0.5, 0, 0,
                                        0, 0, 0.5, 0,
                                        100, 0, 0, 1 ]);
  var dist = getDistance(target, 'transform', matrix1, matrix2);
  assert_approx_equals(dist,
                       Math.sqrt(100 * 100 +                      // translate
                                 0.5 * 0.5 * 3 +                  // scale
                                 (Math.PI / 6) * (Math.PI / 6)),  // rotate
                       EPSILON,
                       'distance of matrix');
}, 'Test distance of matrix3d functions');

test(function(t) {
  var target = addDiv(t);
  var cos_180 = Math.cos(Math.PI);
  var sin_180 = Math.sin(Math.PI);
  // matrix1 => translate3d(100px, 50px, -10px) skew(45deg).
  var matrix1 = createMatrixFromArray([ 1, 0, 0, 0,
                                        Math.tan(Math.PI/4.0), 1, 0, 0,
                                        0, 0, 1, 0,
                                        100, 50, -10, 1]);
  // matrix2 => translate3d(1000px, 0, 0) rotate3d(1, 0, 0, 180deg).
  var matrix2 = createMatrixFromArray([ 1, 0, 0, 0,
                                        0, cos_180, sin_180, 0,
                                        0, -sin_180, cos_180, 0,
                                        1000, 0, 0, 1 ]);
  var dist = getDistance(target, 'transform', matrix1, matrix2);
  assert_approx_equals(dist,
                       Math.sqrt(900 * 900 + 50 * 50 + 10 * 10 +  // translate
                                 Math.PI * Math.PI +              // rotate
                                 (Math.PI / 4) * (Math.PI / 4)),  // skew angle
                       EPSILON,
                       'distance of matrix');
}, 'Test distance of matrix3d functions with skew factors');

test(function(t) {
  var target = addDiv(t);
  var dist =
    getDistance(target, 'transform',
                'rotate(180deg) translate(1000px)',
                'rotate(360deg) translate(0px)');
  assert_approx_equals(dist, Math.sqrt(1000 * 1000 + Math.PI * Math.PI), EPSILON,
                       'distance of transform lists');
}, 'Test distance of transform lists');

test(function(t) {
  var target = addDiv(t);
  var dist = getDistance(target, 'transform',
                         'translate(100px) rotate(180deg)',
                         'translate(50px) rotate(90deg) scale(5) skew(1rad)');
  assert_approx_equals(dist,
                       Math.sqrt(50 * 50 +
                                 Math.PI / 2 * Math.PI / 2 +
                                 4 * 4 * 2 +
                                 1 * 1),
                       EPSILON,
                       'distance of transform lists');
}, 'Test distance of transform lists where one has extra items');

test(function(t) {
  var target = addDiv(t);
  var dist = getDistance(target, 'transform',
                         'translate(1000px) rotate3d(1, 0, 0, 180deg)',
                         'translate(1000px) scale3d(2.5, 0.5, 1)');
  assert_equals(dist, Math.sqrt(Math.PI * Math.PI + 1.5 * 1.5 + 0.5 * 0.5),
                'distance of transform lists');
}, 'Test distance of mismatched transform lists');

test(function(t) {
  var target = addDiv(t);
  var dist = getDistance(target, 'transform',
                         'translate(100px) skew(1rad)',
                         'translate(1000px) rotate3d(0, 1, 0, -2rad)');
  assert_approx_equals(dist,
                       Math.sqrt(900 * 900 + 1 * 1 + 2 * 2),
                       EPSILON,
                       'distance of transform lists');
}, 'Test distance of mismatched transform lists with skew function');


// Individual transforms
test(function(t) {
  var target = addDiv(t);
  var dist = getDistance(target, 'translate', '50px', 'none');
  assert_equals(dist, Math.sqrt(50 * 50), 'distance of 2D translate and none');
}, 'Test distance of 2D translate property with none');

test(function(t) {
  var target = addDiv(t);
  var dist = getDistance(target, 'translate', '10px 30px', '50px');
  assert_equals(dist, Math.sqrt(40 * 40 + 30 * 30), 'distance of 2D translate');
}, 'Test distance of 2D translate property');

test(function(t) {
  var target = addDiv(t);
  var dist = getDistance(target, 'translate', '10px 30px 50px', '50px');
  assert_equals(dist, Math.sqrt(40 * 40 + 30 * 30 + 50 * 50),
                'distance of 3D translate');
}, 'Test distance of 3D translate property');

test(function(t) {
  var target = addDiv(t);
  var dist = getDistance(target, 'scale', '2', 'none');
  assert_equals(dist, Math.sqrt(1 + 1), 'distance of 2D scale and none');
}, 'Test distance of 2D scale property with none');

test(function(t) {
  var target = addDiv(t);
  var dist = getDistance(target, 'scale', '3', '1 1');
  assert_equals(dist, Math.sqrt(2 * 2 + 2 * 2), 'distance of 2D scale');
}, 'Test distance of 2D scale property');

test(function(t) {
  var target = addDiv(t);
  var dist = getDistance(target, 'scale', '3 2 2', '1 1');
  assert_equals(dist, Math.sqrt(2 * 2 + 1 * 1 + 1 * 1),
                'distance of 3D scale');
}, 'Test distance of 3D scale property');

test(function(t) {
  var target = addDiv(t);
  var dist = getDistance(target, 'rotate', '180deg', 'none');
  assert_equals(dist, Math.PI, 'distance of 2D rotate and none');
}, 'Test distance of 2D rotate property with none');

test(function(t) {
  var target = addDiv(t);
  var dist = getDistance(target, 'rotate', '180deg', '90deg');
  assert_equals(dist, Math.PI / 2.0, 'distance of 2D rotate');
}, 'Test distance of 2D rotate property');

test(function(t) {
  var target = addDiv(t);
  var dist = getDistance(target, 'rotate', 'z 90deg', 'x 90deg');
  let q1 = getQuaternion([0, 0, 1], Math.PI / 2.0);
  let q2 = getQuaternion([1, 0, 0], Math.PI / 2.0);
  assert_approx_equals(dist, computeRotateDistance(q1, q2), EPSILON,
                       'distance of 3D rotate');
}, 'Test distance of 3D rotate property');

</script>
</html>
