library-ts/browser/colors/HSLColor.ts

411 lines
8.5 KiB
TypeScript

import { HTMLColor } from './HTMLColor';
import { RGBColor } from './RGBColor';
import { MathX } from '../math/MathX';
import { YBColorSpace } from './YBColorSpace';
export class HSLColor extends HTMLColor
{
private _h = 0;
get h(){ return this._h; }
set h( value:number )
{
this._h = value;
}
private _s = 0;
get s(){ return this._s; }
set s( value:number )
{
this._s = value;
}
private _l = 0;
get l(){ return this._l; }
set l( value:number )
{
this._l = value;
}
// Ranges: From zero to [ 360, 100, 100, 1 ]
constructor( h:number, s:number, l:number, a:number = 1 )
{
super();
this._h = h;
this._s = s;
this._l = l;
this._a = a;
}
toHSL(){ return this;}
clone()
{
return new HSLColor( this.h, this.s, this.l, this.a );
}
copyFrom( other:HTMLColor )
{
if ( this === other )
{
return;
}
let otherInHSL = other.toHSL();
this._h = otherInHSL.h;
this._s = otherInHSL.s;
this._l = otherInHSL.l;
this._a = otherInHSL.a;
}
shift( h:number, s:number, l:number )
{
this._h = ( this._h + h ) % 360;
this._s = MathX.clamp( this._s + s, 0, 100 );
this._l = MathX.clamp( this._l + l, 0, 100 );
return this;
}
shiftClamped( h:number, s:number, l:number )
{
this._h = YBColorSpace.changeHue( this._h, h );
this._s = MathX.clamp( this._s + s, 0, 100 );
this._l = MathX.clamp( this._l + l, 0, 100 );
}
shiftClampedHueBlendedFast( h:number, s:number, l:number, blendRadius:number = 20, blendStart:number = 1 )
{
let distanceToYellow = Math.min( 1, Math.abs( this._h - 60 ) / blendRadius );
let distanceToBlue = Math.min( 1, Math.abs( this._h - 240 ) / blendRadius );
this._h = YBColorSpace.changeHue( this._h, h * distanceToYellow * distanceToBlue );
this._s = MathX.clamp( this._s + s, 0, 100 );
this._l = MathX.clamp( this._l + l, 0, 100 );
}
shiftClampedHueBlended( h:number, s:number, l:number, blendRadius:number = 20, blendStart:number = 1 )
{
let distanceToYellow = Math.abs( MathX.angleDifference( this._h, 60 ) );
let distanceToBlue = Math.abs( MathX.angleDifference( this._h, 240 ) );
let newHue = YBColorSpace.changeHue( this._h, h );
let blendPower = 1.5;
let cached = { hue:this._h, change:h, newHue:newHue, distanceToYellow, distanceToBlue, clampedHue:0, lerpAmount:0 };
if ( distanceToYellow < blendRadius || distanceToBlue < blendRadius )
{
if ( distanceToYellow < distanceToBlue )
{
let lerpAmount = 1;
if ( distanceToYellow > blendStart )
{
lerpAmount = MathX.map( distanceToYellow, blendStart, blendRadius, 1, 0 );
}
newHue = MathX.lerpAngle( newHue, 60, Math.pow( lerpAmount, blendPower ) );
cached.lerpAmount = lerpAmount;
cached.clampedHue = newHue;
}
else
{
let lerpAmount = 1;
if ( distanceToBlue > blendStart )
{
lerpAmount = MathX.map( distanceToBlue, blendStart, blendRadius, 1, 0 );
}
newHue = MathX.lerpAngle( newHue, 240, Math.pow( lerpAmount, blendPower ) );
}
}
// console.log( cached )
this._h = newHue;
this._s = MathX.clamp( this._s + s, 0, 100 );
this._l = MathX.clamp( this._l + l, 0, 100 );
}
shiftClampedHueSmoothed( h:number, s:number, l:number, kernelWidth:number = 30, kernel:number[]=[0.1,1,10,10,10,1,0.1] )
{
let center = { x:0, y:0 };
let toRad = Math.PI * 2 / 360;
let toDeg = 1 / toRad;
for ( let i = 0; i < kernel.length; i++ )
{
let state = i / ( kernel.length - 1 ) - 0.5;
let hueOffset = state * kernelWidth;
let kernelHue = ( this._h + hueOffset ) % 360;
let hue = YBColorSpace.changeHue( kernelHue, h );
let x = Math.cos( hue * toRad ) * kernel[ i ];
let y = Math.sin( hue * toRad ) * kernel[ i ];
center.x += x;
center.y += y;
}
this._h = Math.atan2( center.y, center.x ) * toDeg;
this._s = MathX.clamp( this._s + s, 0, 100 );
this._l = MathX.clamp( this._l + l, 0, 100 );
}
warmen( amount:number )
{
amount = MathX.clamp( amount, 0, 180 );
let h = this._h;
if ( this.isOnGreenSide() )
{
this._h = Math.max( 60, h - amount );
}
else
{
if ( h >= 240 )
{
h -= 360;
}
this._h = Math.min( 60, h + amount );
}
return this;
}
coolen( amount:number )
{
amount = MathX.clamp( amount, 0, 180 );
let h = this._h;
if ( this.isOnGreenSide() )
{
this._h = Math.min( 240, h + amount );
}
else
{
if ( h < 60 )
{
h += 360;
}
this._h = Math.max( 240, h - amount );
}
return this;
}
isOnGreenSide()
{
return this._h >= 60 && this._h < 240;
}
isOnRedSide()
{
return ! this.isOnGreenSide;
}
shiftWarmerHue( amount:number )
{
if ( this._h > 120 && this._h < 300 )
{
amount = -amount;
}
this._h = MathX.repeat( this._h + amount, 360 );
return this;
}
toString():string
{
return `hsla(${this._h},${this._s}%,${this._l}%,${this._a})`;
}
clamp():HSLColor
{
this._h = MathX.repeat( this._h, 360 );
this._s = MathX.clamp( this._s, 0, 100 );
this._l = MathX.clamp( this._l, 0, 100 );
return this;
}
equals( other:HTMLColor )
{
if ( ! other )
{
return false;
}
let otherHSL = other.toHSL();
if ( otherHSL.h !== this.h )
{
return false;
}
if ( otherHSL.s !== this.s )
{
return false;
}
if ( otherHSL.l !== this.l )
{
return false;
}
if ( otherHSL.a !== this.a )
{
return false;
}
return true;
}
public toRGB( ):RGBColor
{
let s = this._s / 100;
let l = this._l / 100;
let m2 = ( l <= 0.5 ) ?
( l * ( 1 + s ) ) :
( l + s - l * s );
let m1 = 2 * l - m2;
if ( s == 0 )
{
return new RGBColor( l, l, l, this.a );
}
let r = HSLColor.computeChannel( m1, m2, this.h + 120 );
let g = HSLColor.computeChannel( m1, m2, this.h );
let b = HSLColor.computeChannel( m1, m2, this.h - 120 );
return new RGBColor( r, g, b, this.a );
}
private static computeChannel( n1:number, n2:number, hue:number ):number
{
hue = MathX.repeat( hue, 360 );
if ( hue < 60 )
{
return n1 + ( n2 - n1 ) * hue / 60;
}
else if ( hue < 180 )
{
return n2;
}
else if ( hue < 240 )
{
return n1 + ( n2 - n1 ) * ( 240 - hue ) / 60;
}
else
{
return n1;
}
}
public static fromRGBColor( c:RGBColor ):HSLColor
{
let cmin = Math.min( Math.min( c.r, c.g ), c.b );
let cmax = Math.max( Math.max( c.r, c.g ), c.b );
let l = ( cmin + cmax ) / 2;
if ( cmin == cmax )
{
return new HSLColor( 0, 0, l * 100, c.a );
}
let delta = cmax - cmin;
let s = ( l <= .5 ) ?
( delta / ( cmax + cmin ) ) :
( delta / ( 2 - ( cmax + cmin ) ) );
let h = 0;
if ( c.r == cmax )
{
h = ( c.g - c.b ) / delta;
}
else if ( c.g == cmax )
{
h = 2 + ( c.b - c.r ) / delta;
}
else if ( c.b == cmax )
{
h = 4 + ( c.r - c.g ) / delta;
}
h = MathX.repeat( h * 60, 360 );
return new HSLColor( h, s * 100, l * 100, c.a );
}
static fromString( colorString:string )
{
colorString = colorString.trim();
let hslStart = /hsla?\(/;
if ( ! hslStart.test( colorString ) )
{
console.error( "Does not start with", hslStart, " ==> '" + colorString + "'" );
return null;
}
colorString = colorString.replace( hslStart, "" );
colorString = colorString.replace( /\)$/, "" );
colorString = colorString.replace( /%/g, "" );
let numbers = JSON.parse( "[" + colorString + "]" ) as number[];
if ( ! Array.isArray( numbers ) )
{
console.error( "Is not a list of numbers", " ==> '" + colorString + "'" );
return null;
}
let notNumeric = numbers.findIndex( n => typeof n !== "number" );
if ( notNumeric !== -1 )
{
console.error( "Found a not-numeric at index:", notNumeric, " ==> '" + colorString + "'" );
return null;
}
if ( ! ( numbers.length === 3 || numbers.length === 4 ) )
{
return null;
}
if ( numbers.length === 3 )
{
numbers.push( 1 );
}
return new HSLColor( numbers[ 0 ], numbers[ 1 ], numbers[ 2 ] , numbers[ 3 ] );
}
}