UNPKG

pencil.js

Version:

Nice modular interactive 2D drawing library.

276 lines (251 loc) 8.47 kB
import convert from "color-convert"; import { constrain, truncate, average, equals, lerp } from "@pencil.js/math"; /** * @module Color */ /** * Turn 2 bits hexadecimal number into a ratio between 0 and 1 * @param {Number} hex - 2 bits hexadecimal number * @param {Number} n - Position of this number in the full chain * @return {Number} */ function hexToRatio (hex, n) { // eslint-disable-next-line no-bitwise return ((hex >> (8 * n)) & 0xff) / 0xff; } /** * Turn a ratio number (between 0 and 1) into a 2 bits hexadecimal integer (between 0 and 255) * @param {Number} ratio - Any number * @return {Number} */ function ratioToNum (ratio) { return truncate((ratio * 0xff) + 0.5); } /** * Turn a ratio number (between 0 and 1) into a 2 bits hexadecimal string * @param {Number} ratio - Any number * @return {String} */ function ratioToHex (ratio) { return ratioToNum(ratio).toString(16).padStart(2, "0"); } /** * Color class * @class */ export default class Color { /** * @typedef {Color|String|Number|Array<Number>} ColorDefinition */ /** * Color constructor * @param {ColorDefinition} colorDefinition - Many types accepted (other Color instance, color name, hex string, hex number, red/green/blue/alpha value) * @example * new Color("indigo"); // All CSS color names * new Color("#123456"); // Hex string definition * new Color("#123"); // Hex shorthand string definition, #123 <=> #112233 * new Color(0x123456); // Hex number definition * new Color(0.1, 0.2, 0.3); // Red, Green, Blue definition * Every definition can have one more optional parameter for alpha (opacity) * new Color("violet", 0.5); */ constructor (...colorDefinition) { this.red = 0; this.green = 0; this.blue = 0; this.alpha = 1; this.set(...colorDefinition); } /** * Create a new copy of this color * @return {Color} */ clone () { return (new Color(this)); } /** * Return an array with red, green and blue value * @example [0.1, 0.2, 0.3] * @return {Array<Number>} */ get array () { return [ this.red, this.green, this.blue, ]; } /** * Return hexadecimal rgb notation * @example "#123456" * @return {String} */ get rgb () { return `#${this.array.map(channel => ratioToHex(channel)).join("")}`; } /** * Return rgba notation * @example "rgba(10,20,30,0.5)" * @return {String} */ get rgba () { return `rgba(${this.array.map(channel => ratioToNum(channel)).concat(this.alpha).join(",")})`; } /** * Return the closest CSS color name * @example "aliceblue" * @return {String} */ get name () { return convert.rgb.keyword(this.array.map(channel => ratioToNum(channel))); } // TODO: do we need more getters ? User only need to interact with Color, not read values. /** * Change this values * @param {ColorDefinition} colorDefinition - Any supported color definition (see constructor) * @return {Color} Itself */ set (...colorDefinition) { if (colorDefinition.length > 0 && colorDefinition.length < 3) { const param = colorDefinition[0]; if (param instanceof Color) { this.red = param.red; this.green = param.green; this.blue = param.blue; this.alpha = param.alpha; } else { let hex = param; if (typeof param === "string") { if (param.startsWith("#")) { const hexString = param.substr(1); const str = hexString.length < 4 ? hexString.split("").map(char => char.repeat(2)).join("") : hexString; hex = Number.parseInt(str, 16); } else { const rgb = convert.keyword.rgb(param.toLocaleLowerCase()) || [0, 0, 0]; this.red = rgb[0] / 0xff; this.green = rgb[1] / 0xff; this.blue = rgb[2] / 0xff; } } if (typeof hex === "number") { this.red = hexToRatio(hex, 2); this.green = hexToRatio(hex, 1); this.blue = hexToRatio(hex, 0); } const alpha = colorDefinition[1]; if (alpha !== undefined) { this.alpha = constrain(alpha, 0, 1); } } } else if (colorDefinition.length > 2) { this.red = constrain(colorDefinition[0], 0, 1); this.green = constrain(colorDefinition[1], 0, 1); this.blue = constrain(colorDefinition[2], 0, 1); const alpha = colorDefinition[3]; if (alpha !== undefined) { this.alpha = constrain(alpha, 0, 1); } } return this; } /** * Change to its greyscale value * @return {Color} Itself */ grey () { const weights = [0.299, 0.587, 0.114]; const target = average(...this.array.map((channel, index) => channel * weights[index])); return this.set(target, target, target); } /** * Change hue value (0 = red, 0.5 = blue, 1 = red, 1.5 = blue ...) * @param {Number} value - Any value between 0 and 1 * @return {Color} Itself */ hue (value) { const hsl = convert.rgb.hsl(this.array.map(channel => ratioToNum(channel))); hsl[0] = (value % 1) * 360; return this.set(...convert.hsl.rgb(hsl).map(channel => channel / 0xff)); } /** * Change saturation value (0 = grey, 1 = pure color) * @param {Number} value - Any value between 0 and 1 * @return {Color} Itself */ saturation (value) { const { array } = this; const target = average(Math.min(...array), Math.max(...array)); return this.set(...array.map(channel => target + (value * (channel - target)))); } /** * Change lightness value (0 = black, 0.5 = pure color, 1 = white) * @param {Number} value - Any value between 0 and 1 * @return {Color} Itself */ lightness (value) { const fn = value < 0.5 ? channel => channel * value : channel => channel + ((1 - channel) * (value - 0.5) * 2); return this.set(...this.array.map(fn)); } /** * Invert the color value * @return {Color} Itself */ reverse () { return this.set(...this.array.map(channel => 1 - channel)); } /** * Restrict color's channels to set amount of value * @param {Number} number - Number of allowed value on each channel * @return {Color} Itself */ level (number) { return this.set(...this.array.map(channel => truncate((channel * number) + 1) / (number + 1))); } /** * Change the color toward another color * @param {ColorDefinition} colorDefinition - Any other color * @param {Number} ratio - Ratio of distance to move (0 = no change, 0.5 = equal mix, 1 = same as target color) * @return {Color} Itself */ lerp (colorDefinition, ratio) { const color = Color.from(colorDefinition); const thisArray = this.array.concat(this.alpha); const colorArray = color.array.concat(color.alpha); return this.set(...thisArray.map((channel, index) => lerp(channel, colorArray[index], ratio))); } /** * @return {String} */ toString () { if (equals(this.alpha, 1)) { return this.rgb; } return this.rgba; } /** * Return a json ready array * @return {Array<Number>} */ toJSON () { return this.array.concat(this.alpha); } /** * Return an instance from a generic definition * @param {ColorDefinition} colorDefinition - Any valid color definition (see constructor) * @return {Color} */ static from (...colorDefinition) { const param = colorDefinition[0]; if (param instanceof Color || param === null) { return param; } return new Color(...colorDefinition); } }