/**
* The MatrixBuilder eases the creation of LED matrix definitions for your L8
* Smartlight.
*
* It provides a couple of convenience functions to create and manipulate matrix
* arrays.
*
* The constructor either takes a baseColor for the whole matrix, or a complete
* matrix as a starting point for manipulations.
*
* The `baseColorOrMatrix` argument is optional. If none is supplied an LED
* off-state is chosen as default for every LED in the matrix.
*
* @example ```
* var l8 = new L8();
* //... Initialize L8...
*
* var builder = new MatrixBuilder();
* var matrix = builder.rectangle(
* {r: 0, g: 0, b: 15},
* 2, 2,
* 5, 5,
* false
* )
* .row({r: 15, g: 0, b: 0}, 3)
* .row({r: 15, g: 0, b: 0}, 4)
* .column({r: 15, g: 0, b: 0}, 3}),
* .column({r: 15, g: 0, b: 0}, 4)
* .toMatrix();
*
* l8.setMatrix(matrix);
* ```
*
* The given examples draws a blue border with a red crosshair on the L8
*
* @param {{r:Number, g:Number, b:Number}|Array} [baseColorOrMatrix]
* @constructor
*/
var MatrixBuilder = function(baseColorOrMatrix) {
/**
* List of all transformation operations to be applied to the initial
* matrix upon calls to `toMatrix`.
*
* @type {Array}
* @private
*/
this.operations_ = [];
/**
* The initial matrix all operations will be applied against.
*
* @type {Array}
* @private
*/
this.initialMatrix_ = null;
if (baseColorOrMatrix === undefined) {
// By default all LEDs are off
baseColorOrMatrix = {r: 0, g: 0, b: 0};
}
if (baseColorOrMatrix.constructor !== [].constructor) {
// Assuming it is a base color
this.validateColors_([baseColorOrMatrix]);
this.initialMatrix_ = this.createArrayWithSize(8*8).map(function() {
return baseColorOrMatrix;
});
} else {
// A matrix has been provided. We copy it as we don't want to unintentionally change
// it during the building process.
this.initialMatrix_ = this.createArrayWithSize(8*8).map(function(value, index) {
return baseColorOrMatrix[index];
});
}
};
/**
* Execute a `map` like operation on a matrix taking into account its special nature.
*
* The operation is mostly equivalent to a normal `array.map` call. However special
* considerations about the matrix will be taken into account, like lines and columns.
*
* The callback is executed with the following arguments:
*
* - color
* - column (x-coordinate)
* - row (y-coordinate)
* - index
*
* The matrix will be modified in place
*
* The following example will remove the *red* component of any pixel inside the
* given matrix.
*
* @example ```
* var matrix = // some matrix
* MatrixBuilder.map(matrix, function(originalColor, row, column, index) {
* return {r: 0, g: originalColor.g, b: originalColor.b};
* });
*
* l8.setMatrix(matrix, ...);
* ```
*
* @param {Array} matrix
* @param {Function} fn
* @static
*/
MatrixBuilder.map = function(matrix, fn) {
matrix.forEach(function(value, index) {
matrix[index] = fn(value, index % 8, Math.floor(index / 8), index);
})
};
/**
* Validate coordinates are valid numbers between 0-7
*
* If one of the coordinates is invalid an exception will be thrown.
*
* @param {Number[]} coordinates
* @private
*/
MatrixBuilder.prototype.validateCoordinates_ = function(coordinates) {
coordinates.forEach(function(coordinate) {
if (typeof coordinate !== "number" || coordinate < 0 || coordinate > 7) {
throw new RangeError("Valid LED coordinate (0-7) expected, got " + coordinate);
}
});
};
/**
* Validate colors are valid r, g, b objects
*
* If one of the colors is invalid an exception will be thrown.
*
* @param {Object[]} colors
* @param {Number} colors.r
* @param {Number} colors.g
* @param {Number} colors.b
*
* @private
*/
MatrixBuilder.prototype.validateColors_ = function(colors) {
colors.forEach(function(color) {
if (color.r === undefined || color.g === undefined || color.b === undefined) {
throw new RangeError("Color object with r,g and b properties expected, got: " + JSON.stringify(baseColorOrMatrix));
}
});
};
/**
* Create an Array with a given size and values at every index position.
*
* This array may easily be used to execute map/reduce operations to fill/create
* a new array structure.
*
* @param {Number} size
* @returns {Array}
*/
MatrixBuilder.prototype.createArrayWithSize = function(size) {
return Array.apply(null, new Array(size));
};
/**
* Add a transformation operation to the MatrixBuilder
*
* Operations are functions, which will be called in order of registration upon
* the matrix. They are supposed to transform or manipulate it in any way relevant
* to the creation process.
*
* Operations are provided with an optional parameters object, which may be
* supplied during the call to `toMatrix`, which allows to introduce variables
* into the building pipeline.
*
* @param fn
* @returns {MatrixBuilder}
*/
MatrixBuilder.prototype.operation = function(fn) {
this.operations_.push(fn);
return this;
};
/**
* Draw pixels inside a certain row of the grid.
*
* Optionally a start and end column may be provided. If none are given the start
* and end of the line are assumed.
*
* @param {{r: Number, g: Number, b: Number}} color
* @param {Number} y
* @param {Number} x0
* @param {Number} x1
* @returns {MatrixBuilder}
*/
MatrixBuilder.prototype.row = function(color, y, x0, x1) {
x0 = x0 || 0;
x1 = x1 || 7;
this.validateColors_([color]);
this.validateCoordinates_([y, x0, x1]);
return this.operation(function(matrix, variables){
MatrixBuilder.map(matrix, function(originalColor, column, row) {
return (row === y && column >= x0 && column <= x1) ? color : originalColor;
});
});
};
/**
* Draw pixels inside a certain column of the grid.
*
* Optionally a start and end row may be provided. If none are given the start
* and end of the column are assumed.
*
* @param {{r: Number, g: Number, b: Number}} color
* @param {Number} x
* @param {Number} y0
* @param {Number} y1
* @returns {MatrixBuilder}
*/
MatrixBuilder.prototype.column = function(color, x, y0, y1) {
y0 = y0 || 0;
y1 = y1 || 7;
this.validateColors_([color]);
this.validateCoordinates_([x, y0, y1]);
return this.operation(function(matrix, variables){
MatrixBuilder.map(matrix, function(originalColor, column, row) {
return (column === x && row >= y0 && row <= y1) ? color : originalColor;
});
});
};
/**
* Draw a rectangle to the pixel grid
*
* The grid is defined by specifying its upper left and lower right coordinates.
*
* Optionally it may be specified if the grid should be filled or not. By
* default the grid will be filled.
*
* @param {{r: Number, g: Number, b: Number}} color
* @param {Number} x0
* @param {Number} y0
* @param {Number} x1
* @param {Number} y1
* @param {Boolean} [filled]
*
* @returns {MatrixBuilder}
*/
MatrixBuilder.prototype.rectangle = function(color, x0, x1, y0, y1, filled) {
if (filled === undefined) {filled = true;}
this.validateColors_([color]);
this.validateCoordinates_([x0, x1, y0, y1]);
if (filled === true) {
return this.operation(function(matrix, variables){
MatrixBuilder.map(matrix, function(originalColor, column, row) {
return (column >= x0 && column <= x1 && row >= y0 && row <= y1) ? color : originalColor;
});
});
} else {
return this.operation(function(matrix, variables){
MatrixBuilder.map(matrix, function(originalColor, column, row) {
return (
(column >= x0 && column <= x1 && (row === y0 || row === y1))
|| (row >= y0 && row <= y1 && (column === x0 || column === x1))
? color : originalColor
);
});
});
}
};
/**
* Generate the matrix using all defined operations as well as the variables
*
* The given variables will be provided to each operation.
* Operations will be executed in the defined order.
*
* @param {Object} [variables]
* @returns {Array}
*/
MatrixBuilder.prototype.toMatrix = function(variables) {
variables = variables || {};
// Start with a copy of the original matrix
var currentMatrix = this.createArrayWithSize(8*8).map(function(value, index) {
return this.initialMatrix_[index];
}.bind(this));
this.operations_.forEach(function(operation) {
operation(currentMatrix, variables);
});
return currentMatrix;
};
exports.MatrixBuilder = MatrixBuilder;