1// Copyright 2014 Google Inc. All rights reserved. 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15(function(scope, testing) { 16 var decomposeMatrix = (function() { 17 function determinant(m) { 18 return m[0][0] * m[1][1] * m[2][2] + 19 m[1][0] * m[2][1] * m[0][2] + 20 m[2][0] * m[0][1] * m[1][2] - 21 m[0][2] * m[1][1] * m[2][0] - 22 m[1][2] * m[2][1] * m[0][0] - 23 m[2][2] * m[0][1] * m[1][0]; 24 } 25 26 // from Wikipedia: 27 // 28 // [A B]^-1 = [A^-1 + A^-1B(D - CA^-1B)^-1CA^-1 -A^-1B(D - CA^-1B)^-1] 29 // [C D] [-(D - CA^-1B)^-1CA^-1 (D - CA^-1B)^-1 ] 30 // 31 // Therefore 32 // 33 // [A [0]]^-1 = [A^-1 [0]] 34 // [C 1 ] [ -CA^-1 1 ] 35 function inverse(m) { 36 var iDet = 1 / determinant(m); 37 var a = m[0][0], b = m[0][1], c = m[0][2]; 38 var d = m[1][0], e = m[1][1], f = m[1][2]; 39 var g = m[2][0], h = m[2][1], k = m[2][2]; 40 var Ainv = [ 41 [(e * k - f * h) * iDet, (c * h - b * k) * iDet, 42 (b * f - c * e) * iDet, 0], 43 [(f * g - d * k) * iDet, (a * k - c * g) * iDet, 44 (c * d - a * f) * iDet, 0], 45 [(d * h - e * g) * iDet, (g * b - a * h) * iDet, 46 (a * e - b * d) * iDet, 0] 47 ]; 48 var lastRow = []; 49 for (var i = 0; i < 3; i++) { 50 var val = 0; 51 for (var j = 0; j < 3; j++) { 52 val += m[3][j] * Ainv[j][i]; 53 } 54 lastRow.push(val); 55 } 56 lastRow.push(1); 57 Ainv.push(lastRow); 58 return Ainv; 59 } 60 61 function transposeMatrix4(m) { 62 return [[m[0][0], m[1][0], m[2][0], m[3][0]], 63 [m[0][1], m[1][1], m[2][1], m[3][1]], 64 [m[0][2], m[1][2], m[2][2], m[3][2]], 65 [m[0][3], m[1][3], m[2][3], m[3][3]]]; 66 } 67 68 function multVecMatrix(v, m) { 69 var result = []; 70 for (var i = 0; i < 4; i++) { 71 var val = 0; 72 for (var j = 0; j < 4; j++) { 73 val += v[j] * m[j][i]; 74 } 75 result.push(val); 76 } 77 return result; 78 } 79 80 function normalize(v) { 81 var len = length(v); 82 return [v[0] / len, v[1] / len, v[2] / len]; 83 } 84 85 function length(v) { 86 return Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]); 87 } 88 89 function combine(v1, v2, v1s, v2s) { 90 return [v1s * v1[0] + v2s * v2[0], v1s * v1[1] + v2s * v2[1], 91 v1s * v1[2] + v2s * v2[2]]; 92 } 93 94 function cross(v1, v2) { 95 return [v1[1] * v2[2] - v1[2] * v2[1], 96 v1[2] * v2[0] - v1[0] * v2[2], 97 v1[0] * v2[1] - v1[1] * v2[0]]; 98 } 99 100 // TODO: Implement 2D matrix decomposition. 101 // http://dev.w3.org/csswg/css-transforms/#decomposing-a-2d-matrix 102 function decomposeMatrix(matrix) { 103 var m3d = [ 104 matrix.slice(0, 4), 105 matrix.slice(4, 8), 106 matrix.slice(8, 12), 107 matrix.slice(12, 16) 108 ]; 109 110 // skip normalization step as m3d[3][3] should always be 1 111 if (m3d[3][3] !== 1) { 112 return null; 113 } 114 115 var perspectiveMatrix = []; 116 for (var i = 0; i < 4; i++) { 117 perspectiveMatrix.push(m3d[i].slice()); 118 } 119 120 for (var i = 0; i < 3; i++) { 121 perspectiveMatrix[i][3] = 0; 122 } 123 124 if (determinant(perspectiveMatrix) === 0) { 125 return false; 126 } 127 128 var rhs = []; 129 130 var perspective; 131 if (m3d[0][3] || m3d[1][3] || m3d[2][3]) { 132 rhs.push(m3d[0][3]); 133 rhs.push(m3d[1][3]); 134 rhs.push(m3d[2][3]); 135 rhs.push(m3d[3][3]); 136 137 var inversePerspectiveMatrix = inverse(perspectiveMatrix); 138 var transposedInversePerspectiveMatrix = 139 transposeMatrix4(inversePerspectiveMatrix); 140 perspective = multVecMatrix(rhs, transposedInversePerspectiveMatrix); 141 } else { 142 perspective = [0, 0, 0, 1]; 143 } 144 145 var translate = m3d[3].slice(0, 3); 146 147 var row = []; 148 row.push(m3d[0].slice(0, 3)); 149 var scale = []; 150 scale.push(length(row[0])); 151 row[0] = normalize(row[0]); 152 153 var skew = []; 154 row.push(m3d[1].slice(0, 3)); 155 skew.push(dot(row[0], row[1])); 156 row[1] = combine(row[1], row[0], 1.0, -skew[0]); 157 158 scale.push(length(row[1])); 159 row[1] = normalize(row[1]); 160 skew[0] /= scale[1]; 161 162 row.push(m3d[2].slice(0, 3)); 163 skew.push(dot(row[0], row[2])); 164 row[2] = combine(row[2], row[0], 1.0, -skew[1]); 165 skew.push(dot(row[1], row[2])); 166 row[2] = combine(row[2], row[1], 1.0, -skew[2]); 167 168 scale.push(length(row[2])); 169 row[2] = normalize(row[2]); 170 skew[1] /= scale[2]; 171 skew[2] /= scale[2]; 172 173 var pdum3 = cross(row[1], row[2]); 174 if (dot(row[0], pdum3) < 0) { 175 for (var i = 0; i < 3; i++) { 176 scale[i] *= -1; 177 row[i][0] *= -1; 178 row[i][1] *= -1; 179 row[i][2] *= -1; 180 } 181 } 182 183 var t = row[0][0] + row[1][1] + row[2][2] + 1; 184 var s; 185 var quaternion; 186 187 if (t > 1e-4) { 188 s = 0.5 / Math.sqrt(t); 189 quaternion = [ 190 (row[2][1] - row[1][2]) * s, 191 (row[0][2] - row[2][0]) * s, 192 (row[1][0] - row[0][1]) * s, 193 0.25 / s 194 ]; 195 } else if (row[0][0] > row[1][1] && row[0][0] > row[2][2]) { 196 s = Math.sqrt(1 + row[0][0] - row[1][1] - row[2][2]) * 2.0; 197 quaternion = [ 198 0.25 * s, 199 (row[0][1] + row[1][0]) / s, 200 (row[0][2] + row[2][0]) / s, 201 (row[2][1] - row[1][2]) / s 202 ]; 203 } else if (row[1][1] > row[2][2]) { 204 s = Math.sqrt(1.0 + row[1][1] - row[0][0] - row[2][2]) * 2.0; 205 quaternion = [ 206 (row[0][1] + row[1][0]) / s, 207 0.25 * s, 208 (row[1][2] + row[2][1]) / s, 209 (row[0][2] - row[2][0]) / s 210 ]; 211 } else { 212 s = Math.sqrt(1.0 + row[2][2] - row[0][0] - row[1][1]) * 2.0; 213 quaternion = [ 214 (row[0][2] + row[2][0]) / s, 215 (row[1][2] + row[2][1]) / s, 216 0.25 * s, 217 (row[1][0] - row[0][1]) / s 218 ]; 219 } 220 221 return [translate, scale, skew, quaternion, perspective]; 222 } 223 return decomposeMatrix; 224 })(); 225 226 function dot(v1, v2) { 227 var result = 0; 228 for (var i = 0; i < v1.length; i++) { 229 result += v1[i] * v2[i]; 230 } 231 return result; 232 } 233 234 function multiplyMatrices(a, b) { 235 return [ 236 a[0] * b[0] + a[4] * b[1] + a[8] * b[2] + a[12] * b[3], 237 a[1] * b[0] + a[5] * b[1] + a[9] * b[2] + a[13] * b[3], 238 a[2] * b[0] + a[6] * b[1] + a[10] * b[2] + a[14] * b[3], 239 a[3] * b[0] + a[7] * b[1] + a[11] * b[2] + a[15] * b[3], 240 241 a[0] * b[4] + a[4] * b[5] + a[8] * b[6] + a[12] * b[7], 242 a[1] * b[4] + a[5] * b[5] + a[9] * b[6] + a[13] * b[7], 243 a[2] * b[4] + a[6] * b[5] + a[10] * b[6] + a[14] * b[7], 244 a[3] * b[4] + a[7] * b[5] + a[11] * b[6] + a[15] * b[7], 245 246 a[0] * b[8] + a[4] * b[9] + a[8] * b[10] + a[12] * b[11], 247 a[1] * b[8] + a[5] * b[9] + a[9] * b[10] + a[13] * b[11], 248 a[2] * b[8] + a[6] * b[9] + a[10] * b[10] + a[14] * b[11], 249 a[3] * b[8] + a[7] * b[9] + a[11] * b[10] + a[15] * b[11], 250 251 a[0] * b[12] + a[4] * b[13] + a[8] * b[14] + a[12] * b[15], 252 a[1] * b[12] + a[5] * b[13] + a[9] * b[14] + a[13] * b[15], 253 a[2] * b[12] + a[6] * b[13] + a[10] * b[14] + a[14] * b[15], 254 a[3] * b[12] + a[7] * b[13] + a[11] * b[14] + a[15] * b[15] 255 ]; 256 } 257 258 // TODO: This can probably be made smaller. 259 function convertItemToMatrix(item) { 260 switch (item.t) { 261 // TODO: Handle units other than rads and degs. 262 case 'rotatex': 263 var rads = item.d[0].rad || 0; 264 var degs = item.d[0].deg || 0; 265 var angle = (degs * Math.PI / 180) + rads; 266 return [1, 0, 0, 0, 267 0, Math.cos(angle), Math.sin(angle), 0, 268 0, -Math.sin(angle), Math.cos(angle), 0, 269 0, 0, 0, 1]; 270 case 'rotatey': 271 var rads = item.d[0].rad || 0; 272 var degs = item.d[0].deg || 0; 273 var angle = (degs * Math.PI / 180) + rads; 274 return [Math.cos(angle), 0, -Math.sin(angle), 0, 275 0, 1, 0, 0, 276 Math.sin(angle), 0, Math.cos(angle), 0, 277 0, 0, 0, 1]; 278 case 'rotate': 279 case 'rotatez': 280 var rads = item.d[0].rad || 0; 281 var degs = item.d[0].deg || 0; 282 var angle = (degs * Math.PI / 180) + rads; 283 return [Math.cos(angle), Math.sin(angle), 0, 0, 284 -Math.sin(angle), Math.cos(angle), 0, 0, 285 0, 0, 1, 0, 286 0, 0, 0, 1]; 287 case 'rotate3d': 288 var x = item.d[0]; 289 var y = item.d[1]; 290 var z = item.d[2]; 291 var rads = item.d[3].rad || 0; 292 var degs = item.d[3].deg || 0; 293 var angle = (degs * Math.PI / 180) + rads; 294 295 var sqrLength = x * x + y * y + z * z; 296 if (sqrLength === 0) { 297 x = 1; 298 y = 0; 299 z = 0; 300 } else if (sqrLength !== 1) { 301 var length = Math.sqrt(sqrLength); 302 x /= length; 303 y /= length; 304 z /= length; 305 } 306 307 var s = Math.sin(angle / 2); 308 var sc = s * Math.cos(angle / 2); 309 var sq = s * s; 310 return [ 311 1 - 2 * (y * y + z * z) * sq, 312 2 * (x * y * sq + z * sc), 313 2 * (x * z * sq - y * sc), 314 0, 315 316 2 * (x * y * sq - z * sc), 317 1 - 2 * (x * x + z * z) * sq, 318 2 * (y * z * sq + x * sc), 319 0, 320 321 2 * (x * z * sq + y * sc), 322 2 * (y * z * sq - x * sc), 323 1 - 2 * (x * x + y * y) * sq, 324 0, 325 326 0, 0, 0, 1 327 ]; 328 case 'scale': 329 return [item.d[0], 0, 0, 0, 330 0, item.d[1], 0, 0, 331 0, 0, 1, 0, 332 0, 0, 0, 1]; 333 case 'scalex': 334 return [item.d[0], 0, 0, 0, 335 0, 1, 0, 0, 336 0, 0, 1, 0, 337 0, 0, 0, 1]; 338 case 'scaley': 339 return [1, 0, 0, 0, 340 0, item.d[0], 0, 0, 341 0, 0, 1, 0, 342 0, 0, 0, 1]; 343 case 'scalez': 344 return [1, 0, 0, 0, 345 0, 1, 0, 0, 346 0, 0, item.d[0], 0, 347 0, 0, 0, 1]; 348 case 'scale3d': 349 return [item.d[0], 0, 0, 0, 350 0, item.d[1], 0, 0, 351 0, 0, item.d[2], 0, 352 0, 0, 0, 1]; 353 // FIXME: Skew behaves differently in Blink, FireFox and here. Need to work out why. 354 case 'skew': 355 var xDegs = item.d[0].deg || 0; 356 var xRads = item.d[0].rad || 0; 357 var yDegs = item.d[1].deg || 0; 358 var yRads = item.d[1].rad || 0; 359 var xAngle = (xDegs * Math.PI / 180) + xRads; 360 var yAngle = (yDegs * Math.PI / 180) + yRads; 361 return [1, Math.tan(yAngle), 0, 0, 362 Math.tan(xAngle), 1, 0, 0, 363 0, 0, 1, 0, 364 0, 0, 0, 1]; 365 case 'skewx': 366 var rads = item.d[0].rad || 0; 367 var degs = item.d[0].deg || 0; 368 var angle = (degs * Math.PI / 180) + rads; 369 return [1, 0, 0, 0, 370 Math.tan(angle), 1, 0, 0, 371 0, 0, 1, 0, 372 0, 0, 0, 1]; 373 case 'skewy': 374 var rads = item.d[0].rad || 0; 375 var degs = item.d[0].deg || 0; 376 var angle = (degs * Math.PI / 180) + rads; 377 return [1, Math.tan(angle), 0, 0, 378 0, 1, 0, 0, 379 0, 0, 1, 0, 380 0, 0, 0, 1]; 381 // TODO: Work out what to do with non-px values. 382 case 'translate': 383 var x = item.d[0].px || 0; 384 var y = item.d[1].px || 0; 385 return [1, 0, 0, 0, 386 0, 1, 0, 0, 387 0, 0, 1, 0, 388 x, y, 0, 1]; 389 case 'translatex': 390 var x = item.d[0].px || 0; 391 return [1, 0, 0, 0, 392 0, 1, 0, 0, 393 0, 0, 1, 0, 394 x, 0, 0, 1]; 395 case 'translatey': 396 var y = item.d[0].px || 0; 397 return [1, 0, 0, 0, 398 0, 1, 0, 0, 399 0, 0, 1, 0, 400 0, y, 0, 1]; 401 case 'translatez': 402 var z = item.d[0].px || 0; 403 return [1, 0, 0, 0, 404 0, 1, 0, 0, 405 0, 0, 1, 0, 406 0, 0, z, 1]; 407 case 'translate3d': 408 var x = item.d[0].px || 0; 409 var y = item.d[1].px || 0; 410 var z = item.d[2].px || 0; 411 return [1, 0, 0, 0, 412 0, 1, 0, 0, 413 0, 0, 1, 0, 414 x, y, z, 1]; 415 case 'perspective': 416 var p = item.d[0].px ? (-1 / item.d[0].px) : 0; 417 return [ 418 1, 0, 0, 0, 419 0, 1, 0, 0, 420 0, 0, 1, p, 421 0, 0, 0, 1]; 422 case 'matrix': 423 return [item.d[0], item.d[1], 0, 0, 424 item.d[2], item.d[3], 0, 0, 425 0, 0, 1, 0, 426 item.d[4], item.d[5], 0, 1]; 427 case 'matrix3d': 428 return item.d; 429 default: 430 WEB_ANIMATIONS_TESTING && console.assert(false, 'Transform item type ' + item.t + 431 ' conversion to matrix not yet implemented.'); 432 } 433 } 434 435 function convertToMatrix(transformList) { 436 if (transformList.length === 0) { 437 return [1, 0, 0, 0, 438 0, 1, 0, 0, 439 0, 0, 1, 0, 440 0, 0, 0, 1]; 441 } 442 return transformList.map(convertItemToMatrix).reduce(multiplyMatrices); 443 } 444 445 function makeMatrixDecomposition(transformList) { 446 return [decomposeMatrix(convertToMatrix(transformList))]; 447 } 448 449 scope.dot = dot; 450 scope.makeMatrixDecomposition = makeMatrixDecomposition; 451 452})(webAnimations1, webAnimationsTesting); 453