colorful.js

A tiny, robust color utility


Keywords
color, colour, conversion, rgb, rgba, hex, keywords, alpha, colourful, css, lightroom, lrcolor, agcolor
License
MIT
Install
npm install colorful.js@0.3.0

Documentation

Colorful.js

Setup – Defining a color – Output – Comparisons – Queries – Validation – Build and test

Colorful.js is a tiny component which creates color objects from a variety of input formats. It outputs color in as many formats, checks if colors are equal, and provides insights into other properties, like transparency.

In a nutshell:

var color = new Color( "rgb(0, 50%, 50%)" );

color.asHexUC()       // => "#008080"
color.asRgba()        // => "rgba(0, 128, 128, 1)"
color.asRgbArray()    // => [0, 128, 128]
// etc

color.equals( "teal" )                    // => true
color.equals( { r: 0, g: 128, b: 128 } )  // => true

color.isTransparent() // => false

Dependencies and setup

Underscore is the only dependency. Include colorful.js after Underscore is loaded.

Colorful.js can be loaded in the global context of the browser, or as a module (AMD, CJS/Node). The module contains the Color class:

var Color = require( "colorful.js" ).Color;

As a browser global, the Color class is attached to the window directly - simply use Color.

The stable version of Colorful.js is available in the dist directory (dev, prod). If you use Bower, fetch the files with bower install colorful.js. With npm, it is npm install colorful.js.

Defining a color

A color object can be created from an array, object, or a host of string color formats. The new keyword is optional.

var c1 = new Color( "rgb(0, 50%, 50%)" ),
    c2 =     Color( "rgb(0, 50%, 50%)" );    // identical

Input formats

All of the formats below are recognized.

Color( "turquoise" )    // CSS color keywords
Color( "transparent" )  // CSS value "transparent"

Color( "#FF90A3" )      // long hex format, upper case
Color( "#ff90a3" )      // long hex format, lower case
Color( "#09A" )         // short hex format, upper case
Color( "#09a" )         // short hex format, lower case

Color( "rgb(0, 153, 170)" )               // rgb() string
Color( "rgba(0, 153, 170, 1)" )           // rgba() string
Color( "rgb(0%, 6.152%, 67%)" )           // rgb() string, percentages
Color( "rgba(0%, 6.152%, 67%, 0.3)" )     // rgba() string, RGB percentages

Color( [4, 144.23, 163] )                 // RGB array
Color( [4, 144.23, 163, 0.5] )            // RGBA array
Color( [".623%", "100.0%", "67.258%"] )   // RGB array, percentages
Color( [".623%", "100%", "67.2%", 0.5] )  // RGBA array, RGB percentages

Color( { r: 4, g: 144.23, b: 163 } )               // RGB hash
Color( { r: 4, g: 144.23, b: 163, a: 0.5 } )       // RGBA hash
Color( { r: ".623%", g: "100.0%", b: "67.2%" } )   // RGB hash, percentages
Color( { r: "0.6%", g: "10%", b: "7%", a: 0.5 } )  // RGBA hash, RGB percentages

// In an array or a hash, numeric and percentage values can be mixed:
Color( ["0.6%", 144.23, "7%"] )
Color( { r: "0.6%", g: 144.23, b: "7%", a: 0.5 } )

The obscure AgColor format used by Adobe Lightroom is also supported.

Invalid color values

Arguments which don't match any of the patterns above are not considered to be a color.

var c = Color( "foo" );
c.isColor()  // => false

Invalid arguments don't cause an error when the color object is created. But they might throw an error later on, e.g. if a method is called which requires a valid color.

var c = Color( "foo" ),
    rgbFormat = c.asRgb();   // throws an error

Output

All of the output methods require that the input represents a valid color. Non-color inputs throw an error when an output method is called.

In addition, an error is thrown if the color is transparent but the output format doesn't include an alpha channel:

var color = Color( "rgba(255, 128, 255, 0.5)" ),  // transparent
    rgba = color.asRgba(),                        // works
    rgb = color.asRgb(),                          // throws an error
    hex = color.asHex();                          // throws an error

asHex( [options] )

Returns the color as a hex string (#RRGGBB), in lower case and with a leading # by default.

Output can be configured with the boolean options lowerCase, or upperCase , and prefix (true: with leading #).

var color = Color( "darkturquoise" );

color.asHex()                                      // => "#00ced1"
color.asHex( { lowerCase: false, prefix: false } ) // => "00CED1"
color.asHex( { upperCase: true } )                 // => "#00CED1"

asHexLC()

Returns the color as a lower-case hex string.

Color( "darkturquoise" ).asHexLC()    // => "#00ced1"

asHexUC()

Returns the color as an upper-case hex string.

Color( "darkturquoise" ).asHexUC()    // => "#00CED1"

asRgb()

Returns the color as an rgb() string. Each channel is represented by an integer on a scale of 0 to 255.

Color( "darkturquoise" ).asRgb()    // => "rgb(0, 206, 209)"

asRgbPercent( [options] )

Returns the color as an rgb() string. Each channel is represented by a percentage. The number of decimal digits can be specified with options.precision.

The precision can be set to a number in the range of 0 to 20, or to the string "max" for maximum precision. By default, percentages are returned as integers (precision: 0).

var color = Color( "darkturquoise" );

color.asRgbPercent()                        // => "rgb(0%, 81%, 82%)"
color.asRgbPercent( { precision: 3 } )      // => "rgb(0%, 80.784%, 81.961%)"
color.asRgbPercent( { precision: "max" } )  // => "rgb(0%, 80.7843137254902%, 81.96078431372548%)"

asRgbArray( [options] )

Returns the color as an RGB array [R, G, B]. Each channel is represented by an integer on a scale of 0 to 255.

For greater accuracy, use the precision option, as in asRgbPercent().

Color( "darkturquoise" ).asRgbArray()    // => [0, 206, 209]

asRgba()

Returns the color as an rgba() string. Each RGB channel is represented by an integer on a scale of 0 to 255, and the alpha channel on a scale of 0 to 1.

Color( "darkturquoise" ).asRgba()    // => "rgba(0, 206, 209, 1)"

asRgbaPercent( [options] )

Returns the color as an rgba() string. Each RGB channel is represented by a percentage, and the alpha channel on a scale of 0 to 1.

Percentages are returned as integers by default. For greater accuracy, use the precision option, as in asRgbPercent().

var color = Color( "darkturquoise" );

color.asRgbPercent()                    // => "rgba(0%, 81%, 82%, 1)"
color.asRgbPercent( { precision: 3 } )  // => "rgba(0%, 80.784%, 81.961%, 1)"

asRgbaArray( [options] )

Returns the color as an RGBA array [R, G, B, A]. Each RGB channel is represented on a scale of 0 to 255, and the alpha channel on a scale of 0 to 1.

RGB channels are returned as integers by default. For greater accuracy, use the precision option, as in asRgbPercent().

Color( "darkturquoise" ).asRgbaArray()    // => [0, 206, 209, 1]

asComputed()

Returns the color as a browser would. When querying the computed value of a color, a browser returns it as an rgb() string if the color is opaque, and as an rgba() string if the color is transparent.

Color( [0, 206, 209, 1.0] ).asComputed()    // => "rgb(0, 206, 209)"
Color( [0, 206, 209, 0.5] ).asComputed()    // => "rgba(0, 206, 209, 0.5)"

Comparisons

equals( otherColor, [options] )

Returns whether or not a color equals another, with an optional tolerance.

The other color can be passed in any of the formats acceptable to Color, or as a Color object.

var color = Color( "#00FF7F" );

color.equals( "rgba(0, 255, 127, 1)" )    // => true
color.equals( "springgreen" )             // => true
color.equals( Color( [0, 255, 127] ) )    // => true

Rounding

Color channels are converted to a 0-255 RGB scale and rounded to integers before comparison.

That loss of precision is deliberate. It is supposed to compensate for insignificant fractional differences which can easily occur during color computations (especially when colors are expressed in percentages).

var c1 = Color( "rgb(0, 128, 254)" );

// When expressed in percentages with a precision of 10 decimals, c1 would be
// 0%, 50.1960784314%, 99.6078431373%
c1.equals( "rgb(0%, 50%, 99.6%)" )    // => true, despite not matching exactly

// Colors expressed in full percentage points (integers) are too coarse to 
// match some colors, though.
c1.equals( "rgb(0%, 50%, 100%)" )     // => false, maps to 0, 128, 255
c1.equals( "rgb(0%, 50%, 99%)" )      // => false, maps to 0, 128, 252

options.tolerance

A tolerance for the RGB channels can be specified as an option.

The tolerance is meant to allow additional leeway for rounding errors. It can also be used for insignificant differences in color which are not visible to the human eye.

The tolerance does not apply to the alpha channel. It must always match exactly.

var c1 = Color( [0, 128, 254] );
     c1.equals( [0, 127, 255], { tolerance: 1 } )        // => true

// Tolerance is not applied to the alpha channel
var c2 = Color( [0, 128, 254, 0.50000000001] );
     c2.equals( [0, 128, 254, 0.5], { tolerance: 1 } )   // => false

Non-colors

The method returns true only if both values represent equal colors. Non-colors are never considered equal, even if they are created from the same input.

var c1 = Color( "foo" ),
    c2 = Color( "foo" );

c1.equals( c2 )    // => false

strictlyEquals( otherColor )

Returns whether or not a color exactly equals another.

No rounding to integer values is applied here.

The other color can be passed in any of the formats acceptable to Color, or as a Color object.

Non-colors are never considered equal, even if they are created from the same value.

var c1 = Color( "rgb(0, 128, 254)" );

c1.strictlyEquals( "#0080FE" )              // => true

// When expressed in percentages with a precision of 10 decimals, c1 would be
// 0%, 50.1960784314%, 99.6078431373%
c1.strictlyEquals( "rgb(0%, 50%, 99.6%)" )  // => false, does not match exactly
        c1.equals( "rgb(0%, 50%, 99.6%)" )  // => true, see equals()

Queries

isColor()

Returns whether or not the color object represents a valid color.

Color( "#AABBCC" ).isColor()    // => true
Color( "foo" ).isColor()        // => false

isOpaque()

Returns whether or not the color object represents a valid color and is opaque.

Color( "#AABBCC" ).isOpaque()                   // => true
Color( "rgba(0%, 60%, 67%, .5)" ).isOpaque()    // => false
Color( "foo" ).isOpaque()                       // => false

isTransparent()

Returns whether or not the color object represents a valid color and is transparent.

Color( "rgba(0%, 60%, 67%, .5)" ).isTransparent()    // => true
Color( "foo" ).isTransparent()                       // => false

Validation

ensureColor()

Throws an error if the color object does not represent a valid color.

Color( "#AABBCC" ).ensureColor()    // ok
Color( "foo" ).ensureColor()        // throws an error

ensureOpaque()

Throws an error if the color object does not represent a valid, opaque color.

Color( "#AABBCC" ).ensureOpaque()                   // ok
Color( "rgba(0%, 60%, 67%, .5)" ).ensureOpaque()    // throws an error
Color( "foo" ).ensureOpaque()                       // throws an error

ensureTransparent()

Throws an error if the color object does not represent a valid, transparent color.

Color( "rgba(0%, 60%, 67%, .5)" ).ensureTransparent()    // ok
Color( "#AABBCC" ).ensureTransparent()                   // throws an error
Color( "foo" ).ensureTransparent()                       // throws an error

AgColor support

The obscure AgColor format, used internally by Adobe Lightroom, is also recognized.

Hardly anyone will need it, but for the sake of completeness, it should be mentioned here.

AgColor is defined in RGBA format, with the channels expressed as fractions on a scale of 0 to 1. AgColor is supported both as an input format

var color = Color( "AgColor(0, 1, 0.672587491, 0.5)" );

and as an output format

color.asAgColor() // => "AgColor( 0, 1, 0.672587491, 0.5 )"

Build process and tests

If you'd like to fix, customize or otherwise improve the project: here are your tools.

Setup

npm sets up the environment for you.

  • The only thing you've got to have on your machine (besides Git) is Node.js. Download the installer here.
  • Clone the project and open a command prompt in the project directory.
  • Run the setup with npm run setup.
  • Make sure the Grunt CLI is installed as a global Node module. If not, or if you are not sure, run npm install -g grunt-cli from the command prompt.

Running tests, creating a new build

Considerations for testing

To run the tests on remote clients (e.g. mobile devices), start a web server with grunt interactive and visit http://[your-host-ip]:9400/web-mocha/ with the client browser. Running the tests in a browser like this is slow, so it might make sense to disable the power-save/sleep/auto-lock timeout on mobile devices. Use grunt test (see below) for faster local testing.

Tool chain and commands

The test tool chain: Grunt (task runner), Karma (test runner), Mocha (test framework), Chai (assertion library), Sinon (mocking framework). The good news: you don't need to worry about any of this.

A handful of commands manage everything for you:

  • Run the tests in a terminal with grunt test.
  • Run the tests in a browser interactively, live-reloading the page when the source or the tests change: grunt interactive.
  • If the live reload bothers you, you can also run the tests in a browser without it: grunt webtest.
  • Run the linter only with grunt lint or grunt hint. (The linter is part of grunt test as well.)
  • Build the dist files (also running tests and linter) with grunt build, or just grunt.
  • Build continuously on every save with grunt ci.
  • Change the version number throughout the project with grunt setver --to=1.2.3. Or just increment the revision with grunt setver --inc. (Remember to rebuild the project with grunt afterwards.)
  • grunt getver will quickly tell you which version you are at.

Finally, if need be, you can set up a quick demo page to play with the code. First, edit the files in the demo directory. Then display demo/index.html, live-reloading your changes to the code or the page, with grunt demo. Libraries needed for the demo/playground should go into the Bower dev dependencies – in the project-wide bower.json – or else be managed by the dedicated bower.json in the demo directory.

The grunt interactive and grunt demo commands spin up a web server, opening up the whole project to access via http. So please be aware of the security implications. You can restrict that access to localhost in Gruntfile.js if you just use browsers on your machine.

Changing the tool chain configuration

In case anything about the test and build process needs to be changed, have a look at the following config files:

  • karma.conf.js (changes to dependencies, additional test frameworks)
  • Gruntfile.js (changes to the whole process)
  • web-mocha/_index.html (changes to dependencies, additional test frameworks)

New test files in the spec directory are picked up automatically, no need to edit the configuration for that.

Release notes

v0.3.0

  • Added asComputed()

v0.2.0

  • Renamed array output methods
  • Allowed decimals for numeric RGB input (in rgb(), rgba() strings, arrays, hashes)
  • Added a precision option to asRgbArray(), asRgbaArray()
  • Fixed accidental rounding of percentages in strictlyEquals()

v0.1.0

  • Initial public release

License

MIT.

Copyright (c) 2016, 2017 Michael Heim.

Code in the data provider test helper: (c) 2014 Box, Inc., Apache 2.0 license. See file.