Joining RGB to HSV#

combined rgb and hsv

First ensure that the HSV schema fits together with the RGB schema. As none of the HSV widgets have a name clash with the RGB widgets it should be fairly straightforward. Copy the file 07entryscalemod.py as our start point, into this file bring in the hsv standalone functions.

Import the math functions. Remove LerpHex, add circle, hue_gradient, hsv_to_rgb, polar2cart, cart2polar and replace sb_okay. Modify the Spinboxes to include the upper limit 255 and change the handle functions to make the tk variable an integer. Test the rgb side.

TtkScale remains the same. Change the class name RgbSeleect to RgbHsvSelect, add the hsv tk variables, add the wheel constants, add the initial settings for hsv tkvariables. Add the hsv handle functions and door_bell. All the hsv build should be able to be added to the existing build. Add the click and drag functions. Add the two check functions.

Run the program, the rgb side needs to be adjusted to move to the top of the frame. Change the rgb labelframe to have a sticky 'n', and the opacity label frame no longer has self.fr as its parent but the rgb labelframe fr1, which means it has to be below the blue spacing label and span all 3 columns.

Join the RGB HSV Output#

At present the two sides run independantly, so make an rgb change known to hsv and vice versa. A supervisory handle is required that receives calls whenever one of the tk variables triggers the individual handles, or when the hash value is changed. Alpha will not trigger a change. Whenever ring is changed we call door_bell which can be linked to the supervisory handle, overlord. As the hsv side already moves the cursor it is only necessary to link the rgb side to the cursor. Remember that the gradients need to be redrawn by overlord:

def overlord(self, rgb=None, hsv=None):
# calls from handles
if rgb:
    red, green, blue = rgb[0], rgb[1], rgb[2]
    hue, sat, value = rgb_to_hsv(red, green, blue)
    from_colour = hsv_to_rgb(*(hue, 0, value))
    to_colour = hsv_to_rgb(*(hue, 100, value))
    draw_gradient(
        self.scan,
        from_colour,
        to_colour,
        width=self.canvas_w, height=self.canvas_h)
    from_colour = hsv_to_rgb(*(hue, sat, 0))
    to_colour = hsv_to_rgb(*(hue, sat, 100))
    draw_gradient(
        self.vcan,
        from_colour,
        to_colour,
        width=self.canvas_w, height=self.canvas_h)
    self.hvar.set(hue)
    self.svar.set(sat)
    self.vvar.set(value)
    X, Y = polar2cart(hue, sat, self.wheel_w, self.wheel_iw)
    ring_radius = self.ring_radius
    for i in self.can_hsv.find_withtag("ring"):
                self.can_hsv.coords(
                i,
                X - ring_radius,
                Y - ring_radius,
                X + ring_radius,
                Y + ring_radius)

elif hsv:
    hue, sat, value = hsv[0], hsv[1], hsv[2]
    red, green, blue = hsv_to_rgb(hue, sat, value)
    draw_agradient(self.acan, (127, 127, 127), (red, green, blue),
                   self.e, width=self.canvas_w, height=self.canvas_h)
    alpha = self.avar.get()
    vdraw_gradient(self.cmcan, (red, green, blue), self.e, alpha=alpha,
                   width=self.canvas_b, height=self.canvas_b)
    draw_gradient(self.rcan, (0, green, blue), (255, green, blue),
                  width=self.canvas_w, height=self.canvas_h)
    draw_gradient(self.gcan, (red, 0, blue), (red, 255, blue),
                  width=self.canvas_w, height=self.canvas_h)
    draw_gradient(self.bcan, (red, green, 0), (red, green, 255),
                  width=self.canvas_w, height=self.canvas_h)
    self.evar.set(rgb2hash(red, green, blue))
    self.rvar.set(red)
    self.gvar.set(green)
    self.bvar.set(blue)

A typical call to overlord would be:

self.overlord(hsv=(hue, sat, value))

Note

It is important not to have changes creating feedback from one side to the other. Make sure that updating the tk variables does not propagate further changes.

We require a function to convert rgb to hsv, this is also copied from colorsys with a normalised input and a denormalised output:

rgb_to_hsv(red, green, blue):
    red = min(max(red, 0), 255) / 255
    green = min(max(green, 0), 255) / 255
    blue = min(max(blue, 0), 255) / 255
    maxc = max(red, green, blue)
    minc = min(red, green, blue)
    value = maxc
    if minc == maxc:
        return 0.0, 0.0, value
    sat = int(((maxc - minc) / maxc) * 100 + 0.5)
    rc = (maxc - red) / (maxc - minc)
    gc = (maxc - green) / (maxc - minc)
    bc = (maxc - blue) / (maxc - minc)
    if red == maxc:
        hue = bc - gc
    elif green == maxc:
        hue = 2.0 + rc - bc
    else:
        hue = 4.0 + gc - rc
    hue = (hue / 6.0) % 1.0

    return int(hue * 360 + 0.5), sat, int(value * 100 + 0.5)

Next change labels from tkinter to a themed type, so that they blend in with the background.

Run this and you should see the cursor move when either the hsv side or rgb side is altered. Any change to the rgb or hsv scales should result in gradient and final colour changes on both sides - quite fun really.

Resizing the Widgets#

If we had been using pack then all the container widgets would need the fill and expand options, but as we are using the grid management system, it is a bit more complicated. All widgets that adjust horizontally require a sticky 'ew' option, so if it has a 'n' it becomes 'new', existing 'ew' stay. All container widgets also require sticky 'ew', this does not apply to root. We now have to inform all container widgets which columns are required to expand. In our case the columns containing the gradients and scales for the colour components need to expand. Columns containing labels or the final colour and its related colour can stay a constant width.

Use w.columnconfigure(0, weight=1) where 0 is the relevant column for the widget w and weight is the proportionality of expansion for each column. If this is omitted the columns cannot adjust when the window is changed in size. For simple widgets this is all that is needed, but if the widget contains an image then the image must change at the same rate as the column is being changed. The easiest method is to add a bind to the relevant widget, in our case the Canvas. We require a function to be triggered whenever the configuration alters w.bind("<Configure>", self.function).

Within 12rgbandhsv.py we can use separate functions for each canvas, so that only the relevant gradient is redrawn for each canvas. As each canvas is being adjusted sequentially this makes sense. Within the bind function the new width of the canvas is discovered, then this is used to redraw the gradient. At the same time remember to change the default canvas width's value. The redrawn gradient is then imported as normal into canvas. Ensure that none of the default widths are reactivated, To prevent the colour wheel being changed alter its grid layout, start it from column 0 (the label column), centralise it on column 1 by using columnspan=3.

After changing from fixed sizes widgets to that can expand horizontally the relative sizes of the canvas and scale alters. Unless corrected the canvas expands to the size of the scale, so add horizontal padding, half the slider length, to the canvas.

combined rgb and hsv with related colours

Combined RGB and HSV with Related Colours#

Show/Hide Code 12rgbandhsv.py

""" Joining hsv gradients and wheel to rgba gradients
    final cursor
"""

from tkinter import Tk, Canvas, IntVar, Frame, PhotoImage, StringVar
from tkinter.ttk import LabelFrame, Scale, Style, Entry, Spinbox, Label
from PIL import Image, ImageDraw, ImageTk
from colourTools import rgb2hash, draw_gradient, draw_agradient, vdraw_gradient, \
    hash2rgb, hsv_to_rgb, hue_gradient, circle, polar2cart, cart2polar, \
    rgb_to_hsv, is_okay, sb_okay


class TtkScale(Scale):
    """Class to draw themed Scale widget

    Parameters
    ----------
    parent : str
        parent widget
    from_ : int
        start of scale
    to : int
        end of scale
    length : int
        length in pixels
    orient : str
        orientation
    variable : str
        tk variable
    digits : int
        length variable when converted to string
    tickinterval : float or int
        how many digits show up in tick interval
    sliderlength : int
        what it says
    command : str
        procedure called when slider moves
    """

    def __init__(self, parent, length, from_=0, to=255, orient='horizontal',
                 variable=0, digits=None, tickinterval=None, sliderlength=16,
                 command=None, enlargement=1):
        self.from_ = from_
        self.to = to
        self.variable = variable

        super().__init__(parent, length=length + sliderlength, orient=orient,
                         variable=variable, from_=from_, to=to, command=command)

        self.digits = digits
        self.length = length
        self.e = enlargement

        self.build(parent, from_, to, sliderlength, tickinterval, length)

    def build(self, parent, from_, to, sliderlength, tickinterval, length):
        """Create ticks, as the themed Scale has no option.

        Parameters
        ----------
        parent : str
            parent widget
        from_ : int
            start of scale
        to : int
            end of scale
        length : int
            length in pixels
        tickinterval : float or int

        """

        sc_range = to - from_

        if tickinterval:
            for i in range(from_, to + 2, tickinterval):
                item = Label(parent, text=i)
                item.place(in_=self, bordermode='outside',
                           relx=sliderlength / length / 2 +
                           i / sc_range * (1 - sliderlength / length),
                           rely=1, anchor='n')


class RgbHsvSelect:
    """Class to construct rgb, hsv gradients and hsv colour wheel

    Parameters
    ----------
    fr0 : str
        parent widget

    Returns
    -------
    None
    """

    def __init__(self, fr0, enlargement):
        self.fr0 = fr0
        self.e = enlargement

        self.rvar = IntVar()
        self.gvar = IntVar()
        self.bvar = IntVar()
        self.avar = IntVar()
        self.evar = StringVar()
        self.hvar = IntVar()
        self.svar = IntVar()
        self.vvar = IntVar()

        self.scale_l = 300*self.e
        self.sliderlength = 16*self.e
        self.canvas_w = self.scale_l-self.sliderlength
        self.canvas_h = 26*self.e
        self.canvas_b = 30*self.e

        self.wheel_w = 317*self.e
        self.wheel_iw = 299*self.e
        self.ring_radius = 10*self.e
        self.ring_width = 3*self.e

        self.build()

        self.rvar.set(255)
        self.gvar.set(0)
        self.bvar.set(0)
        self.avar.set(255)
        self.evar.set('#ff0000')
        self.hvar.set(0)
        self.svar.set(100)
        self.vvar.set(100)

    def rhandle(self, evt=None):
        """command callback for red

        Parameters
        ----------
        evt : str
            command handle

        Results
        -------
        None
        """

        red = self.rvar.get()
        self.rvar.set(red)
        green = self.gvar.get()
        blue = self.bvar.get()
        alpha = self.avar.get()
        draw_gradient(self.gcan, (red, 0, blue), (red, 255, blue),
                      width=self.canvas_w, height=self.canvas_h)
        draw_gradient(self.bcan, (red, green, 0), (red, green, 255),
                      width=self.canvas_w, height=self.canvas_h)
        draw_agradient(self.acan, (127, 127, 127), (red, green, blue),
                       self.e, width=self.canvas_w, height=self.canvas_h)
        vdraw_gradient(self.cmcan, (red, green, blue), self.e, alpha=alpha,
                        width=self.canvas_b, height=self.canvas_b)
        self.evar.set(rgb2hash(red, green, blue))
        self.overlord(rgb=(red, green, blue))

    def ghandle(self, evt=None):
        """command callback for green

        Parameters
        ----------
        evt : str
            command handle
        """

        red = self.rvar.get()
        green = self.gvar.get()
        self.gvar.set(green)
        blue = self.bvar.get()
        alpha = self.avar.get()
        draw_gradient(self.rcan, (0, green, blue), (255, green, blue),
                      width=self.canvas_w, height=self.canvas_h)
        draw_gradient(self.bcan, (red, green, 0), (red, green, 255),
                      width=self.canvas_w, height=self.canvas_h)
        draw_agradient(self.acan, (127, 127, 127), (red, green, blue), self.e,
                       width=self.canvas_w, height=self.canvas_h)
        vdraw_gradient(self.cmcan, (red, green, blue), self.e, alpha=alpha,
                        width=self.canvas_b, height=self.canvas_b)
        self.evar.set(rgb2hash(red, green, blue))
        self.overlord(rgb=(red, green, blue))

    def bhandle(self, evt=None):
        """command callback for blue

        Parameters
        ----------
        evt : str
            command handle
        """

        red = self.rvar.get()
        green = self.gvar.get()
        blue = self.bvar.get()
        self.bvar.set(blue)
        alpha = self.avar.get()
        draw_gradient(self.rcan, (0, green, blue), (255, green, blue),
                      width=self.canvas_w, height=self.canvas_h)
        draw_gradient(self.gcan, (red, 0, blue), (red, 255, blue),
                      width=self.canvas_w, height=self.canvas_h)
        draw_agradient(self.acan, (127, 127, 127), (red, green, blue),
                       self.e, width=self.canvas_w, height=self.canvas_h)
        vdraw_gradient(self.cmcan, (red, green, blue), self.e, alpha=alpha,
                        width=self.canvas_b, height=self.canvas_b)
        self.evar.set(rgb2hash(red, green, blue))
        self.overlord(rgb=(red, green, blue))

    def ahandle(self, evt=None):
        """command callback for alpha

        Parameters
        ----------
        evt : str
            command handle
        """

        red = self.rvar.get()
        green = self.gvar.get()
        blue = self.bvar.get()
        alpha = self.avar.get()
        self.avar.set(alpha)
        vdraw_gradient(self.cmcan, (red, green, blue), self.e, alpha=alpha,
                        width=self.canvas_b, height=self.canvas_b)

    def hhandle(self, evt=None):
        """command callback for hue

        Parameters
        ----------
        evt : str
            command handle
        """

        hue = self.hvar.get()
        self.hvar.set(int(0.5 + hue))
        sat = self.svar.get()
        value = self.vvar.get()
        from_colour = hsv_to_rgb(*(hue, 0, value))
        to_colour = hsv_to_rgb(*(hue, 100, value))
        draw_gradient(self.scan, from_colour, to_colour,
                      width=self.canvas_w, height=self.canvas_h)
        from_colour = hsv_to_rgb(*(hue, sat, 0))
        to_colour = hsv_to_rgb(*(hue, sat, 100))
        draw_gradient(self.vcan, from_colour, to_colour,
                      width=self.canvas_w, height=self.canvas_h)
        X, Y = polar2cart(hue, sat, self.wheel_w, self.wheel_iw)
        ring_radius = self.ring_radius
        for i in self.can_hsv.find_withtag("ring"):
            self.can_hsv.coords(
                i,
                X - ring_radius,
                Y - ring_radius,
                X + ring_radius,
                Y + ring_radius)
        self.overlord(hsv=(hue, sat, value))

    def shandle(self, evt=None):
        """command callback for saturation

        Parameters
        ----------
        evt : str
            command handle
        """

        hue = self.hvar.get()
        sat = self.svar.get()
        self.svar.set(int(0.5 + sat))
        value = self.vvar.get()
        from_colour = hsv_to_rgb(*(hue, 0, value))
        to_colour = hsv_to_rgb(*(hue, 100, value))
        draw_gradient(self.scan, from_colour, to_colour,
                      width=self.canvas_w, height=self.canvas_h)
        from_colour = hsv_to_rgb(*(hue, sat, 0))
        to_colour = hsv_to_rgb(*(hue, sat, 100))
        draw_gradient(self.vcan, from_colour, to_colour,
                      width=self.canvas_w, height=self.canvas_h)
        X, Y = polar2cart(hue, sat, self.wheel_w, self.wheel_iw)
        ring_radius = self.ring_radius
        for i in self.can_hsv.find_withtag("ring"):
            self.can_hsv.coords(
                i,
                X - ring_radius,
                Y - ring_radius,
                X + ring_radius,
                Y + ring_radius)
        self.overlord(hsv=(hue, sat, value))

    def vhandle(self, evt=None):
        """command callback for value

        Parameters
        ----------
        evt : str
            command handle
        """

        hue = self.hvar.get()
        sat = self.svar.get()
        value = self.vvar.get()
        self.vvar.set(int(0.5 + value))
        from_colour = hsv_to_rgb(*(hue, 0, value))
        to_colour = hsv_to_rgb(*(hue, 100, value))
        draw_gradient(self.scan, from_colour, to_colour,
                      width=self.canvas_w, height=self.canvas_h)
        from_colour = hsv_to_rgb(*(hue, sat, 0))
        to_colour = hsv_to_rgb(*(hue, sat, 100))
        draw_gradient(self.vcan, from_colour, to_colour,
                      width=self.canvas_w, height=self.canvas_h)
        self.overlord(hsv=(hue, sat, value))

    def door_bell(self, ring):
        """Calling procedure from cursor binds

        Parameters
        ----------
        ring : tuple of int
            hue, saturation values
        """

        hue, sat = ring
        value = self.vvar.get()
        from_colour = hsv_to_rgb(*(hue, 0, value))
        to_colour = hsv_to_rgb(*(hue, 100, value))
        draw_gradient(self.scan, from_colour, to_colour,
                      width=self.canvas_w, height=self.canvas_h)
        from_colour = hsv_to_rgb(*(hue, sat, 0))
        to_colour = hsv_to_rgb(*(hue, sat, 100))
        draw_gradient(self.vcan, from_colour, to_colour,
                      width=self.canvas_w, height=self.canvas_h)
        self.overlord(hsv=(hue, sat, value))

    def overlord(self, rgb=None, hsv=None):
        """Supervisory procedure to control calls from handles

        Parameters
        ----------
        rgb : tuple of integers
            rgb
        hsv : tuple of integers
            hsv

        Returns
        -------
        draws gradients and sets other colour system
        """

        if rgb:
            red, green, blue = rgb[0], rgb[1], rgb[2]
            hue, sat, value = rgb_to_hsv(red, green, blue)
            from_colour = hsv_to_rgb(*(hue, 0, value))
            to_colour = hsv_to_rgb(*(hue, 100, value))
            draw_gradient(
                self.scan,
                from_colour,
                to_colour,
                width=self.canvas_w, height=self.canvas_h)
            from_colour = hsv_to_rgb(*(hue, sat, 0))
            to_colour = hsv_to_rgb(*(hue, sat, 100))
            draw_gradient(
                self.vcan,
                from_colour,
                to_colour,
                width=self.canvas_w, height=self.canvas_h)
            self.hvar.set(hue)
            self.svar.set(sat)
            self.vvar.set(value)
            X, Y = polar2cart(hue, sat, self.wheel_w, self.wheel_iw)
            ring_radius = self.ring_radius
            for i in self.can_hsv.find_withtag("ring"):
                        self.can_hsv.coords(
                        i,
                        X - ring_radius,
                        Y - ring_radius,
                        X + ring_radius,
                        Y + ring_radius)
            self.related(hue, sat, value, red, green, blue)

        elif hsv:
            hue, sat, value = hsv[0], hsv[1], hsv[2]
            red, green, blue = hsv_to_rgb(hue, sat, value)
            draw_agradient(self.acan, (127, 127, 127), (red, green, blue),
                           self.e, width=self.canvas_w, height=self.canvas_h)
            alpha = self.avar.get()
            vdraw_gradient(self.cmcan, (red, green, blue), self.e, alpha=alpha,
                           width=self.canvas_b, height=self.canvas_b)
            draw_gradient(self.rcan, (0, green, blue), (255, green, blue),
                          width=self.canvas_w, height=self.canvas_h)
            draw_gradient(self.gcan, (red, 0, blue), (red, 255, blue),
                          width=self.canvas_w, height=self.canvas_h)
            draw_gradient(self.bcan, (red, green, 0), (red, green, 255),
                          width=self.canvas_w, height=self.canvas_h)
            self.evar.set(rgb2hash(red, green, blue))
            self.rvar.set(red)
            self.gvar.set(green)
            self.bvar.set(blue)
            self.related(hue, sat, value, red, green, blue)

    def related(self, h, s, v, r, g, b):
        """Creating related Colours, 5 in saturation, 5 for value,
            1 complementary

        Parameters
        ----------
        h : float
            hue
        s : float
            saturation
        v : float
            value
        r : int
            red
        g : int
            green
        b : int
            blue
        """

        sats = [25, 50, 75, 100]

        for ix, sat in enumerate(sats):
            srelcol = rgb2hash(*hsv_to_rgb(h, sat, v))
            self.srccans[ix]['background'] = srelcol
            self.srccans[ix].background = srelcol
            self.srcls[ix]['text'] = srelcol
            vrelcol = rgb2hash(*hsv_to_rgb(h, s, sat))
            self.vrccans[ix]['background'] = vrelcol
            self.vrccans[ix].background = vrelcol
            self.vrcls[ix]['text'] = vrelcol

        comp_col = rgb2hash(255-r, 255-g, 255-b)
        self.cccan['background'] = comp_col
        self.cccan.background = comp_col
        self.ccl['text'] = comp_col

    def resize_rcan(self, event):
        """Bind function for red resizing

        Parameters
        ----------
        event : str
        """
        W = event.width
        green = self.gvar.get()
        blue = self.bvar.get()
        draw_gradient(self.rcan, (0, green, blue), (255, green, blue),
                      width=W, height=self.canvas_h)

    def resize_gcan(self, event):
        """Bind function for green resizing

        Parameters
        ----------
        event : str
        """
        W = event.width
        red = self.rvar.get()
        blue = self.bvar.get()

        draw_gradient(self.gcan, (red, 0, blue), (red, 255, blue),
                      width=W, height=self.canvas_h)

    def resize_bcan(self, event):
        """Bind function for blue resizing

        Parameters
        ----------
        event : str
        """
        W = event.width
        red = self.rvar.get()
        green = self.gvar.get()

        draw_gradient(self.bcan, (red, green, 0), (red, green, 255),
                      width=W, height=self.canvas_h)

    def resize_acan(self, event):
        """Bind function for alpha resizing

        Parameters
        ----------
        event : str
        """
        W = event.width
        red = self.rvar.get()
        green = self.gvar.get()
        blue = self.bvar.get()
        draw_agradient(self.acan, (127, 127, 127), (red, green, blue),
                       self.e, width=W, height=self.canvas_h)

    def resize_hcan(self, event):
        """Bind function for hue resizing

        Parameters
        ----------
        event : str
        """
        W = event.width
        self.canvas_w = W
        hue_gradient(self.hcan, width=W, height=self.canvas_h, steps=360)

    def resize_scan(self, event):
        """Bind function for saturation resizing

        Parameters
        ----------
        event : str
        """
        W = event.width
        self.canvas_w = W
        hue = self.hvar.get()
        value = self.vvar.get()

        from_colour = hsv_to_rgb(*(hue, 0, value))
        to_colour = hsv_to_rgb(*(hue, 100, value))
        draw_gradient(self.scan, from_colour, to_colour,
                      width=W, height=self.canvas_h)

    def resize_vcan(self, event):
        """Bind function for value resizing

        Parameters
        ----------
        event : str
        """
        W = event.width
        self.canvas_w = W
        hue = self.hvar.get()
        sat = self.svar.get()

        from_colour = hsv_to_rgb(*(hue, sat, 0))
        to_colour = hsv_to_rgb(*(hue, sat, 100))
        draw_gradient(self.vcan, from_colour, to_colour,
                      width=W, height=self.canvas_h)

    def build(self):
        """widget construction"""

        lf1 = LabelFrame(self.fr0, text='rgb')
        lf1.grid(column=0, row=0, sticky='new')
        lf1.columnconfigure(1, weight=1)

        rl0 = Label(lf1, text='red  ')
        rl0.grid(column=0, row=0, sticky='s')

        self.rcan = Canvas(lf1, width=self.canvas_w, height=self.canvas_h, bd=0,
                           highlightthickness=0)
        self.rcan.grid(column=1, row=0, sticky='sew', padx=self.sliderlength//2)
        self.rcan.bind("<Configure>", self.resize_rcan)

        rsc = TtkScale(lf1, self.scale_l, from_=0, to=255, variable=self.rvar,
            orient='horizontal', command=self.rhandle, tickinterval=20)
        rsc.grid(column=1, row=1, sticky='new')

        vcmdsb = root.register(sb_okay)

        rsb = Spinbox(lf1, from_=0, to=255, textvariable=self.rvar, validate='key',
                      validatecommand=(vcmdsb, '%d', '%P', '%S', 255), command=self.rhandle, width=5)
        rsb.grid(column=2, row=1, sticky='nw')
        rsb.bind('<KeyRelease>', self.checksb)

        rel = Label(lf1)
        rel.grid(column=2, row=2)

        gl0 = Label(lf1, text='green')
        gl0.grid(column=0, row=3)

        self.gcan = Canvas(lf1, width=self.canvas_w, height=self.canvas_h, bd=0,
                           highlightthickness=0)
        self.gcan.grid(column=1, row=3, sticky='sew', padx=self.sliderlength//2)
        self.gcan.bind("<Configure>", self.resize_gcan)

        gsc = TtkScale(lf1, self.scale_l, from_=0, to=255, variable=self.gvar,
            orient='horizontal', command=self.ghandle, tickinterval=20)
        gsc.grid(column=1, row=4, sticky='new')

        gsb = Spinbox(lf1, from_=0, to=255, textvariable=self.gvar, validate='key',
                      validatecommand=(vcmdsb, '%d', '%P', '%S', 255), command=self.ghandle, width=5)
        gsb.grid(column=2, row=4, sticky='nw')
        gsb.bind('<KeyRelease>', self.checksb)

        gel = Label(lf1)
        gel.grid(column=2, row=5)

        bl0 = Label(lf1, text='blue ')
        bl0.grid(column=0, row=6, sticky='s')

        self.bcan = Canvas(lf1, width=self.canvas_w, height=self.canvas_h, bd=0,
                           highlightthickness=0)
        self.bcan.grid(column=1, row=6, sticky='new', padx=self.sliderlength//2)
        self.bcan.bind("<Configure>", self.resize_bcan)

        bsc = TtkScale(lf1, self.scale_l, from_=0, to=255, variable=self.bvar,
            orient='horizontal', command=self.bhandle, tickinterval=20)
        bsc.grid(column=1, row=7, sticky='new')

        bsb = Spinbox(lf1, from_=0, to=255, textvariable=self.bvar, validate='key',
                      validatecommand=(vcmdsb, '%d', '%P', '%S', 255), command=self.bhandle, width=5)
        bsb.grid(column=2, row=7, sticky='nw')
        bsb.bind('<KeyRelease>', self.checksb)

        bel = Label(lf1)
        bel.grid(column=2, row=8)

        lf3 = LabelFrame(self.fr0, text='colour mix')
        lf3.grid(column=1, row=0, sticky='nw')

        self.cmcan = cmcan = Canvas(lf3, width=self.canvas_b, height=self.canvas_b,
                                    bd=0, highlightthickness=0)
        cmcan.grid(column=0, row=0, sticky='n', columnspan=2)
        cmcan.grid_propagate(0)
        vdraw_gradient(self.cmcan, (255, 0, 0), self.e, alpha=255,
                       width=self.canvas_b, height=self.canvas_b)

        cml = Label(lf3, text='hash\nvalue')
        cml.grid(column=0, row=1)

        vcmd = root.register(is_okay)
        self.ent0 = ent0 = Entry(lf3, width=8, validate='key',
                                 validatecommand=(vcmd, '%i', '%P', '%S'), textvariable=self.evar)
        ent0.grid(column=1, row=1)
        ent0.bind('<KeyRelease>', self.checkhash)

        lf5 = LabelFrame(lf3, text='related colours', style='Wide.TLabelframe')
        lf5.grid(column=0, row=2, sticky='nw', columnspan=2)

        self.srcls = []
        self.vrcls = []
        self.srccans = []
        self.vrccans = []
        relateds = [25, 50, 75, 100]
        stexts = ['25% sat', '50% sat', '75% sat', '100% sat']
        vtexts = ['25% val', '50% val', '75% val', '100% val']

        for ix, rel in enumerate(relateds):
            Label(lf5, text=stexts[ix]).grid(row=1+2*ix, column=0, sticky='n')
            self.srcls.append(Label(lf5))
            self.srcls[ix].grid(row=1+2*ix, column=1, sticky='n')
            self.srccans.append(Canvas(lf5, width=self.canvas_b,
                            height=self.canvas_b, bd=0, highlightthickness=0))
            self.srccans[ix].grid(row=2*ix, column=0, sticky='n', columnspan=2)
            Label(lf5, text=vtexts[ix]).grid(row=9+2*ix, column=0, sticky='n')
            self.vrcls.append(Label(lf5))
            self.vrcls[ix].grid(row=9+2*ix, column=1, sticky='n')
            self.vrccans.append(Canvas(lf5, width=self.canvas_b,
                                height=self.canvas_b, bd=0, highlightthickness=0))
            self.vrccans[ix].grid(row=8+2*ix, column=0, sticky='n', columnspan=2)

        self.cccan = Canvas(lf5, width=self.canvas_b, height=self.canvas_b, bd=0,
                             highlightthickness=0)
        self.cccan.grid(column=0, row=17, sticky='n', columnspan=2)

        self.ccla = Label(lf5, text = "comp'y")
        self.ccla.grid(column=0, row=18, sticky='n')

        self.ccl = Label(lf5, text = "")
        self.ccl.grid(column=1, row=18, sticky='n')
        al0 = Label(lf1, text='alpha')
        al0.grid(column=0, row=10, sticky='s')

        self.acan = Canvas(lf1, width=self.canvas_w, height=self.canvas_h, bd=0,
                           highlightthickness=0)
        self.acan.grid(column=1, row=10, sticky='new', padx=self.sliderlength//2)
        self.acan.bind("<Configure>", self.resize_acan)

        asc = TtkScale(lf1, self.scale_l, from_=0, to=255, variable=self.avar,
            orient='horizontal', command=self.ahandle, tickinterval=20)
        asc.grid(column=1, row=11, sticky='new')

        asb = Spinbox(lf1, from_=0, to=255, textvariable=self.avar, validate='key',
                      validatecommand=(vcmdsb, '%d', '%P', '%S', 255), command=self.ahandle, width=5)
        asb.grid(column=2, row=11, sticky='nw')
        asb.bind('<KeyRelease>', self.checksba)

        ael = Label(lf1, text=' ')
        ael.grid(column=2, row=12, sticky='s')

        draw_gradient(self.rcan, (0, 0, 0), (255, 0, 0),
                      width=self.canvas_w, height=self.canvas_h)
        draw_gradient(self.gcan, (255, 0, 0),
                      (255, 0, 255), width=self.canvas_w, height=self.canvas_h)
        draw_gradient(self.bcan, (255, 0, 0),
                      (255, 255, 0), width=self.canvas_w, height=self.canvas_h)
        draw_agradient(self.acan, (127, 127, 127),
                       (255, 0, 0), self.e, width=self.canvas_w, height=self.canvas_h)

        lf4 = LabelFrame(self.fr0, text='hsv')
        lf4.grid(column=2, row=0, sticky='news')
        lf4.columnconfigure(1, weight=1)

        hl0 = Label(lf4, text='hue  ')
        hl0.grid(column=0, row=0, sticky='s')

        self.hcan = Canvas(lf4, width=self.canvas_w, height=self.canvas_h, bd=0,
                           highlightthickness=0)
        self.hcan.grid(column=1, row=0, sticky='sew', padx=self.sliderlength//2)
        self.hcan.bind("<Configure>", self.resize_hcan)

        hsc = TtkScale(lf4, self.scale_l, from_=0, to=360, variable=self.hvar,
            orient='horizontal', command=self.hhandle, tickinterval=30)
        hsc.grid(column=1, row=1, sticky='new')

        vcmdsb = root.register(sb_okay)

        hsb = Spinbox(lf4, from_=0, to=360, textvariable=self.hvar, validate='key',
                      validatecommand=(vcmdsb, '%P', '%S', 360), command=self.hhandle, width=5)
        hsb.grid(column=2, row=1, sticky='nw')
        hsb.bind('<KeyRelease>', self.checksbh)

        hel = Label(lf4)
        hel.grid(column=2, row=2)

        sl0 = Label(lf4, text='sat  ')
        sl0.grid(column=0, row=3)

        self.scan = Canvas(lf4, width=self.canvas_w, height=self.canvas_h, bd=0,
                           highlightthickness=0)
        self.scan.grid(column=1, row=3, sticky='sew', padx=self.sliderlength//2)
        self.scan.bind("<Configure>", self.resize_scan)

        ssc = TtkScale(lf4, self.scale_l, from_=0, to=100, variable=self.svar,
            orient='horizontal', command=self.shandle, tickinterval=10)
        ssc.grid(column=1, row=4, sticky='new')

        ssb = Spinbox(lf4, from_=0, to=100, textvariable=self.svar, validate='key',
                      validatecommand=(vcmdsb, '%P', '%S', 100), command=self.shandle, width=5)
        ssb.grid(column=2, row=4, sticky='nw')
        ssb.bind('<KeyRelease>', self.checksb100)

        sel = Label(lf4)
        sel.grid(column=2, row=5)

        vl0 = Label(lf4, text='value')
        vl0.grid(column=0, row=6, sticky='s')

        self.vcan = Canvas(lf4, width=self.canvas_w, height=self.canvas_h, bd=0,
                           highlightthickness=0)
        self.vcan.grid(column=1, row=6, sticky='new', padx=self.sliderlength//2)
        self.vcan.bind("<Configure>", self.resize_vcan)

        vsc = TtkScale(lf4, self.scale_l, from_=0, to=100, variable=self.vvar,
            orient='horizontal', command=self.vhandle, tickinterval=10)
        vsc.grid(column=1, row=7, sticky='new')

        vsb = Spinbox(lf4, from_=0, to=100, textvariable=self.vvar, validate='key',
                      validatecommand=(vcmdsb, '%P', '%S', 100), command=self.vhandle, width=5)
        vsb.grid(column=2, row=7, sticky='nw')
        vsb.bind('<KeyRelease>', self.checksb100)

        vel = Label(lf4)
        vel.grid(column=2, row=8)

        # assume initial setting 0,100,100 hsv
        to_colour = hsv_to_rgb(*(0, 100, 100))

        hue_gradient(self.hcan, width=self.canvas_w, height=self.canvas_h)
        draw_gradient(self.scan, (255, 255, 255),
                      to_colour, width=self.canvas_w, height=self.canvas_h)
        draw_gradient(self.vcan, (0, 0, 0), to_colour,
                      width=self.canvas_w, height=self.canvas_h)

        self.can_hsv = can_hsv = Canvas(lf4, width=self.wheel_w,
                                        height=self.wheel_w, bg='#d9d9d9')
        can_hsv.grid(column=0, row=9, columnspan=3, pady=25, sticky='n')
        self.hsv_gamut = PhotoImage(file='../../figures/colour_wheel'+str(self.e)+'.png')
        can_hsv.create_image(0, 0, anchor='nw', image=self.hsv_gamut)
        self.ring = circle(can_hsv, 307*self.e, 158*self.e, self.ring_radius,
                           width=self.ring_width,
                           outline='#555555', activeoutline='black', tags='ring')

        can_hsv.bind('<Button-1>', self.click_ring)
        can_hsv.tag_bind('ring', '<B1-Motion>', self.drag_ring)

        self.related(0, 100, 100, 255, 0, 0)

    def checkhash(self, evt):
        """Procedure called by entry for hash

        Parameters
        ----------
        evt : str
            bind handles

        Results
        -------
        None
        """

        hash0 = self.ent0.get()
        if len(hash0) == 7:
            red, green, blue = hash2rgb(hash0)
            alpha = self.avar.get()
            self.rvar.set(red)
            self.gvar.set(green)
            self.bvar.set(blue)
            draw_agradient(self.acan, (127, 127, 127), (red, green, blue),
                           self.e, width=self.canvas_w, height=self.canvas_h)
            draw_gradient(self.rcan, (0, green, blue), (255, green, blue),
                          width=self.canvas_w, height=self.canvas_h)
            draw_gradient(self.gcan, (red, 0, blue), (red, 255, blue),
                          width=self.canvas_w, height=self.canvas_h)
            draw_gradient(self.bcan, (red, green, 0), (red, green, 255),
                          width=self.canvas_w, height=self.canvas_h)
            vdraw_gradient(self.cmcan, (red, green, blue), self.e, alpha=alpha,
                           width=self.canvas_b, height=self.canvas_b)
            self.overlord(rgb=(red, green, blue))

    def click_ring(self, event):
        """Procedure called when mouse clicks in colour wheel

        Parameters
        ----------
        event : str
            bind handle

        Returns
        -------
        calls handle for cursor
        """

        X = event.x
        Y = event.y
        ring_radius = self.ring_radius

        cx = self.wheel_w // 2
        dx, dy = X - cx, Y - cx
        rad = self.wheel_iw // 2
        if (dx)**2 + (dy)**2 < rad**2:
            for search in self.can_hsv.find_withtag("ring"):
                self.can_hsv.coords(search, X - ring_radius, Y - ring_radius,
                                    X + ring_radius, Y + ring_radius)

        hue, sat = cart2polar(X, Y, self.wheel_w, self.wheel_iw)
        self.hvar.set(hue)
        self.svar.set(sat)
        ring = hue, sat
        self.door_bell(ring)

    def drag_ring(self, event):
        """Procedure called when mouse drags cursor

        Parameters
        ----------
        event : str
            bind handle

        Returns
        -------
        calls handle for cursor
        """

        X = event.x
        Y = event.y
        ring_radius = self.ring_radius
        cx = self.wheel_w // 2
        dx, dy = X - cx, Y - cx
        rad = self.wheel_iw // 2

        if (dx)**2 + (dy)**2 < rad**2:
            self.can_hsv.coords(self.ring, X - ring_radius, Y - ring_radius,
                                X + ring_radius, Y + ring_radius)

        hue, sat = cart2polar(X, Y, self.wheel_w, self.wheel_iw)
        self.hvar.set(hue)
        self.svar.set(sat)
        ring = hue, sat
        self.door_bell(ring)

    def checksba(self, evt):
        """Procedure called by alpha spinbox

        Parameters
        ----------
        evt : str
            bind handles

        Results
        -------
        None
        """

        alpha = self.avar.get()
        red = self.rvar.get()
        green = self.gvar.get()
        blue = self.bvar.get()
        vdraw_gradient(self.cmcan, (red, green, blue), self.e, alpha=alpha,
                       width=self.canvas_b, height=self.canvas_b)

    def checksb(self, evt):
        """Procedure called by rgb colour spinboxes

        Parameters
        ----------
        evt : str
            bind handles

        Results
        -------
        None
        """

        alpha = self.avar.get()
        red = self.rvar.get()
        green = self.gvar.get()
        blue = self.bvar.get()
        draw_agradient(self.acan, (127, 127, 127), (red, green, blue),
                       self.e, width=self.canvas_w, height=self.canvas_h)
        draw_gradient(self.rcan, (0, green, blue), (255, green, blue),
                      width=self.canvas_w, height=self.canvas_h)
        draw_gradient(self.gcan, (red, 0, blue), (red, 255, blue),
                      width=self.canvas_w, height=self.canvas_h)
        draw_gradient(self.bcan, (red, green, 0), (red, green, 255),
                      width=self.canvas_w, height=self.canvas_h)
        vdraw_gradient(self.cmcan, (red, green, blue), self.e, alpha=alpha,
                       width=self.canvas_b, height=self.canvas_b)
        self.evar.set(rgb2hash(red, green, blue))
        self.overlord(rgb=(red, green, blue))

    def checksbh(self, evt):
        """Procedure called by hue spinbox

        Parameters
        ----------
        evt : str
            bind handles

        Results
        -------
        None
        """

        hue = self.hvar.get()
        sat = self.svar.get()
        value = self.vvar.get()
        from_colour = hsv_to_rgb(*(hue, 0, value))
        to_colour = hsv_to_rgb(*(hue, 100, value))
        draw_gradient(self.scan, from_colour, to_colour, width=self.canvas_w,
                      height=self.canvas_h)
        from_colour = hsv_to_rgb(*(hue, sat, 0))
        to_colour = hsv_to_rgb(*(hue, sat, 100))
        draw_gradient(self.vcan, from_colour, to_colour, width=self.canvas_w,
                      height=self.canvas_h)
        self.overlord(hsv=(hue, sat, value))

    def checksb100(self, evt):
        """Procedure called by s,v spinboxes

        Parameters
        ----------
        evt : str
            bind handles

        Results
        -------
        None
        """

        hue = self.hvar.get()
        sat = self.svar.get()
        value = self.vvar.get()
        from_colour = hsv_to_rgb(*(hue, 0, value))
        to_colour = hsv_to_rgb(*(hue, 100, value))
        draw_gradient(self.scan, from_colour, to_colour, width=self.canvas_w,
                      height=self.canvas_h)
        from_colour = hsv_to_rgb(*(hue, sat, 0))
        to_colour = hsv_to_rgb(*(hue, sat, 100))
        draw_gradient(self.vcan, from_colour, to_colour, width=self.canvas_w,
                      height=self.canvas_h)
        self.overlord(hsv=(hue, sat, value))


if __name__ == "__main__":
    root = Tk()
    winsys = root.tk.call("tk", "windowingsystem")
    BASELINE = 1.33398982438864281 if winsys != 'aqua' else 1.000492368291482
    scaling = root.tk.call("tk", "scaling")
    enlargement = e = int(scaling / BASELINE + 0.5)

    img = Image.new("RGBA", (16*e, 10*e), '#00000000')
    trough = ImageTk.PhotoImage(img)

    # constants for creating upward pointing arrow
    WIDTH = 17*e
    HEIGHT = 17*e
    OFFSET = 5*e
    ST0 = WIDTH // 2, HEIGHT - 1 - OFFSET
    LIGHT = 'GreenYellow'
    MEDIUM = 'LawnGreen'
    DARK = '#5D9B90'

    # normal state
    im = Image.new("RGBA", (WIDTH, HEIGHT), '#00000000')
    rdraw = ImageDraw.Draw(im)
    rdraw.polygon([ST0[0], ST0[1], 0, HEIGHT - 1,
                   WIDTH - 1, HEIGHT - 1], fill=LIGHT)
    rdraw.polygon([ST0[0], ST0[1], ST0[0], 0, 0, HEIGHT - 1], fill=MEDIUM)
    rdraw.polygon([ST0[0], ST0[1], WIDTH - 1,
                   HEIGHT - 1, ST0[0], 0], fill=DARK)
    slider = ImageTk.PhotoImage(im)

    # pressed state
    imp = Image.new("RGBA", (WIDTH, HEIGHT), '#00000000')
    draw = ImageDraw.Draw(imp)
    draw.polygon([ST0[0], ST0[1], 0, HEIGHT - 1,
                  WIDTH - 1, HEIGHT - 1], fill=LIGHT)
    draw.polygon([ST0[0], ST0[1], ST0[0], 0, 0, HEIGHT - 1], fill=DARK)
    draw.polygon([ST0[0], ST0[1], WIDTH - 1,
                  HEIGHT - 1, ST0[0], 0], fill=MEDIUM)
    sliderp = ImageTk.PhotoImage(imp)

    style = Style()
    style.theme_settings('default', {
        'Horizontal.Scale.trough': {"element create":
                                    ('image', trough,
                                     {'border': 0, 'sticky': 'wes'})},
        'Horizontal.Scale.slider': {"element create":
                                    ('image', slider,
                                     ('pressed', sliderp),
                                     {'border': 3, 'sticky': 'n'})}})

    style.theme_use('default')
    style.configure('Width.TLabelframe', width=60*e)
    style.configure('TSpinbox', arrowsize=10*e)
    root.columnconfigure(0, weight=1)
    fr = Frame(root)
    fr.grid(row=0, column=0, sticky='nsew')
    fr.columnconfigure(0, weight=1)
    fr.columnconfigure(2, weight=1)
    RgbHsvSelect(fr, enlargement)
    root.mainloop()
combined rgb and hsv with related colours after adjustment

Combined RGB and HSV with Related Colours with Adjustment#