Creating a Cursor for the Colour Wheel#
Colour Wheel imported to basic hsv#
The cursor will be a ring that can be moved over the colour wheel either by using the left mouse button to drag the cursor or clicking to the required position with a mouse. It will be constructed from oval which normally has a transparent fill and a black outline of width 1 pixel. Using the assisting function we can create the circle working directly using centre and radius:
def circle(canvas, x, y, radius, width=None, tags=None, outline=None,
activeoutline=None):
return canvas.create_oval(x + radius, y + radius, x - radius, y - radius,
width=width, tags=tags,
activeoutline=activeoutline, outline=outline)
It will be necessary to enable tags and change the outline colour when the user hovers over the cursor.
Bind the cursor to the left hand mouse button to enable dragging. When clicking use much the same code, but include a clause to find the cursor.
If we were to use canvas.move we need need to know the amount of movement
meaning that the actual and final positions need to be known. Using
canvas.coords only the final position is required, which can be readily
found from the tkinter
canvas. Sometimes coords deforms the image when being
moved, luckily circle does not have this problem.
The coordinates of the canvas will be in cartesian, whereas the colour wheel will show the hue and saturation as polar coordinates. Therefore add conversions between the two systems:
def polar2cart(phi, ray, outer_w, inner_w):
# original image 317x317 using inner 299x299 working area,
# ring can be on outer edge wheel, so allow a space around image
image size used in calculating phi, ray from x,y
phi, ray is h, s of hsv, ray adjusted
centre = outer_w // 2, outer_w // 2
inner = inner_w, inner_w
radius = min(inner) // 2
ray = ray * radius / 100
dx = ray * cos(phi * pi / 180)
dy = ray * sin(phi * pi / 180)
x = centre[0] + dx
y = centre[1] + dy
return int(x+0.5), int(y+0.5)
def cart2polar(x, y, outer_w, inner_w):
# output h,s of hsv; s adjusted
centre = outer_w // 2, outer_w // 2
inner = inner_w, inner_w
radius = min(inner) // 2
dx = x - centre[0]
dy = y - centre[1]
deg = int(0.5 + degrees(atan2(dy, dx)))
if deg < 0:
deg = 360 + deg
ray = int(0.5 + hypot(dx, dy) * 100 / radius)
ray = min(max(ray, 0), 100)
deg = min(max(deg, 0), 360)
return deg, ray
Modifying 08basichsv.py add the required polar conversion functions, the circle drawing function then import the colour wheel image and place the cursor at the right-hand edge level with the centre, corresponding to a hsv of (0,100,100).
Show/Hide Code 10wheelhsv.py
""" Construction three gradients in hsv using PPM image
spinbox validation, working with modified Scale
adding colour wheel and cursor actions
"""
from tkinter import Tk, Canvas, Label, IntVar, Frame, PhotoImage
from tkinter.ttk import LabelFrame, Scale, Style, Spinbox
from PIL import Image, ImageDraw, ImageTk
from colourTools import draw_gradient, hsv_to_rgb, hue_gradient, circle
def sb_okay(text, input_, upper): # '%P','%S'
"""Validation for colour components
Parameters
----------
text : str
text if accepted
input_ : str
current input
upper : int
upper limit
Returns
-------
boolean
"""
if input_.isdigit():
if 0 < len(text) < 4:
return bool(0 <= int(text) <= int(upper))
return False
return False
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, from_=0, to=255, length=300, 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
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, bg='#EFFEFF')
item.place(in_=self, bordermode='outside',
relx=sliderlength*self.e / length*self.e / 2 +
i / sc_range * (1 - sliderlength*self.e / length*self.e),
rely=1, anchor='n')
class HsvSelect:
"""Class to construct hsv gradients and colour wheel
Parameters
----------
fr0 : str
parent widget
Returns
-------
None
"""
def __init__(self, fr0, enlargement):
self.fr0 = fr0
self.hvar = IntVar()
self.svar = IntVar()
self.vvar = IntVar()
self.e = enlargement
self.scale_l = 300*self.e
self.canvas_w = self.scale_l
self.canvas_h = 26*self.e
self.cursor_w = 16*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.hvar.set(0)
self.svar.set(100)
self.vvar.set(100)
def hhandle(self, evt=None):
"""command callback for hue
Parameters
----------
None
Results
-------
None
"""
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)
def shandle(self, evt=None):
"""command callback for saturation
Parameters
----------
None
Results
-------
None
"""
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)
def vhandle(self, evt=None):
"""command callback for value
Parameters
----------
None
Results
-------
None
"""
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)
def build(self):
"""widget construction
Parameters
----------
None
Results
-------
None
"""
fr4 = LabelFrame(self.fr0, text='hsv')
fr4.grid(column=2, row=0, sticky='ns')
hl0 = Label(fr4, text='hue ')
hl0.grid(column=0, row=0, sticky='s')
self.hcan = Canvas(fr4, width=self.canvas_w, height=self.canvas_h, bd=0,
highlightthickness=0)
self.hcan.grid(column=1, row=0, sticky='s')
hsc = TtkScale(fr4, from_=0, to=360, variable=self.hvar, orient='horizontal',
length=self.scale_l, command=self.hhandle, tickinterval=30)
hsc.grid(column=1, row=1, sticky='nw')
vcmdsb = root.register(sb_okay)
hsb = Spinbox(fr4, 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(fr4, height=1)
hel.grid(column=2, row=2)
sl0 = Label(fr4, text='sat ')
sl0.grid(column=0, row=3)
self.scan = Canvas(fr4, width=self.canvas_w, height=self.canvas_h, bd=0,
highlightthickness=0)
self.scan.grid(column=1, row=3, sticky='s')
ssc = TtkScale(fr4, from_=0, to=100, variable=self.svar, orient='horizontal',
length=self.scale_l, command=self.shandle, tickinterval=10)
ssc.grid(column=1, row=4, sticky='nw')
ssb = Spinbox(fr4, 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(fr4, height=1)
sel.grid(column=2, row=5)
vl0 = Label(fr4, text='value')
vl0.grid(column=0, row=6, sticky='s')
self.vcan = Canvas(fr4, width=self.canvas_w, height=self.canvas_h, bd=0,
highlightthickness=0)
self.vcan.grid(column=1, row=6, sticky='n')
vsc = TtkScale(fr4, from_=0, to=100, variable=self.vvar, orient='horizontal',
length=self.scale_l, command=self.vhandle, tickinterval=10)
vsc.grid(column=1, row=7, sticky='nw')
vsb = Spinbox(fr4, 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(fr4, height=1)
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)
can_hsv = Canvas(fr4, width=self.wheel_w,
height=self.wheel_w, bg='#d9d9d9')
can_hsv.grid(column=1, row=9, 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, tags='ring')
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)
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)
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, 10), '#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('TSpinbox', arrowsize=10*e)
#root.geometry("500x600+200+100")
fra0 = Frame(root)
fra0.grid(row=0, column=0, sticky='nsew')
HsvSelect(fra0, enlargement)
root.mainloop()
root.mainloop()
Next make the cursor interact with the mouse actions.