animation_utils.js (27129B)
1 //---------------------------------------------------------------------- 2 // 3 // Common testing functions 4 // 5 //---------------------------------------------------------------------- 6 7 /* eslint-disable mozilla/no-comparison-or-assignment-inside-ok */ 8 9 function advance_clock(milliseconds) { 10 SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(milliseconds); 11 } 12 13 // Test-element creation/destruction and event checking 14 (function () { 15 var gElem; 16 var gEventsReceived = []; 17 18 function new_div(style) { 19 return new_element("div", style); 20 } 21 22 // Creates a new |tagname| element with inline style |style| and appends 23 // it as a child of the element with ID 'display'. 24 // The element will also be given the class 'target' which can be used 25 // for additional styling. 26 function new_element(tagname, style) { 27 if (gElem) { 28 ok(false, "test author forgot to call done_div/done_elem"); 29 } 30 if (typeof style != "string") { 31 ok(false, "test author forgot to pass argument"); 32 } 33 if (!document.getElementById("display")) { 34 ok(false, "no 'display' element to append to"); 35 } 36 gElem = document.createElement(tagname); 37 gElem.setAttribute("style", style); 38 gElem.classList.add("target"); 39 document.getElementById("display").appendChild(gElem); 40 return [gElem, getComputedStyle(gElem, "")]; 41 } 42 43 function listen() { 44 if (!gElem) { 45 ok(false, "test author forgot to call new_div before listen"); 46 } 47 gEventsReceived = []; 48 function listener(event) { 49 gEventsReceived.push(event); 50 } 51 gElem.addEventListener("animationstart", listener); 52 gElem.addEventListener("animationiteration", listener); 53 gElem.addEventListener("animationend", listener); 54 } 55 56 function check_events(eventsExpected, desc) { 57 // This function checks that the list of eventsExpected matches 58 // the received events -- but it only checks the properties that 59 // are present on eventsExpected. 60 is( 61 gEventsReceived.length, 62 eventsExpected.length, 63 "number of events received for " + desc 64 ); 65 for ( 66 var i = 0, 67 i_end = Math.min(eventsExpected.length, gEventsReceived.length); 68 i != i_end; 69 ++i 70 ) { 71 var exp = eventsExpected[i]; 72 var rec = gEventsReceived[i]; 73 for (var prop in exp) { 74 if (prop == "elapsedTime") { 75 // Allow floating point error. 76 ok( 77 Math.abs(rec.elapsedTime - exp.elapsedTime) < 0.000002, 78 "events[" + 79 i + 80 "]." + 81 prop + 82 " for " + 83 desc + 84 " received=" + 85 rec.elapsedTime + 86 " expected=" + 87 exp.elapsedTime 88 ); 89 } else { 90 is( 91 rec[prop], 92 exp[prop], 93 "events[" + i + "]." + prop + " for " + desc 94 ); 95 } 96 } 97 } 98 for (var i = eventsExpected.length; i < gEventsReceived.length; ++i) { 99 ok(false, "unexpected " + gEventsReceived[i].type + " event for " + desc); 100 } 101 gEventsReceived = []; 102 } 103 104 function done_element() { 105 if (!gElem) { 106 ok( 107 false, 108 "test author called done_element/done_div without matching" + 109 " call to new_element/new_div" 110 ); 111 } 112 gElem.remove(); 113 gElem = null; 114 if (gEventsReceived.length) { 115 ok(false, "caller should have called check_events"); 116 } 117 } 118 119 [new_div, new_element, listen, check_events, done_element].forEach( 120 function (fn) { 121 window[fn.name] = fn; 122 } 123 ); 124 window.done_div = done_element; 125 })(); 126 127 function px_to_num(str) { 128 return Number(String(str).match(/^([\d.]+)px$/)[1]); 129 } 130 131 function bezier(x1, y1, x2, y2) { 132 // Cubic bezier with control points (0, 0), (x1, y1), (x2, y2), and (1, 1). 133 function x_for_t(t) { 134 var omt = 1 - t; 135 return 3 * omt * omt * t * x1 + 3 * omt * t * t * x2 + t * t * t; 136 } 137 function y_for_t(t) { 138 var omt = 1 - t; 139 return 3 * omt * omt * t * y1 + 3 * omt * t * t * y2 + t * t * t; 140 } 141 function t_for_x(x) { 142 // Binary subdivision. 143 var mint = 0, 144 maxt = 1; 145 for (var i = 0; i < 30; ++i) { 146 var guesst = (mint + maxt) / 2; 147 var guessx = x_for_t(guesst); 148 if (x < guessx) { 149 maxt = guesst; 150 } else { 151 mint = guesst; 152 } 153 } 154 return (mint + maxt) / 2; 155 } 156 return function bezier_closure(x) { 157 if (x == 0) { 158 return 0; 159 } 160 if (x == 1) { 161 return 1; 162 } 163 return y_for_t(t_for_x(x)); 164 }; 165 } 166 167 function step_end(nsteps) { 168 return function step_end_closure(x) { 169 return Math.floor(x * nsteps) / nsteps; 170 }; 171 } 172 173 function step_start(nsteps) { 174 var stepend = step_end(nsteps); 175 return function step_start_closure(x) { 176 return 1.0 - stepend(1.0 - x); 177 }; 178 } 179 180 var gTF = { 181 ease: bezier(0.25, 0.1, 0.25, 1), 182 linear: function (x) { 183 return x; 184 }, 185 ease_in: bezier(0.42, 0, 1, 1), 186 ease_out: bezier(0, 0, 0.58, 1), 187 ease_in_out: bezier(0.42, 0, 0.58, 1), 188 step_start: step_start(1), 189 step_end: step_end(1), 190 }; 191 192 function is_approx(float1, float2, error, desc) { 193 ok( 194 Math.abs(float1 - float2) < error, 195 desc + ": " + float1 + " and " + float2 + " should be within " + error 196 ); 197 } 198 199 function findKeyframesRule(name) { 200 for (var i = 0; i < document.styleSheets.length; i++) { 201 var match = [].find.call(document.styleSheets[i].cssRules, function (rule) { 202 return rule.type == CSSRule.KEYFRAMES_RULE && rule.name == name; 203 }); 204 if (match) { 205 return match; 206 } 207 } 208 return undefined; 209 } 210 211 function isOMTAWorking() { 212 function waitForDocumentLoad() { 213 return new Promise(function (resolve, reject) { 214 if (document.readyState === "complete") { 215 resolve(); 216 } else { 217 window.addEventListener("load", resolve); 218 } 219 }); 220 } 221 222 function loadPaintListener() { 223 return new Promise(function (resolve, reject) { 224 if (typeof window.waitForAllPaints !== "function") { 225 var script = document.createElement("script"); 226 script.onload = resolve; 227 script.onerror = function () { 228 reject(new Error("Failed to load paint listener")); 229 }; 230 script.src = "/tests/SimpleTest/paint_listener.js"; 231 var firstScript = document.scripts[0]; 232 firstScript.parentNode.insertBefore(script, firstScript); 233 } else { 234 resolve(); 235 } 236 }); 237 } 238 239 // Create keyframes rule 240 const animationName = "a6ce3091ed85"; // Random name to avoid clashes 241 var ruleText = 242 "@keyframes " + 243 animationName + 244 " { from { opacity: 0.5 } to { opacity: 0.5 } }"; 245 var style = document.createElement("style"); 246 style.appendChild(document.createTextNode(ruleText)); 247 document.head.appendChild(style); 248 249 // Create animation target 250 var div = document.createElement("div"); 251 document.body.appendChild(div); 252 253 // Give the target geometry so it is eligible for layerization 254 div.style.width = "100px"; 255 div.style.height = "100px"; 256 div.style.backgroundColor = "white"; 257 258 var utils = SpecialPowers.DOMWindowUtils; 259 260 // Common clean up code 261 var cleanUp = function () { 262 div.remove(); 263 style.remove(); 264 if (utils.isTestControllingRefreshes) { 265 utils.restoreNormalRefresh(); 266 } 267 }; 268 269 return waitForDocumentLoad() 270 .then(loadPaintListener) 271 .then(function () { 272 // Put refresh driver under test control and flush all pending style, 273 // layout and paint to avoid the situation that waitForPaintsFlush() 274 // receives unexpected MozAfterpaint event for those pending 275 // notifications. 276 utils.advanceTimeAndRefresh(0); 277 return waitForPaintsFlushed(); 278 }) 279 .then(function () { 280 div.style.animation = animationName + " 10s"; 281 282 return waitForPaintsFlushed(); 283 }) 284 .then(function () { 285 var opacity = utils.getOMTAStyle(div, "opacity"); 286 cleanUp(); 287 return Promise.resolve(opacity == 0.5); 288 }) 289 .catch(function (err) { 290 cleanUp(); 291 return Promise.reject(err); 292 }); 293 } 294 295 // Checks if off-main thread animation (OMTA) is available, and if it is, runs 296 // the provided callback function. If OMTA is not available or is not 297 // functioning correctly, the second callback, aOnSkip, is run instead. 298 // 299 // This function also does an internal test to verify that OMTA is working at 300 // all so that if OMTA is not functioning correctly when it is expected to 301 // function only a single failure is produced. 302 // 303 // Since this function relies on various asynchronous operations, the caller is 304 // responsible for calling SimpleTest.waitForExplicitFinish() before calling 305 // this and SimpleTest.finish() within aTestFunction and aOnSkip. 306 // 307 // specialPowersForPrefs exists because some SpecialPowers objects apparently 308 // can get prefs and some can't; callers that would normally have one of the 309 // latter but can get their hands on one of the former can pass it in 310 // explicitly. 311 function runOMTATest(aTestFunction, aOnSkip, specialPowersForPrefs) { 312 const OMTAPrefKey = "layers.offmainthreadcomposition.async-animations"; 313 var utils = SpecialPowers.DOMWindowUtils; 314 if (!specialPowersForPrefs) { 315 specialPowersForPrefs = SpecialPowers; 316 } 317 var expectOMTA = 318 utils.layerManagerRemote && 319 // ^ Off-main thread animation cannot be used if off-main 320 // thread composition (OMTC) is not available 321 specialPowersForPrefs.getBoolPref(OMTAPrefKey); 322 323 isOMTAWorking() 324 .then(function (isWorking) { 325 if (expectOMTA) { 326 if (isWorking) { 327 aTestFunction(); 328 } else { 329 // We only call this when we know it will fail as otherwise in the 330 // regular success case we will end up inflating the "passed tests" 331 // count by 1 332 ok(isWorking, "OMTA should work"); 333 aOnSkip(); 334 } 335 } else { 336 todo( 337 isWorking, 338 "OMTA should ideally work, though we don't expect it to work on " + 339 "this platform/configuration" 340 ); 341 aOnSkip(); 342 } 343 }) 344 .catch(function (err) { 345 ok(false, err); 346 aOnSkip(); 347 }); 348 } 349 350 // Common architecture for setting up a series of asynchronous animation tests 351 // 352 // Usage example: 353 // 354 // addAsyncAnimTest(function *() { 355 // .. do work .. 356 // yield functionThatReturnsAPromise(); 357 // .. do work .. 358 // }); 359 // runAllAsyncAnimTests().then(SimpleTest.finish()); 360 // 361 (function () { 362 var tests = []; 363 364 window.addAsyncAnimTest = function (generator) { 365 tests.push(generator); 366 }; 367 368 // Returns a promise when all tests have run 369 window.runAllAsyncAnimTests = function (aOnAbort) { 370 // runAsyncAnimTest returns a Promise that is resolved when the 371 // test is finished so we can chain them together 372 return tests.reduce(function (sequence, test) { 373 return sequence.then(function () { 374 return runAsyncAnimTest(test, aOnAbort); 375 }); 376 }, Promise.resolve() /* the start of the sequence */); 377 }; 378 379 // Takes a generator function that represents a test case. Each point in the 380 // test case that waits asynchronously for some result yields a Promise that 381 // is resolved when the asynchronous action has completed. By chaining these 382 // intermediate results together we run the test to completion. 383 // 384 // This method itself returns a Promise that is resolved when the generator 385 // function has completed. 386 // 387 // This arrangement is based on add_task() which is currently only available 388 // in mochitest-chrome (bug 872229). If add_task becomes available in 389 // mochitest-plain, we can remove this function and use add_task instead. 390 function runAsyncAnimTest(aTestFunc, aOnAbort) { 391 var generator; 392 393 function step(arg) { 394 var next; 395 try { 396 next = generator.next(arg); 397 } catch (e) { 398 return Promise.reject(e); 399 } 400 if (next.done) { 401 return Promise.resolve(next.value); 402 } 403 return Promise.resolve(next.value).then(step, function (err) { 404 throw err; 405 }); 406 } 407 408 // Put refresh driver under test control 409 SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(0); 410 411 // Run test 412 var promise = aTestFunc(); 413 if (!promise.then) { 414 generator = promise; 415 promise = step(); 416 } 417 return promise 418 .catch(function (err) { 419 ok(false, err.message); 420 if (typeof aOnAbort == "function") { 421 aOnAbort(); 422 } 423 }) 424 .then(function () { 425 // Restore clock 426 SpecialPowers.DOMWindowUtils.restoreNormalRefresh(); 427 }); 428 } 429 })(); 430 431 //---------------------------------------------------------------------- 432 // 433 // Helper functions for testing animated values on the compositor 434 // 435 //---------------------------------------------------------------------- 436 437 const RunningOn = { 438 MainThread: 0, 439 Compositor: 1, 440 Either: 2, 441 TodoMainThread: 3, 442 TodoCompositor: 4, 443 }; 444 445 const ExpectComparisonTo = { 446 Pass: 1, 447 Fail: 2, 448 }; 449 450 (function () { 451 window.omta_todo_is = function ( 452 elem, 453 property, 454 expected, 455 runningOn, 456 desc, 457 pseudo 458 ) { 459 return omta_is_approx( 460 elem, 461 property, 462 expected, 463 0, 464 runningOn, 465 desc, 466 ExpectComparisonTo.Fail, 467 pseudo 468 ); 469 }; 470 471 window.omta_is = function ( 472 elem, 473 property, 474 expected, 475 runningOn, 476 desc, 477 pseudo 478 ) { 479 return omta_is_approx( 480 elem, 481 property, 482 expected, 483 0, 484 runningOn, 485 desc, 486 ExpectComparisonTo.Pass, 487 pseudo 488 ); 489 }; 490 491 // Many callers of this method will pass 'undefined' for 492 // expectedComparisonResult. 493 window.omta_is_approx = function ( 494 elem, 495 property, 496 expected, 497 tolerance, 498 runningOn, 499 desc, 500 expectedComparisonResult, 501 pseudo 502 ) { 503 // Check input 504 // FIXME: Auto generate this array. 505 const omtaProperties = [ 506 "transform", 507 "translate", 508 "rotate", 509 "scale", 510 "offset-path", 511 "offset-distance", 512 "offset-rotate", 513 "offset-anchor", 514 "offset-position", 515 "opacity", 516 "background-color", 517 ]; 518 if (!omtaProperties.includes(property)) { 519 ok(false, property + " is not an OMTA property"); 520 return; 521 } 522 var normalize; 523 var compare; 524 var normalizedToString = JSON.stringify; 525 switch (property) { 526 case "offset-path": 527 case "offset-distance": 528 case "offset-rotate": 529 case "offset-anchor": 530 case "offset-position": 531 case "translate": 532 case "rotate": 533 case "scale": 534 if (runningOn == RunningOn.MainThread) { 535 normalize = value => value; 536 compare = function (a, b, error) { 537 return a == b; 538 }; 539 break; 540 } 541 // fall through 542 case "transform": 543 normalize = convertTo3dMatrix; 544 compare = matricesRoughlyEqual; 545 normalizedToString = convert3dMatrixToString; 546 break; 547 case "opacity": 548 normalize = parseFloat; 549 compare = function (a, b, error) { 550 return Math.abs(a - b) <= error; 551 }; 552 break; 553 default: 554 normalize = value => value; 555 compare = function (a, b, error) { 556 return a == b; 557 }; 558 break; 559 } 560 561 if (!!expected.compositorValue) { 562 const originalNormalize = normalize; 563 normalize = value => 564 !!value.compositorValue 565 ? originalNormalize(value.compositorValue) 566 : originalNormalize(value); 567 } 568 569 // Get actual values 570 var compositorStr = SpecialPowers.DOMWindowUtils.getOMTAStyle( 571 elem, 572 property, 573 pseudo 574 ); 575 var computedStr = window.getComputedStyle(elem, pseudo)[property]; 576 577 // Prepare expected value 578 var expectedValue = normalize(expected); 579 if (expectedValue === null) { 580 ok( 581 false, 582 desc + 583 ": test author should provide a valid 'expected' value" + 584 " - got " + 585 expected.toString() 586 ); 587 return; 588 } 589 590 // Check expected value appears in the right place 591 var actualStr; 592 switch (runningOn) { 593 case RunningOn.Either: 594 runningOn = 595 compositorStr !== "" ? RunningOn.Compositor : RunningOn.MainThread; 596 actualStr = compositorStr !== "" ? compositorStr : computedStr; 597 break; 598 599 case RunningOn.Compositor: 600 if (compositorStr === "") { 601 ok(false, desc + ": should be animating on compositor"); 602 return; 603 } 604 actualStr = compositorStr; 605 break; 606 607 case RunningOn.TodoMainThread: 608 todo( 609 compositorStr === "", 610 desc + ": should NOT be animating on compositor" 611 ); 612 actualStr = compositorStr === "" ? computedStr : compositorStr; 613 break; 614 615 case RunningOn.TodoCompositor: 616 todo( 617 compositorStr !== "", 618 desc + ": should be animating on compositor" 619 ); 620 actualStr = compositorStr !== "" ? computedStr : compositorStr; 621 break; 622 623 default: 624 if (compositorStr !== "") { 625 ok(false, desc + ": should NOT be animating on compositor"); 626 return; 627 } 628 actualStr = computedStr; 629 break; 630 } 631 632 var okOrTodo = 633 expectedComparisonResult == ExpectComparisonTo.Fail ? todo : ok; 634 635 // Compare animated value with expected 636 var actualValue = normalize(actualStr); 637 // Note: the actualStr should be empty string when using todoCompositor, so 638 // actualValue is null in this case. However, compare() should handle null 639 // well. 640 okOrTodo( 641 compare(expectedValue, actualValue, tolerance), 642 desc + 643 " - got " + 644 actualStr + 645 ", expected " + 646 normalizedToString(expectedValue) 647 ); 648 649 // For transform-like properties, if we have multiple transform-like 650 // properties, the OMTA value and getComputedStyle() must be different, 651 // so use this flag to skip the following tests. 652 // FIXME: Putting this property on the expected value is a little bit odd. 653 // It's not really a product of the expected value, but rather the kind of 654 // test we're running. That said, the omta_is, omta_todo_is etc. methods are 655 // already pretty complex and adding another parameter would probably 656 // complicate things too much so this is fine for now. If we extend these 657 // functions any more, though, we should probably reconsider this API. 658 if (expected.usesMultipleProperties) { 659 return; 660 } 661 662 if (typeof expected.computed !== "undefined") { 663 // For some tests we specify a separate computed value for comparing 664 // with getComputedStyle. 665 // 666 // In particular, we do this for the individual transform functions since 667 // the form returned from getComputedStyle() reflects the individual 668 // properties (e.g. 'translate: 100px') while the form we read back from 669 // the compositor represents the combined result of all the transform 670 // properties as a single transform matrix (e.g. [0, 0, 0, 0, 100, 0]). 671 // 672 // Despite the fact that we can't directly compare the OMTA value against 673 // the getComputedStyle value in this case, it is still worth checking the 674 // result of getComputedStyle since it will help to alert us if some 675 // discrepancy arises between the way we calculate values on the main 676 // thread and compositor. 677 okOrTodo( 678 computedStr == expected.computed, 679 desc + ": Computed style should be equal to " + expected.computed 680 ); 681 } else if (actualStr === compositorStr) { 682 // For compositor animations do an additional check that they match 683 // the value calculated on the main thread 684 var computedValue = normalize(computedStr); 685 if (computedValue === null) { 686 ok( 687 false, 688 desc + 689 ": test framework should parse computed style" + 690 " - got " + 691 computedStr 692 ); 693 return; 694 } 695 okOrTodo( 696 compare(computedValue, actualValue, 0.0), 697 desc + 698 ": OMTA style and computed style should be equal" + 699 " - OMTA " + 700 actualStr + 701 ", computed " + 702 computedStr 703 ); 704 } 705 }; 706 707 window.matricesRoughlyEqual = function (a, b, tolerance) { 708 // Error handle if a or b is invalid. 709 if (!a || !b) { 710 return false; 711 } 712 713 tolerance = tolerance || 0.00011; 714 for (var i = 0; i < 4; i++) { 715 for (var j = 0; j < 4; j++) { 716 var diff = Math.abs(a[i][j] - b[i][j]); 717 if (diff > tolerance || isNaN(diff)) { 718 return false; 719 } 720 } 721 } 722 return true; 723 }; 724 725 // Converts something representing an transform into a 3d matrix in 726 // column-major order. 727 // The following are supported: 728 // "matrix(...)" 729 // "matrix3d(...)" 730 // [ 1, 0, 0, ... ] 731 // { a: 1, ty: 23 } etc. 732 window.convertTo3dMatrix = function (matrixLike) { 733 if (typeof matrixLike == "string") { 734 return convertStringTo3dMatrix(matrixLike); 735 } else if (Array.isArray(matrixLike)) { 736 return convertArrayTo3dMatrix(matrixLike); 737 } else if (typeof matrixLike == "object") { 738 return convertObjectTo3dMatrix(matrixLike); 739 } 740 return null; 741 }; 742 743 // In future most of these methods should be able to be replaced 744 // with DOMMatrix 745 window.isInvertible = function (matrix) { 746 return getDeterminant(matrix) != 0; 747 }; 748 749 // Converts strings of the format "matrix(...)" and "matrix3d(...)" to a 3d 750 // matrix 751 function convertStringTo3dMatrix(str) { 752 if (str == "none") { 753 return convertArrayTo3dMatrix([1, 0, 0, 1, 0, 0]); 754 } 755 var result = str.match("^matrix(3d)?\\("); 756 if (result === null) { 757 return null; 758 } 759 760 return convertArrayTo3dMatrix( 761 str 762 .substring(result[0].length, str.length - 1) 763 .split(",") 764 .map(function (component) { 765 return Number(component); 766 }) 767 ); 768 } 769 770 // Takes an array of numbers of length 6 (2d matrix) or 16 (3d matrix) 771 // representing a matrix specified in column-major order and returns a 3d 772 // matrix represented as an array of arrays 773 function convertArrayTo3dMatrix(array) { 774 if (array.length == 6) { 775 return convertObjectTo3dMatrix({ 776 a: array[0], 777 b: array[1], 778 c: array[2], 779 d: array[3], 780 e: array[4], 781 f: array[5], 782 }); 783 } else if (array.length == 16) { 784 return [ 785 array.slice(0, 4), 786 array.slice(4, 8), 787 array.slice(8, 12), 788 array.slice(12, 16), 789 ]; 790 } 791 return null; 792 } 793 794 // Return the first defined value in args. 795 function defined(...args) { 796 return args.find(arg => typeof arg !== "undefined"); 797 } 798 799 // Takes an object of the form { a: 1.1, e: 23 } and builds up a 3d matrix 800 // with unspecified values filled in with identity values. 801 function convertObjectTo3dMatrix(obj) { 802 return [ 803 [ 804 defined(obj.a, obj.sx, obj.m11, 1), 805 obj.b || obj.m12 || 0, 806 obj.m13 || 0, 807 obj.m14 || 0, 808 ], 809 [ 810 obj.c || obj.m21 || 0, 811 defined(obj.d, obj.sy, obj.m22, 1), 812 obj.m23 || 0, 813 obj.m24 || 0, 814 ], 815 [obj.m31 || 0, obj.m32 || 0, defined(obj.sz, obj.m33, 1), obj.m34 || 0], 816 [ 817 obj.e || obj.tx || obj.m41 || 0, 818 obj.f || obj.ty || obj.m42 || 0, 819 obj.tz || obj.m43 || 0, 820 defined(obj.m44, 1), 821 ], 822 ]; 823 } 824 825 function convert3dMatrixToString(matrix) { 826 if (is2d(matrix)) { 827 return ( 828 "matrix(" + 829 [ 830 matrix[0][0], 831 matrix[0][1], 832 matrix[1][0], 833 matrix[1][1], 834 matrix[3][0], 835 matrix[3][1], 836 ].join(", ") + 837 ")" 838 ); 839 } 840 return ( 841 "matrix3d(" + 842 matrix 843 .reduce(function (outer, inner) { 844 return outer.concat(inner); 845 }) 846 .join(", ") + 847 ")" 848 ); 849 } 850 851 function is2d(matrix) { 852 return ( 853 matrix[0][2] === 0 && 854 matrix[0][3] === 0 && 855 matrix[1][2] === 0 && 856 matrix[1][3] === 0 && 857 matrix[2][0] === 0 && 858 matrix[2][1] === 0 && 859 matrix[2][2] === 1 && 860 matrix[2][3] === 0 && 861 matrix[3][2] === 0 && 862 matrix[3][3] === 1 863 ); 864 } 865 866 function getDeterminant(matrix) { 867 if (is2d(matrix)) { 868 return matrix[0][0] * matrix[1][1] - matrix[0][1] * matrix[1][0]; 869 } 870 871 return ( 872 matrix[0][3] * matrix[1][2] * matrix[2][1] * matrix[3][0] - 873 matrix[0][2] * matrix[1][3] * matrix[2][1] * matrix[3][0] - 874 matrix[0][3] * matrix[1][1] * matrix[2][2] * matrix[3][0] + 875 matrix[0][1] * matrix[1][3] * matrix[2][2] * matrix[3][0] + 876 matrix[0][2] * matrix[1][1] * matrix[2][3] * matrix[3][0] - 877 matrix[0][1] * matrix[1][2] * matrix[2][3] * matrix[3][0] - 878 matrix[0][3] * matrix[1][2] * matrix[2][0] * matrix[3][1] + 879 matrix[0][2] * matrix[1][3] * matrix[2][0] * matrix[3][1] + 880 matrix[0][3] * matrix[1][0] * matrix[2][2] * matrix[3][1] - 881 matrix[0][0] * matrix[1][3] * matrix[2][2] * matrix[3][1] - 882 matrix[0][2] * matrix[1][0] * matrix[2][3] * matrix[3][1] + 883 matrix[0][0] * matrix[1][2] * matrix[2][3] * matrix[3][1] + 884 matrix[0][3] * matrix[1][1] * matrix[2][0] * matrix[3][2] - 885 matrix[0][1] * matrix[1][3] * matrix[2][0] * matrix[3][2] - 886 matrix[0][3] * matrix[1][0] * matrix[2][1] * matrix[3][2] + 887 matrix[0][0] * matrix[1][3] * matrix[2][1] * matrix[3][2] + 888 matrix[0][1] * matrix[1][0] * matrix[2][3] * matrix[3][2] - 889 matrix[0][0] * matrix[1][1] * matrix[2][3] * matrix[3][2] - 890 matrix[0][2] * matrix[1][1] * matrix[2][0] * matrix[3][3] + 891 matrix[0][1] * matrix[1][2] * matrix[2][0] * matrix[3][3] + 892 matrix[0][2] * matrix[1][0] * matrix[2][1] * matrix[3][3] - 893 matrix[0][0] * matrix[1][2] * matrix[2][1] * matrix[3][3] - 894 matrix[0][1] * matrix[1][0] * matrix[2][2] * matrix[3][3] + 895 matrix[0][0] * matrix[1][1] * matrix[2][2] * matrix[3][3] 896 ); 897 } 898 })(); 899 900 //---------------------------------------------------------------------- 901 // 902 // Promise wrappers for paint_listener.js 903 // 904 //---------------------------------------------------------------------- 905 906 // Returns a Promise that resolves once all paints have completed 907 function waitForPaints() { 908 return new Promise(function (resolve, reject) { 909 waitForAllPaints(resolve); 910 }); 911 } 912 913 // As with waitForPaints but also flushes pending style changes before waiting 914 function waitForPaintsFlushed() { 915 return new Promise(function (resolve, reject) { 916 waitForAllPaintsFlushed(resolve); 917 }); 918 } 919 920 function waitForVisitedLinkColoring(visitedLink, waitProperty, waitValue) { 921 function checkLink(resolve) { 922 if ( 923 SpecialPowers.DOMWindowUtils.getVisitedDependentComputedStyle( 924 visitedLink, 925 "", 926 waitProperty 927 ) == waitValue 928 ) { 929 // Our link has been styled as visited. Resolve. 930 resolve(true); 931 } else { 932 // Our link is not yet styled as visited. Poll for completion. 933 setTimeout(checkLink, 0, resolve); 934 } 935 } 936 return new Promise(function (resolve, reject) { 937 checkLink(resolve); 938 }); 939 }