diff options
-rw-r--r-- | spec/SpecRunner.html | 8 | ||||
-rw-r--r-- | spec/spec/color.js | 100 | ||||
-rw-r--r-- | src/types/Color.js | 521 |
3 files changed, 489 insertions, 140 deletions
diff --git a/spec/SpecRunner.html b/spec/SpecRunner.html index 9d84677..967f327 100644 --- a/spec/SpecRunner.html +++ b/spec/SpecRunner.html @@ -54,15 +54,15 @@ <!-- include spec files here... --> - <script src="spec/adopter.js"></script> + <!-- <script src="spec/adopter.js"></script> <script src="spec/arrange.js"></script> <script src="spec/array.js"></script> <script src="spec/bare.js"></script> <script src="spec/boxes.js"></script> <script src="spec/circle.js"></script> - <script src="spec/clip.js"></script> + <script src="spec/clip.js"></script> --> <script src="spec/color.js"></script> - <script src="spec/container.js"></script> + <!-- <script src="spec/container.js"></script> <script src="spec/defs.js"></script> <script src="spec/doc.js"></script> <script src="spec/easing.js"></script> @@ -100,6 +100,6 @@ <script src="spec/morphing.js"></script> <script src="spec/animator.js"></script> <script src="spec/runner.js"></script> - <script src="spec/queue.js"></script> + <script src="spec/queue.js"></script> --> </body> </html> diff --git a/spec/spec/color.js b/spec/spec/color.js index 410577f..9710d69 100644 --- a/spec/spec/color.js +++ b/spec/spec/color.js @@ -1,3 +1,4 @@ + describe('Color', function() { var color @@ -5,42 +6,87 @@ describe('Color', function() { color = new SVG.Color({ r: 0, g: 102, b: 255 }) }) - it('correclty parses a rgb string', function() { - color = new SVG.Color('rgb(255,0,128)') - expect(color.r).toBe(255) - expect(color.g).toBe(0) - expect(color.b).toBe(128) - }) + describe ('construct: constructs a color in different formats', () => { - it('correclty parses a 3 digit hex string', function() { - color = new SVG.Color('#f06') - expect(color.r).toBe(255) - expect(color.g).toBe(0) - expect(color.b).toBe(102) - }) + it ('constructs a color from an object in the correct color space') - it('correclty parses a 6 digit hex string', function() { - color = new SVG.Color('#0066ff') - expect(color.r).toBe(0) - expect(color.g).toBe(102) - expect(color.b).toBe(255) - }) + it ('constructs a color from an array', () => { + let color = new SVG.Color([ 30, 24, 50 ]) + expect( color.r ).toBe( 30 ) + expect( color.g ).toBe( 24 ) + expect( color.b ).toBe( 50 ) - describe('toHex()', function() { - it('returns a hex color', function() { - expect(color.toHex()).toBe('#0066ff') }) + + it('correclty parses an rgb string', () => { + let color = new SVG.Color('rgb(255,0,128)') + expect(color.r).toBe(255) + expect(color.g).toBe(0) + expect(color.b).toBe(128) + }) + + it('correclty parses a 3 digit hex string', () => { + color = new SVG.Color('#f06') + expect(color.r).toBe(255) + expect(color.g).toBe(0) + expect(color.b).toBe(102) + }) + + it('correclty parses a 6 digit hex string', () => { + color = new SVG.Color('#0066ff') + expect(color.r).toBe(0) + expect(color.g).toBe(102) + expect(color.b).toBe(255) + }) + }) - describe('toRgb()', function() { - it('returns a rgb string color', function() { - expect(color.toRgb()).toBe('rgb(0,102,255)') + describe ('input and output: Importing and exporting colors', () => { + describe('hex()', function() { + it('returns a hex color', function() { + expect(color.hex()).toBe('#0066ff') + }) + }) + + describe('toRgb()', function() { + it('returns a rgb string color', function() { + expect(color.toRgb()).toBe('rgb(0,102,255)') + }) + }) + + describe('brightness()', function() { + it('returns the percieved brightness value of a color', function() { + expect(color.brightness()).toBe(0.346) + }) }) }) - describe('brightness()', function() { - it('returns the percieved brightness value of a color', function() { - expect(color.brightness()).toBe(0.346) + describe('color spaces: The color spaces supported by our library', () => { + + describe('lab()', () => { + it ('can convert rgb to lab') + it ('can convert from lab to rgb') + }) + + describe('lch()', () => { + it ('can convert rgb to lch') + it ('can convert from lch to rgb') + }) + + describe('hsl()', () => { + it ('can convert from rgb to hsl') + it ('can convert from hsl to rgb') + }) + + describe('xyz()', () => { + it ('can convert from rgb to xyz') + it ('can convert from xyz to rgb') + }) + + describe('cymk()', () => { + it ('can convert from rgb to cymk') + it ('can convert from cymk to rgb') }) }) + }) diff --git a/src/types/Color.js b/src/types/Color.js index a96958b..e5104b8 100644 --- a/src/types/Color.js +++ b/src/types/Color.js @@ -1,148 +1,451 @@ -/* - -Color { - constructor (a, b, c, space) { - space: 'hsl' - a: 30 - b: 20 - c: 10 - }, - - toRgb () { return new Color in rgb space } - toHsl () { return new Color in hsl space } - toLab () { return new Color in lab space } - - toArray () { [space, a, b, c] } - fromArray () { convert it back } -} - -// Conversions aren't always exact because of monitor profiles etc... -new Color(h, s, l, 'hsl') !== new Color(r, g, b).hsl() -new Color(100, 100, 100, [space]) -new Color('hsl(30, 20, 10)') - -// Sugar -SVG.rgb(30, 20, 50).lab() -SVG.hsl() -SVG.lab('rgb(100, 100, 100)') -*/ import { hex, isHex, isRgb, rgb, whitespace } from '../modules/core/regex.js' -// Ensure to six-based hex -function fullHex (hex) { +function sixDigitHex ( hex ) { return hex.length === 4 ? [ '#', - hex.substring(1, 2), hex.substring(1, 2), - hex.substring(2, 3), hex.substring(2, 3), - hex.substring(3, 4), hex.substring(3, 4) - ].join('') + hex.substring( 1, 2 ), hex.substring( 1, 2 ), + hex.substring( 2, 3 ), hex.substring( 2, 3 ), + hex.substring( 3, 4 ), hex.substring( 3, 4 ) + ].join( '' ) : hex } -// Component to hex value -function compToHex (comp) { - var hex = comp.toString(16) +function componentHex ( component ) { + const integer = Math.round( component ) + const hex = integer.toString( 16 ) return hex.length === 1 ? '0' + hex : hex } +function is ( object, space ) { + for ( const key of space ) { + if ( object[key] == null ) { + return false + } + } + return true +} + +function getParameters ( a ) { + const params = is( a, 'rgb' ) ? { _a: a.r, _b: a.g, _c: a.b, space: 'rgb' } + : is( a, 'xyz' ) ? { _a: a.x, _b: a.y, _c: a.z, space: 'xyz' } + : is( a, 'hsl' ) ? { _a: a.h, _b: a.s, _c: a.l, space: 'hsl' } + : is( a, 'lab' ) ? { _a: a.l, _b: a.a, _c: a.b, space: 'lab' } + : is( a, 'lch' ) ? { _a: a.l, _b: a.c, _c: a.h, space: 'lch' } + : is( a, 'cmyk' ) ? { _a: a.c, _b: a.m, _c: a.y, _d: a.k, space: 'cmyk' } + : { _a: 0, _b: 0, _c: 0, space: 'rgb' } + return params +} + +function cieSpace ( space ) { + if ( space === 'lab' || space === 'xyz' || space === 'lch' ) { + return true + } else { + return false + } +} + +function hueToRgb ( p, q, t ) { + if ( t < 0 ) t += 1 + if ( t > 1 ) t -= 1 + if ( t < 1 / 6 ) return p + ( q - p ) * 6 * t + if ( t < 1 / 2 ) return q + if ( t < 2 / 3 ) return p + ( q - p ) * ( 2 / 3 - t ) * 6 + return p +} + export default class Color { - constructor (...args) { - this.init(...args) - } - - init (color, g, b) { - let match - - // initialize defaults - this.r = 0 - this.g = 0 - this.b = 0 - - if (!color) return - - // parse color - if (typeof color === 'string') { - if (isRgb.test(color)) { - // get rgb values - match = rgb.exec(color.replace(whitespace, '')) - - // parse numeric values - this.r = parseInt(match[1]) - this.g = parseInt(match[2]) - this.b = parseInt(match[3]) - } else if (isHex.test(color)) { - // get hex values - match = hex.exec(fullHex(color)) - - // parse numeric values - this.r = parseInt(match[1], 16) - this.g = parseInt(match[2], 16) - this.b = parseInt(match[3], 16) + + constructor ( a = 0, b = 0, c = 0, d = 0, space = 'rgb' ) { + + // If the user gave us an array, make the color from it + if ( typeof a === 'number' ) { + + // Allow for the case that we don't need d... + space = typeof d === 'string' ? d : space + d = typeof d === 'string' ? undefined : d + + // Assign the values straight to the color + Object.assign( this, { _a: a, _b: b, _c: c, _d: d, space } ) + + } else if ( a instanceof Array ) { + + this.space = b || 'rgb' + Object.assign( this, { _a: a[0], _b: a[1], _c: a[2], _d: a[3] } ) + + } else if ( a instanceof Object ) { + + // Set the object up and assign its values directly + const values = getParameters( a ) + Object.assign( this, values ) + + } else if ( typeof a === 'string' ) { + + if ( isRgb.test( a ) ) { + + const noWhitespace = a.replace( whitespace, '' ) + const [ _a, _b, _c ] = rgb.exec( noWhitespace ) + .slice( 1, 4 ).map( v => parseInt( v ) ) + Object.assign( this, { _a, _b, _c, space: 'rgb' } ) + + } else if ( isHex.test( a ) ) { + + const hexParse = v => parseInt( v, 16 ) + const [ , _a, _b, _c ] = hex.exec( sixDigitHex( a ) ).map( hexParse ) + Object.assign( this, { _a, _b, _c, space: 'rgb' } ) + + } else throw Error( `Unsupported string format, can't construct Color` ) + } + + // Now add the components as a convenience + const { _a, _b, _c, _d } = this + const components = this.space === 'rgb' ? { r: _a, g: _b, b: _c } + : this.space === 'xyz' ? { x: _a, y: _b, z: _c } + : this.space === 'hsl' ? { h: _a, s: _b, l: _c } + : this.space === 'lab' ? { l: _a, a: _b, b: _c } + : this.space === 'lch' ? { l: _a, c: _b, h: _c } + : this.space === 'cmyk' ? { c: _a, y: _b, m: _c, k: _d } + : {} + Object.assign( this, components ) + } + + opacity ( opacity = 1 ) { + + this.opacity = opacity + + } + + /* + + */ + + brightness () { + const { _a: r, _b: g, _c: b } = this.rgb() + const value = ( r / 255 * 0.30 ) + ( g / 255 * 0.59 ) + ( b / 255 * 0.11 ) + return value + } + + /* + Conversion Methods + */ + + rgb () { + + if ( this.space === 'rgb' ) { + return this + + } else if ( cieSpace( this.space ) ) { + + // Convert to the xyz color space + let { x, y, z } = this + if ( this.space === 'lab' || this.space === 'lch' ) { + + // Get the values in the lab space + let { l, a, b } = this + if ( this.space === 'lch' ) { + let { c, h } = this + const dToR = Math.PI / 180 + a = c * Math.cos( dToR * h ) + b = c * Math.sin( dToR * h ) + } + + // Undo the nonlinear function + const yL = ( l + 16 ) / 116 + const xL = a / 500 + y + const zL = y - b / 200 + + // Get the xyz values + const ct = 16 / 116 + const mx = 0.008856 + const nm = 7.787 + x = 0.95047 * ( ( xL ** 3 > mx ) ? xL ** 3 : ( xL - ct ) / nm ) + y = 1.00000 * ( ( yL ** 3 > mx ) ? yL ** 3 : ( yL - ct ) / nm ) + z = 1.08883 * ( ( zL ** 3 > mx ) ? zL ** 3 : ( zL - ct ) / nm ) } - } else if (Array.isArray(color)) { - this.r = color[0] - this.g = color[1] - this.b = color[2] - } else if (typeof color === 'object') { - this.r = color.r - this.g = color.g - this.b = color.b - } else if (arguments.length === 3) { - this.r = color - this.g = g - this.b = b + + // Convert xyz to unbounded rgb values + const rU = x * 3.2406 + y * -1.5372 + z * -0.4986 + const gU = x * -0.9689 + y * 1.8758 + z * 0.0415 + const bU = x * 0.0557 + y * -0.2040 + z * 1.0570 + + // Convert the values to true rgb values + let pow = Math.pow + let bd = 0.0031308 + const r = ( rU > bd ) ? ( 1.055 * pow( rU, 1 / 2.4 ) - 0.055 ) : 12.92 * rU + const g = ( gU > bd ) ? ( 1.055 * pow( gU, 1 / 2.4 ) - 0.055 ) : 12.92 * gU + const b = ( bU > bd ) ? ( 1.055 * pow( bU, 1 / 2.4 ) - 0.055 ) : 12.92 * bU + + // Make and return the color + const color = new Color( r, g, b ) + return color + + } else if ( this.space === 'hsl' ) { + + // stackoverflow.com/questions/2353211/hsl-to-rgb-color-conversion + // Get the current hsl values + const { h, s, l } = this + + // If we are grey, then just make the color directly + if ( s === 0 ) { + let color = new Color( l, l, l ) + return color + } + + // TODO I have no idea what this does :D If you figure it out, tell me! + const q = l < 0.5 ? l * ( 1 + s ) : l + s - l * s + const p = 2 * l - q + + // Get the rgb values + const r = hueToRgb( p, q, h + 1 / 3 ) + const g = hueToRgb( p, q, h ) + const b = hueToRgb( p, q, h - 1 / 3 ) + + // Make a new color + const color = new Color( r, g, b ) + return color + + } else if ( this.space === 'cmyk' ) { + + // https://gist.github.com/felipesabino/5066336 + // Get the normalised cmyk values + const { _a, _b, _c, _d } = this + const [ c, m, y, k ] = [ _a, _b, _c, _d ].map( v => v / 100 ) + + // Get the rgb values + const r = 1 - Math.min( 1, c * ( 1 - k ) + k ) + const g = 1 - Math.min( 1, m * ( 1 - k ) + k ) + const b = 1 - Math.min( 1, y * ( 1 - k ) + k ) + + // Form the color and return it + const color = new Color( r, g, b ) + return color + + } else { + return this + } + } + + lab () { + + // Get the xyz color + const { x, y, z } = this.xyz() + + // Get the lab components + const l = ( 116 * y ) - 16 + const a = 500 * ( x - y ) + const b = 200 * ( y - z ) + + // Construct and return a new color + const color = new Color( l, a, b, 'lab' ) + return color + } + + xyz () { + + // Normalise the red, green and blue values + const { _a: r255, _b: g255, _c: b255 } = this.rgb() + const [ r, g, b ] = [ r255, g255, b255 ].map( v => v / 255 ) + + // Convert to the lab rgb space + const rL = ( r > 0.04045 ) ? Math.pow( ( r + 0.055 ) / 1.055, 2.4 ) : r / 12.92 + const gL = ( g > 0.04045 ) ? Math.pow( ( g + 0.055 ) / 1.055, 2.4 ) : g / 12.92 + const bL = ( b > 0.04045 ) ? Math.pow( ( b + 0.055 ) / 1.055, 2.4 ) : b / 12.92 + + // Convert to the xyz color space without bounding the values + const xU = ( rL * 0.4124 + gL * 0.3576 + bL * 0.1805 ) / 0.95047 + const yU = ( rL * 0.2126 + gL * 0.7152 + bL * 0.0722 ) / 1.00000 + const zU = ( rL * 0.0193 + gL * 0.1192 + bL * 0.9505 ) / 1.08883 + + // Get the proper xyz values by applying the bounding + const x = ( xU > 0.008856 ) ? Math.pow( xU, 1 / 3 ) : ( 7.787 * xU ) + 16 / 116 + const y = ( yU > 0.008856 ) ? Math.pow( yU, 1 / 3 ) : ( 7.787 * yU ) + 16 / 116 + const z = ( zU > 0.008856 ) ? Math.pow( zU, 1 / 3 ) : ( 7.787 * zU ) + 16 / 116 + + // Make and return the color + const color = new Color( x, y, z, 'xyz' ) + return color + } + + lch () { + + // Get the lab color directly + const { l, a, b } = this.lab() + + // Get the chromaticity and the hue using polar coordinates + const c = Math.sqrt( a ** 2 + b ** 2 ) + let h = 180 * Math.atan2( b, a ) / Math.PI + if ( h < 0 ) { + h *= -1 + h = 360 - h } - return this + // Make a new color and return it + const color = new Color( l, c, h, 'lch' ) + return color } - // Default to hex conversion - toString () { - return this.toHex() + hsl () { + + // Get the rgb values + const { _a, _b, _c } = this.rgb() + const [ r, g, b ] = [ _a, _b, _c ].map( v => v / 255 ) + + // Find the maximum and minimum values to get the lightness + const max = Math.max( r, g, b ) + const min = Math.min( r, g, b ) + const l = ( max + min ) / 2 + + // If the r, g, v values are identical then we are grey + const isGrey = max === min + + // Calculate the hue and saturation + const delta = max - min + const s = isGrey ? 0 + : l > 0.5 ? delta / ( 2 - max - min ) + : delta / ( max + min ) + const h = isGrey ? 0 + : max === r ? ( ( g - b ) / delta + ( g < b ? 6 : 0 ) ) / 6 + : max === g ? ( ( b - r ) / delta + 2 ) / 6 + : max === b ? ( ( r - g ) / delta + 4 ) / 6 + : 0 + + // Construct and return the new color + const color = new Color( h, s, l, 'hsl' ) + return color } - toArray () { - return [this.r, this.g, this.b] + cmyk () { + + // Get the rgb values for the current color + const { _a, _b, _c } = this.rgb() + const [ r, g, b ] = [ _a, _b, _c ].map( v => v / 255 ) + + // Get the cmyk values in an unbounded format + const k = 100 * Math.min( 1 - r, 1 - g, 1 - b ) + const c = 100 * ( 1 - r - k ) / ( 1 - k ) + const m = 100 * ( 1 - g - k ) / ( 1 - k ) + const y = 100 * ( 1 - b - k ) / ( 1 - k ) + + // Construct the new color + const color = new Color( c, m, y, k, 'cmyk' ) + return color + } + + /* + Modifying the color + */ + + brighten ( amount = 0.1 ) { + + } + + darken ( amount = 0.1 ) { + + } + + /* + Mixing methods + */ + + to ( otherColor, space ) { + + // Force both colors to the color of this space (or let the user decide) + space = space || this.space + + // Get the starting and ending colors + // let start = this[ space ]() + // let end = otherColor[ space ]() + + // Return a function that blends between the two colors + return function ( t ) { + + } + } - // Build hex value - toHex () { - return '#' + - compToHex(Math.round(this.r)) + - compToHex(Math.round(this.g)) + - compToHex(Math.round(this.b)) + avearge ( otherColor, space ) { + + } + + /* + Input and Output methods + */ + + hex () { + let { _a, _b, _c } = this.rgb() + let [ r, g, b ] = [ _a, _b, _c ].map( componentHex ) + return `#${r}${g}${b}` + } + + toString () { + return this.hex() } - // Build rgb value toRgb () { - return 'rgb(' + [this.r, this.g, this.b].join() + ')' + let { r, g, b } = this.rgb() + let { max, min, round } = Math + let format = v => max( 0, min( round( v ), 255 ) ) + let [ rV, gV, bV ] = [ r, g, b ].map( format ) + let string = `rgb(${rV},${gV},${bV})` + return string } - // Calculate true brightness - brightness () { - return (this.r / 255 * 0.30) + - (this.g / 255 * 0.59) + - (this.b / 255 * 0.11) + toArray () { + let { _a, _b, _c, _d, space } = this + return [ _a, _b, _c, _d, space ] } - // Testers + static fromArray ( array ) { + + let newColor = new Color( ...array ) + return newColor + + } + + /* + Generating random colors + */ + + static random ( mode = 'vibrant' ) { + + 'sine' + 'pastel' + 'vibrant' + 'dark' + 'rgb' + 'lab' + 'grey' + + } + + /* + Constructing colors + */ + + static temperature ( kelvin ) {} // Test if given value is a color string - static test (color) { + static test ( color ) { + color += '' - return isHex.test(color) || isRgb.test(color) + return isHex.test( color ) || isRgb.test( color ) + } // Test if given value is a rgb object - static isRgb (color) { - return color && typeof color.r === 'number' && - typeof color.g === 'number' && - typeof color.b === 'number' + static isRgb ( color ) { + + return color && typeof color.r === 'number' + && typeof color.g === 'number' + && typeof color.b === 'number' + } // Test if given value is a color - static isColor (color) { - return this.isRgb(color) || this.test(color) + static isColor ( color ) { + + return this.isRgb( color ) || this.test( color ) + } + } |