Related
I try to implement beam collision detection with a predefined track mask in Pygame. My final goal is to give an AI car model vision to see a track it's riding on:
This is my current code where I fire beams to mask and try to find an overlap:
import math
import sys
import pygame as pg
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)
pg.init()
beam_surface = pg.Surface((500, 500), pg.SRCALPHA)
def draw_beam(surface, angle, pos):
# compute beam final point
x_dest = 250 + 500 * math.cos(math.radians(angle))
y_dest = 250 + 500 * math.sin(math.radians(angle))
beam_surface.fill((0, 0, 0, 0))
# draw a single beam to the beam surface based on computed final point
pg.draw.line(beam_surface, BLUE, (250, 250), (x_dest, y_dest))
beam_mask = pg.mask.from_surface(beam_surface)
# find overlap between "global mask" and current beam mask
hit = mask.overlap(beam_mask, (pos[0] - 250, pos[1] - 250))
if hit is not None:
pg.draw.line(surface, BLUE, mouse_pos, hit)
pg.draw.circle(surface, GREEN, hit, 3)
surface = pg.display.set_mode((500, 500))
mask_surface = pg.image.load("../assets/mask.png")
mask = pg.mask.from_surface(mask_surface)
clock = pg.time.Clock()
while True:
for e in pg.event.get():
if e.type == pg.QUIT:
pg.quit()
sys.exit()
mouse_pos = pg.mouse.get_pos()
surface.fill((0, 0, 0))
surface.blit(mask_surface, mask_surface.get_rect())
for angle in range(0, 120, 30):
draw_beam(surface, angle, mouse_pos)
pg.display.update()
clock.tick(30)
Let's describe what happens in the code snippet. One by one, I draw beams to beam_surface, make masks from them, and find overlap with background mask defined by one rectangle and a circle (black color in gifs). If there is a "hit point" (overlap point between both masks), I draw it with a line connecting hit point and mouse position.
It works fine for angles <0,90>:
But it's not working for angles in range <90,360>:
Pygame's overlap() documentation tells this:
Starting at the top left corner it checks bits 0 to W - 1 of the first row ((0, 0) to (W - 1, 0)) then continues to the next row ((0, 1) to (W - 1, 1)). Once this entire column block is checked, it continues to the next one (W to 2 * W - 1).
This means that this approach will work only if the beam hits the mask approximately from the top left corner. Do you have any advice on how to make it work for all of the situations? Is this generally a good approach to solve this problem?
Your approach works fine, if the x and y component of the ray axis points in the positive direction, but it fails if it points in the negative direction. As you pointed out, that is caused by the way pygame.mask.Mask.overlap works:
Starting at the top left corner it checks bits 0 to W - 1 of the first row ((0, 0) to (W - 1, 0)) then continues to the next row ((0, 1) to (W - 1, 1)). Once this entire column block is checked, it continues to the next one (W to 2 * W - 1).
To make the algorithm work, you have to ensure that the rays point always in the positive direction. Hence if the ray points in the negative x direction, then flip the mask and the ray vertical and if the ray points in the negative y direction than flip the ray horizontal.
Use pygame.transform.flip() top create 4 masks. Not flipped, flipped horizontal, flipped vertical and flipped vertical and horizontal:
mask = pg.mask.from_surface(mask_surface)
mask_fx = pg.mask.from_surface(pg.transform.flip(mask_surface, True, False))
mask_fy = pg.mask.from_surface(pg.transform.flip(mask_surface, False, True))
mask_fx_fy = pg.mask.from_surface(pg.transform.flip(mask_surface, True, True))
flipped_masks = [[mask, mask_fy], [mask_fx, mask_fx_fy]]
Determine if the direction of the ray:
c = math.cos(math.radians(angle))
s = math.sin(math.radians(angle))
Get the flipped mask dependent on the direction of the ray:
flip_x = c < 0
flip_y = s < 0
filpped_mask = flipped_masks[flip_x][flip_y]
Compute the flipped target point:
x_dest = 250 + 500 * abs(c)
y_dest = 250 + 500 * abs(s)
Compute the flipped offset:
offset_x = 250 - pos[0] if flip_x else pos[0] - 250
offset_y = 250 - pos[1] if flip_y else pos[1] - 250
Get the nearest intersection point of the flipped ray and mask and unflip the intersection point:
hit = filpped_mask.overlap(beam_mask, (offset_x, offset_y))
if hit is not None and (hit[0] != pos[0] or hit[1] != pos[1]):
hx = 500 - hit[0] if flip_x else hit[0]
hy = 500 - hit[1] if flip_y else hit[1]
hit_pos = (hx, hy)
pg.draw.line(surface, BLUE, mouse_pos, hit_pos)
pg.draw.circle(surface, GREEN, hit_pos, 3)
See the example: repl.it/#Rabbid76/PyGame-PyGame-SurfaceLineMaskIntersect-2
import math
import sys
import pygame as pg
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)
pg.init()
beam_surface = pg.Surface((500, 500), pg.SRCALPHA)
def draw_beam(surface, angle, pos):
c = math.cos(math.radians(angle))
s = math.sin(math.radians(angle))
flip_x = c < 0
flip_y = s < 0
filpped_mask = flipped_masks[flip_x][flip_y]
# compute beam final point
x_dest = 250 + 500 * abs(c)
y_dest = 250 + 500 * abs(s)
beam_surface.fill((0, 0, 0, 0))
# draw a single beam to the beam surface based on computed final point
pg.draw.line(beam_surface, BLUE, (250, 250), (x_dest, y_dest))
beam_mask = pg.mask.from_surface(beam_surface)
# find overlap between "global mask" and current beam mask
offset_x = 250 - pos[0] if flip_x else pos[0] - 250
offset_y = 250 - pos[1] if flip_y else pos[1] - 250
hit = filpped_mask.overlap(beam_mask, (offset_x, offset_y))
if hit is not None and (hit[0] != pos[0] or hit[1] != pos[1]):
hx = 499 - hit[0] if flip_x else hit[0]
hy = 499 - hit[1] if flip_y else hit[1]
hit_pos = (hx, hy)
pg.draw.line(surface, BLUE, pos, hit_pos)
pg.draw.circle(surface, GREEN, hit_pos, 3)
#pg.draw.circle(surface, (255, 255, 0), mouse_pos, 3)
surface = pg.display.set_mode((500, 500))
#mask_surface = pg.image.load("../assets/mask.png")
mask_surface = pg.Surface((500, 500), pg.SRCALPHA)
mask_surface.fill((255, 0, 0))
pg.draw.circle(mask_surface, (0, 0, 0, 0), (250, 250), 100)
pg.draw.rect(mask_surface, (0, 0, 0, 0), (170, 170, 160, 160))
mask = pg.mask.from_surface(mask_surface)
mask_fx = pg.mask.from_surface(pg.transform.flip(mask_surface, True, False))
mask_fy = pg.mask.from_surface(pg.transform.flip(mask_surface, False, True))
mask_fx_fy = pg.mask.from_surface(pg.transform.flip(mask_surface, True, True))
flipped_masks = [[mask, mask_fy], [mask_fx, mask_fx_fy]]
clock = pg.time.Clock()
while True:
for e in pg.event.get():
if e.type == pg.QUIT:
pg.quit()
sys.exit()
mouse_pos = pg.mouse.get_pos()
surface.fill((0, 0, 0))
surface.blit(mask_surface, mask_surface.get_rect())
for angle in range(0, 359, 30):
draw_beam(surface, angle, mouse_pos)
pg.display.update()
clock.tick(30)
Not,the algorithm can be further improved. The ray is always drawn on the bottom right quadrant of the beam_surface. Hence the other 3 quadrants are no longer needed and the size of beam_surface can be reduced to 250x250. The start of the ray is at (0, 0) rather than (250, 250) and the computation of the offsets hast to be slightly adapted:
beam_surface = pg.Surface((250, 250), pg.SRCALPHA)
def draw_beam(surface, angle, pos):
c = math.cos(math.radians(angle))
s = math.sin(math.radians(angle))
flip_x = c < 0
flip_y = s < 0
filpped_mask = flipped_masks[flip_x][flip_y]
# compute beam final point
x_dest = 500 * abs(c)
y_dest = 500 * abs(s)
beam_surface.fill((0, 0, 0, 0))
# draw a single beam to the beam surface based on computed final point
pg.draw.line(beam_surface, BLUE, (0, 0), (x_dest, y_dest))
beam_mask = pg.mask.from_surface(beam_surface)
# find overlap between "global mask" and current beam mask
offset_x = 499-pos[0] if flip_x else pos[0]
offset_y = 499-pos[1] if flip_y else pos[1]
hit = filpped_mask.overlap(beam_mask, (offset_x, offset_y))
if hit is not None and (hit[0] != pos[0] or hit[1] != pos[1]):
hx = 499 - hit[0] if flip_x else hit[0]
hy = 499 - hit[1] if flip_y else hit[1]
hit_pos = (hx, hy)
pg.draw.line(surface, BLUE, pos, hit_pos)
pg.draw.circle(surface, GREEN, hit_pos, 3)
I have a set of keyframes in a list that look like this:
[{
"duration" : 20,
"position" : [0,0],
"scale" : [1, 1],
"angle" : 0,
"rgba" : [255,255,255,255]
},
{
"duration" : 5,
"position" : [0,0],
"scale" : [1, 1.5],
"angle" : 50,
"rgba" : [255,255,255,255]
}]
The idea is being able to do the corresponding transformations every frame. Notice that scale is separated between width and height.
The problem comes form trying to scale width and height independently, while still rotating around a pivot.
I tried modifying some code from: (How to rotate an image around its center while its scale is getting larger(in Pygame))
def blitRotate(surf, image, pos, originPos, angle, zoom):
# calcaulate the axis aligned bounding box of the rotated image
w, h = image.get_size()
box = [pygame.math.Vector2(p) for p in [(0, 0), (w, 0), (w, -h), (0, -h)]]
box_rotate = [p.rotate(angle) for p in box]
min_box = (min(box_rotate, key=lambda p: p[0])[0], min(box_rotate, key=lambda p: p[1])[1])
max_box = (max(box_rotate, key=lambda p: p[0])[0], max(box_rotate, key=lambda p: p[1])[1])
# calculate the translation of the pivot
pivot = pygame.math.Vector2(originPos[0], -originPos[1])
pivot_rotate = pivot.rotate(angle)
pivot_move = pivot_rotate - pivot
# calculate the upper left origin of the rotated image
move = (-originPos[0] + min_box[0] - pivot_move[0], -originPos[1] - max_box[1] + pivot_move[1])
origin = (pos[0] + zoom * move[0], pos[1] + zoom * move[1])
# get a rotated image
rotozoom_image = pygame.transform.rotozoom(image, angle, zoom)
# rotate and blit the image
surf.blit(rotozoom_image, origin)
# draw rectangle around the image
pygame.draw.rect (surf, (255, 0, 0), (*origin, *rotozoom_image.get_size()),2)
but i'm struggling trying to think of the math necessary to make it work, i've tried separating zoom into a dupe, and then instead of doing rotozoom , scaling first with transform.scale and then transform.rotate afterwards but that didn't work either.
To better illustrate what i mean, it would be something like this:
It changes it's width and height but the pivot stays the same
I would suggest adopting a slightly different approach presented here: How to set the pivot point (center of rotation) for pygame.transform.rotate()?
All you have to do to adjust this algorithm is scale the vector from the image center to the pivot point on the image by the zoom factor:
offset_center_to_pivot = pygame.math.Vector2(origin) - image_rect.center
offset_center_to_pivot = (pygame.math.Vector2(origin) - image_rect.center) * scale
The final function that rotates an image around a pivot point, zooms and blits the image might look like this:
def blitRotate(surf, original_image, origin, pivot, angle, scale):
image_rect = original_image.get_rect(topleft = (origin[0] - pivot[0], origin[1]-pivot[1]))
offset_center_to_pivot = (pygame.math.Vector2(origin) - image_rect.center) * scale
rotated_offset = offset_center_to_pivot.rotate(-angle)
rotated_image_center = (origin[0] - rotated_offset.x, origin[1] - rotated_offset.y)
rotozoom_image = pygame.transform.rotozoom(original_image, angle, scale)
rect = rotozoom_image.get_rect(center = rotated_image_center)
surf.blit(rotozoom_image, rect)
The scaling factor can also be specified separately for the x and y axis:
def blitRotate(surf, original_image, origin, pivot, angle, scale_x, scale_y):
image_rect = original_image.get_rect(topleft = (origin[0] - pivot[0], origin[1]-pivot[1]))
offset_center_to_pivot = pygame.math.Vector2(origin) - image_rect.center
offset_center_to_pivot.x *= scale_x
offset_center_to_pivot.y *= scale_y
rotated_offset = offset_center_to_pivot.rotate(-angle)
rotated_image_center = (origin[0] - rotated_offset.x, origin[1] - rotated_offset.y)
scaled_image = pygame.transform.smoothscale(original_image, (image_rect.width * scale_x, image_rect.height * scale_y))
rotozoom_image = pygame.transform.rotate(scaled_image, angle)
rect = rotozoom_image.get_rect(center = rotated_image_center)
surf.blit(rotozoom_image, rect)
See also Rotate surface
Minimal example: repl.it/#Rabbid76/PyGame-RotateZoomPivot
import pygame
pygame.init()
screen = pygame.display.set_mode((500, 500))
clock = pygame.time.Clock()
def blitRotate(surf, original_image, origin, pivot, angle, scale):
image_rect = original_image.get_rect(topleft = (origin[0] - pivot[0], origin[1]-pivot[1]))
offset_center_to_pivot = (pygame.math.Vector2(origin) - image_rect.center) * scale
rotated_offset = offset_center_to_pivot.rotate(-angle)
rotated_image_center = (origin[0] - rotated_offset.x, origin[1] - rotated_offset.y)
rotozoom_image = pygame.transform.rotozoom(original_image, angle, scale)
rect = rotozoom_image.get_rect(center = rotated_image_center)
surf.blit(rotozoom_image, rect)
pygame.draw.rect (surf, (255, 0, 0), rect, 2)
try:
image = pygame.image.load('AirPlane.png')
except:
text = pygame.font.SysFont('Times New Roman', 50).render('image', False, (255, 255, 0))
image = pygame.Surface((text.get_width()+1, text.get_height()+1))
pygame.draw.rect(image, (0, 0, 255), (1, 1, *text.get_size()))
image.blit(text, (1, 1))
w, h = image.get_size()
angle, zoom = 0, 1
done = False
while not done:
clock.tick(60)
for event in pygame.event.get():
if event.type == pygame.QUIT:
done = True
pos = (screen.get_width()/2, screen.get_height()/2)
screen.fill(0)
blitRotate(screen, image, pos, (w/4, h/2), angle, zoom)
angle += 1
zoom += 0.01
if zoom > 5:
zoom = 1
pygame.draw.line(screen, (0, 255, 0), (pos[0]-20, pos[1]), (pos[0]+20, pos[1]), 3)
pygame.draw.line(screen, (0, 255, 0), (pos[0], pos[1]-20), (pos[0], pos[1]+20), 3)
pygame.draw.circle(screen, (0, 255, 0), pos, 7, 0)
pygame.display.flip()
pygame.quit()
exit()
Example 2: repl.it/#Rabbid76/PyGame-RotateZoomPivot
import pygame
pygame.init()
screen = pygame.display.set_mode((400, 300))
clock = pygame.time.Clock()
def blitRotateZoomXY(surf, original_image, origin, pivot, angle, scale_x, scale_y):
image_rect = original_image.get_rect(topleft = (origin[0] - pivot[0], origin[1]-pivot[1]))
offset_center_to_pivot = pygame.math.Vector2(origin) - image_rect.center
offset_center_to_pivot.x *= scale_x
offset_center_to_pivot.y *= scale_y
rotated_offset = offset_center_to_pivot.rotate(-angle)
rotated_image_center = (origin[0] - rotated_offset.x, origin[1] - rotated_offset.y)
scaled_image = pygame.transform.smoothscale(original_image, (image_rect.width * scale_x, image_rect.height * scale_y))
rotozoom_image = pygame.transform.rotate(scaled_image, angle)
rect = rotozoom_image.get_rect(center = rotated_image_center)
surf.blit(rotozoom_image, rect)
cannon = pygame.image.load('icon/cannon.png')
cannon_mount = pygame.image.load('icon/cannon_mount.png')
angle, zoom_x, zoom_y = -90, 1, 1
stage = 0
done = False
while not done:
clock.tick(60)
for event in pygame.event.get():
if event.type == pygame.QUIT:
done = True
pos = (screen.get_width()/3, screen.get_height()*3/4)
screen.fill((192, 192, 192))
blitRotateZoomXY(screen, cannon, pos, (33.5, 120), angle, zoom_x, zoom_y)
screen.blit(cannon_mount, (pos[0]-43, pos[1]-16))
pygame.display.flip()
if stage == 0:
angle += 1
if angle >= -30:
stage += 1
elif stage == 1:
zoom_y -= 0.05
if zoom_y <= 0.7:
stage += 1
elif stage == 2:
zoom_y += 0.05
if zoom_y >= 1:
stage += 1
elif stage == 3:
angle -= 1
if angle <= -90:
stage = 0
pygame.quit()
exit()
def star1():
global angle
x = int(math.cos(angle) * 100) + 800
y = int(math.sin(angle) * 65) + 400
pygame.draw.circle(screen, white, (x, y), 17)
angle += 0.02
The parameters pygame gives for the .draw.circle here only allow for an RGB color to be used to color the figure. I am wondering if there is a way I can pass an image as a color, or use a different method that allows me to place an image on the object/circle. The circles move around so the image also needs to stay with the circle as it moves.
Create a transparent pygame.Surface with the desired size
Draw a white circle in the middle of the Surface
Blend the image (my_image) on this Surface using the blend mode BLEND_RGBA_MIN:
size = 100
circular_image = pygame.Surface((size, size), pygame.SRCALPHA)
pygame.draw.circle(circular_image, (255, 255, 255), (size//2, size//2), size//2)
image_rect = my_image.get_rect(center = circular_image.get_rect().center)
circular_image.blit(my_image, image_rect, special_flags=pygame.BLEND_RGBA_MIN)
Minimal example:
import pygame
pygame.init()
window = pygame.display.set_mode((300, 300))
clock = pygame.time.Clock()
def create_circular_image(size, image):
clip_image = pygame.Surface((size, size), pygame.SRCALPHA)
pygame.draw.circle(clip_image, (255, 255, 255), (size//2, size//2), size//2)
image_rect = my_image.get_rect(center = clip_image.get_rect().center)
clip_image.blit(my_image, image_rect, special_flags=pygame.BLEND_RGBA_MIN)
return clip_image
def create_test_image():
image = pygame.Surface((100, 100))
ts, w, h, c1, c2 = 25, 100, 100, (255, 64, 64), (32, 64, 255)
[pygame.draw.rect(image, c1 if (x+y) % 2 == 0 else c2, (x*ts, y*ts, ts, ts))
for x in range((w+ts-1)//ts) for y in range((h+ts-1)//ts)]
return image
my_image = create_test_image()
circular_image = create_circular_image(100, my_image)
rect = circular_image.get_rect(center = window.get_rect().center)
run = True
while run:
clock.tick(60)
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
keys = pygame.key.get_pressed()
rect.x += (keys[pygame.K_RIGHT] - keys[pygame.K_LEFT]) * 5
rect.y += (keys[pygame.K_DOWN] - keys[pygame.K_UP]) * 5
window.fill((64, 64, 64))
window.blit(circular_image, rect)
pygame.display.flip()
pygame.quit()
exit()
See also:
how to make circular surface in PyGame
How to fill only certain circular parts of the window in PyGame?
Clipping
Write a function that draws an image on a given x, y and pass these x, y values used in the circle function.
Basicly it's just win.blit(image, (x,y)). Don't forget to subtruct radius value. So the image matchs with circle.
In pygame I use pygame.draw.rect(screen, color, rectangle) for all the rectangles in my program. I want to be able to rotate these rectangles to any angle. I have seen the following code to rotate IMAGES but my question is with RECTANGLES.
pygame.transform.rotate(image, angle)
But I am working with rectangles, I don't have an image or "surface" that I can rotate. When I try to rotate a rectangle with
rect = pygame.draw.rect(screen, self.color, self.get_rectang())
rotatedRect = pygame.transform.rotate(rect, self.rotation)
screen.blit(rotatedRect)
This gives TypeError: must be pygame.Surface, not pygame.Rect on the line with .rotate()
My question is, how can I rotate a and display a RECTANGLE(x,y,w,h), not an image, in pygame.
The linked post that this is a "potential duplicate" of is not a duplicate. One answer explains about the consequences of rotating a rectangle and the other uses code for rotating an image.
See the second answer here: Rotating a point about another point (2D)
I think rectangles can only be horiz or vertical in their oreintation. You need to define the corners and rotate them and then draw and fill between them.
The other way is to make a class
class myRect(pygame.Surface):
def __init__(self, parent, xpos, ypos, width, height):
super(myRect, self).__init__(width, height)
self.xpos = xpos
self.ypos = ypos
self.parent = parent
def update(self, parent):
parent.blit(self, (self.xpos, self.ypos))
def rotate(self, angle):
#(your rotation code goes here)
and use that instead, as then you will be able to rotate it using the transform function.
import pygame as py
# define constants
WIDTH = 500
HEIGHT = 500
FPS = 30
# define colors
BLACK = (0 , 0 , 0)
GREEN = (0 , 255 , 0)
# initialize pygame and create screen
py.init()
screen = py.display.set_mode((WIDTH , HEIGHT))
# for setting FPS
clock = py.time.Clock()
rot = 0
rot_speed = 2
# define a surface (RECTANGLE)
image_orig = py.Surface((100 , 100))
# for making transparent background while rotating an image
image_orig.set_colorkey(BLACK)
# fill the rectangle / surface with green color
image_orig.fill(GREEN)
# creating a copy of orignal image for smooth rotation
image = image_orig.copy()
image.set_colorkey(BLACK)
# define rect for placing the rectangle at the desired position
rect = image.get_rect()
rect.center = (WIDTH // 2 , HEIGHT // 2)
# keep rotating the rectangle until running is set to False
running = True
while running:
# set FPS
clock.tick(FPS)
# clear the screen every time before drawing new objects
screen.fill(BLACK)
# check for the exit
for event in py.event.get():
if event.type == py.QUIT:
running = False
# making a copy of the old center of the rectangle
old_center = rect.center
# defining angle of the rotation
rot = (rot + rot_speed) % 360
# rotating the orignal image
new_image = py.transform.rotate(image_orig , rot)
rect = new_image.get_rect()
# set the rotated rectangle to the old center
rect.center = old_center
# drawing the rotated rectangle to the screen
screen.blit(new_image , rect)
# flipping the display after drawing everything
py.display.flip()
py.quit()
a more complex version of the quick replacement, in which you can define an arbitrary rotation center point for your rectangle - even outside of it (tested in python3):
def rectRotated( surface, color, pos, fill, border_radius, rotation_angle, rotation_offset_center = (0,0), nAntialiasingRatio = 1 ):
"""
- rotation_angle: in degree
- rotation_offset_center: moving the center of the rotation: (-100,0) will turn the rectangle around a point 100 above center of the rectangle,
if (0,0) the rotation is at the center of the rectangle
- nAntialiasingRatio: set 1 for no antialising, 2/4/8 for better aliasing
"""
nRenderRatio = nAntialiasingRatio
sw = pos[2]+abs(rotation_offset_center[0])*2
sh = pos[3]+abs(rotation_offset_center[1])*2
surfcenterx = sw//2
surfcentery = sh//2
s = pg.Surface( (sw*nRenderRatio,sh*nRenderRatio) )
s = s.convert_alpha()
s.fill((0,0,0,0))
rw2=pos[2]//2 # halfwidth of rectangle
rh2=pos[3]//2
pg.draw.rect( s, color, ((surfcenterx-rw2-rotation_offset_center[0])*nRenderRatio,(surfcentery-rh2-rotation_offset_center[1])*nRenderRatio,pos[2]*nRenderRatio,pos[3]*nRenderRatio), fill*nRenderRatio, border_radius=border_radius*nRenderRatio )
s = pygame.transform.rotate( s, rotation_angle )
if nRenderRatio != 1: s = pygame.transform.smoothscale(s,(s.get_width()//nRenderRatio,s.get_height()//nRenderRatio))
incfromrotw = (s.get_width()-sw)//2
incfromroth = (s.get_height()-sh)//2
surface.blit( s, (pos[0]-surfcenterx+rotation_offset_center[0]+rw2-incfromrotw,pos[1]-surfcentery+rotation_offset_center[1]+rh2-incfromroth) )
You cannot rotate a rectangle drawn by pygame.draw.rect. You have to create a transparent pygame.Surface and rotate the Surface:
rect_surf = pygame.Surface((widht, height), pygame.SRCLAPHA)
rect_surf.fill(color)
See How do I rotate an image around its center using PyGame?, to rotate the Surface.
I made a class which handles the rotation for you...
Extended from Ashish's design
from pygame import Surface, transform
from consts import screen
class BaseEntity:
def __init__(self, x: int, y: int):
self.x = x
self.y = y
class Rectangle(BaseEntity):
def __init__(self, x: int, y: int, width: int, height: int, color: tuple):
super().__init__(x, y)
self.width = width
self.height = height
self.color = color
self.rotatation = 0
# the rectangle is a surface itself
self.surface = Surface((width, height))
self.surface.set_colorkey((0, 0, 0))
self.surface.fill(color)
self.rect = self.surface.get_rect()
def display(self, angle=None):
# updating values
self.surface.fill(
self.color
) # refill the surface color if you change it somewhere in the program
self.rect = self.surface.get_rect()
self.rect.center = (self.x, self.y)
# renderer
if angle is not None:
self.rotatation = angle
old_center = self.rect.center
new = transform.rotate(self.surface, self.rotatation)
self.rect = new.get_rect()
self.rect.center = old_center
screen.blit(new, self.rect)
Using a bit of trigonometry and the polygon function, I'm able to draw a rotated rectangle.
import math
import pygame.draw
def draw_rectangle(x, y, width, height, color, rotation=0):
"""Draw a rectangle, centered at x, y.
Arguments:
x (int/float):
The x coordinate of the center of the shape.
y (int/float):
The y coordinate of the center of the shape.
width (int/float):
The width of the rectangle.
height (int/float):
The height of the rectangle.
color (str):
Name of the fill color, in HTML format.
"""
points = []
# The distance from the center of the rectangle to
# one of the corners is the same for each corner.
radius = math.sqrt((height / 2)**2 + (width / 2)**2)
# Get the angle to one of the corners with respect
# to the x-axis.
angle = math.atan2(height / 2, width / 2)
# Transform that angle to reach each corner of the rectangle.
angles = [angle, -angle + math.pi, angle + math.pi, -angle]
# Convert rotation from degrees to radians.
rot_radians = (math.pi / 180) * rotation
# Calculate the coordinates of each point.
for angle in angles:
y_offset = -1 * radius * math.sin(angle + rot_radians)
x_offset = radius * math.cos(angle + rot_radians)
points.append((x + x_offset, y + y_offset))
pygame.draw.polygon(screen, color, points)
https://replit.com/#TimSwast1/RotateARectanlge?v=1
a quick replacement of the base pygame function adding rotation:
def rectRotated( surface, color, pos, fill, border_radius, angle ):
"""
- angle in degree
"""
max_area = max(pos[2],pos[3])
s = pg.Surface((max_area,max_area))
s = s.convert_alpha()
s.fill((0,0,0,0))
pg.draw.rect(s, color,(0,0,pos[2],pos[3]),fill, border_radius=border_radius)
s = pygame.transform.rotate(s,angle)
surface.blit( s, (pos[0],pos[1]) )
This code simulates rotating rectangles falling towards the ground. I used it in one of my games to make the background look awesome
import pygame
import random
class Square(pygame.sprite.Sprite):
def __init__(self, x, y):
super(Square, self).__init__()
self.win = win
self.color = (128, 128, 128)
self.speed = 3
self.angle = 0
self.side = random.randint(15, 40)
self.surface = pygame.Surface((self.side, self.side), pygame.SRCALPHA)
self.surface.set_colorkey((200,200,200))
self.rect = self.surface.get_rect(center=(x, y))
def update(self, win):
center = self.rect.center
self.angle = (self.angle + self.speed) % 360
image = pygame.transform.rotate(self.surface , self.angle)
self.rect = image.get_rect()
self.rect.center = center
self.rect.y += 1.5
if self.rect.top >= HEIGHT:
self.kill()
pygame.draw.rect(self.surface, self.color, (0,0, self.side, self.side), 4)
win.blit(image, self.rect)
if __name__ == '__main__':
pygame.init()
SCREEN = WIDTH, HEIGHT = 288, 512
win = pygame.display.set_mode(SCREEN, pygame.NOFRAME)
clock = pygame.time.Clock()
FPS = 60
count = 0
square_group = pygame.sprite.Group()
running = True
while running:
win.fill((200,200,200))
for event in pygame.event.get():
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
running = False
count += 1
if count % 100 == 0:
x = random.randint(40, WIDTH-40)
y = 0
square = Square(x, y)
square_group.add(square)
count = 0
square_group.update(win)
pygame.draw.rect(win, (30,30,30), (0, 0, WIDTH, HEIGHT), 8)
clock.tick(FPS)
pygame.display.update()
pygame.quit()
Here's the output, it's not an gif though
Now if you want color filled rectangle instead of bordered only, update this line on line 31
pygame.draw.rect(self.surface, self.color, (0,0, self.side, self.side))
and if you don't want the rectangle to fall down comment line 26
A concise and fast function to draw a rotated rectangle. Uses NumPy
def rectRotated(self, surface, rect, color, rotation):
"""
Draws a rotated Rect.
surface: pygame.Surface
rect: pygame.Rect
color: pygame.Color
rotation: float (degrees)
return: np.ndarray (vertices)
"""
# calculate the rotation in radians
rot_radians = -rotation * pi / 180
# calculate the points around the center of the rectangle, taking width and height into account
angle = atan2(rect.height / 2, rect.width / 2)
angles = [angle, -angle + pi, angle + pi, -angle]
radius = sqrt((rect.height / 2)**2 + (rect.width / 2)**2)
# create a numpy array of the points
points = np.array([
[rect.x + radius * cos(angle + rot_radians), rect.y + radius * sin(angle + rot_radians)]
for angle in angles
])
# draw the polygon
pygame.draw.polygon(surface, color, points)
# return the vertices of the rectangle
return points
This python code illustrates a sin wave in a pygame window.
I want to draw a square wave in this very same fashion as well, though I have no idea what this code might be or how to draw a square wave / how one is constructed in python.
Does anybody know how I can do this? Is it possible with the same imports or will I need new ones?
Code:
import sys, pygame, math
from pygame.locals import *
# set up a bunch of constants
WHITE = (255, 255, 255)
DARKRED = (128, 0, 0)
RED = (255, 0, 0)
BLACK = ( 0, 0, 0)
BGCOLOR = WHITE
WINDOWWIDTH = 640 # width of the program's window, in pixels
WINDOWHEIGHT = 480 # height in pixels
WIN_CENTERX = int(WINDOWWIDTH / 2) # the midpoint for the width of the window
WIN_CENTERY = int(WINDOWHEIGHT / 2) # the midpoint for the height of the window
FPS = 160 # frames per second to run at
AMPLITUDE = 80 # how many pixels tall the waves with rise/fall.
# standard pygame setup code
pygame.init()
FPSCLOCK = pygame.time.Clock()
DISPLAYSURF = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT))
pygame.display.set_caption('Trig Waves')
fontObj = pygame.font.Font('freesansbold.ttf', 16)
# variables that track visibility modes
showSine = True
pause = False
xPos = 0
step = 0 # the current input f
posRecord = {'sin': []} # keeps track of the ball positions for drawing the waves
# making text Surface and Rect objects for various labels
sinLabelSurf = fontObj.render('sine', True, RED, BGCOLOR)
sinLabelRect = sinLabelSurf.get_rect()
instructionsSurf = fontObj.render('Press Q to toggle wave. P to pause.', True, BLACK, BGCOLOR)
instructionsRect = instructionsSurf.get_rect()
instructionsRect.left = 10
instructionsRect.bottom = WINDOWHEIGHT - 10
# main application loop
while True:
# event handling loop for quit events
for event in pygame.event.get():
if event.type == QUIT or (event.type == KEYUP and event.key == K_ESCAPE):
pygame.quit()
sys.exit()
# check for key presses that toggle pausing and wave visibility
if event.type == KEYUP:
if event.key == K_q:
showSine = not showSine
elif event.key == K_p:
pause = not pause
# fill the screen to draw from a blank state
DISPLAYSURF.fill(BGCOLOR)
# draw instructions
DISPLAYSURF.blit(instructionsSurf, instructionsRect)
# sine wave
yPos = -1 * math.sin(step) * AMPLITUDE
posRecord['sin'].append((int(xPos), int(yPos) + WIN_CENTERY))
if showSine:
# draw the sine ball and label
pygame.draw.circle(DISPLAYSURF, RED, (int(xPos), int(yPos) + WIN_CENTERY), 10)
sinLabelRect.center = (int(xPos), int(yPos) + WIN_CENTERY + 20)
DISPLAYSURF.blit(sinLabelSurf, sinLabelRect)
# draw the waves from the previously recorded ball positions
if showSine:
for x, y in posRecord['sin']:
pygame.draw.circle(DISPLAYSURF, DARKRED, (x, y), 4)
# draw the border
pygame.draw.rect(DISPLAYSURF, BLACK, (0, 0, WINDOWWIDTH, WINDOWHEIGHT), 1)
pygame.display.update()
FPSCLOCK.tick(FPS)
if not pause:
xPos += 0.5
if xPos > WINDOWWIDTH:
xPos = 0
posRecord = {'sin': []}
step = 0
else:
step += 0.008
step %= 2 * math.pi
Python ver.2.6, Pygame ver.1.9.2
I made some modifications to add squared wave.
See places with ### HERE.
import sys, pygame, math
from pygame.locals import *
# set up a bunch of constants
WHITE = (255, 255, 255)
DARKRED = (128, 0, 0)
RED = (255, 0, 0)
BLACK = ( 0, 0, 0)
GREEN = ( 0, 255, 0) ### HERE
BLUE = ( 0, 0, 255) ### HERE
BGCOLOR = WHITE
WINDOWWIDTH = 640 # width of the program's window, in pixels
WINDOWHEIGHT = 480 # height in pixels
WIN_CENTERX = int(WINDOWWIDTH / 2) # the midpoint for the width of the window
WIN_CENTERY = int(WINDOWHEIGHT / 2) # the midpoint for the height of the window
FPS = 160 # frames per second to run at
AMPLITUDE = 80 # how many pixels tall the waves with rise/fall.
# standard pygame setup code
pygame.init()
FPSCLOCK = pygame.time.Clock()
DISPLAYSURF = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT))
pygame.display.set_caption('Trig Waves')
fontObj = pygame.font.Font('freesansbold.ttf', 16)
# variables that track visibility modes
showSine = True
showSquare = True ### HERE
pause = False
xPos = 0
step = 0 # the current input f
### HERE
posRecord = {'sin': [], 'square': []} # keeps track of the ball positions for drawing the waves
# making text Surface and Rect objects for various labels
### HERE
squareLabelSurf = fontObj.render('square', True, BLUE, BGCOLOR)
squareLabelRect = squareLabelSurf.get_rect()
sinLabelSurf = fontObj.render('sine', True, RED, BGCOLOR)
sinLabelRect = sinLabelSurf.get_rect()
instructionsSurf = fontObj.render('Press Q to toggle wave. P to pause.', True, BLACK, BGCOLOR)
instructionsRect = instructionsSurf.get_rect()
instructionsRect.left = 10
instructionsRect.bottom = WINDOWHEIGHT - 10
### HERE
yPosSquare = AMPLITUDE # starting position
# main application loop
while True:
# event handling loop for quit events
for event in pygame.event.get():
if event.type == QUIT or (event.type == KEYUP and event.key == K_ESCAPE):
pygame.quit()
sys.exit()
# check for key presses that toggle pausing and wave visibility
if event.type == KEYUP:
if event.key == K_q:
showSine = not showSine
elif event.key == K_p:
pause = not pause
# fill the screen to draw from a blank state
DISPLAYSURF.fill(BGCOLOR)
# draw instructions
DISPLAYSURF.blit(instructionsSurf, instructionsRect)
# sine wave
yPos = -1 * math.sin(step) * AMPLITUDE
posRecord['sin'].append((int(xPos), int(yPos) + WIN_CENTERY))
if showSine:
# draw the sine ball and label
pygame.draw.circle(DISPLAYSURF, RED, (int(xPos), int(yPos) + WIN_CENTERY), 10)
sinLabelRect.center = (int(xPos), int(yPos) + WIN_CENTERY + 20)
DISPLAYSURF.blit(sinLabelSurf, sinLabelRect)
# draw the waves from the previously recorded ball positions
if showSine:
for x, y in posRecord['sin']:
pygame.draw.circle(DISPLAYSURF, DARKRED, (x, y), 4)
### HERE - drawing horizontal lines
# square
posRecord['square'].append((int(xPos), int(yPosSquare) + WIN_CENTERY))
if showSquare:
# draw the sine ball and label
pygame.draw.circle(DISPLAYSURF, GREEN, (int(xPos), int(yPosSquare) + WIN_CENTERY), 10)
squareLabelRect.center = (int(xPos), int(yPosSquare) + WIN_CENTERY + 20)
DISPLAYSURF.blit(squareLabelSurf, squareLabelRect)
# draw the waves from the previously recorded ball positions
if showSquare:
for x, y in posRecord['square']:
pygame.draw.circle(DISPLAYSURF, BLUE, (x, y), 4)
# draw the border
pygame.draw.rect(DISPLAYSURF, BLACK, (0, 0, WINDOWWIDTH, WINDOWHEIGHT), 1)
pygame.display.update()
FPSCLOCK.tick(FPS)
if not pause:
xPos += 0.5
if xPos > WINDOWWIDTH:
#sine ### HERE
xPos = 0
posRecord['sin'] = []
step = 0
# square ### HERE
yPosSquare = AMPLITUDE
posRecord['square'] = []
else:
#sine ### HERE
step += 0.008
#step %= 2 * math.pi
# square ### HERE
# jump top and bottom every 100 pixels
if xPos % 100 == 0:
yPosSquare *= -1
# add vertical line
for x in range(-AMPLITUDE, AMPLITUDE):
posRecord['square'].append((int(xPos), int(x) + WIN_CENTERY))