create scroll-able rectangles from items in a list - python

I Have a program where it has a list of names. I want to be able to list each of these names onto a rectangle. I want each name to create its own rectangle. This example create a rectangle for each title in the list i put '' at the start of the list as I got an error saying I couldn't divide by zero in python by adding '' fixed that.
import pygame
width,height = 800,600
screen = pygame.display.set_mode((width,height))
mover = 0
games = ['','Space Invaders','Snake']
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
quit()
if event.type == pygame.MOUSEBUTTONDOWN:
if event.button == 4:
if mover > 0:
mover -= 15
if event.button == 5:
if mover < (height-120)-60:
mover += 15
screen.fill((47,79,79))
for game in games:
for num in range(len(games)):
if num != 0:
pygame.draw.rect(screen, (0,51,51), (100,((height-120)/num)-mover,width-200,20))
pygame.draw.rect(screen, (0,51,51), (width-30,100,25,height-120)) #SCROLL BAR
pygame.draw.rect(screen, (0,0,0), (width-29,mover+100,23,60)) #SCROLLING BAR
pygame.draw.line(screen, (0,0,0), (0,100),(width,100), 4)
pygame.display.flip()
I am not sure in how I can get the rectangles closer together so that there is only 5 to 10 pixels between each rectangle. I also feel like there is a better way in doing this.

Define a distance to top of the screen. e.g. 100. Define a distance between the lines. The height of a line is 20. If the gap between the lines should be 10, the the distance is 30. So the start of a line is 110 + num*30 - mover:
for num, game in enumerate(games):
pygame.draw.rect(screen, (0,51,51), (100, 110 + num*30 + mover, width-200, 20))
Note, you can use enumerate, to get tuples of list index and list element.

Related

Detect collision between textbox and circle in pygame

I'm trying to create a game in python where one can drag a textbox around the screen, but whenever it touches the borders of a circle around it, I want the loop to start over, but with a different text (by storing all text-strings in a list, but I'm not that far, yet). This is how far I have come:
import pygame
import ptext
pygame.init()
gameDisplay = pygame.display.set_mode((500, 500))
gameDisplay.fill((255,255,255))
x = 190
y = 230
a = 250
b = 250
text = "ExampleText 1."
def textbox(x,y):
ptext.draw(text, (x,y), color = (0,0,0))
def circle(a,b):
pygame.draw.circle(gameDisplay, (0,0,0), (250, 250), 210, 5)
done = False
while not done:
for event in pygame.event.get():
if event.type == pygame.QUIT:
done = True
elif event.type == pygame.MOUSEMOTION:
if event.buttons[0]:
x += event.rel[0]
y += event.rel[1]
textbox(x,y)
circle(a,b)
pygame.display.flip()
pygame.quit()
quit()
Now I understand I will need to detect collision of the borders of my objects, but here I'm pretty lost. I tried to store the variables of my objects in rectangles and then produce another if statement that recognizes whether or not my objects collide (I used a print command because I haven't gotten to the actual command I want, yet), but that won't print anything and I'm sure I'm on the wrong path, but it is my best effort...
For that I have defined:
text_rect = pygame.Rect(x, y, 10, 30)
circle_rect = pygame.Rect(a,b, 300, 300)
and then in my loop:
if circle_rect.colliderect(text_rect):
print("COLLIDE")
Does anybody have any tip on a better way to define the objects and to create the function I want?
(Edit: Btw.: I'm not too concerned about the fact that when I drag my textbox, it leaves a print of the text, since that doesn't happen in my original script, but would be thankful if anyone knows why it does that in my current example.)
A rectangle has 4 corner points. If the rectangle is "smaller" then the circle (the diameter of the circle is greater than the diagonal of the rectangle), then the rectangle collides with the contour of a circle, if at least one point is out of the circle and at least one point is in the circle.
Define the rectangle and setup a list of the corner points. Further you've to know the radius of the circle:
w, h = 10, 30
rect = pygame.Rect(x, y, 10, 30)
corners = [rect.bottomleft, rect.bottomright, rect.topleft, rect.topright]
radius = 210
Calculate the Euclidean distance of each corner point to the center of the circle (a, b):
import math
dist = [math.sqrt((p[0]-a)**2 + (p[1]-b)**2) for p in corners]
Create to lists, one with the points in the circle (p_in) and one with the points out of the circle (p_out):
p_out = [i for i, d in enumerate(dist) if d > radius]
p_in = [i for i, d in enumerate(dist) if d < radius]
If both list contain any element, then the rectangle intersects the circle contour:
if any(p_in) and any(p_out):
print("COLLIDE")
If len(p_in) is 4, then the rectangle is completely in the circle. If len(p_out) is 4 then the rectangle is completely out of the circle.
if any(p_in) and any(p_out):
print("COLLIDE")
elif len(p_in) == 4:
print("IN")
elif len(p_out) == 4:
print("OUT")
See the simple example, which is based on your code snippet and demonstrates the collision test. The rectangle is attached to the mouse:
import pygame
import math
pygame.init()
gameDisplay = pygame.display.set_mode((500, 500))
done = False
while not done:
for event in pygame.event.get():
if event.type == pygame.QUIT:
done = True
x, y = pygame.mouse.get_pos()
w, h = 10, 30
rect = pygame.Rect(x, y, 10, 30)
a, b = 250, 250
radius = 210
corners = [rect.bottomleft, rect.bottomright, rect.topleft, rect.topright]
dist = [math.sqrt((p[0]-a)**2 + (p[1]-b)**2) for p in corners]
p_out = [i for i, d in enumerate(dist) if d > radius]
p_in = [i for i, d in enumerate(dist) if d < radius]
if any(p_in) and any(p_out):
print("COLLIDE")
elif len(p_in) == 4:
print("IN")
elif len(p_out) == 4:
print("OUT")
gameDisplay.fill((255,255,255))
pygame.draw.rect(gameDisplay, (255, 0, 0), rect)
pygame.draw.circle(gameDisplay, (0,0,0), (a, b), radius, 5)
pygame.display.flip()
pygame.quit()
quit()

Pygame lags with pygame rectangles added

I'm running a python game with approximately 2k rectangles being drawn on screen each frame.
The problem I have is that it runs on 12 fps and I have no idea how to fix that. When I remove all the rectangles it shifts to 100 fps. I'm not rendering all of them at once, but only the ones camera can see currently. How to fix this lag spike issue, is it because I'm using pygame rectangles or because I'm using them wrong?
here's the code
import pygame
black = (0,0,0)
pygame.init()
gameDisplay = pygame.display.set_mode((0,0),pygame.FULLSCREEN)
gameDisplay.fill(black)
gameDisplay.convert()
clock = pygame.time.Clock()
display_width = 1920
display_height = 1080
from opensimplex import OpenSimplex
tmp = OpenSimplex()
dimensions = [100,100]
size = 40
def mapping(x):
y = (x + 1) * 10 + 40
return y
class GroundCell:
def __init__(self,x,y,dim):
self.x = x
self.y = y
self.dim = dim
tempcells = []
allCells = []
for a in range(0,dimensions[0]):
tempcells = []
for b in range(0,dimensions[1]):
tempcells.append(GroundCell(a*size,b*size,mapping(tmp.noise2d(a*0.11,b*0.11))))
allCells.append(tempcells)
font = pygame.font.Font("freesansbold.ttf", 20)
while True:
for event in pygame.event.get():
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
pygame.quit()
quit()
for a in allCells:
for b in a:
if b.x in range(0,display_width) \
and b.y in range(0,display_height) or\
b.x+size in range(0,display_width) \
and b.y+size in range(0,display_height) :
pygame.draw.rect(gameDisplay,(b.dim,b.dim,b.dim),(b.x,b.y,size,size))
fps = font.render('FPS: ' + str(int(clock.get_fps())), 1, (0, 0, 0))
gameDisplay.blit(fps, (20, 20))
pygame.display.update()
clock.tick(120)
gameDisplay.fill(black)
Instead of drawing all those rects to the screen every tick, create a Surface with the noise once, and reuse it.
Some more notes:
Text rendering is expensive. If you use a lot of text rendering, better cache the surfaces created by Font.render. Note there's also the new freetype module in pygame, which is better than the font module in every way.
Your check if a rect is inside the screen is very strange. You could just use something like if 0 < b.x < display_width ... or even use pygame's Rect class, which offers nice methods like contains().
A good and fast way to create a Surface from arbitary data is to use numpy and pygame's surfarray module. Don't get intimidated, it isn't that hard to use.
Here's a running example based on your code:
import pygame
import numpy as np
black = (0,0,0)
pygame.init()
display_width = 1000
display_height = 1000
gameDisplay = pygame.display.set_mode((display_width, display_height))
gameDisplay.fill(black)
clock = pygame.time.Clock()
from opensimplex import OpenSimplex
tmp = OpenSimplex()
dimensions = [100,100]
size = 16
def mapping(x):
y = (x + 1) * 10 + 40
return y
# create an 2d array from the noise
def get_array():
rgbarray = np.zeros((dimensions[0], dimensions[1]))
for x in range(dimensions[0]):
for y in range(dimensions[1]):
c = int(mapping(tmp.noise2d(x*0.11, y*0.11)))
# simple way to convert the color value to all three (r,g,b) channels
rgbarray[x, y] = c | c << 8 | c << 16
return rgbarray
# create the array and copy it into a Surface
data = get_array()
surface = pygame.Surface((dimensions[0], dimensions[1]))
pygame.surfarray.blit_array(surface, data)
# scale the Surface to the desired size
scaled = pygame.transform.scale(surface, (dimensions[0]*size, dimensions[1]*size))
# simple way to cache font rendering
font = pygame.font.Font("freesansbold.ttf", 20)
cache = {}
def render(text):
if not text in cache:
cache[text] = font.render(text, 1, (0, 0, 0))
return cache[text]
x = 0
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
running = False
x-=1
if x < -1000:
x = 0
gameDisplay.blit(scaled, (x, 0))
fps = render('FPS: ' + str(int(clock.get_fps())))
gameDisplay.blit(fps, (20, 20))
pygame.display.update()
clock.tick(120)
As #sloth explained, there are better ways to do the same, but if you want to know what the actual problems are:
You're not drawing 2000 rectangles, you're drawing 10.000 of them, because your dimension is 100x100
You're doing a check if the rectangle is visible in the worst possible way in terms of performance. Just check what happens if you leave the check and don't draw the rectangles. You'll see that the performance is improved but it's still far from 120fps. That's because for every rectangle you generate a list of numbers from 0 to the width of the screen and another list from zero to the height of the screen. You also do that twice. That means, on a 1920x1080 screen: (1920 * 10000) + (1920 * 10000) + (1080 * 10000) + (1080*10000) = 60000000. So, 60 million checks. If you have 120fps, that means 60 million * 120 = 7.2 billion checks per second.
Just changing the check to something similar to if b.x+size < display_width and b.y+size < display_height and b.x > 0 and b.y > 0: will already improve the performance.
That said, it's still 10000 rectangles, and it's still 120fps, which means 1200000 rectangles per second, done with basically no HW acceleration and with a high level language. Don't expect top performance.

Why does this list index go out of range?

I'm taking some baby steps with pygame, and I have gotten this program to work. However it throws a "List index out of range" message after looping through main() several hundred times. The list doesn't change size once it grows to a certain limit, so I am not sure where the error is happening.
Once you run the program, just move the mouse across the display, and a bunch of circles are drawn and grow as the clock ticks. Eventually, it will crash.
I have omitted all comments except the lines where the error is happening, hopefully that will make it easier to find out the cause.
Stacktrace:
Traceback (most recent call last):
File "C:\Users\Devo\AppData\Local\Programs\Python\Python37-32\test files\Psychedelic Circles.py", line 89, in <module>
main()
File "C:\Users\Devo\AppData\Local\Programs\Python\Python37-32\test files\Psychedelic Circles.py", line 49, in main
drawCircles(snakeLength)
File "C:\Users\Devo\AppData\Local\Programs\Python\Python37-32\test files\Psychedelic Circles.py", line 75, in drawCircles
pygame.draw.circle(SCREEN, colorList[i], coords[i], abs(i-(len(coords))) * 5, 0)#### Why does the list index go out of range?
IndexError: list index out of range
import pygame, sys, random
from pygame.locals import*
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
GREEN = (0, 255, 0)
RED = (255, 0, 0)
FUSCHIA = (255, 0, 240)
GRAY = (80, 80, 80)
YELLOW = (255, 255, 0)
ORANGE = (255, 127, 0)
BLUE = (0, 0, 255)
INDIGO = (75, 0, 130)
VIOLET = (148, 0, 211)
FPS = 20
WWIDTH = 1000
WHEIGHT = 700
BIGBUTTON = pygame.Rect(0, 0, 1000, 700)
rainbowTuple = (RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, VIOLET)
def main():
global SCREEN, FPS, colorList, coords, snakeLength
pygame.init()
clock = pygame.time.Clock()
size = (WWIDTH, WHEIGHT)
SCREEN = pygame.display.set_mode(size)
pygame.display.set_caption('Psychedelic Circles')
colorList = []
coords = []
snakeLength = 50 ### Change this value to make the circles disappear more quickly or slowly
while True:
clickedButton = None
SCREEN.fill(GRAY)
drawButtons()
checkForQuit()
for event in pygame.event.get():
if event.type == MOUSEMOTION:
mousex, mousey = event.pos
clickedButton = getButtonClicked(mousex, mousey)
if clickedButton == FUSCHIA:
sendCoords(mousex, mousey)
drawCircles(snakeLength)
pygame.display.update()
clock.tick(FPS)
def terminate():
pygame.quit()
sys.exit()
def sendCoords(x, y):
coords.append((x, y))
colorList.append(random.choice(rainbowTuple))
def checkForQuit():
for event in pygame.event.get(QUIT):
terminate()
for event in pygame.event.get(KEYUP):
if event.key == K_ESCAPE:
terminate()
pygame.event.post(event)
def drawButtons():
pygame.draw.rect(SCREEN, FUSCHIA, BIGBUTTON)
def drawCircles(snakeLength):
for i in range(len(coords)):
pygame.draw.circle(SCREEN, colorList[i], coords[i], abs(i-(len(coords))) * 5, 0)#### Why does the list index go out of range?
if i > snakeLength :
popList()
def popList():
coords.pop(0)
colorList.pop(0)
def getButtonClicked(x, y):
if BIGBUTTON.collidepoint((x, y)):
return FUSCHIA
return None
if __name__ == '__main__':
main()
I suspect this error only occurs when there are multiple mouse move events in the event queue. Normally, pygame is fast enough to render the screen while accruing no more than one new user input event, so sendCoords will only be called once in between drawCircles calls. In that case, coords never exceeds a size of 52. But if several mouse move events accrue (perhaps due to system lag, or because the user is jiggling his mouse really fast), then sendCoords may be called many more times in a row. So by the time drawCircles executes, coords can have 53 elements, or even more.
This becomes a problem when you reach drawCircles:
def drawCircles(snakeLength):
for i in range(len(coords)):
pygame.draw.circle(SCREEN, colorList[i], coords[i], abs(i-(len(coords))) * 5, 0)#### Why does the list index go out of range?
if i > snakeLength :
popList()
Let's say this function executes when coords contains 53 elements, and snakeLength is 50. The loop will iterate normally until i equals 51. Then the i > snakeLength will evaluate to True, and popList will be called. Now coords is one element smaller, and has a length of 52. That iteration of the loop ends, and the next iteration starts. i will equal 52. The pygame.draw.circle line will attempt to access coords[i], but because coords no longer has 53 elements, coords[i] will raise an IndexError trying to access the 53rd element.
Python is not smart enough to understand that the for i in range(len(coords)) loop should end one iteration earlier than usual if coords reduces in size by one. It happily iterates all the way up to the original length of the list, heedless of whether this might cause a crash.
One possible solution is to move popList outside of the list, so the size of coords doesn't change while you're iterating over it.
def drawCircles(snakeLength):
for i in range(len(coords)):
pygame.draw.circle(SCREEN, colorList[i], coords[i], abs(i-(len(coords))) * 5, 0)#### Why does the list index go out of range?
while len(coords) > snakeLength :
popList()
You might be thinking "But why is it OK to modify coords' length in this while loop, when it wasn't OK to do it in the for loop?" The critical distinction is the evaluation time of the two statements. range(len(coords)) exectues exactly once before the loop starts, so modifications to coords won't be noticed. but len(coords) > snakeLength executes at the beginning of very iteration of the while loop, so changes to coords gets noticed right away.

White spots when drawing mutliple lines close to each other

How can I get rid of white spots when drawing multiple circles close to each other in pygame?
Here is my code:
import pygame
from pygame import gfxdraw
from math import pow, atan2
def getColor(r, col1, col2, fun="lin"):
if fun =="pol2":
r = pow(r,2)
col1_r = tuple([r*x for x in col1])
col2_r = tuple([(1-r)*x for x in col2])
final_col = tuple(sum(i) for i in zip(col1_r, col2_r))
return final_col
def draw(sizeX, sizeY):
# Initialize the game engine
pygame.init()
screen = pygame.display.set_mode([sizeX, sizeY])
#Loop until the user clicks the close button.
done = False
clock = pygame.time.Clock()
while not done:
# This limits the while loop to a max of 10 times per second.
# Leave this out and we will use all CPU we can.
clock.tick(10)
for event in pygame.event.get(): # User did something
if event.type == pygame.QUIT: # If user clicked close
done=True # Flag that we are done so we exit this loop
screen.fill(WHITE)
for y in range(200,500):
for x in range(0,10):
gfxdraw.arc(screen, 400, 400, y, x*15, (x+1)*15, getColor(x/10,(0,0,(y-200)/2),(255,255,(y-200)/2), fun="lin"))
pygame.display.flip()
pygame.quit()
This phenomenon is called aliasing and happens when you take a continuous signal and samples it. In your case, gfx.draw() uses continuous functions (the trigonometric functions) to calculate which pixel to draw the color onto. Since theses calculations are in floats and have to be rounded to integers, it may happen that some pixels are missed.
To fix this you need an anti-aliasing filter. There are many different types such as low pass (blurring), oversampling etc.
Since these holes almost always are one pixel I'd create a function that identifies these holes and fills them with the average of it's neighbours colors. The problem is that Pygame is not very good at manually manipulating pixels, so it can be slow depending on the size of the image. Although, Pygame has a module called surfarray that's built on numpy which allows you to access pixels easier and faster, so that will speed it up some. Of course, it'll require you to install numpy.
I couldn't get your program to work, so next time make sure you really have a Minimal, Complete, and Verifiable example. The following code is just based on the image you provided.
import numpy as np
import pygame
pygame.init()
RADIUS = 1080 // 2
FPS = 30
screen = pygame.display.set_mode((RADIUS * 2, RADIUS * 2))
clock = pygame.time.Clock()
circle_size = (RADIUS * 2, RADIUS * 2)
circle = pygame.Surface(circle_size)
background_color = (255, 255, 255)
circle_color = (255, 0, 0)
pygame.draw.circle(circle, circle_color, (RADIUS, RADIUS), RADIUS, RADIUS // 2)
def remove_holes(surface, background=(0, 0, 0)):
"""
Removes holes caused by aliasing.
The function locates pixels of color 'background' that are surrounded by pixels of different colors and set them to
the average of their neighbours. Won't fix pixels with 2 or less adjacent pixels.
Args:
surface (pygame.Surface): the pygame.Surface to anti-aliasing.
background (3 element list or tuple): the color of the holes.
Returns:
anti-aliased pygame.Surface.
"""
width, height = surface.get_size()
array = pygame.surfarray.array3d(surface)
contains_background = (array == background).all(axis=2)
neighbours = (0, 1), (0, -1), (1, 0), (-1, 0)
for row in range(1, height-1):
for col in range(1, width-1):
if contains_background[row, col]:
average = np.zeros(shape=(1, 3), dtype=np.uint16)
elements = 0
for y, x in neighbours:
if not contains_background[row+y, col+x]:
elements += 1
average += array[row+y, col+x]
if elements > 2: # Only apply average if more than 2 neighbours is not of background color.
array[row, col] = average // elements
return pygame.surfarray.make_surface(array)
def main():
running = True
image = pygame.image.load('test.png').convert()
# image = circle
pos = image.get_rect(center=(RADIUS, RADIUS))
while running:
clock.tick(FPS)
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
running = False
elif event.key == pygame.K_1:
print('Reset circle.')
image = circle
elif event.key == pygame.K_2:
print('Starting removing holes.')
time = pygame.time.get_ticks()
image = remove_holes(image, background=(255, 255, 255))
time = pygame.time.get_ticks() - time
print('Finished removing holes in {:.4E} s.'.format(time / 1000))
screen.fill(background_color)
screen.blit(image, pos)
pygame.display.update()
if __name__ == '__main__':
main()
Result
Before
After
Time
As I said before, it's not a very fast operation. Here are some benchmarks based on the circle in the example:
Surface size: (100, 100) | Time: 1.1521E-02 s
Surface size: (200, 200) | Time: 4.3365E-02 s
Surface size: (300, 300) | Time: 9.7489E-02 s
Surface size: (400, 400) | Time: 1.7257E-01 s
Surface size: (500, 500) | Time: 2.6911E-01 s
Surface size: (600, 600) | Time: 3.8759E-01 s
Surface size: (700, 700) | Time: 5.2999E-01 s
Surface size: (800, 800) | Time: 6.9134E-01 s
Surface size: (900, 900) | Time: 9.1454E-01 s
And with your image:
Time: 1.6557E-01 s

Ellipse Tool in Python part 2

I am currently making changes to the way my ellipse tool works as it was not working the correct way previously. I am creating it for my paint program using python 2.7.5 and pygame. I have recently encountered this error:
Traceback (most recent call last):
File "C:\Users\Wisdom1\Desktop\Comp Science Files\Canvas.py", line 164, in <module>
draw.ellipse(screen,(c),(x,y,radx,rady),sz2)
ValueError: width greater than ellipse radius
This occurs when I try to create an ellipse going in every direction except downward to the right from a point. I understand the error I just do not know how to fix it. Here is my ellipse tool:
if mb[0] == 1 and canvas.collidepoint(mx,my):
screen.set_clip(canvas)
if tool == "ellipse":
screen.blit(copy,(0,0))
radx = max(mx-x,1)
rady = max(my-y,1)
draw.ellipse(screen,(c),(x,y,radx,rady),sz2)
screen.set_clip(None)
Sz2 is a size variable that begins at 10 and decreases or increases by 3 each time the mouse wheel is moved down or up. Any help is appreciated. Thank you
You could also use a ternary statement in the form of:
draw.ellipse(screen,(c),(x,y,radx,rady), sz2 if sz2 < max(radx, raxy) else 0)
Sincerely,
Another Massey student working on a Sunday night ;)
If sz2, the thickness of the curve, is greater than the minor radius of the ellipse, pygame raises a ValueError. So you could protect against this by using an if-statement:
if sz2 < min(radx, rady)//2:
pygame.draw.ellipse(self.screen, green, box, sz2)
else:
# sz2=0 fills the ellipse
pygame.draw.ellipse(self.screen, green, box, 0)
radx, rady is the width and height of the Rect bounding the ellipse. So the minor radius is half the smaller of radx and rady.
Here is some runnable code showing that the if-statement works:
"""Based on http://www.pygame.org/docs/tut/intro/intro.html"""
import sys
import pygame
pygame.init()
size = (width, height) = (320, 240)
screen = pygame.display.set_mode(size)
black = (0,0,0)
green = [0, 255, 0]
radx, rady = 50, 70
box = [160-radx//2, 120-rady//2, radx, rady]
width = 1
delta = 2
while True:
for event in pygame.event.get():
if ((event.type == pygame.QUIT) or
(event.type == pygame.KEYDOWN and
event.key == pygame.K_ESCAPE)):
sys.exit()
screen.fill(black)
if 0 < width < min(radx, rady)//2:
pygame.draw.ellipse(screen, green, box, width)
else:
if width > 0:
pygame.draw.ellipse(screen, green, box, 0)
delta *= -1
width += delta
pygame.display.flip()
pygame.time.delay(100)

Categories