layout/style/test/test_animations_omta.html

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/layout/style/test/test_animations_omta.html	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,954 @@
     1.4 +<!DOCTYPE HTML>
     1.5 +<html>
     1.6 +<!--
     1.7 +https://bugzilla.mozilla.org/show_bug.cgi?id=964646
     1.8 +-->
     1.9 +<!--
    1.10 +
    1.11 + ========= PLEASE KEEP THIS IN SYNC WITH test_animations.html =========
    1.12 +
    1.13 + This test mimicks the content of test_animations.html but performs tests
    1.14 + specific to animations that run on the compositor thread since they require
    1.15 + special (asynchronous) handling. Furthermore, these tests check that
    1.16 + animations that are expected to run on the compositor thread, are actually
    1.17 + doing so.
    1.18 +
    1.19 + If you are making changes to this file or to test_animations.html, please
    1.20 + try to keep them consistent where appropriate.
    1.21 +
    1.22 +-->
    1.23 +<head>
    1.24 +  <meta charset="utf-8">
    1.25 +  <title>Test for css3-animations running on the compositor thread (Bug
    1.26 +         964646)</title>
    1.27 +  <script type="application/javascript"
    1.28 +    src="/tests/SimpleTest/SimpleTest.js"></script>
    1.29 +  <script type="application/javascript"
    1.30 +    src="/tests/SimpleTest/paint_listener.js"></script>
    1.31 +  <script type="application/javascript" src="animation_utils.js"></script>
    1.32 +  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
    1.33 +  <style type="text/css">
    1.34 +    @keyframes transform-anim {
    1.35 +      to {
    1.36 +        transform: translate(100px);
    1.37 +      }
    1.38 +    }
    1.39 +    @keyframes anim1 {
    1.40 +       0% { transform: translate(0px) }
    1.41 +       50% { transform: translate(80px) }
    1.42 +       100% { transform: translate(100px) }
    1.43 +    }
    1.44 +    @keyframes anim2 {
    1.45 +      from { opacity: 0 } to { opacity: 1 }
    1.46 +    }
    1.47 +    @keyframes anim3 {
    1.48 +      from { opacity: 0 } to { opacity: 1 }
    1.49 +    }
    1.50 +
    1.51 +    @keyframes kf1 {
    1.52 +      50% { transform: translate(50px) }
    1.53 +      to { transform: translate(150px) }
    1.54 +    }
    1.55 +    @keyframes kf2 {
    1.56 +      from { transform: translate(150px) }
    1.57 +      50% { transform: translate(50px) }
    1.58 +    }
    1.59 +    @keyframes kf3 {
    1.60 +      25% { transform: translate(100px) }
    1.61 +    }
    1.62 +    @keyframes kf4 {
    1.63 +      to, from { display: inline; transform: translate(37px) }
    1.64 +    }
    1.65 +    @keyframes kf_cascade1 {
    1.66 +      from { transform: translate(50px) }
    1.67 +      50%, from { transform: translate(30px) }      /* wins: 0% */
    1.68 +      75%, 85%, 50% { transform: translate(20px) }  /* wins: 75%, 50% */
    1.69 +      100%, 85% { transform: translate(70px) }      /* wins: 100% */
    1.70 +      85.1% { transform: translate(60px) }          /* wins: 85.1% */
    1.71 +      85% { transform: translate(30px) }            /* wins: 85% */
    1.72 +    }
    1.73 +    @keyframes kf_cascade2 { from, to { opacity: 0.3 } }
    1.74 +    @keyframes kf_cascade2 { from, to { transform: translate(50px) } }
    1.75 +    @keyframes kf_cascade2 { from, to { transform: translate(100px) } }
    1.76 +
    1.77 +    .target {
    1.78 +      /* The animation target needs geometry in order to qualify for OMTA */
    1.79 +      width: 100px;
    1.80 +      height: 100px;
    1.81 +      background-color: white;
    1.82 +    }
    1.83 +  </style>
    1.84 +</head>
    1.85 +<body>
    1.86 +<a target="_blank"
    1.87 +  href="https://bugzilla.mozilla.org/show_bug.cgi?id=964646">Mozilla Bug
    1.88 +  964646</a>
    1.89 +<div id="display"></div>
    1.90 +<pre id="test">
    1.91 +<script type="application/javascript">
    1.92 +"use strict";
    1.93 +
    1.94 +/** Test for css3-animations running on the compositor thread (Bug 964646) **/
    1.95 + 
    1.96 +// Global state
    1.97 +var gAsyncTests     = [],
    1.98 +    gDisplay        = document.getElementById("display"),
    1.99 +    gDiv            = null,
   1.100 +    gEventsReceived = [];
   1.101 +
   1.102 +SimpleTest.waitForExplicitFinish();
   1.103 +runOMTATest(function() {
   1.104 +  // The async test runner returns a Promise that is resolved when the
   1.105 +  // test is finished so we can chain them together
   1.106 +  gAsyncTests.reduce(function(sequence, test) {
   1.107 +    return sequence.then(function() { return runAsyncTest(test); });
   1.108 +  }, Promise.resolve() /* the start of the sequence */)
   1.109 +  // Final step in the sequence
   1.110 +  .then(function() {
   1.111 +    SimpleTest.finish();
   1.112 +  });
   1.113 +}, SimpleTest.finish);
   1.114 +
   1.115 +// Takes a generator function that represents a test case. Each point in the
   1.116 +// test case that waits asynchronously for some result yields a Promise that is
   1.117 +// resolved when the asychronous action has completed. By chaining these
   1.118 +// intermediate results together we run the test to completion.
   1.119 +//
   1.120 +// This method itself returns a Promise that is resolved when the generator
   1.121 +// function has completed.
   1.122 +//
   1.123 +// This arrangement is based on add_task() which is currently only available
   1.124 +// in mochitest-chrome (bug 872229). Once add_task is available in
   1.125 +// mochitest-plain we can remove this function and use add_task instead.
   1.126 +function runAsyncTest(test) {
   1.127 +  var generator;
   1.128 +
   1.129 +  function step(arg) {
   1.130 +    var next;
   1.131 +    try {
   1.132 +      next = generator.next(arg);
   1.133 +    } catch (e) {
   1.134 +      return Promise.reject(e);
   1.135 +    }
   1.136 +    if (next.done) {
   1.137 +      return Promise.resolve(next.value);
   1.138 +    } else {
   1.139 +      return Promise.resolve(next.value)
   1.140 +             .then(step, function(err) { throw err; });
   1.141 +    }
   1.142 +  }
   1.143 +
   1.144 +  // Put refresh driver under test control
   1.145 +  advance_clock(0);
   1.146 +
   1.147 +  // Run test
   1.148 +  generator = test();
   1.149 +  return step()
   1.150 +  .catch(function(err) {
   1.151 +    ok(false, err.message);
   1.152 +    // Clear up the test div in case we aborted the test before doing clean-up
   1.153 +    if (gDiv) {
   1.154 +      done_div();
   1.155 +    }
   1.156 +  }).then(function() {
   1.157 +    // Restore clock
   1.158 +    SpecialPowers.DOMWindowUtils.restoreNormalRefresh();
   1.159 +  });
   1.160 +}
   1.161 +
   1.162 +function addAsyncTest(generator) {
   1.163 +  gAsyncTests.push(generator);
   1.164 +}
   1.165 +
   1.166 +//----------------------------------------------------------------------
   1.167 +//
   1.168 +// Test cases
   1.169 +//
   1.170 +//----------------------------------------------------------------------
   1.171 +
   1.172 +// This test is not in test_animations.html but is here to test that
   1.173 +// transform animations are actually run on the compositor thread as expected.
   1.174 +addAsyncTest(function *() {
   1.175 +  new_div("animation: transform-anim linear 300s");
   1.176 +
   1.177 +  yield waitForPaints();
   1.178 +
   1.179 +  advance_clock(200000);
   1.180 +  omta_is("transform", { tx: 100 * 2 / 3 }, RunningOn.Compositor,
   1.181 +          "OMTA animation is animating as expected");
   1.182 +  done_div();
   1.183 +});
   1.184 +
   1.185 +function *testFillMode(fillMode, fillsBackwards, fillsForwards)
   1.186 +{
   1.187 +  var style = "transform: translate(30px); animation: 10s 3s anim1 linear";
   1.188 +  var desc;
   1.189 +  if (fillMode.length > 0) {
   1.190 +    style += " " + fillMode;
   1.191 +    desc = "fill mode " + fillMode + ": ";
   1.192 +  } else {
   1.193 +    desc = "default fill mode: ";
   1.194 +  }
   1.195 +  new_div(style);
   1.196 +  listen();
   1.197 +
   1.198 +  // Currently backwards fill is not performed on the compositor thread but we
   1.199 +  // should wait for paints so we can test that transform values are *not* being
   1.200 +  // set on the compositor thread.
   1.201 +  yield waitForPaints();
   1.202 +
   1.203 +  if (fillsBackwards)
   1.204 +    omta_is("transform", { tx: 0 }, RunningOn.MainThread,
   1.205 +            desc + "does affect value during delay (0s)");
   1.206 +  else
   1.207 +    omta_is("transform", { tx: 30 }, RunningOn.MainThread,
   1.208 +            desc + "doesn't affect value during delay (0s)");
   1.209 +
   1.210 +  advance_clock(2000);
   1.211 +  if (fillsBackwards)
   1.212 +    omta_is("transform", { tx: 0 }, RunningOn.MainThead,
   1.213 +            desc + "does affect value during delay (0s)");
   1.214 +  else
   1.215 +    omta_is("transform", { tx: 30 }, RunningOn.MainThread,
   1.216 +            desc + "does affect value during delay (0s)");
   1.217 +
   1.218 +  check_events([], "before start in testFillMode");
   1.219 +  advance_clock(1000);
   1.220 +  check_events([{ type: "animationstart", target: gDiv,
   1.221 +                  bubbles: true, cancelable: false,
   1.222 +                  animationName: "anim1", elapsedTime: 0.0,
   1.223 +                  pseudoElement: "" }],
   1.224 +               "right after start in testFillMode");
   1.225 +
   1.226 +  // If we have a backwards fill then at the start of the animation we will end
   1.227 +  // up applying the same value as the fill value. Various optimizations in
   1.228 +  // RestyleManager may filter out this meaning that the animation doesn't get
   1.229 +  // added to the compositor thread until the first time the value changes.
   1.230 +  //
   1.231 +  // As a result we look for this first sample on either the compositor or the
   1.232 +  // computed style
   1.233 +  yield waitForPaints();
   1.234 +  omta_is("transform", { tx: 0 }, RunningOn.Either,
   1.235 +          desc + "affects value at start of animation");
   1.236 +  advance_clock(125);
   1.237 +  // We might not add the animation to compositor until the second sample (due
   1.238 +  // to the optimizations mentioned above) so we should wait for paints before
   1.239 +  // proceeding
   1.240 +  yield waitForPaints();
   1.241 +  omta_is("transform", { tx: 2 }, RunningOn.Compositor,
   1.242 +          desc + "affects value during animation");
   1.243 +  advance_clock(2375);
   1.244 +  omta_is("transform", { tx: 40 }, RunningOn.Compositor,
   1.245 +          desc + "affects value during animation");
   1.246 +  advance_clock(2500);
   1.247 +  omta_is("transform", { tx: 80 }, RunningOn.Compositor,
   1.248 +          desc + "affects value during animation");
   1.249 +  advance_clock(2500);
   1.250 +  omta_is("transform", { tx: 90 }, RunningOn.Compositor,
   1.251 +          desc + "affects value during animation");
   1.252 +  advance_clock(2375);
   1.253 +  omta_is("transform", { tx: 99.5 }, RunningOn.Compositor,
   1.254 +          desc + "affects value during animation");
   1.255 +  check_events([], "before end in testFillMode");
   1.256 +  advance_clock(125);
   1.257 +  check_events([{ type: "animationend", target: gDiv,
   1.258 +                  bubbles: true, cancelable: false,
   1.259 +                  animationName: "anim1", elapsedTime: 10.0,
   1.260 +                  pseudoElement: "" }],
   1.261 +               "right after end in testFillMode");
   1.262 +  
   1.263 +  // Currently the compositor will apply a forwards fill until it gets told by
   1.264 +  // the main thread to clear the animation. As a result we should wait for
   1.265 +  // paints to be flushed before checking that the animated value does *not*
   1.266 +  // appear on the compositor thread.
   1.267 +  yield waitForPaints();
   1.268 +  if (fillsForwards)
   1.269 +    omta_is("transform", { tx: 100 }, RunningOn.MainThread,
   1.270 +            desc + "affects value at end of animation");
   1.271 +  advance_clock(10);
   1.272 +  if (fillsForwards)
   1.273 +    omta_is("transform", { tx: 100 }, RunningOn.MainThread,
   1.274 +            desc + "affects value after animation");
   1.275 +  else
   1.276 +    omta_is("transform", { tx: 30 }, RunningOn.MainThread,
   1.277 +            desc + "does not affect value after animation");
   1.278 +
   1.279 +  done_div();
   1.280 +}
   1.281 +
   1.282 +addAsyncTest(function() { return testFillMode("", false, false); });
   1.283 +addAsyncTest(function() { return testFillMode("none", false, false); });
   1.284 +addAsyncTest(function() { return testFillMode("forwards", false, true); });
   1.285 +addAsyncTest(function() { return testFillMode("backwards", true, false); });
   1.286 +addAsyncTest(function() { return testFillMode("both", true, true); });
   1.287 +
   1.288 +// Test that animations continue running when the animation name
   1.289 +// list is changed.
   1.290 +//
   1.291 +// test_animations.html combines all these tests into one block but this is
   1.292 +// difficult for OMTA because currently there are only two properties to which
   1.293 +// we apply OMTA. Instead we break the test down into a few independent pieces
   1.294 +// in order to exercise the same functionality.
   1.295 +
   1.296 +// Append to list
   1.297 +addAsyncTest(function *() {
   1.298 +  new_div("animation: anim1 linear 10s");
   1.299 +  yield waitForPaints();
   1.300 +    omta_is("transform", { tx: 0 }, RunningOn.Either,
   1.301 +            "just anim1, translate at start");
   1.302 +  advance_clock(1000);
   1.303 +    omta_is("transform", { tx: 16 }, RunningOn.Compositor,
   1.304 +            "just anim1, translate at 1s");
   1.305 +  // append anim2
   1.306 +  gDiv.style.animation = "anim1 linear 10s, anim2 linear 10s";
   1.307 +  yield waitForPaintsFlushed();
   1.308 +    omta_is("transform", { tx: 16 }, RunningOn.Compositor,
   1.309 +            "anim1 + anim2, translate at 1s");
   1.310 +    omta_is("opacity", 0, RunningOn.Compositor,
   1.311 +            "anim1 + anim2, opacity at 1s");
   1.312 +  advance_clock(1000);
   1.313 +    omta_is("transform", { tx: 32 }, RunningOn.Compositor,
   1.314 +            "anim1 + anim2, translate at 2s");
   1.315 +    omta_is("opacity", 0.1, RunningOn.Compositor,
   1.316 +            "anim1 + anim2, opacity at 2s");
   1.317 +  done_div();
   1.318 +});
   1.319 +
   1.320 +// Prepend to list; delete from list
   1.321 +addAsyncTest(function *() {
   1.322 +  new_div("animation: anim1 linear 10s");
   1.323 +  yield waitForPaints();
   1.324 +    omta_is("transform", { tx: 0 }, RunningOn.Either,
   1.325 +            "just anim1, translate at start");
   1.326 +  advance_clock(1000);
   1.327 +    omta_is("transform", { tx: 16 }, RunningOn.Compositor,
   1.328 +                     "just anim1, translate at 1s");
   1.329 +  // prepend anim2
   1.330 +  gDiv.style.animation = "anim2 linear 10s, anim1 linear 10s";
   1.331 +  yield waitForPaintsFlushed();
   1.332 +    omta_is("transform", { tx: 16 }, RunningOn.Compositor,
   1.333 +            "anim2 + anim1, translate at 1s");
   1.334 +    omta_is("opacity", 0, RunningOn.Compositor,
   1.335 +            "anim2 + anim1, opacity at 1s");
   1.336 +  advance_clock(1000);
   1.337 +    omta_is("transform", { tx: 32 }, RunningOn.Compositor,
   1.338 +            "anim2 + anim1, translate at 2s");
   1.339 +    omta_is("opacity", 0.1, RunningOn.Compositor,
   1.340 +            "anim2 + anim1, opacity at 2s");
   1.341 +  // remove anim2 from list
   1.342 +  gDiv.style.animation = "anim1 linear 10s";
   1.343 +  yield waitForPaintsFlushed();
   1.344 +    omta_is("transform", { tx: 32 }, RunningOn.Compositor,
   1.345 +            "just anim1, translate at 2s");
   1.346 +    omta_is("opacity", 1, RunningOn.MainThread, "just anim1, opacity at 2s");
   1.347 +  advance_clock(1000);
   1.348 +    omta_is("transform", { tx: 48 }, RunningOn.Compositor,
   1.349 +            "just anim1, translate at 3s");
   1.350 +    omta_is("opacity", 1, RunningOn.MainThread, "just anim1, opacity at 3s");
   1.351 +  done_div();
   1.352 +});
   1.353 +
   1.354 +// Swap elements
   1.355 +addAsyncTest(function *() {
   1.356 +  new_div("animation: anim1 linear 10s, anim2 linear 10s");
   1.357 +  yield waitForPaints();
   1.358 +    omta_is("transform", { tx: 0 }, RunningOn.Either,
   1.359 +            "anim1 + anim2, translate at start");
   1.360 +    omta_is("opacity", 0, RunningOn.Compositor,
   1.361 +            "anim1 + anim2, opacity at start");
   1.362 +  advance_clock(1000);
   1.363 +    omta_is("transform", { tx: 16 }, RunningOn.Compositor,
   1.364 +            "anim1 + anim2, translate at 1s");
   1.365 +    omta_is("opacity", 0.1, RunningOn.Compositor,
   1.366 +            "anim1 + anim2, opacity at 1s");
   1.367 +  // swap anim1 and anim2, change duration of anim2
   1.368 +  gDiv.style.animation = "anim2 linear 5s, anim1 linear 10s";
   1.369 +  yield waitForPaintsFlushed();
   1.370 +    omta_is("transform", { tx: 16 }, RunningOn.Compositor,
   1.371 +            "anim2 + anim1, translate at 1s");
   1.372 +    omta_is("opacity", 0.2, RunningOn.Compositor,
   1.373 +            "anim2 + anim1, opacity at 1s");
   1.374 +  advance_clock(1000);
   1.375 +    omta_is("transform", { tx: 32 }, RunningOn.Compositor,
   1.376 +            "anim2 + anim1, translate at 2s");
   1.377 +    omta_is("opacity", 0.4, RunningOn.Compositor,
   1.378 +            "anim2 + anim1, opacity at 2s");
   1.379 +  // list anim2 twice, last duration wins, original start time still applies
   1.380 +  gDiv.style.animation = "anim2 linear 5s, anim1 linear 10s, anim2 linear 20s";
   1.381 +  yield waitForPaintsFlushed();
   1.382 +    omta_is("transform", { tx: 32 }, RunningOn.Compositor,
   1.383 +            "anim2 + anim1 + anim2, translate at 2s");
   1.384 +    // Bug 980769
   1.385 +    todo_is(SpecialPowers.DOMWindowUtils.getOMTAStyle(gDiv, "opacity"), "0.1",
   1.386 +            "anim2 + anim1 + anim2, opacity at 2s");
   1.387 +  // drop one of the anim2, and list anim3 as well, which animates
   1.388 +  // the same property as anim2
   1.389 +  gDiv.style.animation = "anim1 linear 10s, anim2 linear 20s, anim3 linear 10s";
   1.390 +  yield waitForPaintsFlushed();
   1.391 +    omta_is("transform", { tx: 32 }, RunningOn.Compositor,
   1.392 +            "anim1 + anim2 + anim3, translate at 2s");
   1.393 +    // Bug 980769
   1.394 +    todo_is(SpecialPowers.DOMWindowUtils.getOMTAStyle(gDiv, "opacity"), "0",
   1.395 +            "anim1 + anim2 + anim3, opacity at 2s");
   1.396 +  advance_clock(1000);
   1.397 +    omta_is("transform", { tx: 48 }, RunningOn.Compositor,
   1.398 +            "anim1 + anim2 + anim3, translate at 3s");
   1.399 +    // Bug 980769
   1.400 +    todo_is(SpecialPowers.DOMWindowUtils.getOMTAStyle(gDiv, "opacity"), "0.1",
   1.401 +            "anim1 + anim2 + anim3, opacity at 3s");
   1.402 +  // now swap the anim3 and anim2 order
   1.403 +  gDiv.style.animation = "anim1 linear 10s, anim3 linear 10s, anim2 linear 20s";
   1.404 +  yield waitForPaintsFlushed();
   1.405 +    omta_is("transform", { tx: 48 }, RunningOn.Compositor,
   1.406 +            "anim1 + anim3 + anim2, translate at 3s");
   1.407 +    // Bug 980769
   1.408 +    todo_is(SpecialPowers.DOMWindowUtils.getOMTAStyle(gDiv, "opacity"), "0.15",
   1.409 +            "anim1 + anim3 + anim2, opacity at 3s");
   1.410 +  advance_clock(2000); // (unlike test_animations.html, we seek 2s forwards here
   1.411 +                       // since at 4s anim2 and anim3 produce the same result so
   1.412 +                       // we can't tell which won.)
   1.413 +    omta_is("transform", { tx: 80 }, RunningOn.Compositor,
   1.414 +            "anim1 + anim3 + anim2, translate at 5s");
   1.415 +    // Bug 980769
   1.416 +    todo_is(SpecialPowers.DOMWindowUtils.getOMTAStyle(gDiv, "opacity"), "0.25",
   1.417 +            "anim1 + anim3 + anim2, opacity at 5s");
   1.418 +  // swap anim3 and anim2 back
   1.419 +  gDiv.style.animation = "anim1 linear 10s, anim2 linear 20s, anim3 linear 10s";
   1.420 +  yield waitForPaintsFlushed();
   1.421 +    omta_is("transform", { tx: 80 }, RunningOn.Compositor,
   1.422 +            "anim1 + anim2 + anim3, translate at 5s");
   1.423 +    // Bug 980769
   1.424 +    todo_is(SpecialPowers.DOMWindowUtils.getOMTAStyle(gDiv, "opacity"), "0.3",
   1.425 +            "anim1 + anim2 + anim3, opacity at 5s");
   1.426 +  // seek past end of anim1
   1.427 +  advance_clock(5100);
   1.428 +  yield waitForPaints();
   1.429 +    omta_is("transform", { tx: 0 }, RunningOn.MainThread,
   1.430 +            "anim1 + anim2 + anim3, translate at 10.1s");
   1.431 +  // Change the animation fill mode on the completed animation.
   1.432 +  gDiv.style.animation =
   1.433 +    "anim1 linear 10s forwards, anim2 linear 20s, anim3 linear 10s";
   1.434 +  yield waitForPaintsFlushed();
   1.435 +    omta_is("transform", { tx: 100 }, RunningOn.MainThread,
   1.436 +            "anim1 + anim2 + anim3, translate at 10.1s with fill mode");
   1.437 +  advance_clock(900);
   1.438 +    omta_is("transform", { tx: 100 }, RunningOn.MainThread,
   1.439 +            "anim1 + anim2 + anim3, translate at 11s with fill mode");
   1.440 +  // Change the animation duration on the completed animation, so it is
   1.441 +  // no longer completed.
   1.442 +  // XXX Not sure about this---there seems to be a bug in test_animations.html
   1.443 +  // in that it drops the fill mode but the test comment says it has a fill mode
   1.444 +  gDiv.style.animation = "anim1 linear 20s, anim2 linear 20s, anim3 linear 10s";
   1.445 +  yield waitForPaintsFlushed();
   1.446 +    omta_is("transform", { tx: 82 }, RunningOn.Compositor,
   1.447 +            "anim1 + anim2 + anim3, translate at 11s with fill mode");
   1.448 +    // Bug 980769 - We should get 0.9 but instead 
   1.449 +    todo_is(SpecialPowers.DOMWindowUtils.getOMTAStyle(gDiv, "opacity"), "0.9",
   1.450 +            "anim1 + anim2 + anim3, opacity at 11s");
   1.451 +  done_div();
   1.452 +});
   1.453 +
   1.454 +/*
   1.455 + * css3-animations:  3. Keyframes
   1.456 + * http://dev.w3.org/csswg/css3-animations/#keyframes
   1.457 + */
   1.458 +
   1.459 +// Test the rules on keyframes that lack a 0% or 100% rule:
   1.460 +// (simultaneously, test that reverse animations have their keyframes
   1.461 +// run backwards)
   1.462 +
   1.463 +addAsyncTest(function *() {
   1.464 +  // 100px at 0%, 50px at 50%, 150px at 100%
   1.465 +  new_div("transform: translate(100px); " +
   1.466 +          "animation: kf1 ease 1s alternate infinite");
   1.467 +  advance_clock(0);
   1.468 +  yield waitForPaints();
   1.469 +  omta_is("transform", { tx: 100 }, RunningOn.Compositor, "no-0% at 0.0s");
   1.470 +  advance_clock(100);
   1.471 +  omta_is_approx("transform", { tx: 100 - 50 * gTF.ease(0.2) },
   1.472 +                 RunningOn.Compositor, 0.01, "no-0% at 0.1s");
   1.473 +  advance_clock(200);
   1.474 +  omta_is_approx("transform", { tx: 100 - 50 * gTF.ease(0.6) },
   1.475 +                 RunningOn.Compositor, 0.01, "no-0% at 0.3s");
   1.476 +  advance_clock(200);
   1.477 +  omta_is("transform", { tx: 50 }, RunningOn.Compositor, "no-0% at 0.5s");
   1.478 +  advance_clock(200);
   1.479 +  omta_is_approx("transform", { tx: 50 + 100 * gTF.ease(0.4) },
   1.480 +                 RunningOn.Compositor, 0.01, "no-0% at 0.7s");
   1.481 +  advance_clock(200);
   1.482 +  omta_is_approx("transform", { tx: 50 + 100 * gTF.ease(0.8) },
   1.483 +                 RunningOn.Compositor, 0.01, "no-0% at 0.9s");
   1.484 +  advance_clock(100);
   1.485 +  omta_is("transform", { tx: 150 }, RunningOn.Compositor, "no-0% at 1.0s");
   1.486 +  advance_clock(100);
   1.487 +  omta_is_approx("transform", { tx: 50 + 100 * gTF.ease(0.8) },
   1.488 +                 RunningOn.Compositor, 0.01, "no-0% at 1.1s");
   1.489 +  advance_clock(300);
   1.490 +  omta_is_approx("transform", { tx: 50 + 100 * gTF.ease(0.2) },
   1.491 +                 RunningOn.Compositor, 0.01, "no-0% at 1.4s");
   1.492 +  advance_clock(300);
   1.493 +  omta_is_approx("transform", { tx: 100 - 50 * gTF.ease(0.6) },
   1.494 +                 RunningOn.Compositor, 0.01, "no-0% at 1.7s");
   1.495 +  advance_clock(200);
   1.496 +  omta_is_approx("transform", { tx: 100 - 50 * gTF.ease(0.2) },
   1.497 +                 RunningOn.Compositor, 0.01, "no-0% at 1.9s");
   1.498 +  advance_clock(100);
   1.499 +  omta_is("transform", { tx: 100 }, RunningOn.Compositor, "no-0% at 2.0s");
   1.500 +  done_div();
   1.501 +
   1.502 +  // 150px at 0%, 50px at 50%, 100px at 100%
   1.503 +  new_div("transform: translate(100px); " +
   1.504 +          "animation: kf2 ease-in 1s alternate infinite");
   1.505 +  yield waitForPaints();
   1.506 +  omta_is("transform", { tx: 150 }, RunningOn.Compositor, "no-100% at 0.0s");
   1.507 +  advance_clock(100);
   1.508 +  omta_is_approx("transform", { tx: 150 - 100 * gTF.ease_in(0.2) },
   1.509 +                 RunningOn.Compositor, 0.01, "no-100% at 0.1s");
   1.510 +  advance_clock(200);
   1.511 +  omta_is_approx("transform", { tx: 150 - 100 * gTF.ease_in(0.6) },
   1.512 +                 RunningOn.Compositor, 0.01, "no-100% at 0.3s");
   1.513 +  advance_clock(200);
   1.514 +  omta_is("transform", { tx: 50 }, RunningOn.Compositor, "no-100% at 0.5s");
   1.515 +  advance_clock(200);
   1.516 +  omta_is_approx("transform", { tx: 50 + 50 * gTF.ease_in(0.4) },
   1.517 +                 RunningOn.Compositor, 0.01, "no-100% at 0.7s");
   1.518 +  advance_clock(200);
   1.519 +  omta_is_approx("transform", { tx: 50 + 50 * gTF.ease_in(0.8) },
   1.520 +                 RunningOn.Compositor, 0.01, "no-100% at 0.9s");
   1.521 +  advance_clock(100);
   1.522 +  omta_is("transform", { tx: 100 }, RunningOn.Compositor, "no-100% at 1.0s");
   1.523 +  advance_clock(100);
   1.524 +  omta_is_approx("transform", { tx: 50 + 50 * gTF.ease_in(0.8) },
   1.525 +                 RunningOn.Compositor, 0.01, "no-100% at 1.1s");
   1.526 +  advance_clock(300);
   1.527 +  omta_is_approx("transform", { tx: 50 + 50 * gTF.ease_in(0.2) },
   1.528 +                 RunningOn.Compositor, 0.01, "no-100% at 1.4s");
   1.529 +  advance_clock(300);
   1.530 +  omta_is_approx("transform", { tx: 150 - 100 * gTF.ease_in(0.6) },
   1.531 +                 RunningOn.Compositor, 0.01, "no-100% at 1.7s");
   1.532 +  advance_clock(200);
   1.533 +  omta_is_approx("transform", { tx: 150 - 100 * gTF.ease_in(0.2) },
   1.534 +                 RunningOn.Compositor, 0.01, "no-100% at 1.9s");
   1.535 +  advance_clock(100);
   1.536 +  omta_is("transform", { tx: 150 }, RunningOn.Compositor, "no-100% at 2.0s");
   1.537 +  done_div();
   1.538 +
   1.539 +  // 50px at 0%, 100px at 25%, 50px at 100%
   1.540 +  new_div("transform: translate(50px); " +
   1.541 +          "animation: kf3 ease-out 1s alternate infinite");
   1.542 +  yield waitForPaints();
   1.543 +  omta_is("transform", { tx: 50 }, RunningOn.Compositor,
   1.544 +          "no-0%-no-100% at 0.0s");
   1.545 +  advance_clock(50);
   1.546 +  omta_is_approx("transform", { tx: 50 + 50 * gTF.ease_out(0.2) },
   1.547 +                 RunningOn.Compositor, 0.01, "no-0%-no-100% at 0.05s");
   1.548 +  advance_clock(100);
   1.549 +  omta_is_approx("transform", { tx: 50 + 50 * gTF.ease_out(0.6) },
   1.550 +                 RunningOn.Compositor, 0.01, "no-0%-no-100% at 0.15s");
   1.551 +  advance_clock(100);
   1.552 +  omta_is("transform", { tx: "100px" }, RunningOn.Compositor,
   1.553 +          "no-0%-no-100% at 0.25s");
   1.554 +  advance_clock(300);
   1.555 +  omta_is_approx("transform", { tx: 100 - 50 * gTF.ease_out(0.4) },
   1.556 +                 RunningOn.Compositor, 0.01, "no-0%-no-100% at 0.55s");
   1.557 +  advance_clock(300);
   1.558 +  omta_is_approx("transform", { tx: 100 - 50 * gTF.ease_out(0.8) },
   1.559 +                 RunningOn.Compositor, 0.01, "no-0%-no-100% at 0.85s");
   1.560 +  advance_clock(150);
   1.561 +  omta_is("transform", { tx: 50 }, RunningOn.Compositor,
   1.562 +          "no-0%-no-100% at 1.0s");
   1.563 +  advance_clock(150);
   1.564 +  omta_is_approx("transform", { tx: 100 - 50 * gTF.ease_out(0.8) },
   1.565 +                 RunningOn.Compositor, 0.01, "no-0%-no-100% at 1.15s");
   1.566 +  advance_clock(450);
   1.567 +  omta_is_approx("transform", { tx: 100 - 50 * gTF.ease_out(0.2) },
   1.568 +                 RunningOn.Compositor, 0.01, "no-0%-no-100% at 1.6s");
   1.569 +  advance_clock(250);
   1.570 +  omta_is_approx("transform", { tx: 50 + 50 * gTF.ease_out(0.6) },
   1.571 +                 RunningOn.Compositor, 0.01, "no-0%-no-100% at 1.85s");
   1.572 +  advance_clock(100);
   1.573 +  omta_is_approx("transform", { tx: 50 + 50 * gTF.ease_out(0.2) },
   1.574 +                 RunningOn.Compositor, 0.01, "no-0%-no-100% at 1.95s");
   1.575 +  advance_clock(50);
   1.576 +  omta_is("transform", { tx: 50 }, RunningOn.Compositor,
   1.577 +          "no-0%-no-100% at 2.0s");
   1.578 +  done_div();
   1.579 +
   1.580 +  // Test that non-animatable properties are ignored.
   1.581 +  // Simultaneously, test that the block is still honored, and that
   1.582 +  // we still override the value when two consecutive keyframes have
   1.583 +  // the same value.
   1.584 +  new_div("animation: kf4 ease 10s");
   1.585 +  yield waitForPaints();
   1.586 +  var cs = window.getComputedStyle(gDiv);
   1.587 +  is(cs.display, "block",
   1.588 +     "non-animatable properties should be ignored (linear, 0s)");
   1.589 +  omta_is("transform", { tx: 37 }, RunningOn.Compositor,
   1.590 +          "animatable properties should still apply (linear, 0s)");
   1.591 +  advance_clock(1000);
   1.592 +  is(cs.display, "block",
   1.593 +     "non-animatable properties should be ignored (linear, 1s)");
   1.594 +  omta_is("transform", { tx: 37 }, RunningOn.Compositor,
   1.595 +          "animatable properties should still apply (linear, 1s)");
   1.596 +  done_div();
   1.597 +  new_div("animation: kf4 step-start 10s");
   1.598 +  yield waitForPaints();
   1.599 +  cs = window.getComputedStyle(gDiv);
   1.600 +  is(cs.display, "block",
   1.601 +     "non-animatable properties should be ignored (step-start, 0s)");
   1.602 +  omta_is("transform", { tx: 37 }, RunningOn.Compositor,
   1.603 +          "animatable properties should still apply (step-start, 0s)");
   1.604 +  advance_clock(1000);
   1.605 +  is(cs.display, "block",
   1.606 +     "non-animatable properties should be ignored (step-start, 1s)");
   1.607 +  omta_is("transform", { tx: 37 }, RunningOn.Compositor,
   1.608 +          "animatable properties should still apply (step-start, 1s)");
   1.609 +  done_div();
   1.610 +
   1.611 +  // Test cascading of the keyframes within an @keyframes rule.
   1.612 +  new_div("animation: kf_cascade1 linear 10s");
   1.613 +  yield waitForPaints();
   1.614 +  //    0%: 30px
   1.615 +  //   50%: 20px
   1.616 +  //   75%: 20px
   1.617 +  //   85%: 30px
   1.618 +  // 85.1%: 60px
   1.619 +  //  100%: 70px
   1.620 +  omta_is("transform", { tx: 30 }, RunningOn.Compositor, "kf_cascade1 at 0s");
   1.621 +  advance_clock(2500);
   1.622 +  omta_is("transform", { tx: 25 }, RunningOn.Compositor, "kf_cascade1 at 2.5s");
   1.623 +  advance_clock(2500);
   1.624 +  omta_is("transform", { tx: 20 }, RunningOn.Compositor, "kf_cascade1 at 5s");
   1.625 +  advance_clock(2000);
   1.626 +  omta_is("transform", { tx: 20 }, RunningOn.Compositor, "kf_cascade1 at 7s");
   1.627 +  advance_clock(500);
   1.628 +  omta_is("transform", { tx: 20 }, RunningOn.Compositor, "kf_cascade1 at 7.5s");
   1.629 +  advance_clock(500);
   1.630 +  omta_is("transform", { tx: 25 }, RunningOn.Compositor, "kf_cascade1 at 8s");
   1.631 +  advance_clock(500);
   1.632 +  omta_is("transform", { tx: 30 }, RunningOn.Compositor, "kf_cascade1 at 8.5s");
   1.633 +  advance_clock(10);
   1.634 +  // For some reason we get an error of 0.0003 for this test only
   1.635 +  omta_is_approx("transform", { tx: 60 }, RunningOn.Compositor, 0.001,
   1.636 +                 "kf_cascade1 at 8.51s");
   1.637 +  advance_clock(745);
   1.638 +  omta_is("transform", { tx: 65 }, RunningOn.Compositor,
   1.639 +          "kf_cascade1 at 9.2505s");
   1.640 +  done_div();
   1.641 +
   1.642 +  // Test cascading of the @keyframes rules themselves.
   1.643 +  new_div("animation: kf_cascade2 linear 10s");
   1.644 +  yield waitForPaints();
   1.645 +  omta_is("opacity", 1, RunningOn.MainThread,
   1.646 +          "last @keyframes rule with transform should win");
   1.647 +  omta_is("transform", { tx: 100 }, RunningOn.Compositor,
   1.648 +          "last @keyframes rule with transform should win");
   1.649 +  done_div();
   1.650 +});
   1.651 +
   1.652 +//----------------------------------------------------------------------
   1.653 +//
   1.654 +// Helper functions from test_animations.html
   1.655 +//
   1.656 +//----------------------------------------------------------------------
   1.657 +
   1.658 +function new_div(style) {
   1.659 +  if (gDiv !== null) {
   1.660 +    ok(false, "test author forgot to call done_div");
   1.661 +  }
   1.662 +  if (typeof(style) != "string") {
   1.663 +    ok(false, "test author forgot to pass style argument");
   1.664 +  }
   1.665 +  gDiv = document.createElement("div");
   1.666 +  gDiv.classList.add("target");
   1.667 +  gDiv.setAttribute("style", style);
   1.668 +  gDisplay.appendChild(gDiv);
   1.669 +  gDiv.clientTop;
   1.670 +}
   1.671 +
   1.672 +function done_div() {
   1.673 +  if (gDiv === null) {
   1.674 +    ok(false, "test author forgot to call new_div");
   1.675 +  }
   1.676 +  gDisplay.removeChild(gDiv);
   1.677 +  gDiv = null;
   1.678 +}
   1.679 +
   1.680 +function listen() {
   1.681 +  gEventsReceived = [];
   1.682 +  function listener(event) {
   1.683 +    gEventsReceived.push(event);
   1.684 +  }
   1.685 +  gDiv.addEventListener("animationstart", listener, false);
   1.686 +  gDiv.addEventListener("animationiteration", listener, false);
   1.687 +  gDiv.addEventListener("animationend", listener, false);
   1.688 +}
   1.689 +
   1.690 +function check_events(events_expected, desc) {
   1.691 +  // This function checks that the list of events_expected matches
   1.692 +  // the received events -- but it only checks the properties that
   1.693 +  // are present on events_expected.
   1.694 +  is(gEventsReceived.length, events_expected.length,
   1.695 +     "number of events received for " + desc);
   1.696 +  for (var i = 0,
   1.697 +       i_end = Math.min(events_expected.length, gEventsReceived.length);
   1.698 +       i != i_end; ++i) {
   1.699 +    var exp = events_expected[i];
   1.700 +    var rec = gEventsReceived[i];
   1.701 +    for (var prop in exp) {
   1.702 +      if (prop == "elapsedTime") {
   1.703 +        // Allow floating point error.
   1.704 +        ok(Math.abs(rec.elapsedTime - exp.elapsedTime) < 0.000002,
   1.705 +           "events[" + i + "]." + prop + " for " + desc +
   1.706 +           " received=" + rec.elapsedTime + " expected=" + exp.elapsedTime);
   1.707 +      } else {
   1.708 +        is(rec[prop], exp[prop], "events[" + i + "]." + prop + " for " + desc);
   1.709 +      }
   1.710 +    }
   1.711 +  }
   1.712 +  for (i = events_expected.length; i < gEventsReceived.length; ++i) {
   1.713 +    ok(false, "unexpected " + gEventsReceived[i].type + " event for " + desc);
   1.714 +  }
   1.715 +  gEventsReceived = [];
   1.716 +}
   1.717 +
   1.718 +function advance_clock(milliseconds) {
   1.719 +  SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(milliseconds);
   1.720 +}
   1.721 +
   1.722 +//----------------------------------------------------------------------
   1.723 +//
   1.724 +// Helper functions for querying the compositor thread
   1.725 +//
   1.726 +//----------------------------------------------------------------------
   1.727 +
   1.728 +// Returns a Promise that resolves once all paints have completed
   1.729 +function waitForPaints() {
   1.730 +  return new Promise(function(resolve, reject) {
   1.731 +    waitForAllPaints(resolve);
   1.732 +  });
   1.733 +}
   1.734 +
   1.735 +// As with waitForPaints but also flushes pending style changes before waiting
   1.736 +function waitForPaintsFlushed() {
   1.737 +  return new Promise(function(resolve, reject) {
   1.738 +    waitForAllPaintsFlushed(resolve);
   1.739 +  });
   1.740 +}
   1.741 +
   1.742 +//----------------------------------------------------------------------
   1.743 +//
   1.744 +// Helper functions for working with animated values
   1.745 +//
   1.746 +//----------------------------------------------------------------------
   1.747 +
   1.748 +const RunningOn = {
   1.749 +  MainThread: 0,
   1.750 +  Compositor: 1,
   1.751 +  Either: 2
   1.752 +};
   1.753 +
   1.754 +function omta_is(property, expected, runningOn, desc) {
   1.755 +  return omta_is_approx(property, expected, runningOn, 0, desc);
   1.756 +}
   1.757 +
   1.758 +function omta_is_approx(property, expected, runningOn, tolerance, desc) {
   1.759 +  // Check input
   1.760 +  const omtaProperties = [ "transform", "opacity" ];
   1.761 +  if (omtaProperties.indexOf(property) === -1) {
   1.762 +    ok(false, property + " is not an OMTA property");
   1.763 +    return;
   1.764 +  }
   1.765 +  var isTransform = property == "transform";
   1.766 +  var normalize = isTransform ? convertTo3dMatrix : parseFloat;
   1.767 +  var compare = isTransform ?
   1.768 +                matricesRoughlyEqual :
   1.769 +                function(a, b, error) { return Math.abs(a - b) <= error; };
   1.770 +  var normalizedToString = isTransform ?
   1.771 +                           convert3dMatrixToString :
   1.772 +                           JSON.stringify;
   1.773 +
   1.774 +  // Get actual values
   1.775 +  var compositorStr = SpecialPowers.DOMWindowUtils.getOMTAStyle(gDiv, property);
   1.776 +  var computedStr   = window.getComputedStyle(gDiv)[property];
   1.777 +
   1.778 +  // Prepare expected value
   1.779 +  var expectedValue = normalize(expected);
   1.780 +  if (expectedValue === null) {
   1.781 +    ok(false, desc + ": test author should provide a valid 'expected' value" +
   1.782 +              " - got " + expected.toString());
   1.783 +    return;
   1.784 +  }
   1.785 +
   1.786 +  // Check expected value appears in the right place
   1.787 +  var actualStr;
   1.788 +  switch (runningOn) {
   1.789 +    case RunningOn.Either:
   1.790 +      runningOn = compositorStr !== "" ?
   1.791 +                  RunningOn.Compositor :
   1.792 +                  RunningOn.MainThread;
   1.793 +      actualStr = compositorStr !== "" ? compositorStr : computedStr;
   1.794 +      break;
   1.795 +
   1.796 +    case RunningOn.Compositor:
   1.797 +      if (compositorStr === "") {
   1.798 +        ok(false, desc + ": should be animating on compositor");
   1.799 +        return;
   1.800 +      }
   1.801 +      actualStr = compositorStr;
   1.802 +      break;
   1.803 +
   1.804 +    default:
   1.805 +      if (compositorStr !== "") {
   1.806 +        ok(false, desc + ": should NOT be animating on compositor");
   1.807 +        return;
   1.808 +      }
   1.809 +      actualStr = computedStr;
   1.810 +      break;
   1.811 +  }
   1.812 +
   1.813 +  // Compare animated value with expected
   1.814 +  var actualValue = normalize(actualStr);
   1.815 +  if (actualValue === null) {
   1.816 +    ok(false, desc + ": should return a valid result - got " + actualStr);
   1.817 +    return;
   1.818 +  }
   1.819 +  ok(compare(expectedValue, actualValue, tolerance),
   1.820 +     desc + " - got " + actualStr + ", expected " +
   1.821 +     normalizedToString(expectedValue));
   1.822 +
   1.823 +  // For compositor animations do an additional check that they match
   1.824 +  // the value calculated on the main thread
   1.825 +  if (runningOn === RunningOn.Compositor) {
   1.826 +    var computedValue = normalize(computedStr);
   1.827 +    if (computedValue === null) {
   1.828 +      ok(false, desc + ": test framework should parse computed style" +
   1.829 +                " - got " + computedStr);
   1.830 +      return;
   1.831 +    }
   1.832 +    ok(compare(computedValue, actualValue, 0),
   1.833 +       desc + ": OMTA style and computed style should be equal" +
   1.834 +       " - OMTA " + actualStr + ", computed " + computedStr);
   1.835 +  }
   1.836 +}
   1.837 +
   1.838 +function matricesRoughlyEqual(a, b, tolerance) {
   1.839 +  tolerance = tolerance || 0.0001;
   1.840 +  for (var i = 0; i < 4; i++) {
   1.841 +    for (var j = 0; j < 4; j++) {
   1.842 +      if (Math.abs(a[i][j] - b[i][j]) > tolerance)
   1.843 +        return false;
   1.844 +    }
   1.845 +  }
   1.846 +  return true;
   1.847 +}
   1.848 +
   1.849 +// Converts something representing an transform into a 3d matrix in column-major
   1.850 +// order.
   1.851 +// The following are supported:
   1.852 +//  "matrix(...)"
   1.853 +//  "matrix3d(...)"
   1.854 +//  [ 1, 0, 0, ... ]
   1.855 +//  { a: 1, ty: 23 } etc.
   1.856 +function convertTo3dMatrix(matrixLike) {
   1.857 +  if (typeof(matrixLike) == "string") {
   1.858 +    return convertStringTo3dMatrix(matrixLike);
   1.859 +  } else if (Array.isArray(matrixLike)) {
   1.860 +    return convertArrayTo3dMatrix(matrixLike);
   1.861 +  } else if (typeof(matrixLike) == "object") {
   1.862 +    return convertObjectTo3dMatrix(matrixLike);
   1.863 +  } else {
   1.864 +    return null;
   1.865 +  }
   1.866 +}
   1.867 +
   1.868 +// Converts strings of the format "matrix(...)" and "matrix3d(...)" to a 3d
   1.869 +// matrix
   1.870 +function convertStringTo3dMatrix(str) {
   1.871 +  if (str == "none")
   1.872 +    return convertArrayTo3dMatrix([1, 0, 0, 1, 0, 0]);
   1.873 +  var result = str.match("^matrix(3d)?\\(");
   1.874 +  if (result === null)
   1.875 +    return null;
   1.876 +
   1.877 +  return convertArrayTo3dMatrix(
   1.878 +      str.substring(result[0].length, str.length-1)
   1.879 +         .split(",")
   1.880 +         .map(function(component) {
   1.881 +           return Number(component);
   1.882 +         })
   1.883 +    );
   1.884 +}
   1.885 +
   1.886 +// Takes an array of numbers of length 6 (2d matrix) or 16 (3d matrix)
   1.887 +// representing a matrix specified in column-major order and returns a 3d matrix
   1.888 +// represented as an array of arrays
   1.889 +function convertArrayTo3dMatrix(array) {
   1.890 +  if (array.length == 6) {
   1.891 +    return convertObjectTo3dMatrix(
   1.892 +      { a: array[0], b: array[1],
   1.893 +        c: array[2], d: array[3],
   1.894 +        e: array[4], f: array[5] } );
   1.895 +  } else if (array.length == 16) {
   1.896 +    return [
   1.897 +      array.slice(0, 3),
   1.898 +      array.slice(4, 7),
   1.899 +      array.slice(8, 11),
   1.900 +      array.slice(12, 15)
   1.901 +    ];
   1.902 +  } else {
   1.903 +    return null;
   1.904 +  }
   1.905 +}
   1.906 +
   1.907 +// Takes an object of the form { a: 1.1, e: 23 } and builds up a 3d matrix
   1.908 +// with unspecified values filled in with identity values.
   1.909 +function convertObjectTo3dMatrix(obj) {
   1.910 +  return [
   1.911 +    [
   1.912 +      obj.a || obj.sx || obj.m11 || 1,
   1.913 +      obj.b || obj.m12 || 0,
   1.914 +      obj.m13 || 0,
   1.915 +      obj.m14 || 0
   1.916 +    ], [
   1.917 +      obj.c || obj.m21 || 0,
   1.918 +      obj.d || obj.sy || obj.m22 || 1,
   1.919 +      obj.m23 || 0,
   1.920 +      obj.m24 || 0
   1.921 +    ], [
   1.922 +      obj.m31 || 0,
   1.923 +      obj.m32 || 0,
   1.924 +      obj.sz || obj.m33 || 1,
   1.925 +      obj.m34 || 0
   1.926 +    ], [
   1.927 +      obj.e || obj.tx || obj.m41 || 0,
   1.928 +      obj.f || obj.ty || obj.m42 || 0,
   1.929 +      obj.tz || obj.m43 || 0,
   1.930 +      obj.m44 || 1
   1.931 +    ]
   1.932 +  ];
   1.933 +}
   1.934 +
   1.935 +function convert3dMatrixToString(matrix) {
   1.936 +  if (is2d(matrix)) {
   1.937 +    return "matrix(" +
   1.938 +           [ matrix[0][0], matrix[0][1],
   1.939 +             matrix[1][0], matrix[1][1],
   1.940 +             matrix[3][0], matrix[3][1] ].join(", ") + ")";
   1.941 +  } else {
   1.942 +    return "matrix3d(" +
   1.943 +            matrix.reduce(function(outer, inner) {
   1.944 +                return outer.concat(inner);
   1.945 +            }).join(", ") + ")";
   1.946 +  }
   1.947 +}
   1.948 +
   1.949 +function is2d(matrix) {
   1.950 +  return matrix[0][2] === 0 && matrix[0][3] === 0 &&
   1.951 +         matrix[1][2] === 0 && matrix[1][3] === 0 &&
   1.952 +         matrix[2][0] === 0 && matrix[2][1] === 0 &&
   1.953 +         matrix[2][2] === 1 && matrix[2][3] === 0 &&
   1.954 +         matrix[3][2] === 0 && matrix[3][3] === 1;
   1.955 +}
   1.956 +</script>
   1.957 +</html>

mercurial