411 lines
8.5 KiB
TypeScript
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 ] );
|
|
}
|
|
}
|