Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Creating Rounded Edges for a Polygon in 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:

enter image description here

So how can I change this code to remove the stretched roundness and keep it a constant radius?

like image 998
Bhavye Mathur Avatar asked Jun 22 '26 23:06

Bhavye Mathur


1 Answers

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.


enter image description here


#! 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')
    ...

enter image description here


like image 148
Reblochon Masque Avatar answered Jun 24 '26 11:06

Reblochon Masque