Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/palette.git
/**
 * Given a set of hexadecimal colour codes, this will create SVG boxes of
 * the colour codes, sorted into colour bins by perceived brightness.
 */
const chroma = require( "chroma-js" );
const readline = require( "readline" );

// Max # of bins = 360 / DEGREES_PER_BIN
const DEGREES_PER_BIN = 16;

// Circle radius, in pixels.
const COLOUR_RADIUS = 15;

// Box width, in pixels.
const COLOUR_WIDTH = 35;

// Box height, in pixels.
const COLOUR_HEIGHT = 35;

// Distance between horizontal boxes, in pixels.
const COLOUR_SEP_X = 5;

// Distance between vertical boxes, in pixels.
const COLOUR_SEP_Y = 5;

// Remove colours closer in similarity than this threshold.
const THRESHOLD_SIMILARITY = 7;

// Create a gray bin for colours saturated less than this threshold.
const THRESHOLD_CHROMA = 10;

/**
 * Read the hex codes into an array from standard input.
 * Bin the hex codes into HSL at a given # of degrees of separation.
 * Remove colours not allowed.
 * Order bin colours to nearest L (of Lab).
 * Write the results to standard output as SVG element.
 */
const main = function() {
  read_hex_codes()
    .then( bin_by_hue )
    .then( sort_by_lightness )
    .then( remove_colours )
    .then( write_hex_codes );
}

/**
 * Reads hex codes from standard input and passes them to the callback
 * function as an array.
 */
const read_hex_codes = function() {
  return new Promise( function( resolve, reject ) {
    var colours = [];

    const r = readline.createInterface({
      input: process.stdin,
      output: process.stdout,
      terminal: false
    });

    r.on( 'line', function( line ) {
      colours.push( line );
    }).on( 'close', function() {
      resolve( colours );
    });
  });
}

/**
 * Bin all the colours by hue.
 */
const bin_by_hue = function( colours ) {
  return new Promise( function( resolve, reject ) {
    var bins = [];

    colours.forEach( function( colour ) {
      // Convert to colourspace (cs)
      const cs = chroma( colour ).lch();
      const c = Math.round( cs[ 1 ] );
      const h = chroma( colour ).hsv()[ 0 ];

      // Special grayscale bin.
      var bin = 0;

      // Determine the bin, if not grayscale.
      if( c > THRESHOLD_CHROMA ) {
        bin = ~~(h / DEGREES_PER_BIN) + 1;
      }

      if( typeof bins[ bin ] == 'undefined' ) {
        bins[ bin ] = [];
      }

      bins[ bin ].push( colour );
    });

    resolve( bins );
  });
}

/**
 * Sorts the colours by their luminance.
 */
const sort_by_lightness = function( bins ) {
  return new Promise( function( resolve, reject ) {
    var result = [];

    bins.forEach( function( bin ) {
      var sorted_bin = [];

      while( bin.length > 0 ) {
        var min = 1;
        var darkest;

        bin.forEach( function( colour ) {
          var lum = chroma( colour ).luminance();

          if( lum < min ) {
            min = lum;
            darkest = colour;
          }
        });

        var index = bin.indexOf( darkest );
        bin.splice( index, 1 );
        sorted_bin.push( darkest );
      }

      result.push( sorted_bin )
    });

    resolve( result );
  });
}

/**
 * Remove unwanted colours from each bin.
 */
const remove_colours = function( bins ) {
  return new Promise( function( resolve, reject ) {
    var resolved = [];

    bins.forEach( function( bin ) {
      var modified = true;

      while( modified ) {
        modified = false;

        bin = bin.filter( function( colour, index ) {
          var retain = true;
          var hue = chroma( colour ).hsl();

          if( hue[1] > 0.6 && hue[2] > 0.7 ) {
            // Remove pastels.
            retain = false;
          }
          else if( index + 1 < bin.length ) {
            // Remove similar colours.
            const distance = chroma.distance( colour, bin[ index + 1 ] );
            retain = distance > THRESHOLD_SIMILARITY;
          }

          // Repeat filtering until no duplicates remain.
          if( retain === false ) {
            modified = true;
          }

          return retain;
        });
      }

      resolved.push( bin );
    });

    resolve( resolved );
  });
}

/**
 * Write the bins to standard output.
 */
const write_hex_codes = function( bins ) {
  var x = COLOUR_SEP_X + COLOUR_RADIUS;

  // Remove the empty bins and then write the SVG element.
  bins
    .filter( n => Object.keys( n ).length !== 0 )
    .forEach( function( bin ) {
      let y = COLOUR_RADIUS;

      bin.forEach( function( colour ) {
        y -= (COLOUR_HEIGHT + COLOUR_SEP_Y);
        /*
        console.log(
          '<rect x="' + x +
          '" y="' + y +
          '" height="' + COLOUR_HEIGHT +
          '" width="' + COLOUR_WIDTH +
          '" style="fill:' + colour + '"/>'
        );
        */

        console.log(
          '<circle cx="' + x +
          '" cy="' + y +
          '" r="' + COLOUR_RADIUS +
          '" style="fill:' + colour + '"/>'
        );
      });

      x += (COLOUR_WIDTH + COLOUR_SEP_X);
    });
}

/**
 * Main entry point.
 */
main();