First commit.

Signed-off-by: Chen Xiao <abigwc@gmail.com>
This commit is contained in:
Chen Xiao
2026-05-08 14:43:16 +08:00
commit 0b64e2de94
10989 changed files with 2253791 additions and 0 deletions
+169
View File
@@ -0,0 +1,169 @@
/* IMPORT */
import _ from '~/utils';
import Type from '~/channels/type';
import {TYPE} from '~/constants';
import type {RGBA, HSLA, CHANNELS} from '~/types';
/* MAIN */
class Channels {
/* VARIABLES */
color?: string;
changed: boolean;
data: CHANNELS; //TSC: It should really be "Partial<CHANNELS>", but TS gets excessively noisy
type: Type;
/* CONSTRUCTOR */
constructor ( data: RGBA | HSLA | CHANNELS, color?: string ) {
this.color = color;
this.changed = false;
this.data = data as CHANNELS; //TSC
this.type = new Type ();
}
/* API */
set ( data: RGBA | HSLA | CHANNELS, color?: string ): this {
this.color = color;
this.changed = false;
this.data = data as CHANNELS; //TSC
this.type.type = TYPE.ALL;
return this;
}
/* HELPERS */
_ensureHSL (): void {
const data = this.data;
const {h, s, l} = data;
if ( h === undefined ) data.h = _.channel.rgb2hsl ( data, 'h' );
if ( s === undefined ) data.s = _.channel.rgb2hsl ( data, 's' );
if ( l === undefined ) data.l = _.channel.rgb2hsl ( data, 'l' );
}
_ensureRGB (): void {
const data = this.data;
const {r, g, b} = data;
if ( r === undefined ) data.r = _.channel.hsl2rgb ( data, 'r' );
if ( g === undefined ) data.g = _.channel.hsl2rgb ( data, 'g' );
if ( b === undefined ) data.b = _.channel.hsl2rgb ( data, 'b' );
}
/* GETTERS */
get r (): number {
const data = this.data;
const r = data.r;
if ( !this.type.is ( TYPE.HSL ) && r !== undefined ) return r;
this._ensureHSL ();
return _.channel.hsl2rgb ( data, 'r' );
}
get g (): number {
const data = this.data;
const g = data.g;
if ( !this.type.is ( TYPE.HSL ) && g !== undefined ) return g;
this._ensureHSL ();
return _.channel.hsl2rgb ( data, 'g' );
}
get b (): number {
const data = this.data;
const b = data.b;
if ( !this.type.is ( TYPE.HSL ) && b !== undefined ) return b;
this._ensureHSL ();
return _.channel.hsl2rgb ( data, 'b' );
}
get h (): number {
const data = this.data;
const h = data.h;
if ( !this.type.is ( TYPE.RGB ) && h !== undefined ) return h;
this._ensureRGB ();
return _.channel.rgb2hsl ( data, 'h' );
}
get s (): number {
const data = this.data;
const s = data.s;
if ( !this.type.is ( TYPE.RGB ) && s !== undefined ) return s;
this._ensureRGB ();
return _.channel.rgb2hsl ( data, 's' );
}
get l (): number {
const data = this.data;
const l = data.l;
if ( !this.type.is ( TYPE.RGB ) && l !== undefined ) return l;
this._ensureRGB ();
return _.channel.rgb2hsl ( data, 'l' );
}
get a (): number {
return this.data.a;
}
/* SETTERS */
set r ( r: number ) {
this.type.set ( TYPE.RGB );
this.changed = true;
this.data.r = r;
}
set g ( g: number ) {
this.type.set ( TYPE.RGB );
this.changed = true;
this.data.g = g;
}
set b ( b: number ) {
this.type.set ( TYPE.RGB );
this.changed = true;
this.data.b = b;
}
set h ( h: number ) {
this.type.set ( TYPE.HSL );
this.changed = true;
this.data.h = h;
}
set s ( s: number ) {
this.type.set ( TYPE.HSL );
this.changed = true;
this.data.s = s;
}
set l ( l: number ) {
this.type.set ( TYPE.HSL );
this.changed = true;
this.data.l = l;
}
set a ( a: number ) {
this.changed = true;
this.data.a = a;
}
}
/* EXPORT */
export default Channels;
+12
View File
@@ -0,0 +1,12 @@
/* IMPORT */
import Channels from '~/channels';
/* MAIN */
const channels = new Channels ( { r: 0, g: 0, b: 0, a: 0 }, 'transparent' );
/* EXPORT */
export default channels;
+46
View File
@@ -0,0 +1,46 @@
/* IMPORT */
import {TYPE} from '~/constants';
/* MAIN */
class Type {
/* VARIABLES */
type: number = TYPE.ALL;
/* API */
get (): number {
return this.type;
}
set ( type: number ): void {
if ( this.type && this.type !== type ) throw new Error ( 'Cannot change both RGB and HSL channels at the same time' );
this.type = type;
}
reset (): void {
this.type = TYPE.ALL;
}
is ( type: number ): boolean {
return this.type === type;
}
}
/* EXPORT */
export default Type;
+66
View File
@@ -0,0 +1,66 @@
/* IMPORT */
import _ from '~/utils';
import ChannelsReusable from '~/channels/reusable';
import {DEC2HEX} from '~/constants';
import type {Channels} from '~/types';
/* MAIN */
const Hex = {
/* VARIABLES */
re: /^#((?:[a-f0-9]{2}){2,4}|[a-f0-9]{3})$/i,
/* API */
parse: ( color: string ): Channels | void => {
if ( color.charCodeAt ( 0 ) !== 35 ) return; // '#'
const match = color.match ( Hex.re );
if ( !match ) return;
const hex = match[1];
const dec = parseInt ( hex, 16 );
const length = hex.length;
const hasAlpha = length % 4 === 0;
const isFullLength = length > 4;
const multiplier = isFullLength ? 1 : 17;
const bits = isFullLength ? 8 : 4;
const bitsOffset = hasAlpha ? 0 : -1;
const mask = isFullLength ? 255 : 15;
return ChannelsReusable.set ({
r: ( ( dec >> ( bits * ( bitsOffset + 3 ) ) ) & mask ) * multiplier,
g: ( ( dec >> ( bits * ( bitsOffset + 2 ) ) ) & mask ) * multiplier,
b: ( ( dec >> ( bits * ( bitsOffset + 1 ) ) ) & mask ) * multiplier,
a: hasAlpha ? ( dec & mask ) * multiplier / 255 : 1
}, color );
},
stringify: ( channels: Channels ): string => {
const {r, g, b, a} = channels;
if ( a < 1 ) { // #RRGGBBAA
return `#${DEC2HEX[Math.round ( r )]}${DEC2HEX[Math.round ( g )]}${DEC2HEX[Math.round ( b )]}${DEC2HEX[Math.round ( a * 255 )]}`;
} else { // #RRGGBB
return `#${DEC2HEX[Math.round ( r )]}${DEC2HEX[Math.round ( g )]}${DEC2HEX[Math.round ( b )]}`;
}
}
};
/* EXPORT */
export default Hex;
+82
View File
@@ -0,0 +1,82 @@
/* IMPORT */
import _ from '~/utils';
import ChannelsReusable from '~/channels/reusable';
import type {Channels} from '~/types';
/* MAIN */
const HSL = {
/* VARIABLES */
re: /^hsla?\(\s*?(-?(?:\d+(?:\.\d+)?|(?:\.\d+))(?:e-?\d+)?(?:deg|grad|rad|turn)?)\s*?(?:,|\s)\s*?(-?(?:\d+(?:\.\d+)?|(?:\.\d+))(?:e-?\d+)?%)\s*?(?:,|\s)\s*?(-?(?:\d+(?:\.\d+)?|(?:\.\d+))(?:e-?\d+)?%)(?:\s*?(?:,|\/)\s*?\+?(-?(?:\d+(?:\.\d+)?|(?:\.\d+))(?:e-?\d+)?(%)?))?\s*?\)$/i,
hueRe: /^(.+?)(deg|grad|rad|turn)$/i,
/* HELPERS */
_hue2deg: ( hue: string ): number => {
const match = hue.match ( HSL.hueRe );
if ( match ) {
const [, number, unit] = match;
switch ( unit ) {
case 'grad': return _.channel.clamp.h ( parseFloat ( number ) * .9 );
case 'rad': return _.channel.clamp.h ( parseFloat ( number ) * 180 / Math.PI );
case 'turn': return _.channel.clamp.h ( parseFloat ( number ) * 360 );
}
}
return _.channel.clamp.h ( parseFloat ( hue ) );
},
/* API */
parse: ( color: string ): Channels | void => {
const charCode = color.charCodeAt ( 0 );
if ( charCode !== 104 && charCode !== 72 ) return; // 'h'/'H'
const match = color.match ( HSL.re );
if ( !match ) return;
const [, h, s, l, a, isAlphaPercentage] = match;
return ChannelsReusable.set ({
h: HSL._hue2deg ( h ),
s: _.channel.clamp.s ( parseFloat ( s ) ),
l: _.channel.clamp.l ( parseFloat ( l ) ),
a: a ? _.channel.clamp.a ( isAlphaPercentage ? parseFloat ( a ) / 100 : parseFloat ( a ) ) : 1
}, color );
},
stringify: ( channels: Channels ): string => {
const {h, s, l, a} = channels;
if ( a < 1 ) { // HSLA
return `hsla(${_.lang.round ( h )}, ${_.lang.round ( s )}%, ${_.lang.round ( l )}%, ${a})`;
} else { // HSL
return `hsl(${_.lang.round ( h )}, ${_.lang.round ( s )}%, ${_.lang.round ( l )}%)`;
}
}
};
/* EXPORT */
export default HSL;
+67
View File
@@ -0,0 +1,67 @@
/* IMPORT */
import _ from '~/utils';
import Hex from '~/color/hex';
import HSL from '~/color/hsl';
import Keyword from '~/color/keyword';
import RGB from '~/color/rgb';
import {TYPE} from '~/constants';
import type {Channels} from '~/types';
/* MAIN */
const Color = {
/* VARIABLES */
format: {
keyword: Keyword,
hex: Hex,
rgb: RGB,
rgba: RGB,
hsl: HSL,
hsla: HSL
},
/* API */
parse: ( color: string | Channels ): Channels => {
if ( typeof color !== 'string' ) return color;
const channels = Hex.parse ( color ) || RGB.parse ( color ) || HSL.parse ( color ) || Keyword.parse ( color ); // Color providers ordered with performance in mind
if ( channels ) return channels;
throw new Error ( `Unsupported color format: "${color}"` );
},
stringify: ( channels: Channels ): string => {
// SASS returns a keyword if possible, but we avoid doing that as it's slower and doesn't really add any value
if ( !channels.changed && channels.color ) return channels.color;
if ( channels.type.is ( TYPE.HSL ) || channels.data.r === undefined ) {
return HSL.stringify ( channels );
} else if ( channels.a < 1 || !Number.isInteger ( channels.r ) || !Number.isInteger ( channels.g ) || !Number.isInteger ( channels.b ) ) {
return RGB.stringify ( channels );
} else {
return Hex.stringify ( channels );
}
}
};
/* EXPORT */
export default Color;
+195
View File
@@ -0,0 +1,195 @@
/* IMPORT */
import Hex from '~/color/hex';
import type {Channels} from '~/types';
/* MAIN */
const Keyword = {
/* VARIABLES */
colors: {
aliceblue: '#f0f8ff',
antiquewhite: '#faebd7',
aqua: '#00ffff',
aquamarine: '#7fffd4',
azure: '#f0ffff',
beige: '#f5f5dc',
bisque: '#ffe4c4',
black: '#000000',
blanchedalmond: '#ffebcd',
blue: '#0000ff',
blueviolet: '#8a2be2',
brown: '#a52a2a',
burlywood: '#deb887',
cadetblue: '#5f9ea0',
chartreuse: '#7fff00',
chocolate: '#d2691e',
coral: '#ff7f50',
cornflowerblue: '#6495ed',
cornsilk: '#fff8dc',
crimson: '#dc143c',
cyanaqua: '#00ffff',
darkblue: '#00008b',
darkcyan: '#008b8b',
darkgoldenrod: '#b8860b',
darkgray: '#a9a9a9',
darkgreen: '#006400',
darkgrey: '#a9a9a9',
darkkhaki: '#bdb76b',
darkmagenta: '#8b008b',
darkolivegreen: '#556b2f',
darkorange: '#ff8c00',
darkorchid: '#9932cc',
darkred: '#8b0000',
darksalmon: '#e9967a',
darkseagreen: '#8fbc8f',
darkslateblue: '#483d8b',
darkslategray: '#2f4f4f',
darkslategrey: '#2f4f4f',
darkturquoise: '#00ced1',
darkviolet: '#9400d3',
deeppink: '#ff1493',
deepskyblue: '#00bfff',
dimgray: '#696969',
dimgrey: '#696969',
dodgerblue: '#1e90ff',
firebrick: '#b22222',
floralwhite: '#fffaf0',
forestgreen: '#228b22',
fuchsia: '#ff00ff',
gainsboro: '#dcdcdc',
ghostwhite: '#f8f8ff',
gold: '#ffd700',
goldenrod: '#daa520',
gray: '#808080',
green: '#008000',
greenyellow: '#adff2f',
grey: '#808080',
honeydew: '#f0fff0',
hotpink: '#ff69b4',
indianred: '#cd5c5c',
indigo: '#4b0082',
ivory: '#fffff0',
khaki: '#f0e68c',
lavender: '#e6e6fa',
lavenderblush: '#fff0f5',
lawngreen: '#7cfc00',
lemonchiffon: '#fffacd',
lightblue: '#add8e6',
lightcoral: '#f08080',
lightcyan: '#e0ffff',
lightgoldenrodyellow: '#fafad2',
lightgray: '#d3d3d3',
lightgreen: '#90ee90',
lightgrey: '#d3d3d3',
lightpink: '#ffb6c1',
lightsalmon: '#ffa07a',
lightseagreen: '#20b2aa',
lightskyblue: '#87cefa',
lightslategray: '#778899',
lightslategrey: '#778899',
lightsteelblue: '#b0c4de',
lightyellow: '#ffffe0',
lime: '#00ff00',
limegreen: '#32cd32',
linen: '#faf0e6',
magenta: '#ff00ff',
maroon: '#800000',
mediumaquamarine: '#66cdaa',
mediumblue: '#0000cd',
mediumorchid: '#ba55d3',
mediumpurple: '#9370db',
mediumseagreen: '#3cb371',
mediumslateblue: '#7b68ee',
mediumspringgreen: '#00fa9a',
mediumturquoise: '#48d1cc',
mediumvioletred: '#c71585',
midnightblue: '#191970',
mintcream: '#f5fffa',
mistyrose: '#ffe4e1',
moccasin: '#ffe4b5',
navajowhite: '#ffdead',
navy: '#000080',
oldlace: '#fdf5e6',
olive: '#808000',
olivedrab: '#6b8e23',
orange: '#ffa500',
orangered: '#ff4500',
orchid: '#da70d6',
palegoldenrod: '#eee8aa',
palegreen: '#98fb98',
paleturquoise: '#afeeee',
palevioletred: '#db7093',
papayawhip: '#ffefd5',
peachpuff: '#ffdab9',
peru: '#cd853f',
pink: '#ffc0cb',
plum: '#dda0dd',
powderblue: '#b0e0e6',
purple: '#800080',
rebeccapurple: '#663399',
red: '#ff0000',
rosybrown: '#bc8f8f',
royalblue: '#4169e1',
saddlebrown: '#8b4513',
salmon: '#fa8072',
sandybrown: '#f4a460',
seagreen: '#2e8b57',
seashell: '#fff5ee',
sienna: '#a0522d',
silver: '#c0c0c0',
skyblue: '#87ceeb',
slateblue: '#6a5acd',
slategray: '#708090',
slategrey: '#708090',
snow: '#fffafa',
springgreen: '#00ff7f',
tan: '#d2b48c',
teal: '#008080',
thistle: '#d8bfd8',
transparent: '#00000000',
turquoise: '#40e0d0',
violet: '#ee82ee',
wheat: '#f5deb3',
white: '#ffffff',
whitesmoke: '#f5f5f5',
yellow: '#ffff00',
yellowgreen: '#9acd32'
},
/* API */
parse: ( color: string ): Channels | void => {
color = color.toLowerCase ();
const hex = Keyword.colors[color];
if ( !hex ) return;
return Hex.parse ( hex );
},
stringify: ( channels: Channels ): string | undefined => {
const hex = Hex.stringify ( channels );
for ( const name in Keyword.colors ) {
if ( Keyword.colors[name] === hex ) return name;
}
return;
}
};
/* EXPORT */
export default Keyword;
+59
View File
@@ -0,0 +1,59 @@
/* IMPORT */
import _ from '~/utils';
import ChannelsReusable from '~/channels/reusable';
import type {Channels} from '~/types';
/* MAIN */
const RGB = {
/* VARIABLES */
re: /^rgba?\(\s*?(-?(?:\d+(?:\.\d+)?|(?:\.\d+))(?:e\d+)?(%?))\s*?(?:,|\s)\s*?(-?(?:\d+(?:\.\d+)?|(?:\.\d+))(?:e\d+)?(%?))\s*?(?:,|\s)\s*?(-?(?:\d+(?:\.\d+)?|(?:\.\d+))(?:e\d+)?(%?))(?:\s*?(?:,|\/)\s*?\+?(-?(?:\d+(?:\.\d+)?|(?:\.\d+))(?:e\d+)?(%?)))?\s*?\)$/i,
/* API */
parse: ( color: string ): Channels | void => {
const charCode = color.charCodeAt ( 0 );
if ( charCode !== 114 && charCode !== 82 ) return; // 'r'/'R'
const match = color.match ( RGB.re );
if ( !match ) return;
const [, r, isRedPercentage, g, isGreenPercentage, b, isBluePercentage, a, isAlphaPercentage] = match;
return ChannelsReusable.set ({
r: _.channel.clamp.r ( isRedPercentage ? parseFloat ( r ) * 2.55 : parseFloat ( r ) ),
g: _.channel.clamp.g ( isGreenPercentage ? parseFloat ( g ) * 2.55 : parseFloat ( g ) ),
b: _.channel.clamp.b ( isBluePercentage ? parseFloat ( b ) * 2.55 : parseFloat ( b ) ),
a: a ? _.channel.clamp.a ( isAlphaPercentage ? parseFloat ( a ) / 100 : parseFloat ( a ) ) : 1
}, color );
},
stringify: ( channels: Channels ): string => {
const {r, g, b, a} = channels;
if ( a < 1 ) { // RGBA
return `rgba(${_.lang.round ( r )}, ${_.lang.round ( g )}, ${_.lang.round ( b )}, ${_.lang.round ( a )})`;
} else { // RGB
return `rgb(${_.lang.round ( r )}, ${_.lang.round ( g )}, ${_.lang.round ( b )})`;
}
}
};
/* EXPORT */
export default RGB;
+20
View File
@@ -0,0 +1,20 @@
/* IMPORT */
import _ from '~/utils';
/* MAIN */
const DEC2HEX: Record<number, string> = {};
for ( let i = 0; i <= 255; i++ ) DEC2HEX[i] = _.unit.dec2hex ( i ); // Populating dynamically, striking a balance between code size and performance
const TYPE = <const> {
ALL: 0,
RGB: 1,
HSL: 2
};
/* EXPORT */
export {DEC2HEX, TYPE};
+4
View File
@@ -0,0 +1,4 @@
/* EXPORT */
export * from '~/methods';
+29
View File
@@ -0,0 +1,29 @@
/* IMPORT */
import Color from '~/color';
import change from '~/methods/change';
import type {CHANNELS, Channels} from '~/types';
/* MAIN */
const adjust = ( color: string | Channels, channels: Partial<CHANNELS> ): string => {
const ch = Color.parse ( color );
const changes: Partial<CHANNELS> = {};
for ( const c in channels ) {
if ( !channels[c] ) continue;
changes[c] = ch[c] + channels[c];
}
return change ( color, changes );
};
/* EXPORT */
export default adjust;
+24
View File
@@ -0,0 +1,24 @@
/* IMPORT */
import _ from '~/utils';
import Color from '~/color';
import type {CHANNEL, Channels} from '~/types';
/* MAIN */
const adjustChannel = ( color: string | Channels, channel: CHANNEL, amount: number ): string => {
const channels = Color.parse ( color );
const amountCurrent = channels[channel];
const amountNext = _.channel.clamp[channel]( amountCurrent + amount );
if ( amountCurrent !== amountNext ) channels[channel] = amountNext;
return Color.stringify ( channels );
};
/* EXPORT */
export default adjustChannel;
+17
View File
@@ -0,0 +1,17 @@
/* IMPORT */
import channel from '~/methods/channel';
import type {Channels} from '~/types';
/* MAIN */
const alpha = ( color: string | Channels ): number => {
return channel ( color, 'a' );
};
/* EXPORT */
export default alpha;
+17
View File
@@ -0,0 +1,17 @@
/* IMPORT */
import channel from '~/methods/channel';
import type {Channels} from '~/types';
/* MAIN */
const blue = ( color: string | Channels ): number => {
return channel ( color, 'b' );
};
/* EXPORT */
export default blue;
+26
View File
@@ -0,0 +1,26 @@
/* IMPORT */
import _ from '~/utils';
import Color from '~/color';
import type {CHANNELS, Channels} from '~/types';
/* MAIN */
const change = ( color: string | Channels, channels: Partial<CHANNELS> ): string => {
const ch = Color.parse ( color );
for ( const c in channels ) {
ch[c] = _.channel.clamp[c]( channels[c] );
}
return Color.stringify ( ch );
};
/* EXPORT */
export default change;
+18
View File
@@ -0,0 +1,18 @@
/* IMPORT */
import _ from '~/utils';
import Color from '~/color';
import type {CHANNEL, Channels} from '~/types';
/* MAIN */
const channel = ( color: string | Channels, channel: CHANNEL ): number => {
return _.lang.round ( Color.parse ( color )[channel] );
};
/* EXPORT */
export default channel;
+17
View File
@@ -0,0 +1,17 @@
/* IMPORT */
import adjustChannel from '~/methods/adjust_channel';
import type {Channels} from '~/types';
/* MAIN */
const complement = ( color: string | Channels ): string => {
return adjustChannel ( color, 'h', 180 );
};
/* EXPORT */
export default complement;
+23
View File
@@ -0,0 +1,23 @@
/* IMPORT */
import _ from '~/utils';
import luminance from '~/methods/luminance';
/* MAIN */
const contrast = ( color1: string, color2: string ): number => {
const luminance1 = luminance ( color1 );
const luminance2 = luminance ( color2 );
const max = Math.max ( luminance1, luminance2 );
const min = Math.min ( luminance1, luminance2 );
const ratio = ( max + Number.EPSILON ) / ( min + Number.EPSILON );
return _.lang.round ( _.lang.clamp ( ratio, 1, 10 ) );
};
/* EXPORT */
export default contrast;
+17
View File
@@ -0,0 +1,17 @@
/* IMPORT */
import adjustChannel from '~/methods/adjust_channel';
import type {Channels} from '~/types';
/* MAIN */
const darken = ( color: string | Channels, amount: number ): string => {
return adjustChannel ( color, 'l', -amount );
};
/* EXPORT */
export default darken;
+17
View File
@@ -0,0 +1,17 @@
/* IMPORT */
import adjustChannel from '~/methods/adjust_channel';
import type {Channels} from '~/types';
/* MAIN */
const desaturate = ( color: string | Channels, amount: number ): string => {
return adjustChannel ( color, 's', -amount );
};
/* EXPORT */
export default desaturate;
+17
View File
@@ -0,0 +1,17 @@
/* IMPORT */
import change from '~/methods/change';
import type {Channels} from '~/types';
/* MAIN */
const grayscale = ( color: string | Channels ): string => {
return change ( color, { s: 0 } );
};
/* EXPORT */
export default grayscale;
+17
View File
@@ -0,0 +1,17 @@
/* IMPORT */
import channel from '~/methods/channel';
import type {Channels} from '~/types';
/* MAIN */
const green = ( color: string | Channels ): number => {
return channel ( color, 'g' );
};
/* EXPORT */
export default green;
+25
View File
@@ -0,0 +1,25 @@
/* IMPORT */
import _ from '~/utils';
import ChannelsReusable from '~/channels/reusable';
import Color from '~/color';
/* MAIN */
const hsla = ( h: number, s: number, l: number, a: number = 1 ): string => {
const channels = ChannelsReusable.set ({
h: _.channel.clamp.h ( h ),
s: _.channel.clamp.s ( s ),
l: _.channel.clamp.l ( l ),
a: _.channel.clamp.a ( a )
});
return Color.stringify ( channels );
};
/* EXPORT */
export default hsla;
+17
View File
@@ -0,0 +1,17 @@
/* IMPORT */
import channel from '~/methods/channel';
import type {Channels} from '~/types';
/* MAIN */
const hue = ( color: string | Channels ): number => {
return channel ( color, 'h' );
};
/* EXPORT */
export default hue;
+92
View File
@@ -0,0 +1,92 @@
/* IMPORT */
import hex from '~/methods/rgba'; // Alias
import rgb from '~/methods/rgba'; // Alias
import rgba from '~/methods/rgba';
import hsl from '~/methods/hsla'; // Alias
import hsla from '~/methods/hsla';
import toKeyword from '~/methods/to_keyword';
import toHex from '~/methods/to_hex';
import toRgba from '~/methods/to_rgba';
import toHsla from '~/methods/to_hsla';
import channel from '~/methods/channel';
import red from '~/methods/red';
import green from '~/methods/green';
import blue from '~/methods/blue';
import hue from '~/methods/hue';
import saturation from '~/methods/saturation';
import lightness from '~/methods/lightness';
import alpha from '~/methods/alpha';
import opacity from '~/methods/alpha'; // Alias
import contrast from '~/methods/contrast';
import luminance from '~/methods/luminance';
import isDark from '~/methods/is_dark';
import isLight from '~/methods/is_light';
import isTransparent from '~/methods/is_transparent';
import isValid from '~/methods/is_valid';
import saturate from '~/methods/saturate';
import desaturate from '~/methods/desaturate';
import lighten from '~/methods/lighten';
import darken from '~/methods/darken';
import opacify from '~/methods/opacify';
import fadeIn from '~/methods/opacify'; // Alias
import transparentize from '~/methods/transparentize';
import fadeOut from '~/methods/transparentize'; // Alias
import complement from '~/methods/complement';
import grayscale from '~/methods/grayscale';
import adjust from '~/methods/adjust';
import change from '~/methods/change';
import invert from '~/methods/invert';
import mix from '~/methods/mix';
import scale from '~/methods/scale';
/* EXPORT */
export {
/* CREATE */
hex,
rgb,
rgba,
hsl,
hsla,
/* CONVERT */
toKeyword,
toHex,
toRgba,
toHsla,
/* GET - CHANNEL */
channel,
red,
green,
blue,
hue,
saturation,
lightness,
alpha,
opacity,
/* GET - MORE */
contrast,
luminance,
isDark,
isLight,
isTransparent,
isValid,
/* EDIT - CHANNEL */
saturate,
desaturate,
lighten,
darken,
opacify,
fadeIn,
transparentize,
fadeOut,
complement,
grayscale,
/* EDIT - MORE */
adjust,
change,
invert,
mix,
scale
};
+24
View File
@@ -0,0 +1,24 @@
/* IMPORT */
import Color from '~/color';
import mix from '~/methods/mix';
import type {Channels} from '~/types';
/* MAIN */
const invert = ( color: string | Channels, weight: number = 100 ): string => {
const inverse = Color.parse ( color );
inverse.r = 255 - inverse.r;
inverse.g = 255 - inverse.g;
inverse.b = 255 - inverse.b;
return mix ( inverse, color, weight );
};
/* EXPORT */
export default invert;
+17
View File
@@ -0,0 +1,17 @@
/* IMPORT */
import isLight from '~/methods/is_light';
import type {Channels} from '~/types';
/* MAIN */
const isDark = ( color: string | Channels ): boolean => {
return !isLight ( color );
};
/* EXPORT */
export default isDark;
+17
View File
@@ -0,0 +1,17 @@
/* IMPORT */
import luminance from '~/methods/luminance';
import type {Channels} from '~/types';
/* MAIN */
const isLight = ( color: string | Channels ): boolean => {
return luminance ( color ) >= .5;
};
/* EXPORT */
export default isLight;
+17
View File
@@ -0,0 +1,17 @@
/* IMPORT */
import alpha from '~/methods/alpha';
import type {Channels} from '~/types';
/* MAIN */
const isTransparent = ( color: string | Channels ): boolean => {
return !alpha ( color );
};
/* EXPORT */
export default isTransparent;
+26
View File
@@ -0,0 +1,26 @@
/* IMPORT */
import Color from '~/color';
/* MAIN */
const isValid = ( color: string ): boolean => {
try {
Color.parse ( color );
return true;
} catch {
return false;
}
};
/* EXPORT */
export default isValid;
+17
View File
@@ -0,0 +1,17 @@
/* IMPORT */
import adjustChannel from '~/methods/adjust_channel';
import type {Channels} from '~/types';
/* MAIN */
const lighten = ( color: string | Channels, amount: number ): string => {
return adjustChannel ( color, 'l', amount );
};
/* EXPORT */
export default lighten;
+17
View File
@@ -0,0 +1,17 @@
/* IMPORT */
import channel from '~/methods/channel';
import type {Channels} from '~/types';
/* MAIN */
const lightness = ( color: string | Channels ): number => {
return channel ( color, 'l' );
};
/* EXPORT */
export default lightness;
+23
View File
@@ -0,0 +1,23 @@
/* IMPORT */
import _ from '~/utils';
import Color from '~/color';
import type {Channels} from '~/types';
/* MAIN */
//SOURCE: https://planetcalc.com/7779
const luminance = ( color: string | Channels ): number => {
const {r, g, b} = Color.parse ( color );
const luminance = .2126 * _.channel.toLinear ( r ) + .7152 * _.channel.toLinear ( g ) + .0722 * _.channel.toLinear ( b );
return _.lang.round ( luminance );
};
/* EXPORT */
export default luminance;
+33
View File
@@ -0,0 +1,33 @@
/* IMPORT */
import Color from '~/color';
import rgba from '~/methods/rgba';
import type {Channels} from '~/types';
/* MAIN */
//SOURCE: https://github.com/sass/dart-sass/blob/7457d2e9e7e623d9844ffd037a070cf32d39c348/lib/src/functions/color.dart#L718-L756
const mix = ( color1: string | Channels, color2: string | Channels, weight: number = 50 ): string => {
const {r: r1, g: g1, b: b1, a: a1} = Color.parse ( color1 );
const {r: r2, g: g2, b: b2, a: a2} = Color.parse ( color2 );
const weightScale = weight / 100;
const weightNormalized = ( weightScale * 2 ) - 1;
const alphaDelta = a1 - a2;
const weight1combined = ( ( weightNormalized * alphaDelta ) === -1 ) ? weightNormalized : ( weightNormalized + alphaDelta ) / ( 1 + weightNormalized * alphaDelta );
const weight1 = ( weight1combined + 1 ) / 2;
const weight2 = 1 - weight1;
const r = ( r1 * weight1 ) + ( r2 * weight2 );
const g = ( g1 * weight1 ) + ( g2 * weight2 );
const b = ( b1 * weight1 ) + ( b2 * weight2 );
const a = ( a1 * weightScale ) + ( a2 * ( 1 - weightScale ) );
return rgba ( r, g, b, a );
};
/* EXPORT */
export default mix;
+17
View File
@@ -0,0 +1,17 @@
/* IMPORT */
import adjustChannel from '~/methods/adjust_channel';
import type {Channels} from '~/types';
/* MAIN */
const opacify = ( color: string | Channels, amount: number ): string => {
return adjustChannel ( color, 'a', amount );
};
/* EXPORT */
export default opacify;
+17
View File
@@ -0,0 +1,17 @@
/* IMPORT */
import channel from '~/methods/channel';
import type {Channels} from '~/types';
/* MAIN */
const red = ( color: string | Channels ): number => {
return channel ( color, 'r' );
};
/* EXPORT */
export default red;
+36
View File
@@ -0,0 +1,36 @@
/* IMPORT */
import _ from '~/utils';
import ChannelsReusable from '~/channels/reusable';
import Color from '~/color';
import change from '~/methods/change';
import type {Channels} from '~/types';
/* TYPES */
type IRgba = {
( color: string | Channels, opacity: number ): string,
( r: number, g: number, b: number, a?: number ): string
};
/* MAIN */
const rgba: IRgba = ( r: string | Channels | number, g: number, b: number = 0, a: number = 1 ): string => { //TSC: `b` shouldn't have a default value
if ( typeof r !== 'number' ) return change ( r, { a: g } );
const channels = ChannelsReusable.set ({
r: _.channel.clamp.r ( r ),
g: _.channel.clamp.g ( g ),
b: _.channel.clamp.b ( b ),
a: _.channel.clamp.a ( a )
});
return Color.stringify ( channels );
};
/* EXPORT */
export default rgba;
+17
View File
@@ -0,0 +1,17 @@
/* IMPORT */
import adjustChannel from '~/methods/adjust_channel';
import type {Channels} from '~/types';
/* MAIN */
const saturate = ( color: string | Channels, amount: number ): string => {
return adjustChannel ( color, 's', amount );
};
/* EXPORT */
export default saturate;
+17
View File
@@ -0,0 +1,17 @@
/* IMPORT */
import channel from '~/methods/channel';
import type {Channels} from '~/types';
/* MAIN */
const saturation = ( color: string | Channels ): number => {
return channel ( color, 's' );
};
/* EXPORT */
export default saturation;
+29
View File
@@ -0,0 +1,29 @@
/* IMPORT */
import _ from '~/utils';
import Color from '~/color';
import adjust from '~/methods/adjust';
import type {CHANNELS, Channels} from '~/types';
/* MAIN */
const scale = ( color: string | Channels, channels: Partial<CHANNELS> ): string => {
const ch = Color.parse ( color );
const adjustments: Partial<CHANNELS> = {};
const delta = ( amount: number, weight: number, max: number ) => weight > 0 ? ( max - amount ) * weight / 100 : amount * weight / 100;
for ( const c in channels ) {
adjustments[c] = delta ( ch[c], channels[c], _.channel.max[c] );
}
return adjust ( color, adjustments );
};
/* EXPORT */
export default scale;
+16
View File
@@ -0,0 +1,16 @@
/* IMPORT */
import Color from '~/color';
/* MAIN */
const toHex = ( color: string ): string => {
return Color.format.hex.stringify ( Color.parse ( color ) );
};
/* EXPORT */
export default toHex;
+16
View File
@@ -0,0 +1,16 @@
/* IMPORT */
import Color from '~/color';
/* MAIN */
const toHsla = ( color: string ): string => {
return Color.format.hsla.stringify ( Color.parse ( color ) );
};
/* EXPORT */
export default toHsla;
+16
View File
@@ -0,0 +1,16 @@
/* IMPORT */
import Color from '~/color';
/* MAIN */
const toKeyword = ( color: string ): string | undefined => {
return Color.format.keyword.stringify ( Color.parse ( color ) );
};
/* EXPORT */
export default toKeyword;
+16
View File
@@ -0,0 +1,16 @@
/* IMPORT */
import Color from '~/color';
/* MAIN */
const toRgba = ( color: string ): string => {
return Color.format.rgba.stringify ( Color.parse ( color ) );
};
/* EXPORT */
export default toRgba;
+17
View File
@@ -0,0 +1,17 @@
/* IMPORT */
import adjustChannel from '~/methods/adjust_channel';
import type {Channels} from '~/types';
/* MAIN */
const transparentize = ( color: string | Channels, amount: number ): string => {
return adjustChannel ( color, 'a', -amount );
};
/* EXPORT */
export default transparentize;
+35
View File
@@ -0,0 +1,35 @@
/* IMPORT */
import type Channels from './channels';
/* MAIN */
type ALPHA = {
a: number // Alpha (0~1)
};
type RGB = {
r: number, // Red (0~255)
g: number, // Green (0~255)
b: number // Blue (0~255)
};
type RGBA = RGB & ALPHA;
type HSL = {
h: number, // Hue (0~360)
s: number, // Saturation (0~100)
l: number // Lightness (0~100)
};
type HSLA = HSL & ALPHA;
type CHANNEL = 'r' | 'g' | 'b' | 'h' | 's' | 'l' | 'a';
type CHANNELS = Record<CHANNEL, number>;
/* EXPORT */
export type {Channels};
export type {ALPHA, RGB, RGBA, HSL, HSLA, CHANNEL, CHANNELS};
+122
View File
@@ -0,0 +1,122 @@
/* IMPORT */
import type {RGB, HSL} from '~/types';
/* MAIN */
const Channel = {
/* CLAMP */
min: {
r: 0,
g: 0,
b: 0,
s: 0,
l: 0,
a: 0
},
max: {
r: 255,
g: 255,
b: 255,
h: 360,
s: 100,
l: 100,
a: 1
},
clamp: {
r: ( r: number ) => r >= 255 ? 255 : ( r < 0 ? 0 : r ),
g: ( g: number ) => g >= 255 ? 255 : ( g < 0 ? 0 : g ),
b: ( b: number ) => b >= 255 ? 255 : ( b < 0 ? 0 : b ),
h: ( h: number ) => h % 360,
s: ( s: number ) => s >= 100 ? 100 : ( s < 0 ? 0 : s ),
l: ( l: number ) => l >= 100 ? 100 : ( l < 0 ? 0 : l ),
a: ( a: number ) => a >= 1 ? 1 : ( a < 0 ? 0 : a )
},
/* CONVERSION */
//SOURCE: https://planetcalc.com/7779
toLinear: ( c: number ): number => {
const n = c / 255;
return c > .03928 ? Math.pow ( ( ( n + .055 ) / 1.055 ), 2.4 ) : n / 12.92;
},
//SOURCE: https://gist.github.com/mjackson/5311256
hue2rgb: ( p: number, q: number, t: number ): number => {
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;
},
hsl2rgb: ( { h, s, l }: HSL, channel: keyof RGB ): number => {
if ( !s ) return l * 2.55; // Achromatic
h /= 360;
s /= 100;
l /= 100;
const q = ( l < .5 ) ? l * ( 1 + s ) : ( l + s ) - ( l * s );
const p = 2 * l - q;
switch ( channel ) {
case 'r': return Channel.hue2rgb ( p, q, h + 1/3 ) * 255;
case 'g': return Channel.hue2rgb ( p, q, h ) * 255;
case 'b': return Channel.hue2rgb ( p, q, h - 1/3 ) * 255;
}
},
rgb2hsl: ( { r, g, b }: RGB, channel: keyof HSL ): number => {
r /= 255;
g /= 255;
b /= 255;
const max = Math.max ( r, g, b );
const min = Math.min ( r, g, b );
const l = ( max + min ) / 2;
if ( channel === 'l' ) return l * 100;
if ( max === min ) return 0; // Achromatic
const d = max - min;
const s = ( l > .5 ) ? d / ( 2 - max - min ) : d / ( max + min );
if ( channel === 's' ) return s * 100;
switch ( max ) {
case r: return ( ( g - b ) / d + ( g < b ? 6 : 0 ) ) * 60;
case g: return ( ( b - r ) / d + 2 ) * 60;
case b: return ( ( r - g ) / d + 4 ) * 60;
default: return -1; //TSC: TypeScript is stupid and complains if there isn't this useless default statement
}
}
};
/* EXPORT */
export default Channel;
+18
View File
@@ -0,0 +1,18 @@
/* IMPORT */
import channel from '~/utils/channel';
import lang from '~/utils/lang';
import unit from '~/utils/unit';
/* MAIN */
const Utils = {
channel,
lang,
unit
};
/* EXPORT */
export default Utils;
+26
View File
@@ -0,0 +1,26 @@
/* MAIN */
const Lang = {
/* API */
clamp: ( number: number, lower: number, upper: number ): number => {
if ( lower > upper ) return Math.min ( lower, Math.max ( upper, number ) );
return Math.min ( upper, Math.max ( lower, number ) );
},
round: ( number: number ): number => { // 10 digits rounding
return Math.round ( number * 10000000000 ) / 10000000000;
}
};
/* EXPORT */
export default Lang;
+20
View File
@@ -0,0 +1,20 @@
/* MAIN */
const Unit = {
/* API */
dec2hex: ( dec: number ): string => {
const hex = Math.round ( dec ).toString ( 16 );
return hex.length > 1 ? hex : `0${hex}`;
}
};
/* EXPORT */
export default Unit;