import { colord, extend } from '@pixi/colord';
import namesPlugin from '@pixi/colord/plugins/names';
import type { AnyColor, HslaColor, HslColor, HsvaColor, HsvColor, RgbaColor, RgbColor } from '@pixi/colord';
extend([namesPlugin]);
/**
* Pixi supports multiple color formats, including CSS color strings, hex, numbers, and arrays.
*
* When providing values for any of the color properties, you can use any of the ColorSource formats.
* ```typescript
* import { Color } from 'pixi.js';
*
* // All of these are valid:
* sprite.tint = 'red';
* sprite.tint = 0xff0000;
* sprite.tint = '#ff0000';
* sprite.tint = new Color('red');
*
* // Same for graphics fill/stroke colors and other color values:
* graphics.fill({ color: 'red' });
* graphics.fill({ color: 0xff0000 });
* graphics.stroke({ color: '#ff0000' });
* graphics.stroke({ color: new Color('red')};
* ```
* @namespace color
*/
/**
* RGBA color array.
*
* `[number, number, number, number]`
* @memberof color
*/
export type RgbaArray = [number, number, number, number];
/**
* Valid formats to use when defining any color properties, also valid for the Color constructor.
*
* These types are extended from [colord](https://www.npmjs.com/package/colord) with some PixiJS-specific extensions.
*
* Possible value types are:
* - [Color names](https://www.w3.org/TR/css-color-4/#named-colors):
* `'red'`, `'green'`, `'blue'`, `'white'`, etc.
* - RGB hex integers (`0xRRGGBB`):
* `0xff0000`, `0x00ff00`, `0x0000ff`, etc.
* - [RGB(A) hex strings](https://www.w3.org/TR/css-color-4/#hex-notation):
* - 6 digits (`RRGGBB`): `'ff0000'`, `'#00ff00'`, `'0x0000ff'`, etc.
* - 3 digits (`RGB`): `'f00'`, `'#0f0'`, `'0x00f'`, etc.
* - 8 digits (`RRGGBBAA`): `'ff000080'`, `'#00ff0080'`, `'0x0000ff80'`, etc.
* - 4 digits (`RGBA`): `'f008'`, `'#0f08'`, `'0x00f8'`, etc.
* - RGB(A) objects:
* `{ r: 255, g: 0, b: 0 }`, `{ r: 255, g: 0, b: 0, a: 0.5 }`, etc.
* - [RGB(A) strings](https://www.w3.org/TR/css-color-4/#rgb-functions):
* `'rgb(255, 0, 0)'`, `'rgb(100% 0% 0%)'`, `'rgba(255, 0, 0, 0.5)'`, `'rgba(100% 0% 0% / 50%)'`, etc.
* - RGB(A) arrays:
* `[1, 0, 0]`, `[1, 0, 0, 0.5]`, etc.
* - RGB(A) Float32Array:
* `new Float32Array([1, 0, 0])`, `new Float32Array([1, 0, 0, 0.5])`, etc.
* - RGB(A) Uint8Array:
* `new Uint8Array([255, 0, 0])`, `new Uint8Array([255, 0, 0, 128])`, etc.
* - RGB(A) Uint8ClampedArray:
* `new Uint8ClampedArray([255, 0, 0])`, `new Uint8ClampedArray([255, 0, 0, 128])`, etc.
* - HSL(A) objects:
* `{ h: 0, s: 100, l: 50 }`, `{ h: 0, s: 100, l: 50, a: 0.5 }`, etc.
* - [HSL(A) strings](https://www.w3.org/TR/css-color-4/#the-hsl-notation):
* `'hsl(0, 100%, 50%)'`, `'hsl(0deg 100% 50%)'`, `'hsla(0, 100%, 50%, 0.5)'`, `'hsla(0deg 100% 50% / 50%)'`, etc.
* - HSV(A) objects:
* `{ h: 0, s: 100, v: 100 }`, `{ h: 0, s: 100, v: 100, a: 0.5 }`, etc.
* - Color objects.
* @since 7.2.0
* @memberof color
*/
export type ColorSource =
| string
| number
| number[]
| Float32Array
| Uint8Array
| Uint8ClampedArray
| HslColor
| HslaColor
| HsvColor
| HsvaColor
| RgbColor
| RgbaColor
| Color
| number;
type ColorSourceTypedArray = Float32Array | Uint8Array | Uint8ClampedArray;
/**
* Color utility class. Can accept any ColorSource format in its constructor.
* ```js
* import { Color } from 'pixi.js';
*
* new Color('red').toArray(); // [1, 0, 0, 1]
* new Color(0xff0000).toArray(); // [1, 0, 0, 1]
* new Color('ff0000').toArray(); // [1, 0, 0, 1]
* new Color('#f00').toArray(); // [1, 0, 0, 1]
* new Color('0xff0000ff').toArray(); // [1, 0, 0, 1]
* new Color('#f00f').toArray(); // [1, 0, 0, 1]
* new Color({ r: 255, g: 0, b: 0, a: 0.5 }).toArray(); // [1, 0, 0, 0.5]
* new Color('rgb(255, 0, 0, 0.5)').toArray(); // [1, 0, 0, 0.5]
* new Color([1, 1, 1]).toArray(); // [1, 1, 1, 1]
* new Color([1, 0, 0, 0.5]).toArray(); // [1, 0, 0, 0.5]
* new Color(new Float32Array([1, 0, 0, 0.5])).toArray(); // [1, 0, 0, 0.5]
* new Color(new Uint8Array([255, 0, 0, 255])).toArray(); // [1, 0, 0, 1]
* new Color(new Uint8ClampedArray([255, 0, 0, 255])).toArray(); // [1, 0, 0, 1]
* new Color({ h: 0, s: 100, l: 50, a: 0.5 }).toArray(); // [1, 0, 0, 0.5]
* new Color('hsl(0, 100%, 50%, 50%)').toArray(); // [1, 0, 0, 0.5]
* new Color({ h: 0, s: 100, v: 100, a: 0.5 }).toArray(); // [1, 0, 0, 0.5]
* ```
* @since 7.2.0
* @memberof color
*/
export class Color
{
/**
* Default Color object for static uses
* @example
* import { Color } from 'pixi.js';
* Color.shared.setValue(0xffffff).toHex(); // '#ffffff'
*/
public static readonly shared = new Color();
/**
* Temporary Color object for static uses internally.
* As to not conflict with Color.shared.
* @ignore
*/
private static readonly _temp = new Color();
/** Pattern for hex strings */
// eslint-disable-next-line @typescript-eslint/naming-convention
private static readonly HEX_PATTERN = /^(#|0x)?(([a-f0-9]{3}){1,2}([a-f0-9]{2})?)$/i;
/** Internal color source, from constructor or set value */
private _value: Exclude<ColorSource, Color> | null;
/** Normalized rgba component, floats from 0-1 */
private _components: Float32Array;
/** Cache color as number */
private _int: number;
/** An array of the current Color. Only populated when `toArray` functions are called */
private _arrayRgba: number[] | null;
private _arrayRgb: number[] | null;
/**
* @param {ColorSource} value - Optional value to use, if not provided, white is used.
*/
constructor(value: ColorSource = 0xffffff)
{
this._value = null;
this._components = new Float32Array(4);
this._components.fill(1);
this._int = 0xffffff;
this.value = value;
}
/** Get red component (0 - 1) */
get red(): number
{
return this._components[0];
}
/** Get green component (0 - 1) */
get green(): number
{
return this._components[1];
}
/** Get blue component (0 - 1) */
get blue(): number
{
return this._components[2];
}
/** Get alpha component (0 - 1) */
get alpha(): number
{
return this._components[3];
}
/**
* Set the value, suitable for chaining
* @param value
* @see Color.value
*/
public setValue(value: ColorSource): this
{
this.value = value;
return this;
}
/**
* The current color source.
*
* When setting:
* - Setting to an instance of `Color` will copy its color source and components.
* - Otherwise, `Color` will try to normalize the color source and set the components.
* If the color source is invalid, an `Error` will be thrown and the `Color` will left unchanged.
*
* Note: The `null` in the setter's parameter type is added to match the TypeScript rule: return type of getter
* must be assignable to its setter's parameter type. Setting `value` to `null` will throw an `Error`.
*
* When getting:
* - A return value of `null` means the previous value was overridden (e.g., multiply,
* {@link Color.premultiply premultiply} or round).
* - Otherwise, the color source used when setting is returned.
*/
set value(value: ColorSource | null)
{
// Support copying from other Color objects
if (value instanceof Color)
{
this._value = this._cloneSource(value._value);
this._int = value._int;
this._components.set(value._components);
}
else if (value === null)
{
throw new Error('Cannot set Color#value to null');
}
else if (this._value === null || !this._isSourceEqual(this._value, value))
{
this._value = this._cloneSource(value);
this._normalize(this._value);
}
}
get value(): Exclude<ColorSource, Color> | null
{
return this._value;
}
/**
* Copy a color source internally.
* @param value - Color source
*/
private _cloneSource(value: Exclude<ColorSource, Color> | null): Exclude<ColorSource, Color> | null
{
if (typeof value === 'string' || typeof value === 'number' || value instanceof Number || value === null)
{
return value;
}
else if (Array.isArray(value) || ArrayBuffer.isView(value))
{
return value.slice(0);
}
else if (typeof value === 'object' && value !== null)
{
return { ...value };
}
return value;
}
/**
* Equality check for color sources.
* @param value1 - First color source
* @param value2 - Second color source
* @returns `true` if the color sources are equal, `false` otherwise.
*/
private _isSourceEqual(value1: Exclude<ColorSource, Color>, value2: Exclude<ColorSource, Color>): boolean
{
const type1 = typeof value1;
const type2 = typeof value2;
// Mismatched types
if (type1 !== type2)
{
return false;
}
// Handle numbers/strings and things that extend Number
// important to do the instanceof Number first, as this is "object" type
else if (type1 === 'number' || type1 === 'string' || value1 instanceof Number)
{
return value1 === value2;
}
// Handle Arrays and TypedArrays
else if (
(Array.isArray(value1) && Array.isArray(value2))
|| (ArrayBuffer.isView(value1) && ArrayBuffer.isView(value2))
)
{
if (value1.length !== value2.length)
{
return false;
}
return value1.every((v, i) => v === value2[i]);
}
// Handle Objects
else if (value1 !== null && value2 !== null)
{
const keys1 = Object.keys(value1) as (keyof typeof value1)[];
const keys2 = Object.keys(value2) as (keyof typeof value2)[];
if (keys1.length !== keys2.length)
{
return false;
}
return keys1.every((key) => value1[key] === value2[key]);
}
return value1 === value2;
}
/**
* Convert to a RGBA color object.
* @example
* import { Color } from 'pixi.js';
* new Color('white').toRgb(); // returns { r: 1, g: 1, b: 1, a: 1 }
*/
public toRgba(): RgbaColor
{
const [r, g, b, a] = this._components;
return { r, g, b, a };
}
/**
* Convert to a RGB color object.
* @example
* import { Color } from 'pixi.js';
* new Color('white').toRgb(); // returns { r: 1, g: 1, b: 1 }
*/
public toRgb(): RgbColor
{
const [r, g, b] = this._components;
return { r, g, b };
}
/** Convert to a CSS-style rgba string: `rgba(255,255,255,1.0)`. */
public toRgbaString(): string
{
const [r, g, b] = this.toUint8RgbArray();
return `rgba(${r},${g},${b},${this.alpha})`;
}
/**
* Convert to an [R, G, B] array of clamped uint8 values (0 to 255).
* @example
* import { Color } from 'pixi.js';
* new Color('white').toUint8RgbArray(); // returns [255, 255, 255]
* @param {number[]|Uint8Array|Uint8ClampedArray} [out] - Output array
*/
public toUint8RgbArray(): number[];
public toUint8RgbArray<T extends number[] | Uint8Array | Uint8ClampedArray>(out: T): T;
public toUint8RgbArray<T extends number[] | Uint8Array | Uint8ClampedArray>(out?: T): T
{
const [r, g, b] = this._components;
if (!this._arrayRgb)
{
this._arrayRgb = [];
}
out ||= this._arrayRgb as T;
out[0] = Math.round(r * 255);
out[1] = Math.round(g * 255);
out[2] = Math.round(b * 255);
return out;
}
/**
* Convert to an [R, G, B, A] array of normalized floats (numbers from 0.0 to 1.0).
* @example
* import { Color } from 'pixi.js';
* new Color('white').toArray(); // returns [1, 1, 1, 1]
* @param {number[]|Float32Array} [out] - Output array
*/
public toArray(): number[];
public toArray<T extends number[] | Float32Array>(out: T): T;
public toArray<T extends number[] | Float32Array>(out?: T): T
{
if (!this._arrayRgba)
{
this._arrayRgba = [];
}
out ||= this._arrayRgba as T;
const [r, g, b, a] = this._components;
out[0] = r;
out[1] = g;
out[2] = b;
out[3] = a;
return out;
}
/**
* Convert to an [R, G, B] array of normalized floats (numbers from 0.0 to 1.0).
* @example
* import { Color } from 'pixi.js';
* new Color('white').toRgbArray(); // returns [1, 1, 1]
* @param {number[]|Float32Array} [out] - Output array
*/
public toRgbArray(): number[];
public toRgbArray<T extends number[] | Float32Array>(out: T): T;
public toRgbArray<T extends number[] | Float32Array>(out?: T): T
{
if (!this._arrayRgb)
{
this._arrayRgb = [];
}
out ||= this._arrayRgb as T;
const [r, g, b] = this._components;
out[0] = r;
out[1] = g;
out[2] = b;
return out;
}
/**
* Convert to a hexadecimal number.
* @example
* import { Color } from 'pixi.js';
* new Color('white').toNumber(); // returns 16777215
*/
public toNumber(): number
{
return this._int;
}
/**
* Convert to a BGR number
* @example
* import { Color } from 'pixi.js';
* new Color(0xffcc99).toBgrNumber(); // returns 0x99ccff
*/
public toBgrNumber(): number
{
const [r, g, b] = this.toUint8RgbArray();
return (b << 16) + (g << 8) + r;
}
/**
* Convert to a hexadecimal number in little endian format (e.g., BBGGRR).
* @example
* import { Color } from 'pixi.js';
* new Color(0xffcc99).toLittleEndianNumber(); // returns 0x99ccff
* @returns {number} - The color as a number in little endian format.
*/
public toLittleEndianNumber(): number
{
const value = this._int;
return (value >> 16) + (value & 0xff00) + ((value & 0xff) << 16);
}
/**
* Multiply with another color. This action is destructive, and will
* override the previous `value` property to be `null`.
* @param {ColorSource} value - The color to multiply by.
*/
public multiply(value: ColorSource): this
{
const [r, g, b, a] = Color._temp.setValue(value)._components;
this._components[0] *= r;
this._components[1] *= g;
this._components[2] *= b;
this._components[3] *= a;
this._refreshInt();
this._value = null;
return this;
}
/**
* Converts color to a premultiplied alpha format. This action is destructive, and will
* override the previous `value` property to be `null`.
* @param alpha - The alpha to multiply by.
* @param {boolean} [applyToRGB=true] - Whether to premultiply RGB channels.
* @returns {Color} - Itself.
*/
public premultiply(alpha: number, applyToRGB = true): this
{
if (applyToRGB)
{
this._components[0] *= alpha;
this._components[1] *= alpha;
this._components[2] *= alpha;
}
this._components[3] = alpha;
this._refreshInt();
this._value = null;
return this;
}
/**
* Premultiplies alpha with current color.
* @param {number} alpha - The alpha to multiply by.
* @param {boolean} [applyToRGB=true] - Whether to premultiply RGB channels.
* @returns {number} tint multiplied by alpha
*/
public toPremultiplied(alpha: number, applyToRGB = true): number
{
if (alpha === 1.0)
{
return (0xff << 24) + this._int;
}
if (alpha === 0.0)
{
return applyToRGB ? 0 : this._int;
}
let r = (this._int >> 16) & 0xff;
let g = (this._int >> 8) & 0xff;
let b = this._int & 0xff;
if (applyToRGB)
{
r = ((r * alpha) + 0.5) | 0;
g = ((g * alpha) + 0.5) | 0;
b = ((b * alpha) + 0.5) | 0;
}
return ((alpha * 255) << 24) + (r << 16) + (g << 8) + b;
}
/**
* Convert to a hexadecimal string.
* @example
* import { Color } from 'pixi.js';
* new Color('white').toHex(); // returns "#ffffff"
*/
public toHex(): string
{
const hexString = this._int.toString(16);
return `#${'000000'.substring(0, 6 - hexString.length) + hexString}`;
}
/**
* Convert to a hexadecimal string with alpha.
* @example
* import { Color } from 'pixi.js';
* new Color('white').toHexa(); // returns "#ffffffff"
*/
public toHexa(): string
{
const alphaValue = Math.round(this._components[3] * 255);
const alphaString = alphaValue.toString(16);
return this.toHex() + '00'.substring(0, 2 - alphaString.length) + alphaString;
}
/**
* Set alpha, suitable for chaining.
* @param alpha
*/
public setAlpha(alpha: number): this
{
this._components[3] = this._clamp(alpha);
return this;
}
/**
* Normalize the input value into rgba
* @param value - Input value
*/
private _normalize(value: Exclude<ColorSource, Color>): void
{
let r: number | undefined;
let g: number | undefined;
let b: number | undefined;
let a: number | undefined;
// Number is a primitive so typeof works fine, but in the case
// that someone creates a class that extends Number, we also
// need to check for instanceof Number
if (
(typeof value === 'number' || value instanceof Number)
&& (value as number) >= 0
&& (value as number) <= 0xffffff
)
{
const int = value as number; // cast required because instanceof Number is ambiguous for TS
r = ((int >> 16) & 0xff) / 255;
g = ((int >> 8) & 0xff) / 255;
b = (int & 0xff) / 255;
a = 1.0;
}
else if (
(Array.isArray(value) || value instanceof Float32Array)
// Can be rgb or rgba
&& value.length >= 3
&& value.length <= 4
)
{
// make sure all values are 0 - 1
value = this._clamp(value);
[r, g, b, a = 1.0] = value;
}
else if (
(value instanceof Uint8Array || value instanceof Uint8ClampedArray)
// Can be rgb or rgba
&& value.length >= 3
&& value.length <= 4
)
{
// make sure all values are 0 - 255
value = this._clamp(value, 0, 255);
[r, g, b, a = 255] = value;
r /= 255;
g /= 255;
b /= 255;
a /= 255;
}
else if (typeof value === 'string' || typeof value === 'object')
{
if (typeof value === 'string')
{
const match = Color.HEX_PATTERN.exec(value);
if (match)
{
// Normalize hex string, remove 0x or # prefix
value = `#${match[2]}`;
}
}
const color = colord(value as AnyColor);
if (color.isValid())
{
({ r, g, b, a } = color.rgba);
r /= 255;
g /= 255;
b /= 255;
}
}
// Cache normalized values for rgba and hex integer
if (r !== undefined)
{
this._components[0] = r as number;
this._components[1] = g as number;
this._components[2] = b as number;
this._components[3] = a as number;
this._refreshInt();
}
else
{
throw new Error(`Unable to convert color ${value}`);
}
}
/** Refresh the internal color rgb number */
private _refreshInt(): void
{
// Clamp values to 0 - 1
this._clamp(this._components);
const [r, g, b] = this._components;
this._int = ((r * 255) << 16) + ((g * 255) << 8) + ((b * 255) | 0);
}
/**
* Clamps values to a range. Will override original values
* @param value - Value(s) to clamp
* @param min - Minimum value
* @param max - Maximum value
*/
private _clamp<T extends number | number[] | ColorSourceTypedArray>(value: T, min = 0, max = 1): T
{
if (typeof value === 'number')
{
return Math.min(Math.max(value, min), max) as T;
}
value.forEach((v, i) =>
{
value[i] = Math.min(Math.max(v, min), max);
});
return value;
}
/**
* Check if the value is a color-like object
* @param value - Value to check
* @returns True if the value is a color-like object
* @static
* @example
* import { Color } from 'pixi.js';
* Color.isColorLike('white'); // returns true
* Color.isColorLike(0xffffff); // returns true
* Color.isColorLike([1, 1, 1]); // returns true
*/
public static isColorLike(value: unknown): value is ColorSource
{
return (
typeof value === 'number'
|| typeof value === 'string'
|| value instanceof Number
|| value instanceof Color
|| Array.isArray(value)
|| value instanceof Uint8Array
|| value instanceof Uint8ClampedArray
|| value instanceof Float32Array
|| ((value as RgbColor).r !== undefined
&& (value as RgbColor).g !== undefined
&& (value as RgbColor).b !== undefined)
|| ((value as RgbaColor).r !== undefined
&& (value as RgbaColor).g !== undefined
&& (value as RgbaColor).b !== undefined
&& (value as RgbaColor).a !== undefined)
|| ((value as HslColor).h !== undefined
&& (value as HslColor).s !== undefined
&& (value as HslColor).l !== undefined)
|| ((value as HslaColor).h !== undefined
&& (value as HslaColor).s !== undefined
&& (value as HslaColor).l !== undefined
&& (value as HslaColor).a !== undefined)
|| ((value as HsvColor).h !== undefined
&& (value as HsvColor).s !== undefined
&& (value as HsvColor).v !== undefined)
|| ((value as HsvaColor).h !== undefined
&& (value as HsvaColor).s !== undefined
&& (value as HsvaColor).v !== undefined
&& (value as HsvaColor).a !== undefined)
);
}
}