pencil-js/pencil.js

View on GitHub
src/modules/color/color.js

Summary

Maintainability
C
7 hrs
Test Coverage
A
100%
import { colord, extend } from "colord";
import namesPlugin from "colord/plugins/names";
import mixPlugin from "colord/plugins/mix";
import { truncate, constrain, degreeCircle } from "@pencil.js/math";

extend([namesPlugin, mixPlugin]);

/**
 * @module Color
 */

const parsedKey = Symbol("_parsed");

/**
 * 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"); // Any valid CSS color name
     * 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[parsedKey] = null;
        this.set(...colorDefinition);
    }

    /**
     * Create a new copy of this color
     * @return {Color}
     */
    clone () {
        return new Color(this);
    }

    /**
     * Get the red channel value
     * @return {Number}
     */
    get red () {
        return this[parsedKey].rgba.r / 255;
    }

    /**
     * Set the red channel
     * @param {Number} value - New value
     */
    set red (value) {
        this[parsedKey].rgba.r = constrain(value * 255, 0, 255);
    }

    /**
     * Get the green channel value
     * @return {Number}
     */
    get green () {
        return this[parsedKey].rgba.g / 255;
    }

    /**
     * Set the green channel
     * @param {Number} value - New value
     */
    set green (value) {
        this[parsedKey].rgba.g = constrain(value * 255, 0, 255);
    }

    /**
     * Get the blue channel value
     * @return {Number}
     */
    get blue () {
        return this[parsedKey].rgba.b / 255;
    }

    /**
     * Set the blue channel
     * @param {Number} value - New value
     */
    set blue (value) {
        this[parsedKey].rgba.b = constrain(value * 255, 0, 255);
    }

    /**
     * Get the transparency channel value
     * @return {Number}
     */
    get alpha () {
        return this[parsedKey].rgba.a;
    }

    /**
     * Set the transparency channel
     * @param {Number} value - New value
     */
    set alpha (value) {
        this[parsedKey].rgba.a = constrain(value, 0, 1);
    }

    /**
     * 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 hex () {
        return this[parsedKey].toHex();
    }

    /**
     * Return hexadecimal rgb notation
     * @example "#123456"
     * @return {String}
     */
    get rgb () {
        return this.hex;
    }

    /**
     * Return the closest CSS color name
     * @example "aliceblue"
     * @return {String}
     */
    get name () {
        return this[parsedKey].toName({
            closest: true,
        });
    }

    // 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) {
        const [first] = colorDefinition;
        let input;
        let alpha;
        if (first instanceof Color) {
            input = first[parsedKey].rgba;
            [, alpha = input.a] = colorDefinition;
        }
        else if (typeof first === "string") {
            input = first;
            [, alpha = 1] = colorDefinition;
        }
        else if (first === undefined || first === null) {
            input = "#000";
            alpha = 1;
        }
        else if (colorDefinition.length < 3) {
            input = `#${truncate(colorDefinition[0]).toString(16)}`;
            [, alpha = 1] = colorDefinition;
        }
        else {
            const [r, g, b] = colorDefinition.slice(0, 3).map(value => value * 255);
            [,,, alpha = 1] = colorDefinition;
            input = {
                r,
                g,
                b,
            };
        }
        this[parsedKey] = colord(input).alpha(alpha);
        return this;
    }

    /**
     * Change to its greyscale value
     * @return {Color} Itself
     */
    grey () {
        this[parsedKey] = this[parsedKey].grayscale();
        return this;
    }

    /**
     * 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) {
        this[parsedKey] = this[parsedKey].hue(value * degreeCircle);
        return this;
    }

    /**
     * Change saturation value (0 = grey, 1 = pure color)
     * @param {Number} value - Any value between 0 and 1
     * @return {Color} Itself
     */
    saturation (value) {
        const hsl = this[parsedKey].toHsl();
        hsl.s = value * 100;
        this[parsedKey] = colord(hsl);
        return this;
    }

    /**
     * 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 hsl = this[parsedKey].toHsl();
        hsl.l = value * 100;
        this[parsedKey] = colord(hsl);
        return this;
    }

    /**
     * Invert the color value
     * @return {Color} Itself
     */
    reverse () {
        this[parsedKey] = this[parsedKey].invert();
        return this;
    }

    /**
     * Restrict the color space to an amount of possible value
     * @param {Number} number - Number of allowed value
     * @return {Color} Itself
     */
    level (number) {
        const { r, g, b } = this[parsedKey].rgba;
        const p = 255 / number;
        this[parsedKey].rgba.r = truncate(r / p) * p + (p / 2);
        this[parsedKey].rgba.g = truncate(g / p) * p + (p / 2);
        this[parsedKey].rgba.b = truncate(b / p) * p + (p / 2);
        return this;
    }

    /**
     * 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);
        this[parsedKey] = this[parsedKey].mix(color[parsedKey], ratio);
        return this;
    }

    /**
     * @return {String}
     */
    toString () {
        return this.hex;
    }

    /**
     * Return a json ready array
     * @return {Array<Number>}
     */
    toJSON () {
        return [
            ...this.array,
            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) {
            return param;
        }
        return new Color(...colorDefinition);
    }
}

Color.prototype.gray = Color.prototype.grey;
Color.prototype.mix = Color.prototype.lerp;