Source: Graphable.js

// noinspection JSUnusedGlobalSymbols

import {round} from '@backstrap/math';
import {cleanCopy} from './helpers';
import {Coords} from './Coords';

/**
 * Convert color properties to a CSS color string.
 * @param {Object} color
 * @param {number} color.hue - color hue (0-360)
 * @param {number} color.saturation - color saturation percentage
 * @param {number} color.lightness - color lightness percentage
 * @returns {string}
 * @private
 */
const hslColor = function ({hue, saturation, lightness}) {
    return `hsl(${hue},${saturation}%,${lightness}%)`;
}

/**
 * Class representing a graphable object.
 * Provides support for graphing options including color and opacity,
 * in addition to a local coordinate system via its parent class, Coords.
 * @extends Coords
 */
export class Graphable extends Coords {
    /** @type {geometryOptions} */
    #options = {
        color: 'hsl(200,95%,50%)',
    };

    #color = {
        hue: 200,
        saturation: 95,
        lightness: 50,
    }

    /**
     * @param {Graphable|Coords} [parent]
     */
    constructor(parent = null) {
        super(parent);
        this.#options = parent && parent.#options ? parent.options() : this.#options;
        this.#color = parent && parent.#color ? {...parent.#color} : this.#color;
    }

    /**
     * Returns a deep clone of the options object.
     * @returns {geometryOptions}
     */
    options() {
        return cleanCopy(this.#options);
    }

    /**
     * @param {geometryOptions} options
     * @returns {this}
     */
    setOptions(options) {
        this.#options = Object.assign({}, this.#options, options);
        return this;
    }

    /**
     * Should only be given settings for object-valued options.
     * @param {geometrySubOptions} options
     * @returns {this}
     */
    setSubOptions(options) {
        const updated = {};
        Object.keys(options).forEach(
            key => updated[key] = Object.assign({}, this.#options[key], options[key])
        );
        this.#options = Object.assign({}, this.#options, updated);
        return this;
    }

    /**
     * @param {number} opacity
     * @returns {this}
     */
    opacity(opacity) {
        return this.setOptions({opacity});
    }

    /**
     * @param {string} color
     * @returns {this}
     */
    color(color) {
        return this.setOptions({color});
    }

    /**
     * @param {number} [value] - grey value (0-100)
     * @returns {this}
     */
    grey(value = 0) {
        this.#color.saturation = 0;
        this.#color.lightness = round(1000*value)/1000;
        return this.color(hslColor(this.#color));
    }

    /**
     * @param {number} [hue] - hue as degrees on a color wheel (0-360)
     * @returns {this}
     */
    hue(hue = 0) {
        this.#color.hue = round(1000*hue)/1000;
        return this.color(hslColor(this.#color));
    }

    /**
     * @param {number} [saturation] - saturation percentage
     * @returns {this}
     */
    saturation(saturation = 0) {
        this.#color.saturation = round(1000*saturation)/1000;
        return this.color(hslColor(this.#color));
    }

    /**
     * @param {number} [lightness] - lightness percentage
     * @returns {this}
     */
    lightness(lightness = 0) {
        this.#color.lightness = round(1000*lightness)/1000;
        return this.color(hslColor(this.#color));
    }
}