Shadow is a Dart/JavaScript library for computing 3D projections and rendering them in HTML5 web pages.

To see this code in action, click the Next link at top right, and continue from there.

## In Brief

To use Shadow, you decide on a geometric configuration:

• An eye position
• A projection plane (specified by its normal vector)
• The distance of the eye to the plane
• The distance of the plane to the origin
• The width and height of the viewing rectangle in the projection plane.

Shadow comprises two main classes and several auxiliary classes. They are defined in dependency order. You can click the links to jump directly to them.

• Matrix — Basic matrix algebra
• Geometry — Basic geometric linear algebra
• Config — Configuration parameters
• Derived — Parameters dependent on Config
• ProjectedPoint — The result of projecting a point
• Projector — Projects points from object space to display rect
• Drawer — Plots points in an HTML canvas

## Matrix

/** * Elementary matrix operations. (For vectors, we just use plain Lists.) */ class Matrix { int height = 3; int width = 3; List<List<double>> contents; // Default value is 3x3 identity matrix. Matrix() { contents = [ [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0] ]; } //------------------------------------------------------------------------------ Matrix.withArray(this.contents) { // List should be a literal, not shared. height = contents.length; width = contents[0].length; // Rows better be all same length!! } //------------------------------------------------------------------------------ Matrix.withMatrix(Matrix other) { final contents = new List<List<double>>(other.height); for (var r = 0; r < other.height; r++) { // Unexpectedly, final can apply inside a loop where row gets reassigned. final row = new List<double>(other.width); for (var c = 0; c < other.width; c++) { row[c] = other.contents[r][c]; } contents[r] = row; } height = other.height; width = other.width; } //------------------------------------------------------------------------------ double at(int r, int c) { return contents[r][c]; } //------------------------------------------------------------------------------ String toString() { List<String> s = []; for (var r = 0; r < contents.length; r++) { s.add(contents[r].join(',')); } return s.join('\n'); } //------------------------------------------------------------------------------ // Not an in-place operation. Hence the past-tense name. Matrix transposed() { var result = new List<List<double>>(width); for (var col = 0; col < width; col++) { result[col] = new List<double>(height); for (var row = 0; row < height; row++) { result[col][row] = contents[row][col]; } } return new Matrix.withArray(result); } //------------------------------------------------------------------------------ // Special-case for applying matrices to vectors. // Returns a plain Dart List rather than a Matrix object. // "Normal" order, that is, result = Mv with v a COLUMN vector. List<double> apply(List<double> vector) { if (width != vector.length) { throw 'Shadow error: incompatible sizes in Matrix.apply().'; } var result = new List<double>(height); for (var row = 0; row < height; row++) { result[row] = 0.0; for (var col = 0; col < width; col++) { result[row] += contents[row][col] * vector[col]; } } return result; } //------------------------------------------------------------------------------ // Special-case for applying matrices to vectors. // Returns a plain Dart List rather than a Matrix object. // "Left-hand" order, that is, result = vM with v a ROW vector. List<double> applyLeft(List<double> vector) { if (height != vector.length) { throw 'Shadow error: incompatible sizes in Matrix.applyLeft().'; } var result = new List<double>(width); for (var col = 0; col < width; col++) { result[col] = 0.0; for (var row = 0; row < height; row++) { result[col] += contents[row][col] * vector[row]; } } return result; } //------------------------------------------------------------------------------ // General case of multiplying two matrices. // Returns M*other. Matrix multBy(Matrix other) { if (width != other.height) { throw 'Shadow error: incompatible sizes in Matrix.multBy().'; } var result = new List<List<double>>(height); for (var row = 0; row < height; row++) { result[row] = new List<double>(other.width); for (var col = 0; col < other.width; col++) { var sum = 0.0; for (var k = 0; k < width; k++) { // Here we reach right into the other's contents. sum += contents[row][k] * other.contents[k][col]; } result[row][col] = sum; } } return new Matrix.withArray(result); } //------------------------------------------------------------------------------ // In-place implied by present-tense. (As opposed, e.g. to ‘scaled’.) void scale(double scalar) { for (var row = 0; row < height; row++) { for (var col = 0; col < width; col++) { contents[row][col] *= scalar; } } } //------------------------------------------------------------------------------ void scaleElement(double scalar, int row, int col) { contents[row][col] *= scalar; } //------------------------------------------------------------------------------ void setElement(double value, int row, int col) { contents[row][col] = value; } //------------------------------------------------------------------------------ // Special-case 3x3 matrix inverse. Matrix inverse33() { if (width != 3 || height != 3) { throw 'Shadow error: not 3x3 in Matrix.inverse33().'; } // Just for more readable code. final a = contents[0][0]; final b = contents[0][1]; final c = contents[0][2]; final d = contents[1][0]; final e = contents[1][1]; final f = contents[1][2]; final g = contents[2][0]; final h = contents[2][1]; // Skip i,j since they are already final k = contents[2][2]; // so overloaded with meanings. // Find determinant by evaluating sub-determinants. final determinant = a * (e*k - f*h) + b * (f*g - k*d) + c * (d*h - e*g); if (determinant == 0.0) { // TODO: have some tolerance; check for // denormalized number at least. throw 'Shadow error: singular in Matrix.inverse33().'; } final rawResult = [ [e*k - f*h, c*h - b*k, b*f - c*e], [f*g - d*k, a*k - c*g, c*d - a*f], [d*h - e*g, g*b - a*h, a*e - b*d] ]; final result = new Matrix.withArray(rawResult); result.scale(1.0/determinant); return result; } //------------------------------------------------------------------------------ // Special-case code for right-inverse of 3x4 matrix. // Right-inverse X+ of X is Xt * (X * Xt)^-1, where Xt is // transpose of X. If X is MxN, X+ is NxM. Matrix rightInverse34() { if (height != 3 ||width != 4) { throw 'Shadow error: not 3x4 in Matrix.rightInverse().'; } // Set M33 = M34 * M34t. final transposed = this.transposed(); // Use "this" for clarity. final m33 = this.multBy(transposed); // Ditto. final m33inv = m33.inverse33(); return this.transposed().multBy(m33inv); } }

## Geometry

/** * Various geometric convenience functions, scoped in a Class. * (Contra the style guidelines. TODO: factor into another library.) */ class Geometry { // Easy enough to generalize into N-dimensional inner product. // But in this library, it is an error to not be a 3-vector. static double dotProduct3(List<double> x, List<double> y) { if (x.length != 3 || y.length != 3) { throw 'Shadow error: not 3-vector in Geometry.dotProduct3().'; } return x[0]*y[0] + x[1]*y[1] + x[2]*y[2]; } //------------------------------------------------------------------------------ static double length3(List<double> v) { return sqrt(dotProduct3(v,v)); } //------------------------------------------------------------------------------ static List<double> crossProduct3(List<double> x, List<double> y) { return [ x[1]*y[2] - x[2]*y[1], x[2]*y[0] - x[0]*y[2], x[0]*y[1] - x[1]*y[0] ]; } //------------------------------------------------------------------------------ // Finds equation ax + by + cz + d = 0 of plane thru points P, Q, R. static List<double> planeFunctionCoefs(List<double> P, List<double> Q, List<double> R) { final a = (P[1]-Q[1]) * (P[2]-R[2]) - (P[2]-Q[2]) * (P[1]-R[1]); final b = (P[2]-Q[2]) * (P[0]-R[0]) - (P[0]-Q[0]) * (P[2]-R[2]); final c = (P[0]-Q[0]) * (P[1]-R[1]) - (P[1]-Q[1]) * (P[0]-R[0]); final d = -(a*P[0] + b*P[1] + c*P[2]); return [a, b, c, d]; // Coefficients of equation. } //------------------------------------------------------------------------------ // Evaluates plane function. static double planeFunction(List<double> P, List<double> coefficients) { return coefficients[0]*P[0] + coefficients[1]*P[1] + coefficients[2]*P[2] + coefficients[3]; } //------------------------------------------------------------------------------ static bool pointInRect(List<double> P, List<double> leftBot, List<double> rightTop) { return P[0] > leftBot[0] && P[0] < rightTop[0] && P[1] > leftBot[1] && P[1] < rightTop[1]; } }

## Config

/** * Encapsulates the values needed to specify a 3D environment. * * We pass an instance of this into Projector to initialize it. * And we initialize *this* with a plain Map literal, for readability. */ class Config { // Here are sample values and explanations. // These are points and distances in the (x,y,z) space. List<double> viewNorm; // [2, 2, 2], Viewplane normal vector. double eyeDist; // 6, Distance of eye to origin. double planeDist; // 3, Distance of view plane to origin. // Dimensions of rect in view plane, that we "look through". double viewWidth; // 4, Width of view rect. double viewHeight; // 2, Height of view rect. // These are corners of the drawing rect in our device (canvas) plane. // The corners of the view rect get mapped to these points: // [-viewWidth/2, -viewHeight/2] gets mapped to devLeftBot; // [+viewWidth/2, +viewHeight/2] gets mapped to devRightTop. // To avoid aspect distortion, the drawing rect should have the same // proportion as the view rect. // Most commonly the drawing rect is identical to the canvas boundary. // But it could be larger or smaller than the canvas. List<double> devLeftBot; // [0, 0] List<double> devRightTop; // [800, 400] // We initialize with a Map literal, for readability at the call site. Config(Map config) { viewNorm = config['viewNorm']; eyeDist = config['eyeDist']; planeDist = config['planeDist']; viewWidth = config['viewWidth']; viewHeight = config['viewHeight']; devLeftBot = config['devLeftBot']; devRightTop = config['devRightTop']; } }

## Derived

/** * Values derived from the Config instance. * Used internally in Projector. */ class Derived { List<double> eye; // Center of perspectivity (eye location). List<double> refPoint; // Viewplane reference point. List<double> viewUp; // Viewplane up direction. double clipAhead; // Viewplane eq evaluated at forward clip plane. double clipBehind; // Viewplane eq evaluated at rear clip plane. double eyeConst; // Viewplane eq evaluated at eye. double planeConst; // Constant in eq of viewPlane. double devLeft; // Coordinates (u,v) in the device double devBot; // plane of the drawing rectangle. double devRight; // More convenient to work with than double devTop; // bottom-left and top-right points. double devWidth; double devHeight; Derived() { eye = [0.0,0.0,0.0]; refPoint = [0.0,0.0,0.0]; viewUp = [0.0,0.0,0.0]; // The rest can remain null; will be initialized by // Projector.setViewPlanEquation(). } }

## ProjectedPoint

/** * Represents the projection of a point from the object space into the * display rectangle. The original xyz coordinates are saved for * convenience; the important new values are the display U and V, and * a string describing the visibility of the point. */ class ProjectedPoint { List<double> XYZ; // World coordinates. List<double> UV; // Display device coordinates. int vis; // Visibility status. BEHIND, etc. static const double TOLERANCE = 1e-3; // Tolerance for point to be ideal. // Eventually these should be enums; // see http://news.dartlang.org/2013/04/enum-proposal-for-dart.html. static const int BEHIND = 1; // Behind plane thru eyePlane. static const int IDEAL = 2; // Within TOLERANCE of eyePlane. static const int INVISIBLE = 3; // Ahead but out of cone of vision. static const int VISIBLE = 4; // Visible. //------------------------------------------------------------------------------ bool isVisible() { return vis == VISIBLE; } }