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.
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 want to do a project that displays a planet with continents and climatic zones.
I am currently using PyQt5 for painting, with QPainterPath:
There are two problems:
With this approach, it's hard to define zones inside the continent.
Painting is quite slow (calculations are fast, thanks to numpy).
Minimal code for Path creating way:
import sys
import time
import numpy as np
import math
from PyQt5.QtWidgets import QWidget, QApplication, QSlider
from PyQt5.QtGui import QPainter, QPainterPath, QPen
from PyQt5.QtCore import Qt
class TwoDMap(QWidget):
def __init__(self, size):
super().__init__()
self.size = size
self.W = self.size.width() # width of screen
self.H = self.size.height() # height of screen
self.qp = QPainter()
self.circleRadius = self.H/3
self.pointsAcc = 30 # defines how many points will be between main points
self.interface_start_point = [50, 50]
self.circle_start_x = self.interface_start_point[0] + 100
self.circle_start_y = self.interface_start_point[1] + 200
# vars for converting points from math syster to screen system
self.scrcoord = np.array([self.circle_start_x + self.circleRadius ,
self.circle_start_y + self.circleRadius, 0])
self.scrcoordcoef = np.array([1, -1, 1])
######### Vars for rotatiob ###############
# angles
self.rotation_x = 0
self.rotation_y = 0
self.rotation_z = 0
self.axis_x = np.array([1, 0, 0])
self.axis_y = np.array([0, 1, 0])
self.axis_z = np.array([0, 0, 1])
############################################
# main point of star
points = np.array([
[self.circleRadius, math.pi/10 , -math.pi/10, ],
[self.circleRadius, math.pi/4.5 , math.pi/5.5, ],
[self.circleRadius, math.pi/3 , 0 , ],
[self.circleRadius, math.pi/3 , math.pi/5, ],
[self.circleRadius, math.pi/2 , math.pi/4, ],
[self.circleRadius, math.pi/2.75, math.pi/3, ],
[self.circleRadius, math.pi/2.75, math.pi/2, ],
[self.circleRadius, math.pi/4 , 4*math.pi/10, ],
[self.circleRadius, math.pi/5.5 , 5.5*math.pi/8,],
[self.circleRadius, math.pi/6 , math.pi/3, ],
])
# convert to decart coordinates
self.points = np.apply_along_axis(self.sphericToDecart, 1, points)
self.initUI()
self.showFullScreen()
def initUI(self):
self.setWindowTitle('2DMap')
self.setStyleSheet("background-color: black; color: white; font-size: 20px;")
# sliders for rotating
self.angleScale = 100
self.polz1 = QSlider(Qt.Horizontal, self)
self.polz1.setGeometry(self.interface_start_point[0] + 10, self.interface_start_point[1] + 10, 200, 30)
self.polz1.setRange(int(- 2 * math.pi * self.angleScale), int(2 * math.pi * self.angleScale))
self.polz1.setValue(0)
self.polz1.valueChanged.connect(self.angleChanged)
self.polz2 = QSlider(Qt.Horizontal, self)
self.polz2.setGeometry(self.interface_start_point[0] + 10, self.interface_start_point[1] + 40, 200, 30)
self.polz2.setRange(int(- 2 * math.pi * self.angleScale), int(2 * math.pi * self.angleScale))
self.polz2.setValue(0)
self.polz2.valueChanged.connect(self.angleChanged)
self.polz3 = QSlider(Qt.Horizontal, self)
self.polz3.setGeometry(self.interface_start_point[0] + 10, self.interface_start_point[1] + 70, 200, 30)
self.polz3.setRange(int(- 2 * math.pi * self.angleScale), int(2 * math.pi * self.angleScale))
self.polz3.setValue(0)
self.polz3.valueChanged.connect(self.angleChanged)
def angleChanged(self):
# func for rotating picture with slider
self.rotation_x = self.polz1.value()/(2*self.angleScale)
self.rotation_y = self.polz2.value()/(2*self.angleScale)
self.rotation_z = self.polz3.value()/(2*self.angleScale)
self.update()
def paintEvent(self, e):
self.qp.begin(self)
self.qp.setRenderHints(QPainter.Antialiasing)
self.qp.drawEllipse(int(self.circle_start_x), int(self.circle_start_y), int(self.circleRadius*2), int(self.circleRadius*2))
self.qp.setPen(QPen(Qt.green, 3, Qt.SolidLine))
path_obj = self.getSpherePath(self.getPointsToDraw())
self.qp.drawPath(path_obj)
self.qp.end()
def getPointsToDraw(self):
points: np.ndarray = self.allPointsRotationOffset(self.points)
# preparations for computing points
nw = points
nw2 = np.zeros(points.shape)
nw2[-1:] = points[0]
nw2[:-1] = points[1::1]
crs2 = np.cross(np.cross(nw, nw2), nw)
crs2 = (crs2.T / np.sqrt(np.sum(crs2 * crs2, axis=1)) * self.circleRadius).T
alphas = np.arcsin(np.sqrt(np.sum((nw2 - nw) * (nw2 - nw), axis=1)) / (2 * self.circleRadius)) * 2
# determine the number of points depending on the angle between them
endp = ((self.pointsAcc+1) * (alphas / (np.pi / 2)))
endp: np.ndarray = endp.astype(np.int16)
pnts = [0] * points.shape[0]
for i in range(points.shape[0]):
rngs = range(endp[i]+1) * alphas[i] / endp[i]
rng_cos = np.cos(rngs)
rng_sin = np.sin(rngs)
pnts[i] = self.getInterPoints(nw[i], crs2[i], rng_cos, rng_sin)
return pnts
def getSpherePath(self, allpoints):
# func for creating actual path, that will be drawn on screen
path = QPainterPath()
first_point = True
line_break = False
for points in allpoints: # for all point group
for point in points: # for all points in group
# drawing only points with z > 0
if (point[2] > 0):
if first_point:
# for first point we need to move to it
path.moveTo(point[0], point[1])
first_point = False
if line_break:
# now we have point with z > 0, but before this we had point with z < 0
# that means we need to move to new part
path.moveTo(point[0], point[1])
line_break = False
continue
# connect points (z > 0) with simple line
path.lineTo(point[0], point[1])
else:
# we need to disconect line, because point have z < 0 and must not be shown
line_break = True
return path
def getInterPoints(self, basis_i, basis_j, rng_cos, rng_sin):
# function computes points between two main points
# and covert them to screen coord. system with center on sphere center
arr = np.outer(basis_i, rng_cos) + np.outer(basis_j, rng_sin)
return arr.T * self.scrcoordcoef + self.scrcoord
def allPointsRotationOffset(self, points: np.ndarray) -> np.ndarray:
# Rotate points along axises with angles: rotation_z, rotation_y, rotation_x
rotated_pointsZ = self.kvantRotation(points, self.axis_z, self.rotation_z)
rotated_pointsY = self.kvantRotation(rotated_pointsZ, self.axis_y, self.rotation_y)
rotated_pointsX = self.kvantRotation(rotated_pointsY, self.axis_x, self.rotation_x)
return rotated_pointsX
def kvantRotation(self, p: np.ndarray, u, f):
sin = np.sin(f/2)
Sa = np.cos(f/2)
q = sin * u
qp0 = np.matmul(p, -1 * q)
ABC = np.cross(q, p) + Sa * p
qqp = np.outer(qp0, q)
prm = -1 * np.cross(ABC, q) - qqp
trr = Sa * ABC
ans2 = prm + trr
return ans2
def sphericToDecart(self, point):
# Convert points from spheric system to decart system
xyz = np.array([
math.sin(point[1]) * math.cos(point[2]),
math.sin(point[1]) * math.sin(point[2]),
math.cos(point[1])
]) * point[0]
return xyz
def start():
app = QApplication(sys.argv)
ex = TwoDMap(app.primaryScreen().size())
app.exec_()
if __name__ == '__main__':
start()
So I tried another method: using triangles for painting continents:
But this approach is MUCH slower than I thought it would be and slower than the previous method.
Code for triangles:
class Example(QWidget):
def __init__(self):
super().__init__()
self.W = 800
self.H = 800
self.CoF = 50
self.dx = 0
self.initUI()
def initUI(self):
self.setGeometry(50, 50, self.W, self.H)
self.setWindowTitle('Zamo')
self.poligons = []
self.poligons_points = []
self.triangleZam()
self.show()
#profile
def paintEvent(self, e):
start = time.time()
qp = QPainter()
qp.begin(self)
qp.setRenderHints(QPainter.Antialiasing)
pointsF = [
QPoint(430 - 300 + 200, 370 - 300 + 200),
QPoint(440 - 300 + 200, 500 - 300 + 200),
QPoint(260 - 300 + 200, 550 - 300 + 200),
QPoint(420 - 300 + 200, 600 - 300 + 200),
QPoint(420 - 300 + 200, 800 - 300 + 200),
QPoint(530 - 300 + 200, 640 - 300 + 200),
QPoint(680 - 300 + 200, 740 - 300 + 200),
QPoint(630 - 300 + 200, 560 - 300 + 200),
QPoint(720 - 300 + 200, 430 - 300 + 200),
QPoint(570 - 300 + 200, 450 - 300 + 200),
]
main_poli = QPolygon(pointsF)
main_rect: QRect = main_poli.boundingRect()
qp.setBrush(Qt.lightGray)
qp.setPen(QPen(QColor(0, 0, 0, 255), 1, Qt.SolidLine)) # Qt.lightGray QColor(0, 0, 0, 0)
# self.poligons - list of QPoligon's
# self.poligons_points - list of point of poligons (used for optimization)
# main_poli - star
# main_rect - QRect, bounding the star
for i in range(len(self.poligons)):
qp.setBrush(Qt.lightGray)
if main_rect.contains(self.poligons_points[i][0]): # if triangle inside the Rect, then check for intersecting
if main_poli.intersects(self.poligons[i]):
qp.setBrush(Qt.green)
qp.drawPolygon(self.poligons[i])
qp.setBrush(QColor(0, 0, 0, 0))
qp.setPen(QPen(QColor(255, 0, 0, 150), 1, Qt.SolidLine))
qp.drawPolygon(main_poli)
qp.end()
print(time.time() - start)
def triangleZam(self):
r = int(self.W/self.CoF)
midDot = r / np.sqrt(3)
a, b = 0 - r, 0
c, d = r/2 - r, midDot
e, f = r - r, 0
for j in range(int(self.H/midDot + 1)):
a = 0 - r / (j % 2 + 1)
c = r / 2 - r / (j % 2 + 1)
e = r - r / (j % 2 + 1)
for i in range(int(self.CoF + 1)):
points = QPolygon([QPoint(a, b),QPoint(c, d),QPoint(e, f)])
#qp.setBrush(Qt.darkGray)
points2 = QPolygon([QPoint(c, d),QPoint(e, f),QPoint(c+r, d)])
self.poligons.append(points)
self.poligons.append(points2)
self.poligons_points.append([QPoint(a, b),QPoint(c, d),QPoint(e, f)])
self.poligons_points.append([QPoint(c, d),QPoint(e, f),QPoint(c+r, d)])
a = a + r
c = c + r
e = e + r
b = b + midDot
d = d + midDot
f = f + midDot
So, my question is: is there some way to achieve faster painting in python (algorithms, libraries)? Or maybe is there some resource I can study about 2D rendering basics?
This question already has an answer here:
I have using opencv python form converting the analog clock to a digital data for the hours and minutes but I need it to show for seconds too
(1 answer)
Closed 1 year ago.
I tried to read the analog clock image and display the time using the digital image using the opencv python, I have done for reading the hours and minutes by separating the hours and minutes hand from the image using HoughLineP() using opencv but I am unable to separate the seconds hand from the image, Here is the code I am working with Please help me to separate the Seconds hand and to read the seconds values
import cv2
import math
import numpy as np
import matplotlib.pyplot as plt.
from math import sqrt, acos, degrees
import tkinter as tk
kernel = np.ones((5,5),np.uint8)
img = cv2.imread('input3.jpg')
ret, thresh = cv2.threshold(gray_img, 50, 255, cv2.THRESH_BINARY)
height, width = gray_img.shape
mask = np.zeros((height,width), np.uint8)
edges = cv2.Canny(thresh, 100, 200)
cimg=cv2.cvtColor(gray_img, cv2.COLOR_GRAY2BGR)
circles = cv2.HoughCircles(gray_img, cv2.HOUGH_GRADIENT, 1.2, 100)
for i in circles[0,:]:
# print(i) # -> [429.00003 240.6 226.20001]
i[2] = i[2] + 4
# used to detect the circle in the image
cv2.circle(mask, (int(i[0]), int(i[1])), int(i[2]), (255,255,255), thickness=-1)
masked_data = cv2.bitwise_and(img, img, mask=mask)
_,thresh = cv2.threshold(mask,1,255,cv2.THRESH_BINARY)
contours,hierarchy = cv2.findContours(thresh,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)
# Crop masked_data
crop = masked_data[y + 30 : y + h - 30, x + 30 : x + w - 30
i=crop
height, width, channels = i.shape
ret, mask = cv2.threshold(i, 10, 255, cv2.THRESH_BINARY)
edges = cv2.Canny(i,100,200)
kernel = np.ones((11,11),np.uint8)
kernel1 = np.ones((13,13),np.uint8)
edges = cv2.dilate(edges,kernel,iterations = 1)
edges = cv2.erode(edges,kernel1,iterations = 1)
minLineLength = 1000
maxLineGap = 10
lines = cv2.HoughLinesP(edges,
1,
np.pi/180,
15,
minLineLength,
maxLineGap)
l=[]
# l -> long, s -> short
xl1, xl2, yl1, yl2 = 0, 0, 0 , 0
xs1, xs2, ys1, ys2=0, 0, 0 , 0
for line in lines:
# getting the values from the line
x1, y1, x2, y2 = line[0]
dx = x2 - x1
if(dx<0):
dx = dx * -1
dy = y2 - y1
if(dy < 0):
dy = dy * -1
hypo = sqrt(dx ** 2 + dy ** 2 )
l.append(hypo)
a=len(l) # -> 295
l.sort(reverse=True)
m=0
h=0
for f in range(a):
for line in lines:
# getting the values from the line
x1, y1, x2, y2 = line[0]
dx = x2 - x1
if(dx < 0):
dx = dx * -1
dy = y2 - y1
if(dy < 0):
dy = dy * -1
hypo2 = sqrt(dx ** 2 + dy ** 2 )
if(hypo2 == l[0]):
m = hypo2
xl1 = x1
xl2 = x2
yl1 = y1
yl2 = y2
# getting line region
cv2.line(crop, (xl1, yl1), (xl2, yl2), (255, 0, 0), 3)
if(m==l[0]):
if(hypo2 ==l[f]):
if((sqrt((xl2 - x2)**2 + (yl2 - y2)**2)) > 20):
if((sqrt((xl1 - x1)**2 + (yl1 - y1)**2))>20):
xs1 = x1
xs2 = x2
ys1 = y1
ys2 = y2
# getting line region
cv2.line(crop, (xs1, ys1), (xs2, ys2), (0, 255, 0), 3)
h=1
break
if(h==1):
break
xcenter = int(width / 2)
ycenter = int(height / 2)
hour1 = abs(xcenter - xs1)
hour2 = abs(xcenter - xs2)
if(hour1 > hour2):
xhour = xs1
yhour = ys1
else:
xhour = xs2
yhour = ys2
min1 = abs(xcenter - xl1)
min2 = abs(xcenter - xl2)
if(min1 > min2):
xmin = xl1
ymin = yl1
else:
xmin = xl2
ymin = yl2
l1 = sqrt( ((xcenter - xhour) ** 2) + ((ycenter - yhour) ** 2) )
l2 = ycenter
l3 = sqrt( ((xcenter - xhour) ** 2) + ((0 - yhour) ** 2) )
cos_theta_hour = ( ( (l1) ** 2 ) + ( (l2) ** 2 ) - ( (l3) ** 2) ) / ( 2 * (l1) * (l2) )
theta_hours_radian = acos(cos_theta_hour)
theta_hours = math.degrees(theta_hours_radian)
if(xhour > xcenter):
right=1
else:
right=0
if(right==1):
hour = int(theta_hours / (6*5))
if(right==0):
hour = 12 - (int(theta_hours / (6*5)))
if(hour==0):
hour=12
l1 = sqrt( ((xcenter - xmin) ** 2) + ((ycenter - ymin) ** 2) )
l2 = ycenter
l3 = sqrt( ((xcenter - xmin) ** 2) + ((0 - ymin) ** 2) )
cos_theta_min = ( ( (l1) ** 2 ) + ( (l2) ** 2 ) - ( (l3) ** 2) ) / ( 2 * (l1) * (l2) )
theta_min_radian = acos(cos_theta_min)
theta_min = math.degrees(theta_min_radian)
if(xmin > xcenter):
right=1
else:
right=0
if(right==1):
minute = int(theta_min / ((6*5)/5))
if(right==0):
minute = 60 - (int(theta_min / ((6*5)/5)))
if(xmin == xcenter):
minutes=30
if (minute < 10):
def display():
value = "{}:0{}".format(hour,minute)
digit.config(text = value)
else:
def display():
value = "{}:{}".format(hour,minute)
digit.config(text = value)
canvas = tk.Tk()
canvas.title("Analog to Digital")
canvas.geometry("300x250")
digit = tk.Label(canvas, font=("ds-digital", 65, "bold"), bg="black", fg="blue", bd = 80)
digit.grid(row=0, column = 1)
display()
canvas.mainloop()
Considering that your approach to retrieve the values of hours and minute is successful, here is a simple solution to find the value of seconds.
There are certain properties associated with the second-hand of your clock image which need a little preparation before estimating its value. The first one is the color. Because the hand is colored in red, you have a good leading point on separating it from the rest of the image. If you can mask the red color of the second-hand in the image using the cv2.inRange() function (read more about it here), you'll be left with a clean image of only the second-hand. After that, you can convert it to black and white using cv2.cvtColor() function. Now, the second problem might be the thin line of second-hand. You can solve that using Morphological Transformations. Using the Erosion_ function, you can grow the white area of the image and thicken the second-hand. After that, use whatever method you were using to find the value for minutes and hours hand and you are good to go.
I want a commet in pygame to shoot across the screen. Here is my commet class
class Commet:
def __init__(self):
self.x = -10
self.y = 10
self.radius = 20
self.commet = pygame.image.load(r"C:\Users\me\OneDrive\Documents\A level python codes\final game\commet.png")
self.commet = pygame.transform.scale(self.commet, (self.radius, self.radius))
self.drop = 0.0000009
self.speed = 2
self.pos = 0
self.commets = []
Then i added 20 commets to the self.commets list.
def tail(self, n): # n is a variable used to denote the length of the self.commets
for i in range(n):
if len(self.commets) <= n - 1:
self.commets.append(Commet())
I am having two problems. The first problem being moving the commet. To move it i did this
def move_tail(self):
for c in self.commets:
c.x += self.speed
for i in range(len(self.commets) - 1):
self.commets[i].y += ((self.commets[i + 1].x) ** 2) * self.drop
For x- coordinate i just added 2 to its value every frame. However, for the yvalue of the commet, i want it to produce a tail-like following effect. I tried assigning the y value of the commet to the square of x value of the commet in the index position one above the commet we are referring to in the list self.commets.I expected the commets to follow each other along a general x = y **2 quadradic curve. They do follow the curve but all at the same rate(i expected them to follow at different rate because all the commets have different x values), which dosent give me the tail-like effect. How would i be able to produce this tail-like effect?
The second part of my question is that i want the commets following the first one get smaller and smaller. I tried decreasing the radius value, which is used to scale the image i imported. The code looks like this
# Decrease radius
for i in range(n):
self.commets[i].radius = i + 1
When i print out the values of radius of the commets on the console, they range from 1 to 20, as i expect them to, but the size of the image that appears on the screen is the same for all the commets in the list.The following code is how i blit the commet
for i in range(n):
self.commets[i].pos = i * 10 # This line maintains a certain x- distance between commets
for c in self.tails:
D.blit(c.commet, (c.x - c.pos, c.y))
if self.pos >= n:
self.pos = n
Given you want your comet to fly from left to right on a FullHD screen.
The comet shall start at the left side at a y coordinate of 900, then reach its highest point at x=1400 and y = 100 and then fall to 600 at the right side of the screen.
A parabola is generally y = ax²+bx+c.
To be independent of the screen resolution, you would of course calculate those values from some percentage, say 900 ~ screen height * 83%, 600 ~ screen height * 55%, 1400 ~ screen width * 73%, 100 ~ screen height * 9%
With three points given, you can calculate a parabola:
class ParabolaFrom3Points:
def __init__(self, points: list):
self.a = (points[0][0] * (points[1][1] - points[2][1]) + points[1][0] * (
points[2][1] - points[0][1]) + points[2][0] * (points[0][1] - points[1][1])) / (
(points[0][0] - points[1][0]) * (points[0][0] - points[2][0]) * (
points[2][0] - points[1][0]))
self.b = (points[0][0] ** 2 * (points[1][1] - points[2][1]) + points[1][0] ** 2 * (
points[2][1] - points[0][1]) + points[2][0] ** 2 * (points[0][1] - points[1][1])) / (
(points[0][0] - points[1][0]) * (points[0][0] - points[2][0]) * (
points[1][0] - points[2][0]))
self.c = (points[0][0] ** 2 * (points[1][0] * points[2][1] - points[2][0] * points[1][1]) +
points[0][0] * (points[2][0] ** 2 * points[1][1] - points[1][0] ** 2 * points[2][1]) +
points[1][0] * points[2][0] * points[0][1] * (points[1][0] - points[2][0])) / (
(points[0][0] - points[1][0]) * (points[0][0] - points[2][0]) * (
points[1][0] - points[2][0]))
def y(self, x: int) -> int:
return int(self.a * x ** 2 + self.b * x + self.c)
The Comet is quite simple then. It just needs to know its parabola function and can then calculate the y from the x.
class Comet:
def __init__(self, radius: int, para: ParabolaFrom3Points):
self.x = -radius # Be invisible at the beginning
self.radius = radius
self.para = para
def move(self, x):
self.x = x
def paint(self, screen):
x = self.x
radius = self.radius
for tail in range(20):
pygame.draw.circle(screen, [255, 255, 255], (int(x), self.para.y(x)), radius)
x = x - radius / 2
radius -= 1
Test code:
import pygame
pygame.init()
pygame.fastevent.init()
clock = pygame.time.Clock()
window = pygame.display.set_mode((1920, 1080))
pygame.display.set_caption('Comet example')
comet = Comet(20, ParabolaFrom3Points([(0, 1080 * 0.83), (1920 * 0.73, 1080 * 0.12), (1920, 1080 * 0.55)]))
for x in range(-20, 1920 + 200, 3):
comet.move(x)
comet.paint(window)
clock.tick(90)
pygame.display.flip()
window.fill([0, 0, 0])
pygame.quit()
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.