/* * Trianglify.js * by @qrohlf * * Licensed under the GPLv3 */ var delaunay = require('delaunay-fast'); var seedrandom = require('seedrandom'); var chroma = require('chroma-js'); //PROBLEM: chroma.js is nearly 32k in size var colorbrewer = require('./colorbrewer'); //We could use the chroma.js colorbrewer, but it's got some ugly stuff so we use our own subset. var Pattern = require('./pattern'); var defaults = { width: 600, // Width of pattern height: 400, // Height of pattern cell_size: 75, // Size of the cells used to generate a randomized grid variance: 0.75, // how much to randomize the grid seed: null, // Seed for the RNG x_colors: 'random', // X color stops y_colors: 'match_x', // Y color stops palette: colorbrewer, // Palette to use for 'random' color option color_space: 'lab', // Color space used for gradient construction & interpolation color_function: null, // Color function f(x, y) that returns a color specification that is consumable by chroma-js stroke_width: 1.51 // Width of stroke. Defaults to 1.51 to fix an issue with canvas antialiasing. }; /********************************************************* * * Main function that is exported to the global namespace * **********************************************************/ function Trianglify(opts) { // apply defaults opts = _merge_opts(defaults, opts); // setup seedable RNG rand = seedrandom(opts.seed); // randomize colors if requested if (opts.x_colors === 'random') opts.x_colors = _random_from_palette(); if (opts.y_colors === 'random') opts.y_colors = _random_from_palette(); if (opts.y_colors === 'match_x') opts.y_colors = opts.x_colors; // some sanity-checking if (!(opts.width > 0 && opts.height > 0)) { throw new Error("Width and height must be numbers greater than 0"); } if (opts.cell_size < 2) { throw new Error("Cell size must be greater than 2."); } // Setup the color gradient function var gradient; if (opts.color_function) { gradient = function(x, y) { return chroma(opts.color_function(x, y)); }; } else { var x_color = chroma.scale(opts.x_colors).mode(opts.color_space); var y_color = chroma.scale(opts.y_colors).mode(opts.color_space); gradient = function(x, y) { return chroma.interpolate(x_color(x), y_color(y), 0.5, opts.color_space); }; } // Figure out key dimensions // it's a pain to prefix width and height with opts all the time, so let's // give them proper variables to refer to var width = opts.width; var height = opts.height; // How many cells we're going to have on each axis (pad by 2 cells on each edge) var cells_x = Math.floor((width + 4 * opts.cell_size) / opts.cell_size); var cells_y = Math.floor((height + 4 * opts.cell_size) / opts.cell_size); // figure out the bleed widths to center the grid var bleed_x = ((cells_x * opts.cell_size) - width)/2; var bleed_y = ((cells_y * opts.cell_size) - height)/2; // how much can out points wiggle (+/-) given the cell padding? var variance = opts.cell_size * opts.variance / 2; // Set up normalizers var norm_x = function(x) { return _map(x, [-bleed_x, width+bleed_x], [0, 1]); }; var norm_y = function(y) { return _map(y, [-bleed_y, height+bleed_y], [0, 1]); }; // generate a point mesh var points = _generate_points(width, height); // delaunay.triangulate gives us indices into the original coordinate array var geom_indices = delaunay.triangulate(points); // iterate over the indices in groups of three to flatten them into polygons, with color lookup var triangles = []; var lookup_point = function(i) { return points[i];}; for (var i=0; i < geom_indices.length; i += 3) { var vertices = [geom_indices[i], geom_indices[i+1], geom_indices[i+2]].map(lookup_point); var centroid = _centroid(vertices); var color = gradient(norm_x(centroid.x), norm_y(centroid.y)).hex(); triangles.push([color, vertices]); } return Pattern(triangles, opts); /********************************************************* * * Private functions * **********************************************************/ function _map(num, in_range, out_range ) { return ( num - in_range[0] ) * ( out_range[1] - out_range[0] ) / ( in_range[1] - in_range[0] ) + out_range[0]; } // generate points on a randomized grid function _generate_points(width, height) { var points = []; for (var i = - bleed_x; i < width + bleed_x; i += opts.cell_size) { for (var j = - bleed_y; j < height + bleed_y; j += opts.cell_size) { var x = i + opts.cell_size/2 + _map(rand(), [0, 1], [-variance, variance]); var y = j + opts.cell_size/2 + _map(rand(), [0, 1], [-variance, variance]); points.push([x, y].map(Math.floor)); } } return points; } //triangles only! function _centroid(d) { return { x: (d[0][0] + d[1][0] + d[2][0])/3, y: (d[0][1] + d[1][1] + d[2][1])/3 }; } // select a random palette from colorbrewer function _random_from_palette() { if (opts.palette instanceof Array) { return opts.palette[Math.floor(rand()*opts.palette.length)]; } var keys = Object.keys(opts.palette); return opts.palette[keys[Math.floor(rand()*keys.length)]]; } // shallow extend (sort of) for option defaults function _merge_opts(defaults, options) { var out = {}; // shallow-copy defaults so we don't mutate the input objects (bad) for (var key in defaults) { out[key] = defaults[key]; } for (key in options) { if (defaults.hasOwnProperty(key)) { out[key] = options[key]; // override defaults with options } else { throw new Error(key+" is not a configuration option for Trianglify. Check your spelling?"); } } return out; } } //end of Trianglify function closure // exports Trianglify.colorbrewer = colorbrewer; Trianglify.defaults = defaults; module.exports = Trianglify;