ColorExtension
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © RicardoSantos

//@version=5

// @description Extra methods for Color manipulation and profiling.
library(title='ColorExtension')

// reference:
//      https://tallys.github.io/color-theory/
//      https://www.canva.com/colors/color-wheel/
//      https://webdesign.tutsplus.com/tutorials/creating-color-schemes-with-less-color-functions--cms-23668

// @function HSL color transform.
// @param hue float, hue color component, hue is a degree on the color wheel from 0 to 360. 0 is red, 120 is green, 240 is blue.
// @param saturation float, saturation color component, saturation is a percentage value, 0 means a shade of gray and 100 is the full color.
// @param lightness float, lightness color component, Lightness is also a percentage; 0 is black, 100 is white.
// @param transparency float, transparency color component, transparency is also a percentage; 0 is opaque, 100 is transparent.
// @returns color
export hsl (float hue=0.0, float saturation=0.0, float lightness=0.0, float transparency=0.0) =>  //{
    switch
        hue < 0.0 or hue > 360.0 => runtime.error(str.format('ColorExtension -> hsl(): "hue" parameter is out of range, expecting 0-360, received: {0}', hue))
        saturation < 0.0 or saturation > 100.0 => runtime.error(str.format('ColorExtension -> hsl(): "saturation" parameter is out of range, expecting 0-100, received: {0}', saturation))
        lightness < 0.0 or lightness > 100.0 => runtime.error(str.format('ColorExtension -> hsl(): "lightness" parameter is out of range, expecting 0-100, received: {0}', lightness))
        transparency < 0.0 or transparency > 100.0 => runtime.error(str.format('ColorExtension -> hsl(): "transparency" parameter is out of range, expecting 0-100, received: {0}', transparency))
    float _h_30 = hue / 30.0
    float _l1 = lightness * 0.01
    float _a = saturation * math.min(_l1, 1.0 - _l1) * 0.01
    float _rk = _h_30 % 12.0
    float _r = 255.0 * (_l1 - _a * math.max(math.min(_rk - 3.0, 9.0 - _rk, 1.0), -1.0))
    float _gk = (8.0 + _h_30) % 12.0
    float _g = 255.0 * (_l1 - _a * math.max(math.min(_gk - 3.0, 9.0 - _gk, 1.0), -1.0))
    float _bk = (4.0 + _h_30) % 12.0
    float _b = 255.0 * (_l1 - _a * math.max(math.min(_bk - 3.0, 9.0 - _bk, 1.0), -1.0))
    color.rgb(_r, _g, _b, transparency)
//{ usage:
// h = input.int(defval=127)//, minval=0, maxval=360)
// s = input.int(defval=50)//, minval=0, maxval=100)
// l = input.int(defval=50)//, minval=0, maxval=100)
// t = input.int(defval=0)//, minval=0, maxval=100)
// if barstate.islast
//     line.new(bar_index, 0., bar_index[1], 0., extend=extend.both, color=hsl(h, s, l, t), width=20)
//{ remarks:
//      https://stackoverflow.com/questions/36721830/convert-hsl-to-rgb-and-hex
//      Thanks to someguy for code review.
//}}}

// @function Convert RGB to HSL color values
// @param red float, red color component.
// @param green float, green color component.
// @param blue float, blue color component.
// @returns tuple with 3 float values, hue, saturation and lightness.
export rgb_to_hsl (float red=0.0, float green=0.0, float blue=0.0) =>  //{
    switch
        0 > red   or red   > 255 => runtime.error(str.format('ColorExtension -> rgb_to_hsl(): "red" parameter is out of range, expecting 0-255, received: {0}',     red))
        0 > green or green > 255 => runtime.error(str.format('ColorExtension -> rgb_to_hsl(): "green" parameter is out of range, expecting 0-255, received: {0}', green))
        0 > blue  or blue  > 255 => runtime.error(str.format('ColorExtension -> rgb_to_hsl(): "blue" parameter is out of range, expecting 0-255, received: {0}',   blue))
    float _r1 = red / 255.0
    float _g1 = green / 255.0
    float _b1 = blue / 255.0
    float _max_color = math.max(_r1, _g1, _b1)
    float _min_color = math.min(_r1, _g1, _b1)
    float _diff = _max_color - _min_color
    float _H = 0.0
    float _S = 0.0
    float _L = (_max_color + _min_color) / 2.0
    if _max_color != _min_color
        // saturation calculation:
        switch
            (_L <= 0.5) => _S := _diff / (_max_color + _min_color)
            => _S := _diff / (2.0 - _max_color - _min_color) 
        // hue calculation:
        switch (_max_color)
            _r1 => _H := 60.0 * ((_g1 - _b1) / _diff)
            _g1 => _H := 60.0 * (2.0 + (_b1 - _r1) / _diff)
            => _H := 60.0 * (4.0 + (_r1 - _g1) / _diff)
    if _H < 0.0
        _H += 360.0
    [_H, _S * 100.0, _L * 100.0]
//{ usage:
// int r = input(0), int g = input(0), int b = input(0)
// [th, ts, tl] = rgb_to_hsl(r, g, b)
// if barstate.islast
//     label.new(bar_index, 10., str.format('rgb ({0}, {1}, {2})', r, g, b), color=color.rgb(r, g, b, 0))
//     label.new(bar_index, 0., str.format('hsl ({0}, {1}, {2})', th, ts, tl), color=hsl(th, ts, tl, 0))
//{ remarks:
//      https://gmigdos.wordpress.com/2011/01/13/javascript-convert-rgb-values-to-hsl/
//      https://www.niwa.nu/2013/05/math-behind-colorspace-conversions-rgb-hsl/
//}}}

// @function Convert the provided color into HSL values.
// @param base color, base color.
// @returns tuple with 3 floats.
export color_to_hsl (color base) => //{
    rgb_to_hsl(color.r(base), color.g(base), color.b(base))
// { usage:
// { remarks:
//}}}

// @function Shift the hue/angle of the provided color.
// @param base color, base color.
// @param angle float, angle to shift the hue of the base color.
// @returns color.
export shift_hue (color base, float angle) => //{
    [_h, _s, _l] = color_to_hsl(base)
    hsl((_h + angle) % 360, _s, _l, color.t(base))
// { usage:
// color primary = input.color(color.red)
// float angle = input.float(45.0)
// if barstate.islastconfirmedhistory
//     label.new(bar_index, 10.0, '▆▆▆▆', color=primary, textcolor=shift_hue(primary, angle))
// { remarks:
//      Thanks to Vlad for bringing it up.
//}}}

// @function Saturate the provided color by a rate of the difference to its maximum.
// @param base color, base color.
// @param rate float, default=0.5, rate to saturate the base color (the higher the saturation of the color the less the effect).
// @returns color.
export saturate (color base, float rate=0.5) => //{
    if (rate > 1.0) or (rate < 0.0)
        runtime.error('ColorExtension -> saturate(): "rate" must be between 0.0 and 1.0')
    [_h, _s, _l] = color_to_hsl(base)
    hsl(_h, _s + (100-_s) * rate, _l, color.t(base))
// { usage:
// color primary = input.color(color.red)
// float rate = input.float(0.50)
// if barstate.islastconfirmedhistory
//     label.new(bar_index, 10.0, '▆▆▆▆', color=primary, textcolor=saturate(primary, rate))
// { remarks:
//}}}

// @function Desaturate the provided color by a rate of the difference to its minimum.
// @param base color, base color.
// @param rate float, default=0.5, rate to desaturate the base color (the lower the saturation of the color the less the effect).
// @returns color.
export desaturate (color base, float rate=0.5) => //{
    if (rate > 1.0) or (rate < 0.0)
        runtime.error('ColorExtension -> saturate(): "rate" must be between 0.0 and 1.0')
    [_h, _s, _l] = color_to_hsl(base)
    hsl(_h, _s - _s * rate, _l, color.t(base))
// { usage:
// color primary = input.color(color.red)
// float rate = input.float(0.50)
// if barstate.islastconfirmedhistory
//     label.new(bar_index, 10.0, '▆▆▆▆', color=primary, textcolor=desaturate(primary, rate))
// { remarks:
//}}}

// @function lighten the provided color by a rate of the difference to its maximum.
// @param base color, base color.
// @param rate float, default=0.5, rate to lighten the base color (the higher the lightness of the color the less the effect).
// @returns color.
export lighten (color base, float rate=0.5) => //{
    if (rate > 1.0) or (rate < 0.0)
        runtime.error('ColorExtension -> lighten(): "rate" must be between 0.0 and 1.0')
    [_h, _s, _l] = color_to_hsl(base)
    hsl(_h, _s, _l + (100.0 - _l) * rate, color.t(base))
// { usage:
// color primary = input.color(color.red)
// float rate = input.float(0.50)
// if barstate.islastconfirmedhistory
//     label.new(bar_index, 10.0, '▆▆▆▆', color=primary, textcolor=lighten(primary, rate))
// { remarks:
//}}}

// @function darken the provided color by a rate of the difference to its minimum.
// @param base color, base color.
// @param rate float, default=0.5, rate to darken the base color (the lower the lightness of the color the less the effect).
// @returns color.
export darken (color base, float rate=0.5) => //{
    if (rate > 1.0) or (rate < 0.0)
        runtime.error('ColorExtension -> darken(): "rate" must be between 0.0 and 1.0')
    [_h, _s, _l] = color_to_hsl(base)
    hsl(_h, _s, _l - _l * rate, color.t(base))
// { usage:
// color primary = input.color(color.red)
// float rate = input.float(0.50)
// if barstate.islastconfirmedhistory
//     label.new(bar_index, 10.0, '▆▆▆▆', color=primary, textcolor=darken(primary, rate))
// { remarks:
//}}}

// @function Complementary of selected color
// @param primary color, the primary
// @returns color.
export complement (color primary) => //{
    shift_hue(primary, 180.0)
//{ usage:
// color primary = input.color(color.red)
// if barstate.islastconfirmedhistory
//     label.new(bar_index, 10.0, '▆▆▆▆', color=primary, textcolor=complement(primary))
//{ remarks:
//}}}

// @function Inverts selected color.
// @param primary color, the primary.
// @returns color.
export invert (color primary) => //{
    [_h, _s, _l] = color_to_hsl(primary)
    hsl((360 + _h + 180.0) % 360.0, 100.0 - _l, _s, color.t(primary))
//{ usage:
// color primary = input.color(color.red)
// if barstate.islastconfirmedhistory
//     label.new(bar_index, 10.0, '▆▆▆▆', color=primary, textcolor=complement(primary))
//{ remarks:
//}}}

// @function Color is cool or warm.
// @param base color, the color to check.
// @returns bool.
export is_cool (color base) => //{
    [_h, _, _] = color_to_hsl(base)
    _h > 120.0 and _h < 300.0
//{ usage:
// color primary = input.color(color.red)
// if barstate.islastconfirmedhistory
//     label.new(bar_index, 0.0, str.format('color is {0}.', is_cool(primary) ? 'cool' : 'warm'),color=primary, textcolor=complement(primary))
//{ remarks:
//}}}

// @function Color temperature.
// @param base color, the color to check.
// @returns bool.
export temperature (color base) => //{
    float _r = color.r(base)
    float _g = color.g(base)
    float _b = color.b(base)
    // // convert rgb to tristimulus values:
    // float _x = (-0.14282 * _r) + (1.54924 * _g) + (-0.95641 * _b)
    // float _y = (-0.32466 * _r) + (1.57837 * _g) + (-0.73191 * _b) // illuminance
    // float _z = (-0.68202 * _r) + (0.77073 * _g) + (0.56332 * _b)
    // float _xyz = _x + _y + _z
    // // calculate the normalized chromacity values:
    // float _u = _x / _xyz
    // float _v = _y / _xyz
    // // compute CCT value:
    // float _n = (_u - 0.3320) / (0.1858 - _v)
    // float _cct = 449.0 * math.pow(_n, 3.0) + 3525.0 * math.pow(_n, 2.0) + 6823.3 * _n + 5520.33
    // faster compressed algorithm:
    float _n = ((0.23881) * _r + (0.25499) * _g + (-0.58291) * _b) / ((0.11109) * _r + (-0.85406) * _g + (0.52289) * _b)
    449.0 * math.pow(_n, 3.0) + 3525 * math.pow(_n, 2.0) + 6823.3 * _n + 5520.33
//{ usage:
// color primary = input.color(color.red)
// if barstate.islastconfirmedhistory
//     label.new(bar_index, 0.0, str.format('color is {0}, {1}.', is_cool(primary) ? 'cool' : 'warm', temperature(primary)),color=primary, textcolor=complement(primary))
//{ remarks:
//      https://stackoverflow.com/questions/38876429/how-to-convert-from-rgb-values-to-color-temperature/38889696
//      https://dsp.stackexchange.com/questions/8949/how-to-calculate-the-color-temperature-tint-of-the-colors-in-an-image
//}}}

// @function Color is high key (orange yellow green).
// @param base color, the color to check.
// @returns bool.
export is_high_key (color base) => //{
    [_h, _, _] = color_to_hsl(base)
    _h > 30.0 and _h < 140.0
//{ usage:
// color primary = input.color(color.red)
// if barstate.islastconfirmedhistory
//     label.new(bar_index, 0.0, str.format('color is {0}.', is_high_key(primary) ? 'high' : 'low'),color=primary, textcolor=complement(primary))
//{ remarks:
//}}}

// @function Mix two colors together.
// @param base color, the base color.
// @param mix color, the color to mix.
// @param rate float, default=0.5, the rate of mixture, range within 0.0 and 1.0.
// @returns color.
export mix (color base, color mix, float rate=0.5) => //{
    if rate <= 0.0 or rate >= 1.0
        runtime.error('ColorExtension -> mix(): "rate" must be within 0.0 and 1.0')
    float _t0 = color.t(base)
    float _t1 = color.t(mix)
    [_h0, _s0, _l0] = color_to_hsl(base)
    [_h1, _s1, _l1] = color_to_hsl(mix)
    hsl(_h0 + (_h1 - _h0) * rate  , _s0 + (_s1 - _s0) * rate, _l0 + (_l1 - _l0) * rate, _t0 + (_t1 - _t0) * rate)
//{ usage:
// color base = input.color(color.red)
// color mix_col = input.color(color.red)
// float rate = input.float(1.0)
// if barstate.islastconfirmedhistory
//     label.new(bar_index, 0.0, 'base color.', color=base)
//     label.new(bar_index+10, 0.0, 'mix color.', color=mix_col)
//     label.new(bar_index+20, 0.0, 'result.', color=mix(base, mix_col, rate))
//{ remarks:
//      https://stackoverflow.com/questions/14819058/mixing-two-colors-naturally-in-javascript
//}}}

// @function Selects 2 near spectrum colors (H +/- 45).
// @param primary color, the base color.
// @returns tuple with 2 colors.
export analog (color primary) => //{
    [shift_hue(primary, -45.0), shift_hue(primary, 45.0)]
//{ usage:
// color base = input.color(color.red)
// [min, max] = analog(base)
// if barstate.islastconfirmedhistory
//     label.new(bar_index, 0.0, 'lower.', color=min)
//     label.new(bar_index+10, 0.0, 'primary.', color=base)
//     label.new(bar_index+20, 0.0, 'upper.', color=max)
//{ remarks:
//}}}

// @function Selects 2 far spectrum colors (H +/- 120).
// @param primary color, the base color.
// @returns tuple with 2 colors.
export triadic (color primary) => //{
    [shift_hue(primary, -120.0), shift_hue(primary, 120.0)]
//{ usage:
// color base = input.color(color.red)
// [min, max] = triadic(base)
// if barstate.islastconfirmedhistory
//     label.new(bar_index, 0.0, 'lower.', color=min)
//     label.new(bar_index+10, 0.0, 'primary.', color=base)
//     label.new(bar_index+20, 0.0, 'upper.', color=max)
//{ remarks:
//}}}

// @function Uses primary and the complementary color, + 60º to form a rectangular pattern on the color wheel.
// @param primary color, the base color.
// @returns tuple with 3 colors.
export tetradic (color primary) => //{
    [shift_hue(primary, 60.0), shift_hue(primary, 180.0), shift_hue(primary, 240.0)]
//{ usage:
// color base = input.color(color.red)
// [comp, min, max] = tetradic(base)
// if barstate.islastconfirmedhistory
//     label.new(bar_index, 0.0, 'lower.', color=min)
//     label.new(bar_index+10, 0.0, 'primary.', color=base)
//     label.new(bar_index+10, 10.0, 'complementary.', color=comp)
//     label.new(bar_index+20, 0.0, 'upper.', color=max)
//{ remarks:
//}}}

// @function Uses primary and generate 3 equally spaced (90º) colors.
// @param primary color, the base color.
// @returns tuple with 3 colors.
export square (color primary) => //{
    [shift_hue(primary, 90.0), shift_hue(primary, 180.0), shift_hue(primary, 270.0)]
//{ usage:
// color base = input.color(color.red)
// [comp, min, max] = square(base)
// if barstate.islastconfirmedhistory
//     label.new(bar_index, 0.0, 'lower.', color=min)
//     label.new(bar_index+10, 0.0, 'primary.', color=base)
//     label.new(bar_index+10, 10.0, 'complementary.', color=comp)
//     label.new(bar_index+20, 0.0, 'upper.', color=max)
//{ remarks:
//}}}


// @function Pick color at value in a triangular gradient
// @param value float, value to pick the color in the gradient.
// @param mid_point float, percent at wich the triangular pattern peaks.
// @param min_color color, lowest color in the gradient.
// @param mid_color color, peak color in the gradient.
// @param max_color color, highest color in the gradient.
// @returns color
gradient_triangular(float value=0.0, float mid_point=50.0, color min_color=color.black, color mid_color=color.white, color max_color=color.black) =>  //{
//  @function: finds the color output of a triangular gradient
    color _col = na
    if value <= mid_point
        _col := color.from_gradient(value, 0.0, mid_point, min_color, mid_color)
        _col
    else
        _col := color.from_gradient(value, mid_point, 100.0, mid_color, max_color)
        _col
    _col
//{ usage:
// r1 = ta.rsi(close, 3)
// c1 = gradient_triangular(r1, 20, color.red, color.yellow, color.lime)
// c2 = gradient_triangular(r1, 50, color.red, color.yellow, color.lime)
// c3 = gradient_triangular(r1, 80, color.red, color.yellow, color.lime)
// plot(r1, color=c1, linewidth=15)
// plot(r1, color=c2, linewidth=10)
// plot(r1, color=c3, linewidth=5)
//{ remarks:
//}}}

