aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--spec/SpecRunner.html8
-rw-r--r--spec/spec/color.js100
-rw-r--r--src/types/Color.js521
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 )
+
}
+
}