Im trying to graph a cubic bezier curve however, I am having difficulty with the last part of the program. I cant seem to get tkinter to actually draw the curve. It will currently just draw a small line in the top left of the tkinter window and im not sure if im doing it the wrong way or not.
from tkinter import *
root = Tk()
window = Canvas(root, width=800, height=800)
window.pack()
def bezier_curve():
#create empty list for points
p = []
#loops through 4 times to get 4 control points
for i in range(4):
while True:
#user input
p_input = input("Enter X,Y Coordinates for p" + str(i) + ":")
#splits the string into x and y coordinates
p_components = p_input.split(',')
#checks to see if user hasnt entered two coordinates
if len(p_components) != 2:
print("Missing coordinate please try again.")
p_input = input("Enter starting point X,Y Coordinates:")
#checks to see if the values can not be converted into floats.
try:
x = float(p_components[0])
y = float(p_components[1])
except ValueError:
print("Invalid coordinates", p_components, "please try again.")
#appends the x and y coordinates as a 2 dimensional array.
else:
p.append([float(p_components[0]), float(p_components[1])])
break
print(p[0][0])
#Start x and y coordinates, when t = 0
x_start = p[0][0]
y_start = p[0][1]
#loops through in intervals of 0.1
for t in range(0, 11, 1):
t = i/10
x=(p[0][0]*(1-t)**3+p[1][0]*3*t*(1-t)**2+p[2][0]*3*t**2*(1-t)+p[3][0]*t**3)
y=(p[0][1]*(1-t)**3+p[1][1]*3*t*(1-t)**2+p[2][1]*3*t**2*(1-t)+p[3][1]*t**3)
draw_line = window.create_line(x,y,x_start,y_start)
#updates initial values
x_start = x
y_start = y
bezier_curve()
root.mainloop()
Yes; a small error in the loop for drawing the lines:
#loops through in intervals of 0.1
for t in range(0, 11, 1):
t = i/10
You have assigned t as the loop variable when it should be i.
for t in range(0, 11, 1):
^
This shoud be i
#figbeam answer is correct, and fixes your problem.
I found your input mechanism tedious, so I changed it to allow clicks on the canvas to capture the control points of your bezier curve.
import tkinter as tk
def draw_bezier():
# Start x and y coordinates, when t = 0
x_start = control_points[0][0]
y_start = control_points[0][1]
p = control_points
# loops through
n = 50
for i in range(50):
t = i / n
x = (p[0][0] * (1-t)**3 + p[1][0] * 3 * t * (1-t)**2 + p[2][0] * 3 * t**2 * (1-t) + p[3][0] * t**3)
y = (p[0][1] * (1-t)**3 + p[1][1] * 3 * t * (1-t)**2 + p[2][1] * 3 * t**2 * (1-t) + p[3][1] * t**3)
canvas.create_line(x, y, x_start, y_start)
# updates initial values
x_start = x
y_start = y
def get_point(event):
global control_points
point = x, y = (event.x, event.y)
control_points.append(point)
canvas.create_oval(x, y, x+3, y+3)
if len(control_points) == 4:
draw_bezier()
control_points = []
if __name__ == '__main__':
control_points = []
root = tk.Tk()
canvas = tk.Canvas(root, width=800, height=800)
canvas.pack()
canvas.bind('<Button-1>', get_point)
root.mainloop()
Related
I am writing a script to store movements over a hexgrid using Tkinter. As part of this I want to use a mouse-click on a Tkinter canvas to first identify the click location, and then draw a line between this point and the location previously clicked.
Generally this works, except that after I've drawn a line, it become an object that qualifies for future calls off the find_closest method. This means I can still draw lines between points, but selecting the underlying Hex in the Hexgrid over times becomes nearly impossible. I was wondering if someone could help me find a solution to exclude particular objects (lines) from the find_closest method.
edit: I hope this code example is minimal enough.
import tkinter
from tkinter import *
from math import radians, cos, sin, sqrt
class App:
def __init__(self, parent):
self.parent = parent
self.c1 = Canvas(self.parent, width=int(1.5*340), height=int(1.5*270), bg='white')
self.c1.grid(column=0, row=0, sticky='nsew')
self.clickcount = 0
self.clicks = [(0,0)]
self.startx = int(20*1.5)
self.starty = int(20*1.5)
self.radius = int(20*1.5) # length of a side
self.hexagons = []
self.columns = 10
self.initGrid(self.startx, self.starty, self.radius, self.columns)
self.c1.bind("<Button-1>", self.click)
def initGrid(self, x, y, radius, cols):
"""
2d grid of hexagons
"""
radius = radius
column = 0
for j in range(cols):
startx = x
starty = y
for i in range(6):
breadth = column * (1.5 * radius)
if column % 2 == 0:
offset = 0
else:
offset = radius * sqrt(3) / 2
self.draw(startx + breadth, starty + offset, radius)
starty = starty + 2 * (radius * sqrt(3) / 2)
column = column + 1
def draw(self, x, y, radius):
start_x = x
start_y = y
angle = 60
coords = []
for i in range(6):
end_x = start_x + radius * cos(radians(angle * i))
end_y = start_y + radius * sin(radians(angle * i))
coords.append([start_x, start_y])
start_x = end_x
start_y = end_y
hex = self.c1.create_polygon(coords[0][0], coords[0][1], coords[1][0], coords[1][1], coords[2][0],
coords[2][1], coords[3][0], coords[3][1], coords[4][0], coords[4][1],
coords[5][0], coords[5][1], fill='black')
self.hexagons.append(hex)
def click(self, evt):
self.clickcount = self.clickcount + 1
x, y = evt.x, evt.y
tuple_alfa = (evt.x, evt.y)
self.clicks.append(tuple_alfa)
if self.clickcount >= 2:
start = self.clicks[self.clickcount - 1]
startx = start[0]
starty = start[1]
self.c1.create_line(evt.x, evt.y, startx, starty, fill='white')
clicked = self.c1.find_closest(x, y)[0]
print(clicked)
root = tkinter.Tk()
App(root)
root.mainloop()
I was watching a video by Aperture on youtube: https://youtu.be/1w40fxsyraE?t=325
At the provided timestamp (5:25), he begins talking about a way to create a fractal. I tried to replicate this in a python program, but I am getting a different output. I have no idea why I am getting this output, but the math seems right, so I don't know what to change. Can anyone explain why my output looks different than the one in the video?
import turtle as t
drawer = t.Turtle()
drawer.speed(1000)
# drawer.hideturtle()
drawer.penup()
#make dot A
dotAx = 125
dotAy = 150
drawer.goto(dotAx, dotAy)
drawer.dot(10, "red")
#
#make dot B
dotBx = 185
dotBy = 0
drawer.goto(dotBx, dotBy)
drawer.dot(10, "red")
#
#make dot C
dotCx = 0
dotCy = 0
drawer.goto(dotCx, dotCy)
drawer.dot(10, "red")
#
#make middle dot
dotPx = 100
dotPy = 75
drawer.goto(dotPx, dotPy)
drawer.dot(5,"yellow")
#
#draw dots v
x = 0
drawer.pendown()
while True:
if x == 0:
dotPx = (dotPx + dotAx)/2
dotPy = (dotPy + dotAy)/2
drawer.goto(dotPx, dotPy)
drawer.dot(5,"black")
print("A", dotPx, dotPy)
x+=1
if x == 1:
dotPx = (dotPx + dotBx)/2
dotPy = (dotPy + dotBy)/2
drawer.goto(dotPx, dotPy)
drawer.dot(5, "black")
print("B", dotPx, dotPy)
x+=1
if x == 2:
dotPx = (dotPx + dotCx)/2
dotPy = (dotPy + dotCy)/2
drawer.goto(dotPx, dotPy)
drawer.dot(5, "black")
print("C", dotPx, dotPy)
x = 0
I watched the video and played with your code and can't see the inconsistency. But looking further afield, I found that by changing your x (corner selection) from being cyclic, to instead being random, it works fine:
from turtle import Turtle
from random import randint
turtle = Turtle()
turtle.speed('fastest')
turtle.hideturtle()
turtle.penup()
# make dot A
dotAx, dotAy = 0, 0
turtle.goto(dotAx, dotAy)
turtle.dot(10, 'red')
# make dot B
dotBx, dotBy = 150, 260
turtle.goto(dotBx, dotBy)
turtle.dot(10, 'red')
# make dot C
dotCx, dotCy = 300, 0
turtle.goto(dotCx, dotCy)
turtle.dot(10, 'red')
# make random dot inside triangle
dotPx, dotPy = 100, 75
turtle.goto(dotPx, dotPy)
turtle.dot(5, 'green')
# draw dots
while True:
x = randint(0, 2) # pick a random corner
if x == 0:
dotPx = (dotAx + dotPx)/2
dotPy = (dotAy + dotPy)/2
turtle.goto(dotPx, dotPy)
turtle.dot(5)
elif x == 1:
dotPx = (dotBx + dotPx)/2
dotPy = (dotBy + dotPy)/2
turtle.goto(dotPx, dotPy)
turtle.dot(5)
elif x == 2:
dotPx = (dotCx + dotPx)/2
dotPy = (dotCy + dotPy)/2
turtle.goto(dotPx, dotPy)
turtle.dot(5)
Perhaps this will narrow your search as to why your orignal approach, as suggested by the video, failed. If I were writing this from scratch, and attempting to get finer detail (and speed), I might take greater advantage of turtle and Python by doing:
from turtle import Screen, Turtle, Vec2D
from random import choice
VERTICES = [Vec2D(0, 0), Vec2D(150, 260), Vec2D(300, 0)]
point = Vec2D(100, 75) # random point inside triangle
def doit():
global point
point = (choice(VERTICES) + point) * 0.5
turtle.goto(point)
turtle.dot(2)
screen.update()
screen.ontimer(doit)
screen = Screen()
screen.tracer(False)
turtle = Turtle()
turtle.hideturtle()
turtle.penup()
for vertex in VERTICES:
turtle.goto(vertex)
turtle.dot(5, 'red')
turtle.goto(point)
turtle.dot(5, 'green')
doit()
screen.mainloop()
I was wondering if it was possible to undraw previously drawn shapes from a loop. I have this function that'll create squares on click however I want to make it so that when the same area is clicked a second time the square will undraw.
from graphics import *
def createGrid():
X = 0
Y = 0
gridSize = 5
for i in range(1, gridSize + 1):
for j in range(1, gridSize + 1):
gridSquare = Rectangle(Point(X, Y), Point(X + 100, Y + 100))
gridSquare.draw(win)
X = X + 100
Y = Y + 100
X = 0
def editMode():
SelectedSquare = []
instruction = input("> ")
if instruction == "s":
selectionMode(SelectedSquare)
def selectionMode(SelectedSquare):
editSquare = Rectangle(Point(0, 0), Point(20, 20))
editSquare.setFill("black")
editSquare.draw(win)
while True:
selection = win.getMouse()
clickXPos = selection.getX()
clickYPos = selection.getY()
if clickXPos > 20 and clickYPos > 20:
PosX, PosY = clickXPos - (clickXPos % 100), clickYPos - (clickYPos % 100)
SelectedSquare = SelectedSquare + [Point(PosX, PosY)]
rect = Rectangle(Point(PosX, PosY), Point(PosX + 100, PosY + 100))
rect.setWidth(5)
rect.draw(win)
else:
editSquare.undraw()
break
win = GraphWin("GRID", 500, 500)
createGrid()
while True:
editMode()
As you can see, on click, there will be a thicker border around the grid square that was selected, I would like to be able to
1) remove the thickened border if clicked a second time
2) be able to remove all thickened borders surrounding grid squares
but I just cannot seem to figure this out, any help would be greatly appreciated!
The general problem seems to be that you have no underlying data structure or logic for your program -- you're drawing the interface first and then trying to make its behaviors define the program.
Below I've patched your code to have the points on the selected squares list remember what rectangle was drawn to highlight them, so if they are selected again, the highlight can be undone and the point removed:
from graphics import *
GRID_SIZE = 5
SQUARE_SIZE = 100
EDIT_BUTTON_SIZE = 20
BORDER_WIDTH = 5
def createGrid():
X, Y = 0, 0
for _ in range(1, GRID_SIZE + 1):
for _ in range(1, GRID_SIZE + 1):
gridSquare = Rectangle(Point(X, Y), Point(X + SQUARE_SIZE, Y + SQUARE_SIZE))
gridSquare.draw(win)
X += SQUARE_SIZE
Y += SQUARE_SIZE
X = 0
def editMode():
selectedSquares = []
instruction = input("> ")
if instruction == "s":
selectionMode(selectedSquares)
def checkSelected(point, squares):
for selected in squares:
if point.getX() == selected.getX() and point.getY() == selected.getY():
return selected
return None
def selectionMode(selectedSquares):
editSquare = Rectangle(Point(0, 0), Point(EDIT_BUTTON_SIZE, EDIT_BUTTON_SIZE))
editSquare.setFill("black")
editSquare.draw(win)
while True:
selection = win.getMouse()
clickXPos = selection.getX()
clickYPos = selection.getY()
if clickXPos <= EDIT_BUTTON_SIZE and clickYPos <= EDIT_BUTTON_SIZE:
break
PosX, PosY = clickXPos - clickXPos % SQUARE_SIZE, clickYPos - clickYPos % SQUARE_SIZE
point = Point(PosX, PosY)
selected = checkSelected(point, selectedSquares)
if selected:
selected.rect.undraw()
selectedSquares.remove(selected)
else:
rect = Rectangle(point, Point(PosX + SQUARE_SIZE, PosY + SQUARE_SIZE))
rect.setWidth(BORDER_WIDTH)
rect.draw(win)
point.rect = rect
selectedSquares.append(point)
editSquare.undraw()
win = GraphWin("GRID", GRID_SIZE * SQUARE_SIZE, GRID_SIZE * SQUARE_SIZE)
createGrid()
while True:
editMode()
But this is only a band-aid -- as you add more functionality the issue of a lack of data structure and spelled out logic will continue to frustrate you.
I wrote this function that draw a grid of triangles:
def create_triangles(side_length):
result = []
half_width = int(side_length / 2)
# height = int(side_length * math.sqrt(3) / 2)
height = side_length
max_width = 15 * side_length
max_height = 10 * height
for i in range(0, max_height, height):
if (i / height) % 2 == 0:
for j in range(0, max_width-half_width, half_width):
if j % side_length == 0:
triangle = (i-height/2, j-half_width, i+height/2, j, i-height/2, j+half_width)
else:
triangle = (i-height/2, j, i+height/2, j+half_width, i+height/2, j-half_width)
result.append(triangle)
else:
for j in range(half_width, max_width, half_width):
if j % side_length == 0:
triangle = (i-height/2, j-2*half_width, i+height/2, j-half_width+2, i-height/2, j)
else:
triangle = (i-height/2, j-half_width, i+height/2, j, i+height/2, j-2*half_width)
result.append(triangle)
return result
The current output is this:
As you can see some triangles are misaligned but I don't understand why.
As mentioned in the comments, floating points give you incorrect results; You want to make sure that the shared points representing the vertices of two adjacent triangles are concurrent. A simple approach is to reduce the points coordinates to ints, and organize the calculations so errors do not add up.
In the following examples, the misalignment is corrected, every triangle on the canvas is represented by a polygon, and individually drawn; each triangle can therefore be referenced when moused over, or addressed via an index, or a mapping (not implemented).
import tkinter as tk
import math
WIDTH, HEIGHT = 500, 500
class Point:
"""convenience for point arithmetic
"""
def __init__(self, x, y):
self.x, self.y = x, y
def __add__(self, other):
return Point(self.x + other.x, self.y + other.y)
def __iter__(self):
yield self.x
yield self.y
def tile_with_triangles(canvas, side_length=50):
"""tiles the entire surface of the canvas with triangular polygons
"""
triangle_height = int(side_length * math.sqrt(3) / 2)
half_side = side_length // 2
p0 = Point(0, 0)
p1 = Point(0, side_length)
p2 = Point(triangle_height, half_side)
for idx, x in enumerate(range(-triangle_height, WIDTH+1, triangle_height)):
for y in range(-side_length, HEIGHT+1, side_length):
y += half_side * (idx%2 + 1)
offset = Point(x, y)
pa, pb, pc = p0 + offset, p1 + offset,p2 + offset
canvas.create_polygon(*pa, *pb, *pc, outline='black', fill='', activefill='red')
p2 = Point(-triangle_height, half_side) # flip the model triangle
for idx, x in enumerate(range(-triangle_height, WIDTH+triangle_height+1, triangle_height)):
for y in range(-side_length, HEIGHT+1, side_length):
y += half_side * (idx%2 + 1)
offset = Point(x, y)
pa, pb, pc = p0 + offset, p1 + offset,p2 + offset
canvas.create_polygon(*pa, *pb, *pc, outline='black', fill='', activefill='blue')
root = tk.Tk()
canvas = tk.Canvas(root, width=WIDTH, height=HEIGHT, bg='cyan')
canvas.pack()
tile_with_triangles(canvas) #, side_length=10)
root.mainloop()
I added an active fill property that will change the colors of each triangle when you mouse over.
I am attempting to draw a speedometer using a Tkinter Canvas in Python and am having a few problems with my code that I can't seem to figure out. First off, here is what I have written:
import tkinter as tk
from tkinter import ttk
import math
class DrawMeter(tk.Canvas):
def __init__(self, parent, *args, **kwargs):
tk.Canvas.__init__(self, parent, *args, **kwargs)
self.config(bg = "grey")
if (int(self['height']) * 2 > int(self['width'])):
boxSide = int(self['width'])
else:
boxSide = int(self['height']) * 2
self.boxX = boxSide / 2
self.boxY = boxSide / 2
self.boxRadius = int(0.40 * float(boxSide))
self.start = 0
self.end = 1
self.drawBackground()
self.drawTicks()
self.drawNeedle()
def drawBackground(self):
bgColour = "black"
self.create_arc((self.boxX - self.boxRadius,
self.boxY - self.boxRadius,
self.boxX * 4,
self.boxY * 4),
fill = bgColour, start = 90)
def drawTicks(self):
length = self.boxRadius / 8
for deg in range(5, 85, 6):
rad = math.radians(deg)
self.Tick(rad, length)
for deg in range(5, 91, 18):
rad = math.radians(deg)
self.Tick(rad, length * 2)
def Tick(self, angle, length):
cos = math.cos(angle)
sin = math.sin(angle)
radius = self.boxRadius * 2
X = self.boxX * 2
Y = self.boxY * 2
self.create_line((X - radius * cos,
Y - radius * sin,
X - (radius - length) * cos,
Y - (radius - length) * sin),
fill = "white", width = 2)
def drawText(self, start = 0, end = 100):
interval = end / 5
value = start
length = self.boxRadius / 2
for deg in range(5, 91, 18):
rad = math.radians(deg)
cos = math.cos(rad)
sin = math.sin(rad)
radius = self.boxRadius * 2
self.create_text(self.boxX * 2 - (radius - length - 1) * cos,
self.boxY * 2 - (radius - length - 1) * sin,
text = str("{0:.1f}".format(value)),
fill = "white",
font = ("Arial", 12, "bold"))
value = value + interval
def setRange(self, start, end):
self.start = start
self.end = end
self.drawText(start, end)
def drawNeedle(self):
X = self.boxX * 2
Y = self.boxY * 2
length = self.boxRadius - (self.boxRadius / 4)
self.meterHand = self.create_line(X / 2, Y / 2, X + length, Y + length,
fill = "red", width = 4)
self.create_arc(X - 30, Y - 30, X + 30, Y + 30,
fill = "#c0c0c0", outline = "#c0c0c0", start = 90)
def updateNeedle(self, value):
length = self.boxRadius - (self.boxRadius / 4)
deg = 80 * (value - self.start) / self.end - 180
rad = math.radians(deg)
self.coords(self.meterHand, self.boxX * 2, self.boxY * 2,
self.boxX + length * math.cos(rad),
self.boxY + length * math.sin(rad))
value = 0
def update_frame():
global value
if value < 1:
value = value + 0.01
print(value)
meter.updateNeedle(value)
container.after(200, update_frame)
root = tk.Tk()
container = tk.Frame(root)
container.pack()
meter = DrawMeter(container, height = 200, width = 200, bg = "red")
meter.setRange(0, 1)
meter.pack()
update_frame()
root.mainloop()
So the problem is I am having is that my needle is drawing and updating properly on the screen. When I start the program, the needle starts at around 0.2ish, and goes until about 0.6 and then stop. I feel like my formula for calculating the needle's position is wrong, but I am not sure what about it is wrong.
The way I have it set up, is it takes the the percentage of the total the value is (value = self.start) / self.end) and multiplies it by 80 degrees (because my speedometer starts at the 5 degree marks and ends at 85) and them subtracts 180 so that the number makes the number go clockwise, not counter clockwise.
My placement of the canvas objects could also be off. My attempt was to set up a Speedometer that is a quarter circle at the bottom of the Canvas. However when you use the Canvas create_arc function, it draws the bottom right corner of your arc, in the center of the bounding box, meaning you make it the bottom right corner, I need to make by bounding box double the width and height of the canvas. I'm thinking maybe that threw me off a bit as well.
My problem was that I needed to create an offset value and add it to all four points in the Canvas.coords call.