I need to create a widget that is used to pick a time. QTimeEdit widget doesn't seem intuitive or a good design. So I decided to create a time picker similar to the time picker in smartphones.
I managed to create the clock and click that makes the pointer (something similar to the pointer in the image) move to the currently clicked position (note: it's not perfect, it still looks bad). I would like to have help with making the inner clock
Here is my code:
from PyQt5 import QtWidgets, QtGui, QtCore
import math, sys
class ClockWidget(QtWidgets.QWidget): # I want to be able to reuse this class for other programs also, so please don't hard code values of the list, start and end
def __init__(self, start, end, lst=[], *args, **kwargs):
super(ClockWidget, self).__init__(*args, **kwargs)
self.lst = lst
if not self.lst:
self.lst = [*range(start, end)]
self.index_start = 0 # tune this to move the letters in the circle
self.pointer_angles_multiplier = 9 # just setting the default values
self.current = None
self.rects = []
#property
def index_start(self):
return self._index_start
#index_start.setter
def index_start(self, index):
self._index_start = index
def paintEvent(self, event):
self.rects = []
painter = QtGui.QPainter(self)
pen = QtGui.QPen()
pen.setColor(QtCore.Qt.red)
pen.setWidth(2)
painter.setPen(pen)
x, y = self.rect().x(), self.rect().y()
width, height = self.rect().width(), self.rect().height()
painter.drawEllipse(x, y, x + width, x + height)
s, t, equal_angles, radius = self.angle_calc()
radius -= 30
pen.setColor(QtCore.Qt.green)
pen.setWidth(2)
painter.setPen(pen)
""" pointer angle helps in determining to which position the pointer should be drawn"""
self.pointer_x, self.pointer_y = s + ((radius-30) * math.cos(self.pointer_angles_multiplier * equal_angles)), t \
+ ((radius-30) * math.sin(self.pointer_angles_multiplier * equal_angles))
""" The pendulum like pointer """
painter.drawLine(QtCore.QPointF(s, t), QtCore.QPointF(self.pointer_x, self.pointer_y))
painter.drawEllipse(QtCore.QRectF(QtCore.QPointF(self.pointer_x - 20, self.pointer_y - 40),
QtCore.QPointF(self.pointer_x + 30, self.pointer_y + 10)))
pen.setColor(QtCore.Qt.blue)
pen.setWidth(3)
font = self.font()
font.setPointSize(14)
painter.setFont(font)
painter.setPen(pen)
""" Drawing the number around the circle formula y = t + radius * cos(a)
y = s + radius * sin(a) where angle is in radians (s, t) are the mid point of the circle """
for index, char in enumerate(self.lst, start=self.index_start):
angle = equal_angles * index
y = t + radius * math.sin(angle)
x = s + radius * math.cos(angle)
# print(f"Add: {add_x}, index: {index}; char: {char}")
rect = QtCore.QRectF(x - 30, y - 40, x + 60, y) # clickable point
self.rects.append([index, char, rect]) # appends index, letter, rect
painter.setPen(QtCore.Qt.blue)
painter.drawRect(rect) # helps in visualizing the points where the click can received
print(f"Rect: {rect}; char: {char}")
painter.setPen(QtCore.Qt.red)
points = QtCore.QPointF(x, y)
painter.drawText(points, str(char))
def mousePressEvent(self, event):
for x in self.rects:
index, char, rect = x
if event.button() & QtCore.Qt.LeftButton and rect.contains(event.pos()):
self.pointer_angles_multiplier = index
self.current = char
self.update()
break
def angle_calc(self):
"""
This will simply return (midpoints of circle, divides a circle into the len(list) and return the
angle in radians, radius)
"""
return ((self.rect().width() - self.rect().x()) / 2, (self.rect().height() - self.rect().y()) / 2,
(360 / len(self.lst)) * (math.pi / 180), (self.rect().width() / 2))
def resizeEvent(self, event: QtGui.QResizeEvent):
"""This is supposed to maintain a Square aspect ratio on widget resizing but doesn't work
correctly as you will see when executing"""
if event.size().width() > event.size().height():
self.resize(event.size().height(), event.size().width())
else:
self.resize(event.size().width(), event.size().width())
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
message = ClockWidget(1, 13)
message.index_start = 10
message.show()
sys.exit(app.exec())
The Output:
The blue rectangles represent the clickable region. I would be glad if you could also, make the pointer move to the closest number when clicked inside the clock (Not just move the pointer when the clicked inside the blue region)
There is one more problem in my code, that is the numbers are not evenly spaced from the outer circle. (like the number 12 is closer to the outer circle than the number 6)
Disclaimer: I will not explain the cause of the error but the code I provide I think should give a clear explanation of the errors.
The logic is to calculate the position of the centers of each small circle, and use the exinscribed rectangle to take it as a base to draw the text and check if the point where you click is close to the texts.
from functools import cached_property
import math
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
class ClockWidget(QtWidgets.QWidget):
L = 12
r = 40.0
DELTA_ANGLE = 2 * math.pi / L
current_index = 9
def paintEvent(self, event):
painter = QtGui.QPainter(self)
painter.setRenderHint(QtGui.QPainter.Antialiasing)
R = min(self.rect().width(), self.rect().height()) / 2
margin = 4
Rect = QtCore.QRectF(0, 0, 2 * R - margin, 2 * R - margin)
Rect.moveCenter(self.rect().center())
painter.setBrush(QtGui.QColor("gray"))
painter.drawEllipse(Rect)
rect = QtCore.QRectF(0, 0, self.r, self.r)
if 0 <= self.current_index < 12:
c = self.center_by_index(self.current_index)
rect.moveCenter(c)
pen = QtGui.QPen(QtGui.QColor("red"))
pen.setWidth(5)
painter.setPen(pen)
painter.drawLine(c, self.rect().center())
painter.setBrush(QtGui.QColor("red"))
painter.drawEllipse(rect)
for i in range(self.L):
j = (i + 2) % self.L + 1
c = self.center_by_index(i)
rect.moveCenter(c)
painter.setPen(QtGui.QColor("white"))
painter.drawText(rect, QtCore.Qt.AlignCenter, str(j))
def center_by_index(self, index):
R = min(self.rect().width(), self.rect().height()) / 2
angle = self.DELTA_ANGLE * index
center = self.rect().center()
return center + (R - self.r) * QtCore.QPointF(math.cos(angle), math.sin(angle))
def index_by_click(self, pos):
for i in range(self.L):
c = self.center_by_index(i)
delta = QtGui.QVector2D(pos).distanceToPoint(QtGui.QVector2D(c))
if delta < self.r:
return i
return -1
def mousePressEvent(self, event):
i = self.index_by_click(event.pos())
if i >= 0:
self.current_index = i
self.update()
#property
def hour(self):
return (self.current_index + 2) % self.L + 1
def minumumSizeHint(self):
return QtCore.QSize(100, 100)
def main():
app = QtWidgets.QApplication(sys.argv)
view = ClockWidget()
view.resize(400, 400)
view.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
Related
I'm trying to do a Solar System simulator in python, with the Ursina engine, with physics. It works correctly until the earth (the only planet existing for the moment) gets in the same position on one or two axis than the sun. Then it just starts to shake and clipping and no-clipping of reality for no reason, following an straight line, usually the z axis.
Ursina's discord answer wasn't too helpful, since they lend me a code with that didn't had physics or elliptical orbits, which are the base of what I'm trying to do.
Here's the code:
from ursina import *
from ursina.prefabs.first_person_controller import FirstPersonController
from ursina.texture_importer import load_texture
import math
app = Ursina()
time_multiplicator = 1
G = 0.004
class Sun(Entity):
def __init__(self, position, color, scale, mass):
super().__init__(
parent = scene,
position = position,
origin = (0,0),
model = "sphere",
color = color,
collider = "mesh",
scale = scale
)
self.mass = mass
self.attraction_active = True
sun = Sun(position = (25,5,5), color = color.yellow, scale = 10, mass = 1500)
class Planet(Entity):
def __init__(self, position, color, scale, mass):
super().__init__(
parent = scene,
position = position,
origin = (0,0),
model = "sphere",
color = color,
scale = scale
)
self.mass = mass
self.attraction_active = True
self.initial_velocity = 0.4
def attraction(self):
self.gravitational_attraction = 1 + G * (self.mass * sun.mass)/(distance(sun,self)**2)
self.force_angle = 57.2958 * (math.atan((sun.y - self.y)/(sun.x - self.x))) + 1
self.y_component = self.gravitational_attraction * math.sin(self.force_angle) + 1
self.x_component = self.gravitational_attraction * math.cos(self.force_angle) + 1
print(f"gravitational_attraction ::: {self.gravitational_attraction}")
print(f"force_angle ::: {self.force_angle}")
print(f"y_component ::: {self.y_component}")
print(f"x_component ::: {self.x_component}")
def update(self):
self.attraction()
self.z -= self.initial_velocity * time.dt
self.y += self.y_component * time.dt
self.x += self.x_component * time.dt
blue = Planet(position = (0,0,0), color = color.blue, scale = 1, mass = 100)
print(distance(sun, blue))
EditorCamera()
def input(key):
if key == "q":
camera.look_at(blue)
app.run()
caveat: I just started tinkering with ursina ... pretty neat.
Your orig code had the earth getting sucked into the sun and the jitter was because it was near co-located. I printed the x,y coords and watched a bit.
You have a couple issues with the math and the physics.
You need to keep track of velocities, not just positions, unless I'm missing something with ursina. Recall that we can integrate to get velocities from force and position from velocity...
delta_v = F * dt
delta_pos = velocity * dt
Also, you need to use math.atan2 because it keeps track of both the x and y coordinate signage so that when things cross axes, you still get the correct sign of the angle.
It wasn't clear why you were adding "+1" to everything, so I removed it.
So after that, it is just a (non trivial) matter of putting an initial velocity on the earth so that the orbit is stable and doesn't get (a) sucked in, or (b) go flinging off into space due to lack of orbital capture. I tinkered with the velocities a bit and the below has a rotational orbit, but a weird one. I think you can tinker with the below and get to a working model.
from ursina import *
import sys
from ursina.prefabs.first_person_controller import FirstPersonController
from ursina.texture_importer import load_texture
import math
app = Ursina()
time_multiplicator = .1
G = 0.004
class Sun(Entity):
def __init__(self, position, color, scale, mass):
super().__init__(
parent = scene,
position = position,
origin = (0,0,0),
model = "sphere",
color = color,
collider = "mesh",
scale = scale
)
self.mass = mass
self.attraction_active = True
sun = Sun(position = (25,15,0), color = color.yellow, scale = 10, mass = 1500)
class Planet(Entity):
def __init__(self, position, color, scale, mass):
super().__init__(
parent = scene,
position = position,
origin = (0,0,0),
model = "sphere",
color = color,
scale = scale
)
self.mass = mass
self.attraction_active = True
self.velocity = [1, -3, 0] # vector vx, vy, vz
def attraction(self):
self.gravitational_attraction = G * (self.mass * sun.mass)/(distance(sun,self)**2)
self.force_angle = math.atan2( (-self.y + sun.y),(-self.x + sun.x))
self.y_component = self.gravitational_attraction * math.sin(self.force_angle)
self.x_component = self.gravitational_attraction * math.cos(self.force_angle)
print(f"gravitational_attraction ::: {self.gravitational_attraction}")
print(f"force_angle ::: {self.force_angle}")
print(f"y_component ::: {self.y_component}")
print(f"x_component ::: {self.x_component}")
print(f"x, y ::: {self.x}, {self.y}")
#if self.force_angle < 0: sys.exit(-1)
def update(self):
self.attraction()
# update the velocities, with update of F * dt
self.velocity[2] += 0 # no z velocity
self.velocity[1] += self.y_component * time.dt
self.velocity[0] += self.x_component * time.dt
# now update the positions with update = vel * dt
self.x += self.velocity[0]* time.dt
self.y += self.velocity[1]* time.dt
self.z += self.velocity[2]* time.dt
blue = Planet(position = (0,0,0), color = color.blue, scale = 1, mass = 100)
print(distance(sun, blue))
#EditorCamera()
# def input(key):
# if key == "q":
camera.position=(0,0,-200)
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 have a graphics scene with QGraphicsEllipseitem circles that are movable. I am trying to have the one I am dragging move around the other circle instead of allowing them to overlap aka collide. So far I was able to stop the collision but its not moving around smoothly it snaps to a corner. I dont know how to fix it.
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
import math
class Circleitem(QGraphicsEllipseItem):
def __init__(self, size, brush):
super().__init__()
radius = size / -2
self.setRect(radius, radius, size, size)
self.setBrush(brush)
self.setFlag(self.ItemIsMovable)
self.setFlag(self.ItemIsSelectable)
def paint(self, painter, option, a):
option.state = QStyle.State_None
return super(Circleitem, self).paint(painter,option)
def mouseMoveEvent(self, event):
super().mouseMoveEvent(event)
self.scene().views()[0].parent().movearound()
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.gscene = QGraphicsScene(0, 0, 1000, 1000)
gview = QGraphicsView(self.gscene)
self.setCentralWidget(gview)
self.circle1 = Circleitem (123, brush=QColor(255,255,0))
self.circle2 =Circleitem(80, brush=QColor(0,255,0))
self.gscene.addItem(self.circle1)
self.gscene.addItem(self.circle2)
self.circle1.setPos(500, 500)
self.circle2.setPos(300, 300)
self.show()
def movearound(self):
if self.gscene.selectedItems()[0] == self.circle1:
moveditem = self.circle1
stillitem = self.circle2
else:
moveditem = self.circle2
stillitem = self.circle1
if len(self.gscene.collidingItems(moveditem)) != 0:
xdist = moveditem.x() - stillitem.x()
ydist = moveditem.y() - stillitem.y()
totaldist = moveditem.rect().width()/2 + stillitem.rect().width()/2
totaldist *= math.sqrt(1 + pow(math.pi, 2)/10)/2
if ( abs(xdist) < totaldist or abs(ydist) < totaldist ):
if xdist > 0:
x = stillitem.x() + totaldist
else:
x = stillitem.x() - totaldist
if ydist > 0:
y = stillitem.y() + totaldist
else:
y = stillitem.y() - totaldist
moveditem.setPos(x, y)
app = QApplication([])
win = MainWindow()
app.exec()
It is simpler to keep the logic in Circleitem.mouseMoveEvent and use QLineF to find the distance and new position.
class Circleitem(QGraphicsEllipseItem):
def __init__(self, size, brush):
super().__init__()
radius = size / -2
self.setRect(radius, radius, size, size)
self.setBrush(brush)
self.setFlag(self.ItemIsMovable)
self.setFlag(self.ItemIsSelectable)
def paint(self, painter, option, a):
option.state = QStyle.State_None
return super(Circleitem, self).paint(painter,option)
def mouseMoveEvent(self, event):
super().mouseMoveEvent(event)
colliding = self.collidingItems()
if colliding:
item = colliding[0] # Add offset if points are equal so length > 0
line = QLineF(item.pos(), self.pos() + QPoint(self.pos() == item.pos(), 0))
min_distance = (self.rect().width() + item.rect().width()) / 2
if line.length() < min_distance:
line.setLength(min_distance)
self.setPos(line.p2())
I display an image within a custom QLabel and get clicks on this label. I'm interested in clicks within the image only, and in a position expressed by a number between 0 and 1, 0 being the leftmost or topmost pixel and 1 the rightmost or bottommost pixel, regardless of the image actual size.
I can't get the image rectangle to compute the position. When I call self.pixmap.rect(), width and height are the original image dimensions, not the dimensions of the image which was scaled to fit into the label.
What am I doing doing wrong?
from PyQt5.QtCore import Qt, pyqtSignal
from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QGridLayout
from PyQt5.QtGui import QPixmap
class Window(QWidget):
def __init__(self):
super().__init__()
image_path = '092.jpg'
image_area = Image_Area(QPixmap(image_path))
image_area.left_click.connect(self.image_clicked)
layout = QGridLayout()
layout.addWidget(image_area, 0, 0)
self.setLayout(layout)
# Left click on image
def image_clicked(self, x, y):
print(f'{x:.3f},{y:.3f}')
class Image_Area(QLabel):
left_click = pyqtSignal(float, float)
def __init__(self, pixmap):
super().__init__()
self.pixmap = pixmap
self.setPixmap(pixmap)
self.setScaledContents(False)
self.setMinimumSize(100, 100)
def resizeEvent(self, e):
if self.pixmap != None:
width, height = int(self.width()), int(self.height())
scaled_pixmap = self.pixmap.scaled(width, height, Qt.KeepAspectRatio)
self.setPixmap(scaled_pixmap)
return super().resizeEvent(e)
def mousePressEvent(self, e):
x, y, = e.x(), e.y()
if self.pixmap:
# Get pixmap rectangle
r = self.pixmap.rect()
x0, y0 = r.x(), r.y()
x1, y1 = x0+r.width(), y0+r.height()
# Check we clicked on the pixmap
if x >= x0 and x < x1 and y >= y0 and y < y1:
# emit position relative to pixmap bottom-left corner
x_relative = (x - x0) / (x1 - x0)
y_relative = (y - y0) / (y1 - y0)
self.left_click.emit(x_relative, y_relative)
super().mousePressEvent(e)
app = QApplication([])
window = Window()
window.show()
app.exec()
Update: After the answer was provided, I realized the computation of the click position relative to the image origin was wrong, here is an accurate version for the persons interested:
def mousePressEvent(self, e):
# Mouse position is in label coordinates
x, y, = e.x(), e.y()
if self.pixmap():
# pixmap is not a widget, we don't have its location,
# so we assume the pixmap is centered in the label and
# compute the location with respect to label
label_size = self.size()
pixmap_size = self.pixmap().size()
width = pixmap_size.width()
height = pixmap_size.height()
x0 = int((label_size.width() - width) / 2)
y0 = int((label_size.height() - height) / 2)
# Check we clicked on the pixmap
if (x >= x0 and x < (x0 + width) and
y >= y0 and y < (y0 + height)):
# emit position relative to pixmap top-left corner
x_relative = (x - x0) / width
y_relative = (y - y0) / height
self.left_click.emit(x_relative, y_relative)
super().mousePressEvent(e)
You have to use the QPixmap established in the QLabel that can be obtained through the pixmap() method. The problem is that you are obfuscating the access to that method since you have an attribute with a similar name. So the solution is to use pixmap() and rename the attribute pixmap.
class Image_Area(QLabel):
left_click = pyqtSignal(float, float)
def __init__(self, pixmap):
super().__init__()
self._pixmap = pixmap
self.setPixmap(pixmap)
self.setScaledContents(False)
self.setMinimumSize(100, 100)
def resizeEvent(self, e):
if self._pixmap is not None:
scaled_pixmap = self._pixmap.scaled(self.size(), Qt.KeepAspectRatio)
self.setPixmap(scaled_pixmap)
return super().resizeEvent(e)
def mousePressEvent(self, e):
x, y, = (
e.x(),
e.y(),
)
if self.pixmap:
r = self.pixmap().rect()
x0, y0 = r.x(), r.y()
x1, y1 = x0 + r.width(), y0 + r.height()
if x >= x0 and x < x1 and y >= y0 and y < y1:
x_relative = (x - x0) / (x1 - x0)
y_relative = (y - y0) / (y1 - y0)
self.left_click.emit(x_relative, y_relative)
super().mousePressEvent(e)
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.