1/* 2 * Add some helpers for matrices. This is ported from SkMatrix.cpp and others 3 * to save complexity and overhead of going back and forth between C++ and JS layers. 4 * I would have liked to use something like DOMMatrix, except it 5 * isn't widely supported (would need polyfills) and it doesn't 6 * have a mapPoints() function (which could maybe be tacked on here). 7 * If DOMMatrix catches on, it would be worth re-considering this usage. 8 */ 9 10CanvasKit.Matrix = {}; 11function sdot() { // to be called with an even number of scalar args 12 var acc = 0; 13 for (var i=0; i < arguments.length-1; i+=2) { 14 acc += arguments[i] * arguments[i+1]; 15 } 16 return acc; 17} 18 19// Private general matrix functions used in both 3x3s and 4x4s. 20// Return a square identity matrix of size n. 21var identityN = function(n) { 22 var size = n*n; 23 var m = new Array(size); 24 while(size--) { 25 m[size] = size%(n+1) === 0 ? 1.0 : 0.0; 26 } 27 return m; 28}; 29 30// Stride, a function for compactly representing several ways of copying an array into another. 31// Write vector `v` into matrix `m`. `m` is a matrix encoded as an array in row-major 32// order. Its width is passed as `width`. `v` is an array with length < (m.length/width). 33// An element of `v` is copied into `m` starting at `offset` and moving `colStride` cols right 34// each row. 35// 36// For example, a width of 4, offset of 3, and stride of -1 would put the vector here. 37// _ _ 0 _ 38// _ 1 _ _ 39// 2 _ _ _ 40// _ _ _ 3 41// 42var stride = function(v, m, width, offset, colStride) { 43 for (var i=0; i<v.length; i++) { 44 m[i * width + // column 45 (i * colStride + offset + width) % width // row 46 ] = v[i]; 47 } 48 return m; 49}; 50 51CanvasKit.Matrix.identity = function() { 52 return identityN(3); 53}; 54 55// Return the inverse (if it exists) of this matrix. 56// Otherwise, return null. 57CanvasKit.Matrix.invert = function(m) { 58 // Find the determinant by the sarrus rule. https://en.wikipedia.org/wiki/Rule_of_Sarrus 59 var det = m[0]*m[4]*m[8] + m[1]*m[5]*m[6] + m[2]*m[3]*m[7] 60 - m[2]*m[4]*m[6] - m[1]*m[3]*m[8] - m[0]*m[5]*m[7]; 61 if (!det) { 62 Debug('Warning, uninvertible matrix'); 63 return null; 64 } 65 // Return the inverse by the formula adj(m)/det. 66 // adj (adjugate) of a 3x3 is the transpose of it's cofactor matrix. 67 // a cofactor matrix is a matrix where each term is +-det(N) where matrix N is the 2x2 formed 68 // by removing the row and column we're currently setting from the source. 69 // the sign alternates in a checkerboard pattern with a `+` at the top left. 70 // that's all been combined here into one expression. 71 return [ 72 (m[4]*m[8] - m[5]*m[7])/det, (m[2]*m[7] - m[1]*m[8])/det, (m[1]*m[5] - m[2]*m[4])/det, 73 (m[5]*m[6] - m[3]*m[8])/det, (m[0]*m[8] - m[2]*m[6])/det, (m[2]*m[3] - m[0]*m[5])/det, 74 (m[3]*m[7] - m[4]*m[6])/det, (m[1]*m[6] - m[0]*m[7])/det, (m[0]*m[4] - m[1]*m[3])/det, 75 ]; 76}; 77 78// Maps the given points according to the passed in matrix. 79// Results are done in place. 80// See SkMatrix.h::mapPoints for the docs on the math. 81CanvasKit.Matrix.mapPoints = function(matrix, ptArr) { 82 if (IsDebug && (ptArr.length % 2)) { 83 throw 'mapPoints requires an even length arr'; 84 } 85 for (var i = 0; i < ptArr.length; i+=2) { 86 var x = ptArr[i], y = ptArr[i+1]; 87 // Gx+Hy+I 88 var denom = matrix[6]*x + matrix[7]*y + matrix[8]; 89 // Ax+By+C 90 var xTrans = matrix[0]*x + matrix[1]*y + matrix[2]; 91 // Dx+Ey+F 92 var yTrans = matrix[3]*x + matrix[4]*y + matrix[5]; 93 ptArr[i] = xTrans/denom; 94 ptArr[i+1] = yTrans/denom; 95 } 96 return ptArr; 97}; 98 99function isnumber(val) { return !isNaN(val); } 100 101// generalized iterative algorithm for multiplying two matrices. 102function multiply(m1, m2, size) { 103 104 if (IsDebug && (!m1.every(isnumber) || !m2.every(isnumber))) { 105 throw 'Some members of matrices are NaN m1='+m1+', m2='+m2+''; 106 } 107 if (IsDebug && (m1.length !== m2.length)) { 108 throw 'Undefined for matrices of different sizes. m1.length='+m1.length+', m2.length='+m2.length; 109 } 110 if (IsDebug && (size*size !== m1.length)) { 111 throw 'Undefined for non-square matrices. array size was '+size; 112 } 113 114 var result = Array(m1.length); 115 for (var r = 0; r < size; r++) { 116 for (var c = 0; c < size; c++) { 117 // accumulate a sum of m1[r,k]*m2[k, c] 118 var acc = 0; 119 for (var k = 0; k < size; k++) { 120 acc += m1[size * r + k] * m2[size * k + c]; 121 } 122 result[r * size + c] = acc; 123 } 124 } 125 return result; 126} 127 128// Accept an integer indicating the size of the matrices being multiplied (3 for 3x3), and any 129// number of matrices following it. 130function multiplyMany(size, listOfMatrices) { 131 if (IsDebug && (listOfMatrices.length < 2)) { 132 throw 'multiplication expected two or more matrices'; 133 } 134 var result = multiply(listOfMatrices[0], listOfMatrices[1], size); 135 var next = 2; 136 while (next < listOfMatrices.length) { 137 result = multiply(result, listOfMatrices[next], size); 138 next++; 139 } 140 return result; 141} 142 143// Accept any number 3x3 of matrices as arguments, multiply them together. 144// Matrix multiplication is associative but not commutative. the order of the arguments 145// matters, but it does not matter that this implementation multiplies them left to right. 146CanvasKit.Matrix.multiply = function() { 147 return multiplyMany(3, arguments); 148}; 149 150// Return a matrix representing a rotation by n radians. 151// px, py optionally say which point the rotation should be around 152// with the default being (0, 0); 153CanvasKit.Matrix.rotated = function(radians, px, py) { 154 px = px || 0; 155 py = py || 0; 156 var sinV = Math.sin(radians); 157 var cosV = Math.cos(radians); 158 return [ 159 cosV, -sinV, sdot( sinV, py, 1 - cosV, px), 160 sinV, cosV, sdot(-sinV, px, 1 - cosV, py), 161 0, 0, 1, 162 ]; 163}; 164 165CanvasKit.Matrix.scaled = function(sx, sy, px, py) { 166 px = px || 0; 167 py = py || 0; 168 var m = stride([sx, sy], identityN(3), 3, 0, 1); 169 return stride([px-sx*px, py-sy*py], m, 3, 2, 0); 170}; 171 172CanvasKit.Matrix.skewed = function(kx, ky, px, py) { 173 px = px || 0; 174 py = py || 0; 175 var m = stride([kx, ky], identityN(3), 3, 1, -1); 176 return stride([-kx*px, -ky*py], m, 3, 2, 0); 177}; 178 179CanvasKit.Matrix.translated = function(dx, dy) { 180 return stride(arguments, identityN(3), 3, 2, 0); 181}; 182 183// Functions for manipulating vectors. 184// Loosely based off of SkV3 in SkM44.h but skia also has SkVec2 and Skv4. This combines them and 185// works on vectors of any length. 186CanvasKit.Vector = {}; 187CanvasKit.Vector.dot = function(a, b) { 188 if (IsDebug && (a.length !== b.length)) { 189 throw 'Cannot perform dot product on arrays of different length ('+a.length+' vs '+b.length+')'; 190 } 191 return a.map(function(v, i) { return v*b[i] }).reduce(function(acc, cur) { return acc + cur; }); 192}; 193CanvasKit.Vector.lengthSquared = function(v) { 194 return CanvasKit.Vector.dot(v, v); 195}; 196CanvasKit.Vector.length = function(v) { 197 return Math.sqrt(CanvasKit.Vector.lengthSquared(v)); 198}; 199CanvasKit.Vector.mulScalar = function(v, s) { 200 return v.map(function(i) { return i*s }); 201}; 202CanvasKit.Vector.add = function(a, b) { 203 return a.map(function(v, i) { return v+b[i] }); 204}; 205CanvasKit.Vector.sub = function(a, b) { 206 return a.map(function(v, i) { return v-b[i]; }); 207}; 208CanvasKit.Vector.dist = function(a, b) { 209 return CanvasKit.Vector.length(CanvasKit.Vector.sub(a, b)); 210}; 211CanvasKit.Vector.normalize = function(v) { 212 return CanvasKit.Vector.mulScalar(v, 1/CanvasKit.Vector.length(v)); 213}; 214CanvasKit.Vector.cross = function(a, b) { 215 if (IsDebug && (a.length !== 3 || a.length !== 3)) { 216 throw 'Cross product is only defined for 3-dimensional vectors (a.length='+a.length+', b.length='+b.length+')'; 217 } 218 return [ 219 a[1]*b[2] - a[2]*b[1], 220 a[2]*b[0] - a[0]*b[2], 221 a[0]*b[1] - a[1]*b[0], 222 ]; 223}; 224 225// Functions for creating and manipulating (row-major) 4x4 matrices. Accepted in place of 226// SkM44 in canvas methods, for the same reasons as the 3x3 matrices above. 227// ported from C++ code in SkM44.cpp 228CanvasKit.M44 = {}; 229// Create a 4x4 identity matrix 230CanvasKit.M44.identity = function() { 231 return identityN(4); 232}; 233 234// Anything named vec below is an array of length 3 representing a vector/point in 3D space. 235// Create a 4x4 matrix representing a translate by the provided 3-vec 236CanvasKit.M44.translated = function(vec) { 237 return stride(vec, identityN(4), 4, 3, 0); 238}; 239// Create a 4x4 matrix representing a scaling by the provided 3-vec 240CanvasKit.M44.scaled = function(vec) { 241 return stride(vec, identityN(4), 4, 0, 1); 242}; 243// Create a 4x4 matrix representing a rotation about the provided axis 3-vec. 244// axis does not need to be normalized. 245CanvasKit.M44.rotated = function(axisVec, radians) { 246 return CanvasKit.M44.rotatedUnitSinCos( 247 CanvasKit.Vector.normalize(axisVec), Math.sin(radians), Math.cos(radians)); 248}; 249// Create a 4x4 matrix representing a rotation about the provided normalized axis 3-vec. 250// Rotation is provided redundantly as both sin and cos values. 251// This rotate can be used when you already have the cosAngle and sinAngle values 252// so you don't have to atan(cos/sin) to call roatated() which expects an angle in radians. 253// this does no checking! Behavior for invalid sin or cos values or non-normalized axis vectors 254// is incorrect. Prefer rotated(). 255CanvasKit.M44.rotatedUnitSinCos = function(axisVec, sinAngle, cosAngle) { 256 var x = axisVec[0]; 257 var y = axisVec[1]; 258 var z = axisVec[2]; 259 var c = cosAngle; 260 var s = sinAngle; 261 var t = 1 - c; 262 return [ 263 t*x*x + c, t*x*y - s*z, t*x*z + s*y, 0, 264 t*x*y + s*z, t*y*y + c, t*y*z - s*x, 0, 265 t*x*z - s*y, t*y*z + s*x, t*z*z + c, 0, 266 0, 0, 0, 1 267 ]; 268}; 269// Create a 4x4 matrix representing a camera at eyeVec, pointed at centerVec. 270CanvasKit.M44.lookat = function(eyeVec, centerVec, upVec) { 271 var f = CanvasKit.Vector.normalize(CanvasKit.Vector.sub(centerVec, eyeVec)); 272 var u = CanvasKit.Vector.normalize(upVec); 273 var s = CanvasKit.Vector.normalize(CanvasKit.Vector.cross(f, u)); 274 275 var m = CanvasKit.M44.identity(); 276 // set each column's top three numbers 277 stride(s, m, 4, 0, 0); 278 stride(CanvasKit.Vector.cross(s, f), m, 4, 1, 0); 279 stride(CanvasKit.Vector.mulScalar(f, -1), m, 4, 2, 0); 280 stride(eyeVec, m, 4, 3, 0); 281 282 var m2 = CanvasKit.M44.invert(m); 283 if (m2 === null) { 284 return CanvasKit.M44.identity(); 285 } 286 return m2; 287}; 288// Create a 4x4 matrix representing a perspective. All arguments are scalars. 289// angle is in radians. 290CanvasKit.M44.perspective = function(near, far, angle) { 291 if (IsDebug && (far <= near)) { 292 throw 'far must be greater than near when constructing M44 using perspective.'; 293 } 294 var dInv = 1 / (far - near); 295 var halfAngle = angle / 2; 296 var cot = Math.cos(halfAngle) / Math.sin(halfAngle); 297 return [ 298 cot, 0, 0, 0, 299 0, cot, 0, 0, 300 0, 0, (far+near)*dInv, 2*far*near*dInv, 301 0, 0, -1, 1, 302 ]; 303}; 304// Returns the number at the given row and column in matrix m. 305CanvasKit.M44.rc = function(m, r, c) { 306 return m[r*4+c]; 307}; 308// Accepts any number of 4x4 matrix arguments, multiplies them left to right. 309CanvasKit.M44.multiply = function() { 310 return multiplyMany(4, arguments); 311}; 312 313// Invert the 4x4 matrix if it is invertible and return it. if not, return null. 314// taken from SkM44.cpp (altered to use row-major order) 315// m is not altered. 316CanvasKit.M44.invert = function(m) { 317 if (IsDebug && !m.every(isnumber)) { 318 throw 'some members of matrix are NaN m='+m; 319 } 320 321 var a00 = m[0]; 322 var a01 = m[4]; 323 var a02 = m[8]; 324 var a03 = m[12]; 325 var a10 = m[1]; 326 var a11 = m[5]; 327 var a12 = m[9]; 328 var a13 = m[13]; 329 var a20 = m[2]; 330 var a21 = m[6]; 331 var a22 = m[10]; 332 var a23 = m[14]; 333 var a30 = m[3]; 334 var a31 = m[7]; 335 var a32 = m[11]; 336 var a33 = m[15]; 337 338 var b00 = a00 * a11 - a01 * a10; 339 var b01 = a00 * a12 - a02 * a10; 340 var b02 = a00 * a13 - a03 * a10; 341 var b03 = a01 * a12 - a02 * a11; 342 var b04 = a01 * a13 - a03 * a11; 343 var b05 = a02 * a13 - a03 * a12; 344 var b06 = a20 * a31 - a21 * a30; 345 var b07 = a20 * a32 - a22 * a30; 346 var b08 = a20 * a33 - a23 * a30; 347 var b09 = a21 * a32 - a22 * a31; 348 var b10 = a21 * a33 - a23 * a31; 349 var b11 = a22 * a33 - a23 * a32; 350 351 // calculate determinate 352 var det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06; 353 var invdet = 1.0 / det; 354 355 // bail out if the matrix is not invertible 356 if (det === 0 || invdet === Infinity) { 357 Debug('Warning, uninvertible matrix'); 358 return null; 359 } 360 361 b00 *= invdet; 362 b01 *= invdet; 363 b02 *= invdet; 364 b03 *= invdet; 365 b04 *= invdet; 366 b05 *= invdet; 367 b06 *= invdet; 368 b07 *= invdet; 369 b08 *= invdet; 370 b09 *= invdet; 371 b10 *= invdet; 372 b11 *= invdet; 373 374 // store result in row major order 375 var tmp = [ 376 a11 * b11 - a12 * b10 + a13 * b09, 377 a12 * b08 - a10 * b11 - a13 * b07, 378 a10 * b10 - a11 * b08 + a13 * b06, 379 a11 * b07 - a10 * b09 - a12 * b06, 380 381 a02 * b10 - a01 * b11 - a03 * b09, 382 a00 * b11 - a02 * b08 + a03 * b07, 383 a01 * b08 - a00 * b10 - a03 * b06, 384 a00 * b09 - a01 * b07 + a02 * b06, 385 386 a31 * b05 - a32 * b04 + a33 * b03, 387 a32 * b02 - a30 * b05 - a33 * b01, 388 a30 * b04 - a31 * b02 + a33 * b00, 389 a31 * b01 - a30 * b03 - a32 * b00, 390 391 a22 * b04 - a21 * b05 - a23 * b03, 392 a20 * b05 - a22 * b02 + a23 * b01, 393 a21 * b02 - a20 * b04 - a23 * b00, 394 a20 * b03 - a21 * b01 + a22 * b00, 395 ]; 396 397 398 if (!tmp.every(function(val) { return !isNaN(val) && val !== Infinity && val !== -Infinity; })) { 399 Debug('inverted matrix contains infinities or NaN '+tmp); 400 return null; 401 } 402 return tmp; 403}; 404 405CanvasKit.M44.transpose = function(m) { 406 return [ 407 m[0], m[4], m[8], m[12], 408 m[1], m[5], m[9], m[13], 409 m[2], m[6], m[10], m[14], 410 m[3], m[7], m[11], m[15], 411 ]; 412}; 413 414// Return the inverse of an SkM44. throw an error if it's not invertible 415CanvasKit.M44.mustInvert = function(m) { 416 var m2 = CanvasKit.M44.invert(m); 417 if (m2 === null) { 418 throw 'Matrix not invertible'; 419 } 420 return m2; 421}; 422 423// returns a matrix that sets up a 3D perspective view from a given camera. 424// 425// area - a rect describing the viewport. (0, 0, canvas_width, canvas_height) suggested 426// zscale - a scalar describing the scale of the z axis. min(width, height)/2 suggested 427// cam - an object with the following attributes 428// const cam = { 429// 'eye' : [0, 0, 1 / Math.tan(Math.PI / 24) - 1], // a 3D point locating the camera 430// 'coa' : [0, 0, 0], // center of attention - the 3D point the camera is looking at. 431// 'up' : [0, 1, 0], // a unit vector pointing in the camera's up direction, because eye and 432// // coa alone leave roll unspecified. 433// 'near' : 0.02, // near clipping plane 434// 'far' : 4, // far clipping plane 435// 'angle': Math.PI / 12, // field of view in radians 436// }; 437CanvasKit.M44.setupCamera = function(area, zscale, cam) { 438 var camera = CanvasKit.M44.lookat(cam['eye'], cam['coa'], cam['up']); 439 var perspective = CanvasKit.M44.perspective(cam['near'], cam['far'], cam['angle']); 440 var center = [(area[0] + area[2])/2, (area[1] + area[3])/2, 0]; 441 var viewScale = [(area[2] - area[0])/2, (area[3] - area[1])/2, zscale]; 442 var viewport = CanvasKit.M44.multiply( 443 CanvasKit.M44.translated(center), 444 CanvasKit.M44.scaled(viewScale)); 445 return CanvasKit.M44.multiply( 446 viewport, perspective, camera, CanvasKit.M44.mustInvert(viewport)); 447}; 448 449// An ColorMatrix is a 4x4 color matrix that transforms the 4 color channels 450// with a 1x4 matrix that post-translates those 4 channels. 451// For example, the following is the layout with the scale (S) and post-transform 452// (PT) items indicated. 453// RS, 0, 0, 0 | RPT 454// 0, GS, 0, 0 | GPT 455// 0, 0, BS, 0 | BPT 456// 0, 0, 0, AS | APT 457// 458// Much of this was hand-transcribed from SkColorMatrix.cpp, because it's easier to 459// deal with a Float32Array of length 20 than to try to expose the SkColorMatrix object. 460 461var rScale = 0; 462var gScale = 6; 463var bScale = 12; 464var aScale = 18; 465 466var rPostTrans = 4; 467var gPostTrans = 9; 468var bPostTrans = 14; 469var aPostTrans = 19; 470 471CanvasKit.ColorMatrix = {}; 472CanvasKit.ColorMatrix.identity = function() { 473 var m = new Float32Array(20); 474 m[rScale] = 1; 475 m[gScale] = 1; 476 m[bScale] = 1; 477 m[aScale] = 1; 478 return m; 479}; 480 481CanvasKit.ColorMatrix.scaled = function(rs, gs, bs, as) { 482 var m = new Float32Array(20); 483 m[rScale] = rs; 484 m[gScale] = gs; 485 m[bScale] = bs; 486 m[aScale] = as; 487 return m; 488}; 489 490var rotateIndices = [ 491 [6, 7, 11, 12], 492 [0, 10, 2, 12], 493 [0, 1, 5, 6], 494]; 495// axis should be 0, 1, 2 for r, g, b 496CanvasKit.ColorMatrix.rotated = function(axis, sine, cosine) { 497 var m = CanvasKit.ColorMatrix.identity(); 498 var indices = rotateIndices[axis]; 499 m[indices[0]] = cosine; 500 m[indices[1]] = sine; 501 m[indices[2]] = -sine; 502 m[indices[3]] = cosine; 503 return m; 504}; 505 506// m is a ColorMatrix (i.e. a Float32Array), and this sets the 4 "special" 507// params that will translate the colors after they are multiplied by the 4x4 matrix. 508CanvasKit.ColorMatrix.postTranslate = function(m, dr, dg, db, da) { 509 m[rPostTrans] += dr; 510 m[gPostTrans] += dg; 511 m[bPostTrans] += db; 512 m[aPostTrans] += da; 513 return m; 514}; 515 516// concat returns a new ColorMatrix that is the result of multiplying outer*inner 517CanvasKit.ColorMatrix.concat = function(outer, inner) { 518 var m = new Float32Array(20); 519 var index = 0; 520 for (var j = 0; j < 20; j += 5) { 521 for (var i = 0; i < 4; i++) { 522 m[index++] = outer[j + 0] * inner[i + 0] + 523 outer[j + 1] * inner[i + 5] + 524 outer[j + 2] * inner[i + 10] + 525 outer[j + 3] * inner[i + 15]; 526 } 527 m[index++] = outer[j + 0] * inner[4] + 528 outer[j + 1] * inner[9] + 529 outer[j + 2] * inner[14] + 530 outer[j + 3] * inner[19] + 531 outer[j + 4]; 532 } 533 534 return m; 535};