Creating Rounded Edges for a Polygon in Python - python

I came across this interesting question (How to make a tkinter canvas rectangle with rounded corners?) related to creating rounded rectangles in Tkinter and specifically, this answer by Francisco Gomes (modified a bit):
def roundPolygon(x, y, sharpness):
# The sharpness here is just how close the sub-points
# are going to be to the vertex. The more the sharpness,
# the more the sub-points will be closer to the vertex.
# (This is not normalized)
if sharpness < 2:
sharpness = 2
ratioMultiplier = sharpness - 1
ratioDividend = sharpness
# Array to store the points
points = []
# Iterate over the x points
for i in range(len(x)):
# Set vertex
points.append(x[i])
points.append(y[i])
# If it's not the last point
if i != (len(x) - 1):
# Insert submultiples points. The more the sharpness, the more these points will be
# closer to the vertex.
points.append((ratioMultiplier*x[i] + x[i + 1])/ratioDividend)
points.append((ratioMultiplier*y[i] + y[i + 1])/ratioDividend)
points.append((ratioMultiplier*x[i + 1] + x[i])/ratioDividend)
points.append((ratioMultiplier*y[i + 1] + y[i])/ratioDividend)
else:
# Insert submultiples points.
points.append((ratioMultiplier*x[i] + x[0])/ratioDividend)
points.append((ratioMultiplier*y[i] + y[0])/ratioDividend)
points.append((ratioMultiplier*x[0] + x[i])/ratioDividend)
points.append((ratioMultiplier*y[0] + y[i])/ratioDividend)
# Close the polygon
points.append(x[0])
points.append(y[0])
When I adapted this code to work with my graphics library, it worked well enough! but when I create a 'stretched-square' (a non-square rectangle), the roundness becomes stretched too:
So how can I change this code to remove the stretched roundness and keep it a constant radius?

Here is one approach that uses the built in tcl tk primitives canvas.create_line, and canvas.create_arc to build rectangles of various sizes, and proportions with round corners (arc of a circle).
The corners radii is expressed as a proportion of the shortest side of the rectangle (0.0 --> 0.5), and can be parametrized.
The function make_round_corners_rect returns a tuple containing all canvas item ids as fragments of the rectangle entity. All fragments are tagged with their companions' ids, so accessing the entire object is possible with only one fragment id.
#! python3
import math
import tkinter as tk
from tkinter import TclError
def make_round_corners_rect(canvas, x0, y0, x1, y1, ratio=0.2, npts=12):
if x0 > x1:
x0, x1 = x1, x0
if y0 > y1:
y0, y1 = y1, y0
r = min(x1 - x0, y1 - y0) * ratio
items = []
topleft = x0, y0
tld = x0, y0 + r
tlr = x0 + r, y0
item = canvas.create_arc(x0, y0, x0+2*r, y0+2*r, start=90, extent=90, fill='', outline='black', style=tk.ARC)
items.append(item)
top_right = x1, y0
trl = x1 - r, y0
trd = x1, y0 + r
item = canvas.create_line(*tlr, *trl, fill='black')
items.append(item)
item = canvas.create_arc(x1-2*r, y0, x1, y0+2*r, start=0, extent=90, fill='', outline='black', style=tk.ARC)
items.append(item)
bot_right = x1, y1
bru = x1, y1 - r
brl = x1 - r, y1
item = canvas.create_line(*trd, *bru, fill='black')
items.append(item)
item = canvas.create_arc(x1-2*r, y1-2*r, x1, y1, start=270, extent=90, fill='', outline='black', style=tk.ARC)
items.append(item)
bot_left = x0, y1
blr = x0 + r, y1
blu = x0, y1 - r
item = canvas.create_line(*brl, *blr, fill='black')
items.append(item)
item = canvas.create_arc(x0, y1-2*r, x0+2*r, y1, start=180, extent=90, fill='', outline='black', style=tk.ARC)
items.append(item)
item = canvas.create_line(*blu, *tld, fill='black')
items.append(item)
items = tuple(items)
print(items)
for item_ in items:
for _item in items:
canvas.addtag_withtag(item_, _item)
return items
if __name__ == '__main__':
root = tk.Tk()
canvas = tk.Canvas(root, width=500, height=500)
canvas.pack(expand=True, fill=tk.BOTH)
TL = 100, 100
BR = 400, 200
make_round_corners_rect(canvas, *TL, *BR)
TL = 100, 300
BR = 400, 400
make_round_corners_rect(canvas, *TL, *BR, ratio = .3)
TL = 300, 50
BR = 350, 450
that_rect = make_round_corners_rect(canvas, *TL, *BR, ratio=.4)
for fragment in that_rect:
canvas.itemconfig(fragment, width=4)
try:
canvas.itemconfig(fragment, outline='blue')
except TclError:
canvas.itemconfig(fragment, fill='blue')
TL = 150, 50
BR = 200, 450
make_round_corners_rect(canvas, *TL, *BR, ratio=.07)
TL = 30, 30
BR = 470, 470
that_rect = make_round_corners_rect(canvas, *TL, *BR, ratio=.3)
for fragment in that_rect:
canvas.itemconfig(fragment, dash=(3, 3))
TL = 20, 20
BR = 480, 480
make_round_corners_rect(canvas, *TL, *BR, ratio=.1)
root.mainloop()
The next step, (left to the reader as an exercise), is to encapsulate the round rectangles in a class.
Edit: how to fill a rounded corners rectangle:
It is a bit involved, and in the long run, probably requires an approach where all points are explicitly defined, and the shape is formed as a polygon, instead of the aggregation of tkinter primitives. In this edit, the rounded corners rectangle is filled with two overlapping rectangles, and four disks; it allows to create a filled/unfilled shape, but not to change that property after creation - although it would not require too much work to be able to do this too. (collecting the canvas ids, and turning them on/off on demand, in conjunction with the outline property); however, as mentioned earlier, this would make more sense to encapsulate all this behavior in a class that mimicks the behavior of tk.canvas.items.
def make_round_corners_rect(canvas, x0, y0, x1, y1, ratio=0.2, npts=12, filled=False, fillcolor=''):
...
if filled:
canvas.create_rectangle(x0+r, y0, x1-r, y1, fill=fillcolor, outline='')
canvas.create_rectangle(x0, y0+r, x1, y1-r, fill=fillcolor, outline='')
canvas.create_oval(x0, y0, x0+2*r, y0+2*r, fill=fillcolor, outline='')
canvas.create_oval(x1-2*r, y0, x1, y0+2*r, fill=fillcolor, outline='')
canvas.create_oval(x1-2*r, y1-2*r, x1, y1, fill=fillcolor, outline='')
canvas.create_oval(x0, y1-2*r, x0+2*r, y1, fill=fillcolor, outline='')
...
if __name__ == '__main__':
...
TL = 100, 300
BR = 400, 400
make_round_corners_rect(canvas, *TL, *BR, ratio=.3, filled=True, fillcolor='cyan')
...

Related

How to create an openAI gym Observation space using grid environment

I have build environment in Tkinter, how to create observation space using this environment. I could not understand how to use grid coordinates in array to make observation space, self.observation_space = spaces.Box(np.array([]), np.array([]), dtype=np.int), code is given. I will be thankful if anyone help.
enter code here
# Setting the sizes for the environment
pixels = 40 # pixels
env_height =9 # grid height
env_width = 9 # grid width
# Global variable for dictionary with coordinates for the final route
a = {}
# Creating class for the environment
class Environment(tk.Tk, object):
def __init__(self):
super(Environment, self).__init__()
self.action_space = ['up', 'down', 'left', 'right']
self.n_actions = len(self.action_space)
self.title('RL Q-learning. Sichkar Valentyn')
self.geometry('{0}x{1}'.format(env_height * pixels, env_height * pixels))
self.build_environment()
print(self.geometry('{0}x{1}'.format(env_height * pixels, env_height * pixels)))
# Dictionaries to draw the final route
self.d = {}
self.f = {}
# Key for the dictionaries
self.i = 0
# Writing the final dictionary first time
self.c = True
# Showing the steps for longest found route
self.longest = 0
# Showing the steps for the shortest route
self.shortest = 0
# Function to build the environment
def build_environment(self):
self.canvas_widget = tk.Canvas(self, bg='black',
height=env_height * pixels,
width=env_width * pixels)
# Uploading an image for background
# img_background = Image.open("images/bg.png")
# self.background = ImageTk.PhotoImage(img_background)
# # Creating background on the widget
# self.bg = self.canvas_widget.create_image(0, 0, anchor='nw', image=self.background)
# Creating grid lines
for column in range(0, env_width * pixels, pixels):
x0, y0, x1, y1 = column, 0, column, env_height * pixels
self.canvas_widget.create_line(x0, y0, x1, y1, fill='grey')
for row in range(0, env_height * pixels, pixels):
x0, y0, x1, y1 = 0, row, env_height * pixels, row
self.canvas_widget.create_line(x0, y0, x1, y1, fill='grey')
self.canvas_widget.pack()
if __name__ == '__main__':
env = Environment()

How to animate the creation of this arc in Tkinter? [duplicate]

I am trying to model a simple solar system in Tkinter using circles and moving them around in canvas. However, I am stuck trying to find a way to animate them. I looked around and found the movefunction coupled with after to create an animation loop. I tried fidgeting with the parameters to vary the y offset and create movement in a curved path, but I failed while trying to do this recursively or with a while loop. Here is the code I have so far:
import tkinter
class celestial:
def __init__(self, x0, y0, x1, y1):
self.x0 = x0
self.y0 = y0
self.x1 = x1
self.y1 = y1
sol_obj = celestial(200, 250, 250, 200)
sx0 = getattr(sol_obj, 'x0')
sy0 = getattr(sol_obj, 'y0')
sx1 = getattr(sol_obj, 'x1')
sy1 = getattr(sol_obj, 'y1')
coord_sol = sx0, sy0, sx1, sy1
top = tkinter.Tk()
c = tkinter.Canvas(top, bg='black', height=500, width=500)
c.pack()
sol = c.create_oval(coord_sol, fill='black', outline='white')
top.mainloop()
Here's something that shows one way to do what you want using the tkinter after method to update both the position of the object and the associated canvas oval object. It uses a generator function to compute coordinates along a circular path representing the orbit of one of the Celestial instances (named planet_obj1).
import math
try:
import tkinter as tk
except ImportError:
import Tkinter as tk # Python 2
DELAY = 100
CIRCULAR_PATH_INCR = 10
sin = lambda degs: math.sin(math.radians(degs))
cos = lambda degs: math.cos(math.radians(degs))
class Celestial(object):
# Constants
COS_0, COS_180 = cos(0), cos(180)
SIN_90, SIN_270 = sin(90), sin(270)
def __init__(self, x, y, radius):
self.x, self.y = x, y
self.radius = radius
def bounds(self):
""" Return coords of rectangle surrounding circlular object. """
return (self.x + self.radius*self.COS_0, self.y + self.radius*self.SIN_270,
self.x + self.radius*self.COS_180, self.y + self.radius*self.SIN_90)
def circular_path(x, y, radius, delta_ang, start_ang=0):
""" Endlessly generate coords of a circular path every delta angle degrees. """
ang = start_ang % 360
while True:
yield x + radius*cos(ang), y + radius*sin(ang)
ang = (ang+delta_ang) % 360
def update_position(canvas, id, celestial_obj, path_iter):
celestial_obj.x, celestial_obj.y = next(path_iter) # iterate path and set new position
# update the position of the corresponding canvas obj
x0, y0, x1, y1 = canvas.coords(id) # coordinates of canvas oval object
oldx, oldy = (x0+x1) // 2, (y0+y1) // 2 # current center point
dx, dy = celestial_obj.x - oldx, celestial_obj.y - oldy # amount of movement
canvas.move(id, dx, dy) # move canvas oval object that much
# repeat after delay
canvas.after(DELAY, update_position, canvas, id, celestial_obj, path_iter)
top = tk.Tk()
top.title('Circular Path')
canvas = tk.Canvas(top, bg='black', height=500, width=500)
canvas.pack()
sol_obj = Celestial(250, 250, 25)
planet_obj1 = Celestial(250+100, 250, 15)
sol = canvas.create_oval(sol_obj.bounds(), fill='yellow', width=0)
planet1 = canvas.create_oval(planet_obj1.bounds(), fill='blue', width=0)
orbital_radius = math.hypot(sol_obj.x - planet_obj1.x, sol_obj.y - planet_obj1.y)
path_iter = circular_path(sol_obj.x, sol_obj.y, orbital_radius, CIRCULAR_PATH_INCR)
next(path_iter) # prime generator
top.after(DELAY, update_position, canvas, planet1, planet_obj1, path_iter)
top.mainloop()
Here's what it looks like running:

How to get tkinter canvas object to move around a circle

I have a canvas created image:
On my canvas, I have drawn a circle:
The code to produce this circle:
def create_circle(x, y, r, canvasName): #center coordinates, radius
x0 = x - r
y0 = y - r
x1 = x + r
y1 = y + r
return canvasName.create_oval(x0, y0, x1, y1, outline='red')
create_circle(100, 100, 50, canvas)
I would like to get the canvas created image to follow the canvas drawn circle exactly (go round the circle), by each pixel. How is this possible?
To elaborate, here is a demonstration of what I want the canvas image to do:
You can use root.after to send a periodic call to change the coordinates of your image. After that its just a matter of calculating the new x, y positions of your image in each call.
import tkinter as tk
from math import cos, sin, radians
root = tk.Tk()
root.geometry("500x500")
canvas = tk.Canvas(root, background="black")
canvas.pack(fill="both",expand=True)
image = tk.PhotoImage(file="plane.png").subsample(4,4)
def create_circle(x, y, r, canvasName):
x0 = x - r
y0 = y - r
x1 = x + r
y1 = y + r
return canvasName.create_oval(x0, y0, x1, y1, outline='red')
def move(angle):
if angle >=360:
angle = 0
x = 200 * cos(radians(angle))
y = 200 * sin(radians(angle))
angle+=1
canvas.coords(plane, 250+x, 250+y)
root.after(10, move, angle)
create_circle(250, 250, 200, canvas)
plane = canvas.create_image(450,250,image=image)
root.after(10, move, 0)
root.mainloop()

How to make a circle follow the mouse pointer?

I am trying to make 2D shooter with Python tkinter.
Here are my progress:
from tkinter import *
root = Tk()
c = Canvas(root, height=500, width=500, bg='blue')
c.pack()
circle1x = 250
circle1y = 250
circle2x = 250
circle2y = 250
circle1 = c.create_oval(circle1x, circle1y, circle1x + 10, circle1y + 10, outline='white')
circle2 = c.create_rectangle(circle2x, circle2y,circle2x + 10, circle2y + 10)
pos1 = c.coords(circle1)
pos2 = c.coords(circle2)
c.move(circle1, 250 - pos1[0], 250 - pos1[2])
c.move(circle2, 250 - pos1[0], 250 - pos1[2])
beginWall = c.create_rectangle(0, 200, 500, 210, outline='white')
def move_circle(event):
pass
c.bind('<Motion>', move_circle)
root.mainloop()
But I am trying to make the function called move_circle make circle1 and circle2 follow the mouse pointer . Something like this c.goto(circle1, x, y).
You can do it by modifying the coordinates of the two "circles" in the move_circle() event handler function. A simple calculation is done to make it so the centers of these two objects are positioned at the "tip" of the mouse pointer (see image below).
Note, I also modified your code to more closely follow the PEP 8 - Style Guide for Python Code coding guidelines.
import tkinter as tk
# Constants
CIRCLE1_X = 250
CIRCLE1_Y = 250
CIRCLE2_X = 250
CIRCLE2_Y = 250
SIZE = 10 # Height and width of the two "circle" Canvas objects.
EXTENT = SIZE // 2 # Their height and width as measured from center.
root = tk.Tk()
c = tk.Canvas(root, height=500, width=500, bg='blue')
c.pack()
circle1 = c.create_oval(CIRCLE1_X, CIRCLE1_Y,
CIRCLE1_X + SIZE, CIRCLE1_Y + SIZE,
outline='white')
circle2 = c.create_rectangle(CIRCLE2_X, CIRCLE2_Y,
CIRCLE2_X + SIZE, CIRCLE2_Y + SIZE)
pos1 = c.coords(circle1)
pos2 = c.coords(circle2)
c.move(circle1, 250-pos1[0], 250-pos1[2])
c.move(circle2, 250-pos1[0], 250-pos1[2])
begin_wall = c.create_rectangle(0, 200, 500, 210, outline='white')
def move_circles(event):
# Move two "circle" widgets so they're centered at event.x, event.y.
x0, y0 = event.x - EXTENT, event.y - EXTENT
x1, y1 = event.x + EXTENT, event.y + EXTENT
c.coords(circle1, x0, y0, x1, y1)
c.coords(circle2, x0, y0, x1, y1)
c.bind('<Motion>', move_circles)
root.mainloop()
Here's a screenshot of it running on my Windows computer:

Animating an object to move in a circular path in Tkinter

I am trying to model a simple solar system in Tkinter using circles and moving them around in canvas. However, I am stuck trying to find a way to animate them. I looked around and found the movefunction coupled with after to create an animation loop. I tried fidgeting with the parameters to vary the y offset and create movement in a curved path, but I failed while trying to do this recursively or with a while loop. Here is the code I have so far:
import tkinter
class celestial:
def __init__(self, x0, y0, x1, y1):
self.x0 = x0
self.y0 = y0
self.x1 = x1
self.y1 = y1
sol_obj = celestial(200, 250, 250, 200)
sx0 = getattr(sol_obj, 'x0')
sy0 = getattr(sol_obj, 'y0')
sx1 = getattr(sol_obj, 'x1')
sy1 = getattr(sol_obj, 'y1')
coord_sol = sx0, sy0, sx1, sy1
top = tkinter.Tk()
c = tkinter.Canvas(top, bg='black', height=500, width=500)
c.pack()
sol = c.create_oval(coord_sol, fill='black', outline='white')
top.mainloop()
Here's something that shows one way to do what you want using the tkinter after method to update both the position of the object and the associated canvas oval object. It uses a generator function to compute coordinates along a circular path representing the orbit of one of the Celestial instances (named planet_obj1).
import math
try:
import tkinter as tk
except ImportError:
import Tkinter as tk # Python 2
DELAY = 100
CIRCULAR_PATH_INCR = 10
sin = lambda degs: math.sin(math.radians(degs))
cos = lambda degs: math.cos(math.radians(degs))
class Celestial(object):
# Constants
COS_0, COS_180 = cos(0), cos(180)
SIN_90, SIN_270 = sin(90), sin(270)
def __init__(self, x, y, radius):
self.x, self.y = x, y
self.radius = radius
def bounds(self):
""" Return coords of rectangle surrounding circlular object. """
return (self.x + self.radius*self.COS_0, self.y + self.radius*self.SIN_270,
self.x + self.radius*self.COS_180, self.y + self.radius*self.SIN_90)
def circular_path(x, y, radius, delta_ang, start_ang=0):
""" Endlessly generate coords of a circular path every delta angle degrees. """
ang = start_ang % 360
while True:
yield x + radius*cos(ang), y + radius*sin(ang)
ang = (ang+delta_ang) % 360
def update_position(canvas, id, celestial_obj, path_iter):
celestial_obj.x, celestial_obj.y = next(path_iter) # iterate path and set new position
# update the position of the corresponding canvas obj
x0, y0, x1, y1 = canvas.coords(id) # coordinates of canvas oval object
oldx, oldy = (x0+x1) // 2, (y0+y1) // 2 # current center point
dx, dy = celestial_obj.x - oldx, celestial_obj.y - oldy # amount of movement
canvas.move(id, dx, dy) # move canvas oval object that much
# repeat after delay
canvas.after(DELAY, update_position, canvas, id, celestial_obj, path_iter)
top = tk.Tk()
top.title('Circular Path')
canvas = tk.Canvas(top, bg='black', height=500, width=500)
canvas.pack()
sol_obj = Celestial(250, 250, 25)
planet_obj1 = Celestial(250+100, 250, 15)
sol = canvas.create_oval(sol_obj.bounds(), fill='yellow', width=0)
planet1 = canvas.create_oval(planet_obj1.bounds(), fill='blue', width=0)
orbital_radius = math.hypot(sol_obj.x - planet_obj1.x, sol_obj.y - planet_obj1.y)
path_iter = circular_path(sol_obj.x, sol_obj.y, orbital_radius, CIRCULAR_PATH_INCR)
next(path_iter) # prime generator
top.after(DELAY, update_position, canvas, planet1, planet_obj1, path_iter)
top.mainloop()
Here's what it looks like running:

Categories