dfcreative/color-ranger

View on GitHub
index.js

Summary

Maintainability
D
1 day
Test Coverage
/**
 * A somewhat abstract renderer.
 *
 * @module color-ranger/render
 */

module.exports = render;


var convert = require('color-space');


//default options for default rendering
var defaults = {
    channel: [0,1],
    space: 'rgb',
    sourceSpace: 'rgb'
};


/** Bind self for exports */
render.render = render;
render.chess = renderChess;
render.defaults = defaults;


//37 is the most optimal calc size
//you can scale resulting image with no significant quality loss


/**
 * Render passed rectangular range for the color to imageData.
 *
 * @param {array} rgb A list of rgb values + optional alpha
 * @param {string} space A space to render, no alpha-suffix
 * @param {array} channels List of channel indexes to render
 * @param {array} mins Min values to render range (can be different than space mins)
 * @param {array} maxes Max values to render range (can be different than space maxes)
 * @param {UInt8CappedArray} buffer An image data buffer
 * @param {function} calc A calculator for the step color
 *
 * @return {ImageData} ImageData containing a range
 */
function render(values, buffer, opts) {
    if (!buffer || !buffer.length) throw Error('Buffer must be a valid non-empty UInt8CappedArray');

    if (opts && opts.type === 'chess') return renderChess([255,255,255], values, buffer);


    var size = opts.size || [Math.floor(Math.sqrt(buffer.length / 4))];
    if (size.length === 1) {
        size[1] = size[0];
    }

    var space = opts.space = opts.space || defaults.space;
    var sourceSpace = opts.sourceSpace = opts.sourceSpace || defaults.sourceSpace;

    var channels = opts.channel = opts.channel !== undefined ? opts.channel : defaults.channel;

    var mins = opts.min || [],
        maxes = opts.max || [];

    var calc = opts.type === 'polar' ?  calcPolarStep : calcRectStep;


    //take mins/maxes of target space’s channels
    for (var i = 0; i < channels.length; i++) {
        if (mins.length < channels.length) {
            mins[i] = convert[space].min[channels[i]] || 0;
        }
        if (maxes.length < channels.length) {
            maxes[i] = convert[space].max[channels[i]] || 255;
        }
    }

    var isCMYK = space === 'cmyk';

    //get specific space values
    if (space === sourceSpace) {
        values = values.slice();
    } else {
        values = convert[sourceSpace][space](values);
        if (!isCMYK && values.length === 3) {
            values[3] = values[3];
        }
    }

    //add alpha
    if (values.length === 4) {values[3] = (values[3] === undefined ? 1 : values[3]) * 255;}
    if (values.length === 3) {values[3] = 255;}

    //resolve absent indexes
    var noIdx = [];
    for (i = space.length; i--;) {
        if (i !== channels[0] && i !== channels[1]) {
            noIdx.push(i);
        }
    }
    var noIdx1 = noIdx[0];
    var noIdx2 = noIdx[1];
    var noIdx3 = noIdx[2];

    //get converting fn
    var converter = space === 'rgb' ? function(a) {return a;} : convert[space].rgb;

    for (var x, y = size[1], row, col, res, stepVals = values; y--;) {
        row = y * size[0] * 4;

        for (x = 0; x < size[0]; x++) {
            col = row + x * 4;

            //calculate color
            stepVals = calc(x,y, stepVals, size, channels, mins, maxes);

            if (noIdx1 || noIdx1 === 0) {
                stepVals[noIdx1] = values[noIdx1];
            }
            if (noIdx2 || noIdx2 === 0) {
                stepVals[noIdx2] = values[noIdx2];
            }
            if (noIdx3 || noIdx3 === 0) {
                stepVals[noIdx3] = values[noIdx3];
            }

            //fill image data
            res = converter(stepVals);
            res[3] = isCMYK ? 255 : stepVals[3];

            buffer.set(res, col);
        }
    }

    return buffer;
}


/**
 * Calculate polar step
 *
 * @param {[type]} x [description]
 * @param {[type]} y [description]
 * @param {[type]} vals [description]
 * @param {[type]} size [description]
 * @param {[type]} channels [description]
 * @param {[type]} mins [description]
 * @param {[type]} maxes [description]
 *
 * @return {[type]} [description]
 */
function calcPolarStep(x,y, vals, size, channels, mins, maxes) {
    //cet center
    var cx = size[0]/2, cy = size[1]/2;

    //get radius
    var r = Math.sqrt((cx-x)*(cx-x) + (cy-y)*(cy-y));

    //normalize radius
    var nr = r / cx;

    //get angle
    var a = Math.atan2( cy-y, cx-x );

    //get normalized angle (avoid negative values)
    var na = (a + Math.PI)/Math.PI/2;


    //ch 1 is radius
    if (channels[1] || channels[1] === 0) {
        vals[channels[1]] = mins[1] + (maxes[1] - mins[1]) * nr;
    }

    //ch 2 is angle
    if (channels[0] || channels[0] === 0) {
        vals[channels[0]] = mins[0] + (maxes[0] - mins[0]) * na;
    }

    return vals;
}


/**
 * Calculate step values for a rectangular range
 *
 * @param {array} vals step values to calc
 * @param {array} size size of rect
 * @param {array} mins min c1,c2
 * @param {array} maxes max c1,c2
 *
 * @return {array} [description]
 */
function calcRectStep(x,y, vals, size, channels, mins, maxes) {
    if (channels[1] || channels[1] === 0) {
        vals[channels[1]] = mins[1] + (maxes[1] - mins[1]) * (1 - y / (size[1] - 1));
    }
    if (channels[0] || channels[0] === 0) {
        vals[channels[0]] = mins[0] + (maxes[0] - mins[0]) * x / (size[0] - 1);
    }
    return vals;
}


/**
 * Render transparency grid.
 * Image data is split in two equally
 *
 * @param {array} a Rgb values representing "white" cell
 * @param {array} b Rgb values representing "black" cell
 * @param {ImageData} imgData A data taken as a base for grid
 *
 * @return {ImageData} Return updated imageData
 */
function renderChess (a, b, data) {
    //suppose square data
    var w = Math.floor(Math.sqrt(data.length / 4)), h = w;

    var cellH = ~~(h/2);
    var cellW = ~~(w/2);

    //convert alphas to 255
    if (a.length === 4) a[3] *= 255;
    if (b.length === 4) b[3] *= 255;

    for (var y=0, col, row; y < h; y++) {
        row = y * w * 4;
        for ( var x=0; x < w; x++) {
            col = row + x * 4;
            data.set(x >= cellW ? (y >= cellH ? a : b) : (y >= cellH ? b : a), col);
        }
    }

    return data;
}