Expose 4x4 matrices on canvas in a way similar to SimpleMatrix, add example.

Bug: skia:9866
Change-Id: I718455743e482e4f60a462027b629dc19b1dbad3
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/270201
Commit-Queue: Nathaniel Nifong <nifong@google.com>
Reviewed-by: Kevin Lubick <kjlubick@google.com>
diff --git a/modules/canvaskit/CHANGELOG.md b/modules/canvaskit/CHANGELOG.md
index 35a3b14..d7c2a19 100644
--- a/modules/canvaskit/CHANGELOG.md
+++ b/modules/canvaskit/CHANGELOG.md
@@ -17,9 +17,15 @@
    `SkSurface.requestAnimationFrame` for animation logic).
  - `CanvasKit.parseColorString` which processes color strings like "#2288FF"
  - Particles module now exposes effect uniforms, which can be modified for live-updating.
+ - Experimental 4x4 matrices added in `SkM44`
+ - Vector math functions added in `SkVector`
 
 ### Changed
  - We now compile/ship with Emscripten v1.39.6.
+ - `SkMatrix.multiply` can now accept any number of matrix arguments, multiplying them
+    left-to-right.
+ - SkMatrix.invert now returns null when the matrix is not invertible. Previously it would return an
+   identity matrix. Callers must determine what behavior would be appropriate in this situation.
 
 ### Fixed
  - Support for .otf fonts (.woff and .woff2 still not supported).
diff --git a/modules/canvaskit/canvaskit/extra.html b/modules/canvaskit/canvaskit/extra.html
index 8a70da0..b0d9c08 100644
--- a/modules/canvaskit/canvaskit/extra.html
+++ b/modules/canvaskit/canvaskit/extra.html
@@ -7,8 +7,6 @@
 <style>
   canvas {
     border: 1px dashed #AAA;
-    width: 300px;
-    height: 300px;
   }
 
 </style>
@@ -37,6 +35,9 @@
 <h2> CanvasKit can serialize/deserialize .skp files</h2>
 <canvas id=skp width=300 height=300></canvas>
 
+<h2> 3D perspective transformations </h2>
+<canvas id=camera3d width=500 height=500></canvas>
+
 <script type="text/javascript" src="/node_modules/canvaskit/bin/canvaskit.js"></script>
 
 <script type="text/javascript" charset="utf-8">
@@ -81,6 +82,7 @@
     RTShaderAPI1(CanvasKit);
 
     SkpExample(CanvasKit, skpData);
+    Camera3D(CanvasKit);
   });
 
   fetch('https://storage.googleapis.com/skia-cdn/misc/lego_loader.json').then((resp) => {
@@ -423,4 +425,252 @@
     // Intentionally just draw frame once
     surface.drawOnce(drawFrame);
   }
+
+  function Camera3D(canvas) {
+    const surface = CanvasKit.MakeCanvasSurface('camera3d');
+    if (!surface) {
+      console.error('Could not make surface');
+      return;
+    }
+
+    const sizeX = document.getElementById('camera3d').width;
+    const sizeY = document.getElementById('camera3d').height;
+
+    var clickToWorld = CanvasKit.SkM44.identity();
+    var worldToClick = CanvasKit.SkM44.identity();
+    // rotation of the cube shown in the demo
+    var rotation = CanvasKit.SkM44.identity();
+    // temporary during a click and drag
+    var clickRotation = CanvasKit.SkM44.identity();
+
+    // A virtual sphere used for tumbling the object on screen.
+    const vSphereCenter = [sizeX/2, sizeY/2];
+    const vSphereRadius = Math.min(...vSphereCenter);
+
+    // The rounded rect used for each face
+    const margin = vSphereRadius / 20;
+    const rr = CanvasKit.RRectXY(CanvasKit.LTRBRect(margin, margin,
+      vSphereRadius - margin, vSphereRadius - margin), margin*2.5, margin*2.5);
+
+    const camNear = 0.05;
+    const camFar = 4;
+    const camAngle = Math.PI / 12;
+
+    const camEye = [0, 0, 1 / Math.tan(camAngle/2) - 1];
+    const camCOA = [0, 0, 0];
+    const camUp =  [0, 1, 0];
+
+    var mouseDown = false;
+    var clickDown = [0, 0]; // location of click down
+    var lastMouse = [0, 0]; // last mouse location
+
+    // keep spinning after mouse up. Also start spinning on load
+    var axis = [0.4, 1, 1];
+    var totalSpin = 0;
+    var spinRate = 0.1;
+    var lastRadians = 0;
+    var spinning = setInterval(keepSpinning, 30);
+
+    const textPaint = new CanvasKit.SkPaint();
+    textPaint.setColor(CanvasKit.BLACK);
+    textPaint.setAntiAlias(true);
+    const roboto = CanvasKit.SkFontMgr.RefDefault().MakeTypefaceFromData(robotoData);
+    const textFont = new CanvasKit.SkFont(roboto, 30);
+
+    // Takes an x and y rotation in radians and a scale and returns a 4x4 matrix used to draw a
+    // face of the cube in that orientation.
+    function faceM44(rx, ry, scale) {
+      return CanvasKit.SkM44.multiply(
+        CanvasKit.SkM44.rotated([0,1,0], ry),
+        CanvasKit.SkM44.rotated([1,0,0], rx),
+        CanvasKit.SkM44.translated([0, 0, scale]));
+    }
+
+    const faceScale = vSphereRadius/2
+    const faces = [
+      {matrix: faceM44(         0,         0, faceScale ), color:CanvasKit.RED}, // front
+      {matrix: faceM44(         0,   Math.PI, faceScale ), color:CanvasKit.GREEN}, // back
+
+      {matrix: faceM44( Math.PI/2,         0, faceScale ), color:CanvasKit.BLUE}, // top
+      {matrix: faceM44(-Math.PI/2,         0, faceScale ), color:CanvasKit.CYAN}, // bottom
+
+      {matrix: faceM44(         0, Math.PI/2, faceScale ), color:CanvasKit.MAGENTA}, // left
+      {matrix: faceM44(         0,-Math.PI/2, faceScale ), color:CanvasKit.YELLOW}, // right
+    ];
+
+    // Returns a component of the matrix m indicating whether it faces the camera.
+    // If it's positive for one of the matrices representing the face of the cube,
+    // that face is currently in front.
+    function front(m) {
+      // Is this invertible?
+      var m2 = CanvasKit.SkM44.invert(m);
+      if (m2 === null) {
+        m2 = CanvasKit.SkM44.identity();
+      }
+      // look at the sign of the z-scale of the inverse of m.
+      // that's the number in row 2, col 2.
+      return m2[10]
+    }
+
+    // Return the inverse of an SkM44. throw an error if it's not invertible
+    function mustInvert(m) {
+      var m2 = CanvasKit.SkM44.invert(m);
+      if (m2 === null) {
+        throw "Matrix not invertible";
+      }
+      return m2;
+    }
+
+    function saveCamera(canvas, /* rect */ area, /* scalar */ zscale) {
+      const camera = CanvasKit.SkM44.lookat(camEye, camCOA, camUp);
+      const perspective = CanvasKit.SkM44.perspective(camNear, camFar, camAngle);
+      // Calculate viewport scale. Even through we know these values are all constants in this
+      // example it might be handy to change the size later.
+      const center = [(area.fLeft + area.fRight)/2, (area.fTop + area.fBottom)/2, 0];
+      const viewScale = [(area.fRight - area.fLeft)/2, (area.fBottom - area.fTop)/2, zscale];
+      const viewport = CanvasKit.SkM44.multiply(
+        CanvasKit.SkM44.translated(center),
+        CanvasKit.SkM44.scaled(viewScale));
+
+      // want "world" to be in our big coordinates (e.g. area), so apply this inverse
+      // as part of our "camera".
+      canvas.experimental_saveCamera(
+        CanvasKit.SkM44.multiply(viewport, perspective),
+        CanvasKit.SkM44.multiply(camera, mustInvert(viewport)));
+    }
+
+    function setClickToWorld(canvas, matrix) {
+      const l2d = canvas.experimental_getLocalToDevice();
+      worldToClick = CanvasKit.SkM44.multiply(mustInvert(matrix), l2d);
+      clickToWorld = mustInvert(worldToClick);
+    }
+
+    function drawCubeFace(canvas, m, color) {
+      const trans = new CanvasKit.SkM44.translated([vSphereRadius/2, vSphereRadius/2, 0]);
+      canvas.experimental_concat44(CanvasKit.SkM44.multiply(trans, m, mustInvert(trans)));
+      const znormal = front(canvas.experimental_getLocalToDevice());
+      if (znormal < 0) {
+        return;// skip faces facing backwards
+      }
+      const paint = new CanvasKit.SkPaint();
+      paint.setColor(color);
+      // TODO replace color with a bump shader
+      canvas.drawRRect(rr, paint);
+      canvas.drawText(znormal.toFixed(2), faceScale*0.25, faceScale*0.4, textPaint, textFont);
+    }
+
+    function drawFrame(canvas) {
+      // TODO if not GPU backend, print an error to the console and return.
+      // this demo only works in GL.
+      const clickM = canvas.experimental_getLocalToDevice();
+      canvas.save();
+      canvas.translate(vSphereCenter[0] - vSphereRadius/2, vSphereCenter[1] - vSphereRadius/2);
+      // pass surface dimensions as viewport size.
+      saveCamera(canvas, CanvasKit.LTRBRect(0, 0, vSphereRadius, vSphereRadius), vSphereRadius/2);
+      setClickToWorld(canvas, clickM);
+      for (let f of faces) {
+        const saveCount = canvas.getSaveCount();
+        canvas.save();
+        drawCubeFace(canvas, CanvasKit.SkM44.multiply(clickRotation, rotation, f.matrix), f.color);
+        canvas.restoreToCount(saveCount);
+      }
+      canvas.restore();  // camera
+      canvas.restore();  // center the following content in the window
+
+      // draw virtual sphere outline.
+      const paint = new CanvasKit.SkPaint();
+      paint.setAntiAlias(true);
+      paint.setStyle(CanvasKit.PaintStyle.Stroke);
+      paint.setColor(CanvasKit.Color(64, 255, 0, 1.0));
+      canvas.drawCircle(vSphereCenter[0], vSphereCenter[1], vSphereRadius, paint);
+      canvas.drawLine(vSphereCenter[0], vSphereCenter[1] - vSphereRadius,
+                       vSphereCenter[0], vSphereCenter[1] + vSphereRadius, paint);
+      canvas.drawLine(vSphereCenter[0] - vSphereRadius, vSphereCenter[1],
+                       vSphereCenter[0] + vSphereRadius, vSphereCenter[1], paint);
+    }
+
+    // convert a 2D point in the circle displayed on screen to a 3D unit vector.
+    // the virtual sphere is a technique selecting a 3D direction by clicking on a the projection
+    // of a hemisphere.
+    function vSphereUnitV3(p) {
+      // v = (v - fCenter) * (1 / fRadius);
+      let v = CanvasKit.SkVector.mulScalar(CanvasKit.SkVector.sub(p, vSphereCenter), 1/vSphereRadius);
+
+      // constrain the clicked point within the circle.
+      let len2 = CanvasKit.SkVector.lengthSquared(v);
+      if (len2 > 1) {
+          v = CanvasKit.SkVector.normalize(v);
+          len2 = 1;
+      }
+      // the closer to the edge of the circle you are, the closer z is to zero.
+      const z = Math.sqrt(1 - len2);
+      v.push(z);
+      return v;
+    }
+
+    function computeVSphereRotation(start, end) {
+      const u = vSphereUnitV3(start);
+      const v = vSphereUnitV3(end);
+      // Axis is in the scope of the Camera3D function so it can be used in keepSpinning.
+      axis = CanvasKit.SkVector.cross(u, v);
+      const sinValue = CanvasKit.SkVector.length(axis);
+      const cosValue = CanvasKit.SkVector.dot(u, v);
+
+      let m = new CanvasKit.SkM44.identity();
+      if (Math.abs(sinValue) > 0.000000001) {
+          m = CanvasKit.SkM44.rotatedUnitSinCos(
+            CanvasKit.SkVector.mulScalar(axis, 1/sinValue), sinValue, cosValue);
+          const radians = Math.atan(cosValue / sinValue);
+          spinRate = lastRadians - radians;
+          lastRadians = radians;
+      }
+      return m;
+    }
+
+    function keepSpinning() {
+      totalSpin += spinRate;
+      clickRotation = CanvasKit.SkM44.rotated(axis, totalSpin);
+      spinRate *= .998;
+      if (spinRate < 0.01) {
+        stopSpinning();
+      }
+      surface.requestAnimationFrame(drawFrame);
+    }
+
+    function stopSpinning() {
+        clearInterval(spinning);
+        rotation = CanvasKit.SkM44.multiply(clickRotation, rotation);
+        clickRotation = CanvasKit.SkM44.identity();
+    }
+
+    function interact(e) {
+      const type = e.type;
+      if (type === 'lostpointercapture' || type === 'pointerup' || type == 'pointerleave') {
+        mouseDown = false;
+        if (spinRate > 0.02) {
+          stopSpinning();
+          spinning = setInterval(keepSpinning, 30);
+        }
+        return;
+      } else if (type === 'pointermove') {
+        if (!mouseDown)  { return; }
+        lastMouse = [e.offsetX, e.offsetY];
+        clickRotation = computeVSphereRotation(clickDown, lastMouse);
+      } else if (type === 'pointerdown') {
+        stopSpinning();
+        mouseDown = true;
+        clickDown = [e.offsetX, e.offsetY];
+        lastMouse = clickDown;
+      }
+      surface.requestAnimationFrame(drawFrame);
+    };
+
+    document.getElementById('camera3d').addEventListener('pointermove', interact);
+    document.getElementById('camera3d').addEventListener('pointerdown', interact);
+    document.getElementById('camera3d').addEventListener('lostpointercapture', interact);
+    document.getElementById('camera3d').addEventListener('pointerleave', interact);
+    document.getElementById('camera3d').addEventListener('pointerup', interact);
+
+    surface.requestAnimationFrame(drawFrame);
+  }
 </script>
diff --git a/modules/canvaskit/canvaskit_bindings.cpp b/modules/canvaskit/canvaskit_bindings.cpp
index 130f598..96523a0 100644
--- a/modules/canvaskit/canvaskit_bindings.cpp
+++ b/modules/canvaskit/canvaskit_bindings.cpp
@@ -44,6 +44,7 @@
 #include "include/effects/SkImageFilters.h"
 #include "include/effects/SkRuntimeEffect.h"
 #include "include/effects/SkTrimPathEffect.h"
+#include "include/private/SkM44.h"
 #include "include/utils/SkParsePath.h"
 #include "include/utils/SkShadowUtils.h"
 #include "modules/skshaper/include/SkShaper.h"
@@ -91,6 +92,7 @@
 sk_sp<SkFontMgr> SkFontMgr_New_Custom_Data(const uint8_t** datas, const size_t* sizes, int n);
 #endif
 
+// 3x3 Matrices
 struct SimpleMatrix {
     SkScalar scaleX, skewX,  transX;
     SkScalar skewY,  scaleY, transY;
@@ -110,6 +112,34 @@
     return m;
 }
 
+// Experimental 4x4 matrices, also represented in JS with arrays.
+struct SimpleM44 {
+    SkScalar m0,  m1,  m2,  m3;
+    SkScalar m4,  m5,  m6,  m7;
+    SkScalar m8,  m9,  m10, m11;
+    SkScalar m12, m13, m14, m15;
+};
+
+SkM44 toSkM44(const SimpleM44& sm) {
+    SkM44 result(
+      sm.m0,  sm.m1,  sm.m2,  sm.m3,
+      sm.m4,  sm.m5,  sm.m6,  sm.m7,
+      sm.m8,  sm.m9,  sm.m10, sm.m11,
+      sm.m12, sm.m13, sm.m14, sm.m15);
+    return result;
+}
+
+SimpleM44 toSimpleM44(const SkM44& sm) {
+    SimpleM44 m {
+        sm.rc(0,0), sm.rc(0,1),  sm.rc(0,2),  sm.rc(0,3),
+        sm.rc(1,0), sm.rc(1,1),  sm.rc(1,2),  sm.rc(1,3),
+        sm.rc(2,0), sm.rc(2,1),  sm.rc(2,2),  sm.rc(2,3),
+        sm.rc(3,0), sm.rc(3,1),  sm.rc(3,2),  sm.rc(3,3),
+    };
+    return m;
+}
+
+// Surface creation structs and helpers
 struct SimpleImageInfo {
     int width;
     int height;
@@ -1024,6 +1054,26 @@
             SkImageInfo dstInfo = toSkImageInfo(di);
 
             return self.writePixels(dstInfo, pixels, srcRowBytes, dstX, dstY);
+        }))
+        // Experimental 4x4 matrix functions
+        .function("experimental_saveCamera", optional_override([](SkCanvas& self,
+            const SimpleM44& projection, const SimpleM44& camera) {
+            self.experimental_saveCamera(toSkM44(projection), toSkM44(camera));
+        }))
+        .function("experimental_concat44", optional_override([](SkCanvas& self, const SimpleM44& m) {
+            self.concat44(toSkM44(m));
+        }))
+        .function("experimental_getLocalToDevice", optional_override([](const SkCanvas& self)->SimpleM44 {
+            SkM44 m = self.getLocalToDevice();
+            return toSimpleM44(m);
+        }))
+        .function("experimental_getLocalToWorld", optional_override([](const SkCanvas& self)->SimpleM44 {
+            SkM44 m = self.experimental_getLocalToWorld();
+            return toSimpleM44(m);
+        }))
+        .function("experimental_getLocalToCamera", optional_override([](const SkCanvas& self)->SimpleM44 {
+            SkM44 m = self.experimental_getLocalToCamera();
+            return toSimpleM44(m);
         }));
 
     class_<SkColorFilter>("SkColorFilter")
@@ -1632,7 +1682,7 @@
 
     // A value object is much simpler than a class - it is returned as a JS
     // object and does not require delete().
-    // https://kripken.github.io/emscripten-site/docs/porting/connecting_cpp_and_javascript/embind.html#value-types
+    // https://emscripten.org/docs/porting/connecting_cpp_and_javascript/embind.html#value-types
     value_object<ShapedTextOpts>("ShapedTextOpts")
         .field("font",        &ShapedTextOpts::font)
         .field("leftToRight", &ShapedTextOpts::leftToRight)
@@ -1724,9 +1774,17 @@
         .element(&SimpleMatrix::pers1)
         .element(&SimpleMatrix::pers2);
 
+    value_array<SimpleM44>("SkM44")
+        .element(&SimpleM44::m0).element(&SimpleM44::m1).element(&SimpleM44::m2).element(&SimpleM44::m3)
+        .element(&SimpleM44::m4).element(&SimpleM44::m5).element(&SimpleM44::m6).element(&SimpleM44::m7)
+        .element(&SimpleM44::m8).element(&SimpleM44::m9).element(&SimpleM44::m10).element(&SimpleM44::m11)
+        .element(&SimpleM44::m12).element(&SimpleM44::m13).element(&SimpleM44::m14).element(&SimpleM44::m15);
+
     constant("TRANSPARENT", SK_ColorTRANSPARENT);
     constant("RED",         SK_ColorRED);
+    constant("GREEN",       SK_ColorGREEN);
     constant("BLUE",        SK_ColorBLUE);
+    constant("MAGENTA",     SK_ColorMAGENTA);
     constant("YELLOW",      SK_ColorYELLOW);
     constant("CYAN",        SK_ColorCYAN);
     constant("BLACK",       SK_ColorBLACK);
diff --git a/modules/canvaskit/debug.js b/modules/canvaskit/debug.js
index 5cd9a4a..8121414 100644
--- a/modules/canvaskit/debug.js
+++ b/modules/canvaskit/debug.js
@@ -1,3 +1,4 @@
 function SkDebug(msg) {
   console.warn(msg);
-}
\ No newline at end of file
+}
+/** @const */ var skIsDebug = true;
\ No newline at end of file
diff --git a/modules/canvaskit/externs.js b/modules/canvaskit/externs.js
index 93203d7..5c9bdd2 100644
--- a/modules/canvaskit/externs.js
+++ b/modules/canvaskit/externs.js
@@ -280,6 +280,20 @@
 		MakeMatrixTransform: function() {},
 	},
 
+	// These are defined in interface.js
+	SkM44: {
+		identity: function() {},
+		invert: function() {},
+		multiply: function() {},
+		rotatedUnitSinCos: function() {},
+		rotated: function() {},
+		scaled: function() {},
+		translated: function() {},
+		lookat: function() {},
+		perspective: function() {},
+		rc: function() {},
+	},
+
 	SkMatrix: {
 		identity: function() {},
 		invert: function() {},
@@ -468,6 +482,18 @@
 		_MakeFromText: function() {},
 	},
 
+	// These are defined in interface.js
+	SkVector: {
+		add: function() {},
+		sub: function() {},
+		dot: function() {},
+		cross: function() {},
+		normalize: function() {},
+		mulScalar: function() {},
+		length: function() {},
+		lengthSquared: function() {},
+	},
+
 	SkVertices: {
 		// public API (from C++ bindings)
 		bounds: function() {},
diff --git a/modules/canvaskit/interface.js b/modules/canvaskit/interface.js
index 7cb6c5a..0b95c69 100644
--- a/modules/canvaskit/interface.js
+++ b/modules/canvaskit/interface.js
@@ -16,29 +16,67 @@
   // have a mapPoints() function (which could maybe be tacked on here).
   // If DOMMatrix catches on, it would be worth re-considering this usage.
   CanvasKit.SkMatrix = {};
-  function sdot(a, b, c, d, e, f) {
-    e = e || 0;
-    f = f || 0;
-    return a * b + c * d + e * f;
+  function sdot() { // to be called with an even number of scalar args
+    var acc = 0;
+    for (var i=0; i < arguments.length-1; i+=2) {
+      acc += arguments[i] * arguments[i+1];
+    }
+    return acc;
+  }
+
+
+  // Private general matrix functions used in both 3x3s and 4x4s.
+  // Return a square identity matrix of size n.
+  var identityN = function(n) {
+    var size = n*n;
+    var m = new Array(size);
+    while(size--) {
+      m[size] = size%(n+1) == 0 ? 1.0 : 0.0;
+    }
+    return m;
+  }
+
+  // Stride, a function for compactly representing several ways of copying an array into another.
+  // Write vector `v` into matrix `m`. `m` is a matrix encoded as an array in row-major
+  // order. Its width is passed as `width`. `v` is an array with length < (m.length/width).
+  // An element of `v` is copied into `m` starting at `offset` and moving `colStride` cols right
+  // each row.
+  //
+  // For example, a width of 4, offset of 3, and stride of -1 would put the vector here.
+  // _ _ 0 _
+  // _ 1 _ _
+  // 2 _ _ _
+  // _ _ _ 3
+  //
+  var stride = function(v, m, width, offset, colStride) {
+    for (var i=0; i<v.length; i++) {
+      m[i * width + // column
+        (i * colStride + offset + width) % width // row
+      ] = v[i];
+    }
+    return m;
   }
 
   CanvasKit.SkMatrix.identity = function() {
-    return [
-      1, 0, 0,
-      0, 1, 0,
-      0, 0, 1,
-    ];
+    return identityN(3);
   };
 
   // Return the inverse (if it exists) of this matrix.
   // Otherwise, return the identity.
   CanvasKit.SkMatrix.invert = function(m) {
+    // Find the determinant by the sarrus rule. https://en.wikipedia.org/wiki/Rule_of_Sarrus
     var det = m[0]*m[4]*m[8] + m[1]*m[5]*m[6] + m[2]*m[3]*m[7]
             - m[2]*m[4]*m[6] - m[1]*m[3]*m[8] - m[0]*m[5]*m[7];
     if (!det) {
       SkDebug('Warning, uninvertible matrix');
-      return CanvasKit.SkMatrix.identity();
+      return null;
     }
+    // Return the inverse by the formula adj(m)/det.
+    // adj (adjugate) of a 3x3 is the transpose of it's cofactor matrix.
+    // a cofactor matrix is a matrix where each term is +-det(N) where matrix N is the 2x2 formed
+    // by removing the row and column we're currently setting from the source.
+    // the sign alternates in a checkerboard pattern with a `+` at the top left.
+    // that's all been combined here into one expression.
     return [
       (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,
       (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,
@@ -50,7 +88,7 @@
   // Results are done in place.
   // See SkMatrix.h::mapPoints for the docs on the math.
   CanvasKit.SkMatrix.mapPoints = function(matrix, ptArr) {
-    if (ptArr.length % 2) {
+    if (skIsDebug && (ptArr.length % 2)) {
       throw 'mapPoints requires an even length arr';
     }
     for (var i = 0; i < ptArr.length; i+=2) {
@@ -67,18 +105,56 @@
     return ptArr;
   };
 
-  CanvasKit.SkMatrix.multiply = function(m1, m2) {
-    var result = [0,0,0, 0,0,0, 0,0,0];
-    for (var r = 0; r < 3; r++) {
-      for (var c = 0; c < 3; c++) {
-        // m1 and m2 are 1D arrays pretending to be 2D arrays
-        result[3*r + c] = sdot(m1[3*r + 0], m2[3*0 + c],
-                               m1[3*r + 1], m2[3*1 + c],
-                               m1[3*r + 2], m2[3*2 + c]);
+  function isnumber(val) { return val !== NaN; };
+
+  // gereralized iterative algorithm for multiplying two matrices.
+  function multiply(m1, m2, size) {
+
+    if (skIsDebug && (!m1.every(isnumber) || !m2.every(isnumber))) {
+      throw 'Some members of matrices are NaN m1='+m1+', m2='+m2+'';
+    }
+    if (skIsDebug && (m1.length !== m2.length)) {
+      throw 'Undefined for matrices of different sizes. m1.length='+m1.length+', m2.length='+m2.length;
+    }
+    if (skIsDebug && (size*size !== m1.length)) {
+      throw 'Undefined for non-square matrices. array size was '+size;
+    }
+
+    var result = Array(m1.length);
+    for (var r = 0; r < size; r++) {
+      for (var c = 0; c < size; c++) {
+        // accumulate a sum of m1[r,k]*m2[k, c]
+        var acc = 0;
+        for (var k = 0; k < size; k++) {
+          acc += m1[size * r + k] * m2[size * k + c];
+        }
+        result[r * size + c] = acc;
       }
     }
     return result;
-  }
+  };
+
+  // Accept an integer indicating the size of the matrices being multiplied (3 for 3x3), and any
+  // number of matrices following it.
+  function multiplyMany(size, listOfMatrices) {
+    if (skIsDebug && (listOfMatrices.length < 2)) {
+      throw 'multiplication expected two or more matrices';
+    }
+    var result = multiply(listOfMatrices[0], listOfMatrices[1], size);
+    var next = 2;
+    while (next < listOfMatrices.length) {
+      result = multiply(result, listOfMatrices[next], size);
+      next++;
+    }
+    return result;
+  };
+
+  // Accept any number 3x3 of matrices as arguments, multiply them together.
+  // Matrix multiplication is associative but not commutatieve. the order of the arguments
+  // matters, but it does not matter that this implementation multiplies them left to right.
+  CanvasKit.SkMatrix.multiply = function() {
+    return multiplyMany(3, arguments);
+  };
 
   // Return a matrix representing a rotation by n radians.
   // px, py optionally say which point the rotation should be around
@@ -98,31 +174,241 @@
   CanvasKit.SkMatrix.scaled = function(sx, sy, px, py) {
     px = px || 0;
     py = py || 0;
-    return [
-      sx, 0, px - sx * px,
-      0, sy, py - sy * py,
-      0,  0,            1,
-    ];
+    var m = stride([sx, sy], identityN(3), 3, 0, 1);
+    return stride([px-sx*px, py-sy*py], m, 3, 2, 0);
   };
 
   CanvasKit.SkMatrix.skewed = function(kx, ky, px, py) {
     px = px || 0;
     py = py || 0;
-    return [
-      1, kx, -kx * px,
-      ky, 1, -ky * py,
-      0,  0,        1,
-    ];
+    var m = stride([kx, ky], identityN(3), 3, 1, -1);
+    return stride([-kx*px, -ky*py], m, 3, 2, 0);
   };
 
   CanvasKit.SkMatrix.translated = function(dx, dy) {
-    return [
-      1, 0, dx,
-      0, 1, dy,
-      0, 0,  1,
-    ];
+    return stride(arguments, identityN(3), 3, 2, 0);
   };
 
+  // Functions for manipulating vectors.
+  // Loosely based off of SkV3 in SkM44.h but skia also has SkVec2 and Skv4. This combines them and
+  // works on vectors of any length.
+  CanvasKit.SkVector = {};
+  CanvasKit.SkVector.dot = function(a, b) {
+    if (skIsDebug && (a.length !== b.length)) {
+      throw 'Cannot perform dot product on arrays of different length ('+a.length+' vs '+b.length+')';
+    }
+    return a.map(function(v, i) { return v*b[i] }).reduce(function(acc, cur) { return acc + cur; });
+  }
+  CanvasKit.SkVector.lengthSquared = function(v) {
+    return CanvasKit.SkVector.dot(v, v);
+  }
+  CanvasKit.SkVector.length = function(v) {
+    return Math.sqrt(CanvasKit.SkVector.lengthSquared(v));
+  }
+  CanvasKit.SkVector.mulScalar = function(v, s) {
+    return v.map(function(i) { return i*s });
+  }
+  CanvasKit.SkVector.add = function(a, b) {
+    return a.map(function(v, i) { return v+b[i] });
+  }
+  CanvasKit.SkVector.sub = function(a, b) {
+    return a.map(function(v, i) { return v-b[i]; });
+  }
+  CanvasKit.SkVector.normalize = function(v) {
+    return CanvasKit.SkVector.mulScalar(v, 1/CanvasKit.SkVector.length(v));
+  }
+  CanvasKit.SkVector.cross = function(a, b) {
+    if (skIsDebug && (a.length !== 3 || a.length !== 3)) {
+      throw 'Cross product is only defined for 3-dimensional vectors (a.length='+a.length+', b.length='+b.length+')';
+    }
+    return [
+      a[1]*b[2] - a[2]*b[1],
+      a[2]*b[0] - a[0]*b[2],
+      a[0]*b[1] - a[1]*b[0],
+    ];
+  }
+
+  // Functions for creating and manipulating 4x4 matrices. Accepted in place of SkM44 in canvas
+  // methods, for the same reasons as the 3x3 matrices above.
+  // ported from C++ code in SkM44.cpp
+  CanvasKit.SkM44 = {};
+  // Create a 4x4 identity matrix
+  CanvasKit.SkM44.identity = function() {
+    return identityN(4);
+  }
+
+  // Anything named vec below is an array of length 3 representing a vector/point in 3D space.
+  // Create a 4x4 matrix representing a translate by the provided 3-vec
+  CanvasKit.SkM44.translated = function(vec) {
+    return stride(vec, identityN(4), 4, 3, 0);
+  }
+  // Create a 4x4 matrix representing a scaling by the provided 3-vec
+  CanvasKit.SkM44.scaled = function(vec) {
+    return stride(vec, identityN(4), 4, 0, 1);
+  }
+  // Create a 4x4 matrix representing a rotation about the provided axis 3-vec.
+  // axis does not need to be normalized.
+  CanvasKit.SkM44.rotated = function(axisVec, radians) {
+    return CanvasKit.SkM44.rotatedUnitSinCos(
+      CanvasKit.SkVector.normalize(axisVec), Math.sin(radians), Math.cos(radians));
+  }
+  // Create a 4x4 matrix representing a rotation about the provided normalized axis 3-vec.
+  // Rotation is provided redundantly as both sin and cos values.
+  // This rotate can be used when you already have the cosAngle and sinAngle values
+  // so you don't have to atan(cos/sin) to call roatated() which expects an angle in radians.
+  // this does no checking! Behavior for invalid sin or cos values or non-normalized axis vectors
+  // is incorrect. Prefer rotate().
+  CanvasKit.SkM44.rotatedUnitSinCos = function(axisVec, sinAngle, cosAngle) {
+    var x = axisVec[0];
+    var y = axisVec[1];
+    var z = axisVec[2];
+    var c = cosAngle;
+    var s = sinAngle;
+    var t = 1 - c;
+    return [
+      t*x*x + c,   t*x*y - s*z, t*x*z + s*y, 0,
+      t*x*y + s*z, t*y*y + c,   t*y*z - s*x, 0,
+      t*x*z - s*y, t*y*z + s*x, t*z*z + c,   0,
+      0,           0,           0,           1
+    ];
+  }
+  // Create a 4x4 matrix representing a camera at eyeVec, pointed at centerVec.
+  CanvasKit.SkM44.lookat = function(eyeVec, centerVec, upVec) {
+    var f = CanvasKit.SkVector.normalize(CanvasKit.SkVector.sub(centerVec, eyeVec));
+    var u = CanvasKit.SkVector.normalize(upVec);
+    var s = CanvasKit.SkVector.normalize(CanvasKit.SkVector.cross(f, u));
+
+    var m = CanvasKit.SkM44.identity();
+    // set each column's top three numbers
+    stride(s,                                 m, 4, 0, 0);
+    stride(CanvasKit.SkVector.cross(s, f),      m, 4, 1, 0);
+    stride(CanvasKit.SkVector.mulScalar(f, -1), m, 4, 2, 0);
+    stride(eyeVec,                            m, 4, 3, 0);
+
+    var m2 = CanvasKit.SkM44.invert(m);
+    if (m2 === null) {
+      return CanvasKit.SkM44.identity();
+    }
+    return m2;
+  }
+  // Create a 4x4 matrix representing a perspective. All arguments are scalars.
+  // angle is in radians.
+  CanvasKit.SkM44.perspective = function(near, far, angle) {
+    if (skIsDebug && (far <= near)) {
+      throw "far must be greater than near when constructing SkM44 using perspective.";
+    }
+    var dInv = 1 / (far - near);
+    var halfAngle = angle / 2;
+    var cot = Math.cos(halfAngle) / Math.sin(halfAngle);
+    return [
+      cot, 0,   0,               0,
+      0,   cot, 0,               0,
+      0,   0,   (far+near)*dInv, 2*far*near*dInv,
+      0,   0,   -1,              1,
+    ];
+  }
+  // Returns the number at the given row and column in matrix m.
+  CanvasKit.SkM44.rc = function(m, r, c) {
+    return m[r*4+c];
+  }
+  // Accepts any number of 4x4 matrix arguments, multiplies them left to right.
+  CanvasKit.SkM44.multiply = function() {
+    return multiplyMany(4, arguments);
+  }
+
+  // Invert the 4x4 matrix if it is invertible and return it. if not, return null.
+  // taken from SkM44.cpp (altered to use row-major order)
+  // m is not altered.
+  CanvasKit.SkM44.invert = function(m) {
+    if (skIsDebug && !m.every(isnumber)) {
+      throw 'some members of matrix are NaN m='+m;
+    }
+
+    var a00 = m[0];
+    var a01 = m[4];
+    var a02 = m[8];
+    var a03 = m[12];
+    var a10 = m[1];
+    var a11 = m[5];
+    var a12 = m[9];
+    var a13 = m[13];
+    var a20 = m[2];
+    var a21 = m[6];
+    var a22 = m[10];
+    var a23 = m[14];
+    var a30 = m[3];
+    var a31 = m[7];
+    var a32 = m[11];
+    var a33 = m[15];
+
+    var b00 = a00 * a11 - a01 * a10;
+    var b01 = a00 * a12 - a02 * a10;
+    var b02 = a00 * a13 - a03 * a10;
+    var b03 = a01 * a12 - a02 * a11;
+    var b04 = a01 * a13 - a03 * a11;
+    var b05 = a02 * a13 - a03 * a12;
+    var b06 = a20 * a31 - a21 * a30;
+    var b07 = a20 * a32 - a22 * a30;
+    var b08 = a20 * a33 - a23 * a30;
+    var b09 = a21 * a32 - a22 * a31;
+    var b10 = a21 * a33 - a23 * a31;
+    var b11 = a22 * a33 - a23 * a32;
+
+    // calculate determinate
+    var det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06;
+    var invdet = 1.0 / det;
+
+    // bail out if the matrix is not invertible
+    if (det === 0 || invdet === Infinity) {
+      SkDebug('Warning, uninvertible matrix');
+      return null;
+    }
+
+    b00 *= invdet;
+    b01 *= invdet;
+    b02 *= invdet;
+    b03 *= invdet;
+    b04 *= invdet;
+    b05 *= invdet;
+    b06 *= invdet;
+    b07 *= invdet;
+    b08 *= invdet;
+    b09 *= invdet;
+    b10 *= invdet;
+    b11 *= invdet;
+
+    // store result in row major order
+    var tmp = [
+      a11 * b11 - a12 * b10 + a13 * b09,
+      a12 * b08 - a10 * b11 - a13 * b07,
+      a10 * b10 - a11 * b08 + a13 * b06,
+      a11 * b07 - a10 * b09 - a12 * b06,
+
+      a02 * b10 - a01 * b11 - a03 * b09,
+      a00 * b11 - a02 * b08 + a03 * b07,
+      a01 * b08 - a00 * b10 - a03 * b06,
+      a00 * b09 - a01 * b07 + a02 * b06,
+
+      a31 * b05 - a32 * b04 + a33 * b03,
+      a32 * b02 - a30 * b05 - a33 * b01,
+      a30 * b04 - a31 * b02 + a33 * b00,
+      a31 * b01 - a30 * b03 - a32 * b00,
+
+      a22 * b04 - a21 * b05 - a23 * b03,
+      a20 * b05 - a22 * b02 + a23 * b01,
+      a21 * b02 - a20 * b04 - a23 * b00,
+      a20 * b03 - a21 * b01 + a22 * b00,
+    ];
+
+
+    if (!tmp.every(function(val) { return val !== NaN && val !== Infinity && val !== -Infinity; })) {
+      SkDebug('inverted matrix contains infinities or NaN '+tmp);
+      return null;
+    }
+    return tmp;
+  }
+
+
   // An SkColorMatrix is a 4x4 color matrix that transforms the 4 color channels
   //  with a 1x4 matrix that post-translates those 4 channels.
   // For example, the following is the layout with the scale (S) and post-transform
@@ -135,11 +421,6 @@
   // Much of this was hand-transcribed from SkColorMatrix.cpp, because it's easier to
   // deal with a Float32Array of length 20 than to try to expose the SkColorMatrix object.
 
-  var rScale = 0;
-  var gScale = 6;
-  var bScale = 12;
-  var aScale = 18;
-
   var rPostTrans = 4;
   var gPostTrans = 9;
   var bPostTrans = 14;
@@ -147,21 +428,21 @@
 
   CanvasKit.SkColorMatrix = {};
   CanvasKit.SkColorMatrix.identity = function() {
-    var m = new Float32Array(20);
-    m[rScale] = 1;
-    m[gScale] = 1;
-    m[bScale] = 1;
-    m[aScale] = 1;
-    return m;
+    return Float32Array.of([
+      1, 0, 0, 0, 0,
+      0, 1, 0, 0, 0,
+      0, 0, 1, 0, 0,
+      0, 0, 0, 1, 0,
+    ]);
   }
 
   CanvasKit.SkColorMatrix.scaled = function(rs, gs, bs, as) {
-    var m = new Float32Array(20);
-    m[rScale] = rs;
-    m[gScale] = gs;
-    m[bScale] = bs;
-    m[aScale] = as;
-    return m;
+    return Float32Array.of([
+      rs,  0,  0,  0,  0,
+       0, gs,  0,  0,  0,
+       0,  0, bs,  0,  0,
+       0,  0,  0, as,  0,
+    ]);
   }
 
   var rotateIndices = [
diff --git a/modules/canvaskit/release.js b/modules/canvaskit/release.js
index c1e76a3..9d505a4 100644
--- a/modules/canvaskit/release.js
+++ b/modules/canvaskit/release.js
@@ -1,4 +1,5 @@
 function SkDebug(msg) {
   // by leaving this blank, closure optimizes out calls (and the messages)
   // which trims down code size and marginally improves runtime speed.
-}
\ No newline at end of file
+}
+/** @const */ var skIsDebug = false;
\ No newline at end of file
diff --git a/modules/canvaskit/tests/matrix.spec.js b/modules/canvaskit/tests/matrix.spec.js
index eff2d4e..e8dbbc1 100644
--- a/modules/canvaskit/tests/matrix.spec.js
+++ b/modules/canvaskit/tests/matrix.spec.js
@@ -1 +1,229 @@
-//TODO test mapPoints and such
\ No newline at end of file
+describe('CanvasKit\'s Matrix Helpers', function() {
+
+  let expectArrayCloseTo = function(a, b) {
+    //expect(a).not.toEqual(null);
+    //expect(b).not.toEqual(null);
+    expect(a.length).toEqual(b.length);
+    for (let i=0; i<a.length; i++) {
+      expect(a[i]).toBeCloseTo(b[i], 14); // 14 digits of precision in base 10
+    }
+  };
+
+  describe('3x3 matrices', function() {
+
+    it('can make a translated 3x3 matrix', function(done) {
+      LoadCanvasKit.then(catchException(done, () => {
+        expectArrayCloseTo(
+          CanvasKit.SkMatrix.translated(5, -1),
+            [1, 0,  5,
+             0, 1, -1,
+             0, 0,  1]);
+        done();
+      }));
+    });
+
+    it('can make a scaled 3x3 matrix', function(done) {
+      LoadCanvasKit.then(catchException(done, () => {
+        expectArrayCloseTo(
+          CanvasKit.SkMatrix.scaled(2, 3),
+            [2, 0, 0,
+             0, 3, 0,
+             0, 0, 1]);
+        done();
+      }));
+    });
+
+    it('can make a rotated 3x3 matrix', function(done) {
+      LoadCanvasKit.then(catchException(done, () => {
+        expectArrayCloseTo(
+          CanvasKit.SkMatrix.rotated(Math.PI, 9, 9),
+            [-1,  0, 18,
+              0, -1, 18,
+              0,  0,  1]);
+        done();
+      }));
+    });
+
+    it('can make a skewed 3x3 matrix', function(done) {
+      LoadCanvasKit.then(catchException(done, () => {
+        expectArrayCloseTo(
+          CanvasKit.SkMatrix.skewed(4, 3, 2, 1),
+            [1, 4, -8,
+             3, 1, -3,
+             0, 0,  1]);
+        done();
+      }));
+    });
+
+    it('can multiply 3x3 matrices', function(done) {
+      LoadCanvasKit.then(catchException(done, () => {
+        const a = [
+           0.1,  0.2,  0.3,
+           0.0,  0.6,  0.7,
+           0.9, -0.9, -0.8,
+        ];
+        const b = [
+           2.0,  3.0,  4.0,
+          -3.0, -4.0, -5.0,
+           7.0,  8.0,  9.0,
+        ];
+        const expected = [
+           1.7,  1.9,  2.1,
+           3.1,  3.2,  3.3,
+          -1.1, -0.1,  0.9,
+        ];
+        expectArrayCloseTo(
+          CanvasKit.SkMatrix.multiply(a, b),
+          expected);
+        done();
+      }));
+    });
+
+    it('satisfies the inverse rule for 3x3 matrics', function(done) {
+      // a matrix times its inverse is the identity matrix.
+      LoadCanvasKit.then(catchException(done, () => {
+        const a = [
+           0.1,  0.2,  0.3,
+           0.0,  0.6,  0.7,
+           0.9, -0.9, -0.8,
+        ];
+        const b = CanvasKit.SkMatrix.invert(a);
+        expectArrayCloseTo(
+          CanvasKit.SkMatrix.multiply(a, b),
+          CanvasKit.SkMatrix.identity());
+        done();
+      }));
+    });
+
+    it('maps 2D points correctly with a 3x3 matrix', function(done) {
+      LoadCanvasKit.then(catchException(done, () => {
+        const a = [
+           3,  0, -4,
+           0,  2, 4,
+           0,  0, 1,
+        ];
+        const points = [
+          0, 0,
+          1, 1,
+        ];
+        const expected = [
+          -4, 4,
+          -1, 6,
+        ];
+        expectArrayCloseTo(
+          CanvasKit.SkMatrix.mapPoints(a, points),
+          expected);
+        done();
+      }));
+    });
+
+  }); // describe 3x3
+  describe('4x4 matrices', function() {
+
+    it('can make a translated 4x4 matrix', function(done) {
+      LoadCanvasKit.then(catchException(done, () => {
+        expectArrayCloseTo(
+          CanvasKit.SkM44.translated([5, 6, 7]),
+            [1, 0, 0, 5,
+             0, 1, 0, 6,
+             0, 0, 1, 7,
+             0, 0, 0, 1]);
+        done();
+      }));
+    });
+
+    it('can make a scaled 4x4 matrix', function(done) {
+      LoadCanvasKit.then(catchException(done, () => {
+        expectArrayCloseTo(
+          CanvasKit.SkM44.scaled([5, 6, 7]),
+            [5, 0, 0, 0,
+             0, 6, 0, 0,
+             0, 0, 7, 0,
+             0, 0, 0, 1]);
+        done();
+      }));
+    });
+
+    it('can make a rotated 4x4 matrix', function(done) {
+      LoadCanvasKit.then(catchException(done, () => {
+        expectArrayCloseTo(
+          CanvasKit.SkM44.rotated([1,1,1], Math.PI),
+            [-1/3,  2/3,  2/3, 0,
+              2/3, -1/3,  2/3, 0,
+              2/3,  2/3, -1/3, 0,
+                0,    0,    0, 1]);
+        done();
+      }));
+    });
+
+    it('can make a 4x4 matrix looking from eye to center', function(done) {
+      LoadCanvasKit.then(catchException(done, () => {
+        eye = [1, 0, 0];
+        center = [1, 0, 1];
+        up = [0, 1, 0]
+        expectArrayCloseTo(
+          CanvasKit.SkM44.lookat(eye, center, up),
+            [-1,  0,  0,  1,
+              0,  1,  0,  0,
+              0,  0, -1,  0,
+              0,  0,  0,  1]);
+        done();
+      }));
+    });
+
+    it('can make a 4x4 prespective matrix', function(done) {
+      LoadCanvasKit.then(catchException(done, () => {
+        expectArrayCloseTo(
+          CanvasKit.SkM44.perspective(2, 10, Math.PI/2),
+            [1, 0,   0, 0,
+             0, 1,   0, 0,
+             0, 0, 1.5, 5,
+             0, 0,  -1, 1]);
+        done();
+      }));
+    });
+
+    it('can multiply 4x4 matrices', function(done) {
+      LoadCanvasKit.then(catchException(done, () => {
+        const a = [
+           0.1,  0.2,  0.3,  0.4,
+           0.0,  0.6,  0.7,  0.8,
+           0.9, -0.9, -0.8, -0.7,
+          -0.6, -0.5, -0.4, -0.3,
+        ];
+        const b = [
+           2.0,  3.0,  4.0,  5.0,
+          -3.0, -4.0, -5.0, -6.0,
+           7.0,  8.0,  9.0, 10.0,
+          -4.0, -3.0, -2.0, -1.0,
+        ];
+        const expected = [
+           0.1,  0.7,  1.3,  1.9,
+          -0.1,  0.8,  1.7,  2.6,
+           1.7,  2.0,  2.3,  2.6,
+          -1.3, -2.1, -2.9, -3.7,
+        ];
+        expectArrayCloseTo(
+          CanvasKit.SkM44.multiply(a, b),
+          expected);
+        done();
+      }));
+    });
+
+    it('satisfies the identity rule for 4x4 matrices', function(done) {
+      LoadCanvasKit.then(catchException(done, () => {
+        const a = [
+           0.1,  0.2,  0.3,  0.4,
+           0.0,  0.6,  0.7,  0.8,
+           0.9,  0.9, -0.8, -0.7,
+          -0.6, -0.5, -0.4, -0.3,
+        ];
+        const b = CanvasKit.SkM44.invert(a)
+        expectArrayCloseTo(
+          CanvasKit.SkM44.multiply(a, b),
+          CanvasKit.SkM44.identity());
+        done();
+      }));
+    });
+  }); // describe 4x4
+});
\ No newline at end of file