/**
* @file Palette classes for color palette management
*/
import { Color } from "./color.js";
/* Class representing a color palette */
class Palette {
/**
* Create a palette from an array of Color objects
* @param {Color[]} colors array of Color objects
*/
constructor(colors) {
this._colors = colors;
}
/**
* Create a palette from an array of HEX color strings
* @param {string[]} colors array of HEX color strings
* @returns {Palette} palette object
*/
static fromHEXArray(colors) {
return new Palette(colors.map((c) => Color.fromHex(c)));
}
/**
* Create a palette from an array of RGB color arrays
* @param {number[][]} colors array of RGB color arrays
* @returns {Palette} palette object
*/
static fromRGBArray(colors) {
return new Palette(colors.map((c) => Color.fromRGB(...c)));
}
/**
* Shuffle the colors in the palette
* @typedef {RandomClass} RandomClass An object with a random() method that returns a float between 0 and 1
* @param {RandomClass} rand random number generator with a random() method. Defaults to Math
* @returns {Palette} shuffled palette
*/
shuffle(rand = Math) {
this._colors = this._colors
.map((c) => ({ color: c, order: rand.random() }))
.sort((a, b) => a.order - b.order)
.map((c) => c.color);
return this;
}
/**
* Reverts the order of colors in the palette
* @returns {Palette} inverted palette
*/
reverse() {
this._colors.reverse();
return this;
}
/**
* Rotate the colors in the palette by n positions
* @param {number} n number of positions to rotate
* @returns {Palette} rotated palette
*/
rotate(n) {
const wrap = (n) => {
while (n < 0) n += this._colors.length;
return n % this._colors.length;
};
const steps = wrap(n);
this._colors = this._colors
.slice(steps)
.concat(this._colors.slice(0, steps));
return this;
}
/**
* Return a copy of the palette
* @returns {Palette} copy of the palette
*/
copy() {
return new Palette(this._colors.map((c) => c.copy()));
}
/**
* Get the color at index i. The index wraps around if it exceeds the number of colors
* @param {number} i index of the color
* @returns {Color} color at index i
*/
getColor(i) {
return this._colors[i % this._colors.length];
}
/**
* Get a random color from the palette
* @typedef {RandomClass} RandomClass An object with a random() method that returns a float between 0 and 1
* @param {RandomClass} rand random number generator with a random() method. Defaults to Math
* @returns {Color} random color from the palette
*/
getRandomColor(rand = Math) {
const r = Math.floor(rand.random() * this._colors.length);
return this.getColor(r);
}
/**
* Get a smoothly interpolated color from the palette
* @typedef {EasingFunction} EasingFunction Easing function that takes a value between 0 and 1 and returns a value between 0 and 1
* @param {number} t value between 0 and 1
* @param {EasingFunction|null} easing easing function to use for the interpolation. Defaults to null (linear)
* @returns {Color} smoothly interpolated color from the palette
*/
getSmoothColor(t, easing = null) {
// Clamp t to [0, 1]
t = Math.max(0, Math.min(1, t));
const n = this._colors.length - 1;
const position = t * n;
const integer_part = Math.floor(position);
const fractional_part = position - integer_part;
const c1 = this.getColor(integer_part);
const c2 = this.getColor(integer_part + 1);
return c1.mix(c2, fractional_part, easing);
}
/**
* Get all colors in the palette
* @returns {Color[]} array of colors in the palette
*/
get colors() {
return this._colors;
}
/**
* Get the number of colors in the palette
* @returns {number} number of colors in the palette
*/
get length() {
return this._colors.length;
}
}
class GradientPalette extends Palette {
/**
* Create a gradient palette from an array of Color objects
* @typedef {EasingFunction} EasingFunction Easing function that takes a value between 0 and 1 and returns a value between 0 and 1
* @param {Color} from starting color of the gradient
* @param {Color} to ending color of the gradient
* @param {number} steps number of steps in the gradient
* @param {EasingFunction|null} easing easing function to use for the interpolation. Defaults to null (linear)
*/
constructor(from, to, steps, easing = null) {
const colors = [];
for (let i = 0; i < steps; i++) {
const t = i / (steps - 1);
colors.push(from.mix(to, t, easing));
}
super(colors);
}
/**
* Create a gradient palette from HEX color strings
* @typedef {EasingFunction} EasingFunction Easing function that takes a value between 0 and 1 and returns a value between 0 and 1
* @param {string} from_hex starting HEX color of the gradient
* @param {string} to_hex ending HEX color of the gradient
* @param {number} steps number of steps in the gradient
* @param {EasingFunction|null} easing easing function to use for the interpolation. Defaults to null (linear)
* @returns {GradientPalette} gradient palette object
*/
static fromHEXColors(from_hex, to_hex, steps, easing = null) {
const from = Color.fromHex(from_hex);
const to = Color.fromHex(to_hex);
return new GradientPalette(from, to, steps, easing);
}
/**
* Create a gradient palette from RGB color arrays
* @typedef {EasingFunction} EasingFunction Easing function that takes a value between 0 and 1 and returns a value between 0 and 1
* @param {number[]} from_rgb starting RGB color of the gradient
* @param {number[]} to_rgb ending RGB color of the gradient
* @param {number} steps number of steps in the gradient
* @param {EasingFunction|null} easing easing function to use for the interpolation. Defaults to null (linear)
* @returns {GradientPalette} gradient palette object
*/
static fromRGBColors(from_rgb, to_rgb, steps, easing = null) {
const from = Color.fromRGB(...from_rgb);
const to = Color.fromRGB(...to_rgb);
return new GradientPalette(from, to, steps, easing);
}
}
/* Class representing a factory for palettes */
class PaletteFactory {
/**
* Create a palette factory from an array of palettes
* @param {Array.<Array.<Color>>} palettes array of palettes, each palette is an array of Color objects or HEX color strings
*/
constructor(palettes) {
this._palettes = palettes;
}
/**
* Create a palette factory from an array of HEX palettes
* @param {Array.<Array.<string>>} hex_palettes array of palettes, each palette is an array of HEX color strings
* @returns {PaletteFactory} palette factory object
*/
static fromHEXArray(hex_palettes) {
const palettes = hex_palettes.map((p) => Palette.fromHEXArray(p));
return new PaletteFactory(palettes);
}
/**
* Create a palette factory from an array of RGB palettes
* @param {Array.<Array.<number[]>>} rgb_palettes array of palettes, each palette is an array of RGB color arrays
* @returns {PaletteFactory} palette factory object
*/
static fromRGBArray(rgb_palettes) {
const palettes = rgb_palettes.map((p) => Palette.fromRGBArray(p));
return new PaletteFactory(palettes);
}
/**
* Get a random palette from the factory
* @typedef {RandomClass} RandomClass An object with a random() method that returns a float between 0 and 1
* @param {RandomClass} rand random number generator with a random() method. Defaults to Math
* @param {boolean} randomize whether to randomize the order of colors in the palette. Defaults to true
* @returns {Palette} random palette
*/
getRandomPalette(rand = Math, randomize = true) {
const colors_index = Math.floor(rand.random() * this._palettes.length);
let colors = this._palettes[colors_index].colors.map((c) => c.copy());
if (randomize) {
colors = colors
.map((c) => ({ color: c, order: rand.random() }))
.sort((a, b) => a.order - b.order)
.map((c) => c.color);
}
return new Palette(colors);
}
/**
* Get the palette at index n
* @param {number} n index of the palette
* @returns {Palette} palette at index n
*/
getPalette(n) {
if (n < 0 || n > this._palettes.length - 1)
throw new Error("Palette index out of bounds");
return new Palette(this._palettes[n].colors.map((c) => c.copy()));
}
/**
* Get the number of palettes in the factory
* @returns {number} number of palettes in the factory
*/
get length() {
return this._palettes.length;
}
/**
* Get all palettes in the factory
* @returns {Array.<Palette>} array of palettes in the factory
*/
get palettes() {
return this._palettes.map((p) => p.copy());
}
}
export { Palette, GradientPalette, PaletteFactory };