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